<?php

namespace App\Services\Interest;

use App\Models\Credit;
use App\Models\Installment;
use App\Services\DueDateService;
use App\Support\Concerns\CalculatesCreditSummary;
use App\Support\MoneyCalculator;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Exception;

/**
 * Estrategia de cálculo de interés con balance decreciente (método francés).
 * El interés se calcula sobre el saldo pendiente, resultando en cuotas iguales
 * pero con distribución variable entre capital e interés.
 *
 * Usa Money Value Object con BC Math para precisión financiera exacta.
 */
class DiminishingBalanceStrategy implements InterestCalculationStrategy
{
    use CalculatesCreditSummary;

    public function __construct(private readonly DueDateService $fechas = new DueDateService) {}

    public function calculate(Credit $credit): array
    {
        $principal = Money::of($credit->amount);
        $annualRatePercent = (string) $credit->interest_rate;
        $n = (int) $credit->installments_count;
        $periodicity = $credit->periodicity;

        if ($n <= 0) {
            throw new Exception('El número de cuotas debe ser mayor a cero.');
        }

        // Convertir tasa anual a decimal y luego a tasa periódica
        $annualRateDecimal = MoneyCalculator::percentToDecimal($annualRatePercent);
        $periodsPerYear = $this->getPeriodsPerYear($periodicity);
        $periodicRate = MoneyCalculator::annualToPeriodicRate($annualRateDecimal, $periodsPerYear);

        // Calcular cuota fija usando método francés
        $installmentAmount = MoneyCalculator::frenchMethodPayment($principal, $periodicRate, $n);

        $balance = $principal;

        $start = Carbon::parse($credit->start_date)->startOfDay();
        $forced = $credit->first_due_date_override
            ? Carbon::parse($credit->first_due_date_override)->startOfDay()
            : null;

        $current = $this->calculateFirstDueDate(
            $start,
            $periodicity,
            $credit->due_day_1,
            $credit->due_day_2,
            $forced
        );

        $rows = [];

        for ($i = 1; $i <= $n; $i++) {
            // Interés del período = saldo pendiente * tasa periódica
            $interestAmount = MoneyCalculator::periodInterest($balance, $periodicRate);

            // Capital del período = cuota - interés
            $principalAmount = $installmentAmount->subtract($interestAmount);

            // Ajuste para la última cuota (para cerrar exactamente el saldo)
            if ($i === $n) {
                $principalAmount = $balance;
                $installmentAmount = $principalAmount->add($interestAmount);
            }

            // Actualizar balance
            $balance = $balance->subtract($principalAmount);

            $rows[] = [
                'company_id' => $credit->company_id,
                'installment_number' => $i,
                'due_date' => $current->toDateString(),
                'principal_amount' => $principalAmount->value(),
                'interest_amount' => $interestAmount->value(),
                'total_amount' => $installmentAmount->value(),
                'principal_balance_after' => $balance->max(0)->value(),
                'amount_paid' => 0.0,
                'status' => Installment::STATUS_PENDING,
                'created_at' => now(),
                'updated_at' => now(),
            ];

            if ($i < $n) {
                $current = $this->calculateNextDueDate(
                    $current,
                    $periodicity,
                    $credit->due_day_1,
                    $credit->due_day_2
                );
            }
        }

        return $rows;
    }

    public function getName(): string
    {
        return 'diminishing_balance';
    }

    /**
     * Calcula los períodos por año según la periodicidad.
     */
    private function getPeriodsPerYear(string $periodicity): int
    {
        return match ($periodicity) {
            'daily' => 365,
            'weekly' => 52,
            'biweekly' => 24,
            'monthly' => 12,
            default => 12,
        };
    }

    /*=========================================================
    | CÁLCULO DE FECHAS
    =========================================================*/
    /**
     * Delega en `DueDateService`, que es la ÚNICA definición de cuándo vence la
     * primera cuota. Ver la nota en `FlatRateStrategy`: aquí vivía la otra copia,
     * con el mismo fallo.
     */
    private function calculateFirstDueDate(
        Carbon $start,
        string $periodicity,
        ?int $day1,
        ?int $day2,
        ?Carbon $forced = null,
    ): Carbon {
        if ($forced) {
            return $forced->clone()->startOfDay();
        }

        return $this->fechas->firstDueDate($start, $periodicity, $day1, $day2);
    }

    private function calculateNextDueDate(
        Carbon $last,
        string $periodicity,
        ?int $day1,
        ?int $day2,
    ): Carbon {
        return match ($periodicity) {
            'daily' => $last->clone()->addDay()->startOfDay(),
            'weekly' => $last->clone()->addWeek()->startOfDay(),
            'monthly' => $last->clone()->addMonthNoOverflow()->day((int) $day1)->startOfDay(),
            'biweekly' => $this->getNextBiweeklyNext($last, (int) $day1, (int) $day2),
            default => throw new Exception("Periodicidad '{$periodicity}' no válida."),
        };
    }

    private function getNextBiweeklyNext(Carbon $current, int $day1, int $day2): Carbon
    {
        $day1 = max(1, min(31, $day1));
        $day2 = max(1, min(31, $day2));

        $currentDay = $current->day;

        if ($currentDay < $day1) {
            return $current->clone()->day($day1)->startOfDay();
        }

        if ($currentDay === $day1 || ($currentDay > $day1 && $currentDay < $day2)) {
            return $current->clone()->day($day2)->startOfDay();
        }

        return $current->clone()->addMonthNoOverflow()->day($day1)->startOfDay();
    }
}
