<?php

declare(strict_types=1);

namespace App\Services\Credits;

use App\DTOs\CustomInstallmentData;
use App\DTOs\RestructureWithCustomPlanData;
use App\Models\Credit;
use App\Models\Installment;
use App\ValueObjects\Money;
use Carbon\Carbon;

/**
 * Genera cuotas con montos irregulares (personalizados por el usuario).
 *
 * A diferencia de CreditCalculator que calcula cuotas iguales automáticamente,
 * este generador acepta montos explícitos definidos por el usuario.
 *
 * Características:
 * - Soporta cuotas con diferentes montos
 * - Distribuye principal/interés proporcionalmente si no se especifica
 * - Mantiene principal_balance_after correcto para cada cuota
 * - Garantiza precisión con BC Math via Money VO
 */
class IrregularInstallmentGenerator
{
    /**
     * Genera las cuotas para un crédito con plan personalizado.
     *
     * @param  Credit  $credit  Crédito hijo (ya creado)
     * @param  RestructureWithCustomPlanData  $data  Datos del plan personalizado
     * @param  Money  $totalPrincipal  Capital total a distribuir
     * @return array<array> Array de datos para insertar en installments
     */
    public function generate(
        Credit $credit,
        RestructureWithCustomPlanData $data,
        Money $totalPrincipal,
    ): array {
        $planTotal = $data->totalAmount();
        $totalInterest = $planTotal->subtract($totalPrincipal);

        // Para última cuota: ajustar para absorber diferencias de redondeo
        $accumulatedPrincipal = Money::zero();
        $accumulatedInterest = Money::zero();
        $remainingPrincipal = $totalPrincipal;

        $rows = [];
        $installmentsCount = count($data->installments);

        foreach ($data->installments as $index => $installment) {
            $isLastInstallment = ($index === $installmentsCount - 1);
            $totalAmount = Money::of($installment->totalAmount);

            // Calcular distribución principal/interés
            [$principalAmount, $interestAmount] = $this->calculateDistribution(
                installment: $installment,
                totalAmount: $totalAmount,
                totalPrincipal: $totalPrincipal,
                totalInterest: $totalInterest,
                planTotal: $planTotal,
                isLast: $isLastInstallment,
                accumulatedPrincipal: $accumulatedPrincipal,
                accumulatedInterest: $accumulatedInterest,
            );

            // Acumular para control de redondeo
            $accumulatedPrincipal = $accumulatedPrincipal->add($principalAmount);
            $accumulatedInterest = $accumulatedInterest->add($interestAmount);

            // Actualizar principal restante (principal_balance_after)
            $remainingPrincipal = $remainingPrincipal->subtract($principalAmount);

            $rows[] = [
                'company_id' => $credit->company_id,
                'installment_number' => $installment->number,
                'due_date' => $installment->dueDate->toDateString(),
                'principal_amount' => $principalAmount->value(),
                'interest_amount' => $interestAmount->value(),
                'total_amount' => $totalAmount->value(),
                'principal_balance_after' => $remainingPrincipal->max(0)->value(),
                'amount_paid' => 0,
                'status' => Installment::STATUS_PENDING,
                'created_at' => now(),
                'updated_at' => now(),
            ];
        }

        return $rows;
    }

    /**
     * Calcula la distribución de principal e interés para una cuota.
     *
     * Estrategia:
     * 1. Si el usuario proporcionó valores explícitos, usarlos
     * 2. Si es la última cuota, ajustar para absorber diferencias de redondeo
     * 3. Si no, distribuir proporcionalmente al peso de la cuota
     *
     * @return array{Money, Money} [principal, interest]
     */
    private function calculateDistribution(
        CustomInstallmentData $installment,
        Money $totalAmount,
        Money $totalPrincipal,
        Money $totalInterest,
        Money $planTotal,
        bool $isLast,
        Money $accumulatedPrincipal,
        Money $accumulatedInterest,
    ): array {
        // Caso 1: Usuario especificó valores explícitos
        if ($installment->principalAmount !== null && $installment->interestAmount !== null) {
            return [
                Money::of($installment->principalAmount),
                Money::of($installment->interestAmount),
            ];
        }

        // Caso 2: Última cuota - absorber diferencia de redondeo
        if ($isLast) {
            $principalAmount = $totalPrincipal->subtract($accumulatedPrincipal);
            $interestAmount = $totalInterest->subtract($accumulatedInterest);

            // Validar que no sean negativos (por seguridad)
            $principalAmount = $principalAmount->max(0);
            $interestAmount = $interestAmount->max(0);

            // Ajustar si la suma no coincide con totalAmount
            $sum = $principalAmount->add($interestAmount);
            if (! $sum->equals($totalAmount)) {
                // Ajustar el interés para que cuadre
                $interestAmount = $totalAmount->subtract($principalAmount);
            }

            return [$principalAmount, $interestAmount];
        }

        // Caso 3: Distribución proporcional usando Money (BC Math)
        // cuota_peso = cuota_total / plan_total
        // principal_cuota = total_principal × cuota_peso
        if ($planTotal->isZeroOrNegative()) {
            return [Money::zero(), $totalAmount];
        }

        // Usar BC Math para evitar errores de punto flotante:
        // principalAmount = totalPrincipal * (totalAmount / planTotal)
        // Equivalente a: principalAmount = (totalPrincipal * totalAmount) / planTotal
        $principalAmount = $totalPrincipal
            ->multiply($totalAmount->raw())
            ->divide($planTotal->raw());

        $interestAmount = $totalAmount->subtract($principalAmount);

        // Asegurar que interés no sea negativo por redondeo
        if ($interestAmount->isNegative()) {
            $interestAmount = Money::zero();
            $principalAmount = $totalAmount;
        }

        return [$principalAmount, $interestAmount];
    }

    /**
     * Genera un plan de cuotas sugerido para previsualización.
     *
     * Útil para UI donde el usuario quiere ver cómo quedarían las cuotas
     * antes de confirmar la reestructuración.
     *
     * @param  Money  $totalAmount  Total a distribuir
     * @param  int  $installmentsCount  Número de cuotas
     * @param  Carbon  $startDate  Fecha de inicio
     * @param  string  $periodicity  Periodicidad (daily, weekly, biweekly, monthly)
     * @return array<array{number: int, total_amount: float, due_date: string}>
     */
    public function suggestEqualPlan(
        Money $totalAmount,
        int $installmentsCount,
        Carbon $startDate,
        string $periodicity = 'monthly',
    ): array {
        $allocations = $totalAmount->allocate($installmentsCount);
        $currentDate = $startDate->copy();
        $rows = [];

        foreach ($allocations as $index => $amount) {
            $rows[] = [
                'number' => $index + 1,
                'total_amount' => $amount->value(),
                'due_date' => $currentDate->toDateString(),
            ];

            $currentDate = $this->advanceDate($currentDate, $periodicity);
        }

        return $rows;
    }

    /**
     * Avanza la fecha según la periodicidad.
     */
    private function advanceDate(Carbon $date, string $periodicity): Carbon
    {
        return match ($periodicity) {
            'daily' => $date->copy()->addDay(),
            'weekly' => $date->copy()->addWeek(),
            'biweekly' => $date->copy()->addWeeks(2),
            'monthly' => $date->copy()->addMonthNoOverflow(),
            default => $date->copy()->addMonth(),
        };
    }
}
