<?php

declare(strict_types=1);

namespace App\Validators;

use App\DTOs\RestructureWithCustomPlanData;
use App\ValueObjects\Money;
use Carbon\Carbon;
use InvalidArgumentException;

/**
 * Valida que un plan de cuotas personalizado sea coherente y ejecutable.
 *
 * Validaciones:
 * - Suma de cuotas = saldo pendiente + intereses capitalizados
 * - Fechas ordenadas ascendentemente
 * - Montos positivos
 * - Números de cuota consecutivos
 * - Primera fecha no es pasada
 */
class CustomInstallmentPlanValidator
{
    /**
     * Valida que el plan de cuotas sea coherente con el saldo pendiente.
     *
     * @param  RestructureWithCustomPlanData  $data  Plan de cuotas a validar
     * @param  Money  $remainingBalance  Saldo pendiente del crédito original
     * @param  bool  $allowPastDates  Permitir fechas pasadas (default: false)
     *
     * @throws InvalidArgumentException Si alguna validación falla
     */
    public function validate(
        RestructureWithCustomPlanData $data,
        Money $remainingBalance,
        bool $allowPastDates = false,
    ): void {
        $this->validateTotalAmount($data, $remainingBalance);
        $this->validateDatesAscending($data);
        $this->validatePositiveAmounts($data);
        $this->validateConsecutiveNumbers($data);

        if (! $allowPastDates) {
            $this->validateNoPastDates($data);
        }
    }

    /**
     * Valida que la suma de cuotas coincida exactamente con el saldo pendiente.
     */
    private function validateTotalAmount(
        RestructureWithCustomPlanData $data,
        Money $remainingBalance,
    ): void {
        $expectedTotal = $remainingBalance->add($data->capitalizedInterest);
        $planTotal = $data->totalAmount();

        // Tolerancia de 1 centavo para errores de redondeo
        $difference = $planTotal->subtract($expectedTotal)->abs();
        $tolerance = Money::of(0.01);

        if ($difference->greaterThan($tolerance)) {
            throw new InvalidArgumentException(sprintf(
                'La suma de las cuotas (%s) no coincide con el saldo pendiente + intereses capitalizados (%s). Diferencia: %s',
                $planTotal->formatWithCurrency(),
                $expectedTotal->formatWithCurrency(),
                $planTotal->subtract($expectedTotal)->formatWithCurrency()
            ));
        }
    }

    /**
     * Valida que las fechas estén en orden ascendente.
     */
    private function validateDatesAscending(RestructureWithCustomPlanData $data): void
    {
        $lastDate = null;

        foreach ($data->installments as $installment) {
            if ($lastDate !== null && $installment->dueDate->lte($lastDate)) {
                throw new InvalidArgumentException(sprintf(
                    'Las fechas de las cuotas deben ser ascendentes. Cuota #%d (%s) tiene fecha anterior o igual a la cuota previa (%s).',
                    $installment->number,
                    $installment->dueDate->toDateString(),
                    $lastDate->toDateString()
                ));
            }
            $lastDate = $installment->dueDate;
        }
    }

    /**
     * Valida que todos los montos sean positivos.
     */
    private function validatePositiveAmounts(RestructureWithCustomPlanData $data): void
    {
        foreach ($data->installments as $installment) {
            if ($installment->totalAmount <= 0) {
                throw new InvalidArgumentException(sprintf(
                    'La cuota #%d tiene monto inválido: %s. El monto debe ser mayor a 0.',
                    $installment->number,
                    number_format($installment->totalAmount, 2)
                ));
            }
        }
    }

    /**
     * Valida que los números de cuota sean consecutivos (1, 2, 3...).
     */
    private function validateConsecutiveNumbers(RestructureWithCustomPlanData $data): void
    {
        foreach ($data->installments as $index => $installment) {
            $expectedNumber = $index + 1;

            if ($installment->number !== $expectedNumber) {
                throw new InvalidArgumentException(sprintf(
                    'Los números de cuota deben ser consecutivos. Se esperaba #%d, se recibió #%d.',
                    $expectedNumber,
                    $installment->number
                ));
            }
        }
    }

    /**
     * Valida que la primera fecha no sea en el pasado.
     */
    private function validateNoPastDates(RestructureWithCustomPlanData $data): void
    {
        $today = Carbon::today();
        $firstDueDate = $data->firstDueDate();

        if ($firstDueDate->lt($today)) {
            throw new InvalidArgumentException(sprintf(
                'La fecha de la primera cuota (%s) no puede ser anterior a hoy (%s).',
                $firstDueDate->toDateString(),
                $today->toDateString()
            ));
        }
    }

    /**
     * Valida un plan y retorna un resumen de la validación.
     *
     * Útil para previsualización en UI sin lanzar excepciones.
     *
     * @return array{valid: bool, errors: string[], summary: array}
     */
    public function validateWithSummary(
        RestructureWithCustomPlanData $data,
        Money $remainingBalance,
    ): array {
        $errors = [];
        $expectedTotal = $remainingBalance->add($data->capitalizedInterest);
        $planTotal = $data->totalAmount();

        // Validar suma
        $difference = $planTotal->subtract($expectedTotal);
        if ($difference->abs()->greaterThan(Money::of(0.01))) {
            $errors[] = sprintf(
                'Diferencia en suma: %s',
                $difference->formatWithCurrency()
            );
        }

        // Validar fechas
        $lastDate = null;
        foreach ($data->installments as $installment) {
            if ($lastDate !== null && $installment->dueDate->lte($lastDate)) {
                $errors[] = sprintf('Cuota #%d tiene fecha desordenada', $installment->number);
            }
            $lastDate = $installment->dueDate;
        }

        // Validar montos
        foreach ($data->installments as $installment) {
            if ($installment->totalAmount <= 0) {
                $errors[] = sprintf('Cuota #%d tiene monto inválido', $installment->number);
            }
        }

        return [
            'valid' => empty($errors),
            'errors' => $errors,
            'summary' => [
                'installments_count' => $data->installmentsCount(),
                'plan_total' => $planTotal->value(),
                'expected_total' => $expectedTotal->value(),
                'difference' => $difference->value(),
                'first_due_date' => $data->firstDueDate()->toDateString(),
                'last_due_date' => $data->lastDueDate()->toDateString(),
            ],
        ];
    }
}
