<?php

namespace App\Services\Interest;

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

/**
 * Estrategia de cálculo de interés con tasa fija (flat rate).
 * El interés se calcula sobre el capital inicial y se distribuye igual entre las cuotas.
 *
 * Usa Money Value Object con BC Math para precisión financiera exacta.
 */
class FlatRateStrategy implements InterestCalculationStrategy
{
    use CalculatesCreditSummary;

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

    public function calculate(Credit $credit): array
    {
        $principal = Money::of($credit->amount);
        $totalRate = (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.');
        }

        // Cálculos con Money (precisión BC Math)
        $totalInterest = $principal->percentage($totalRate);

        // Distribución exacta. Se alloca el TOTAL a cobrar y el INTERÉS; el CAPITAL de
        // cada cuota se DERIVA como (total − interés). Así:
        //   • el total por cuota queda UNIFORME cuando el total a cobrar divide exacto
        //     (100.000 al 20% = 120.000 en 30 cuotas ⇒ 30 × 4.000,00, no 29×3.999,99 +
        //     1×4.000,29) — #389;
        //   • se mantiene el invariante por fila total == capital + interés (#91),
        //     porque el capital se define justamente como (total − interés);
        //   • las sumas globales siguen cuadrando: Σtotal = capital+interés y Σinterés =
        //     interés (allocate absorbe su residuo en la última cuota), de donde
        //     Σcapital = Σtotal − Σinterés = capital.
        // Antes se allocaban capital (100.000/30) e interés (20.000/30) por SEPARADO y el
        // residuo de cada uno caía en la última cuota; aunque el total dividiera exacto,
        // la división por componentes rompía la uniformidad del total.
        $totalReceivable = $principal->add($totalInterest);
        $totalParts = $totalReceivable->allocate($n);
        $interestParts = $totalInterest->allocate($n);

        // Calcular fechas
        $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
        );

        // Balance inicial para tracking
        $balance = $principal;

        $rows = [];

        for ($i = 0; $i < $n; $i++) {
            $installmentNumber = $i + 1;
            $interestAmount = $interestParts[$i];
            $totalAmount = $totalParts[$i];
            // Derivar el capital = total − interés: mantiene el invariante por fila
            // total == capital + interés (#91) y deja el total uniforme (#389).
            $principalAmount = $totalAmount->subtract($interestAmount);

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

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

            // Calcular próxima fecha (excepto última cuota)
            if ($i < $n - 1) {
                $current = $this->calculateNextDueDate(
                    $current,
                    $periodicity,
                    $credit->due_day_1,
                    $credit->due_day_2
                );
            }
        }

        return $rows;
    }

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

    /*=========================================================
    | CÁLCULO DE FECHAS
    =========================================================*/
    /**
     * Delega en `DueDateService`, que es la ÚNICA definición de cuándo vence la
     * primera cuota.
     *
     * Antes había una copia aquí y otra en la otra estrategia, y ninguna de las
     * dos cumplía la regla que el servicio sí tenía escrita y probada: la
     * primera cuota no puede caer el día del desembolso. Con la copia, alguien
     * que desembolsaba un lunes y elegía "lunes" dejaba al cliente debiendo una
     * cuota antes de salir por la puerta.
     */
    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();
    }
}
