<?php

declare(strict_types=1);

namespace App\Services\Payments;

use App\Models\CompanyFinancialSettings;
use App\Models\Credit;
use App\Models\Installment;
use App\ValueObjects\Money;

/**
 * Fuente única de las reglas de negocio previas al registro de un pago,
 * compartida por los flujos interactivos (PWA single + panel Filament) para
 * que ambos apliquen exactamente las mismas validaciones:
 *   1. el monto no excede el saldo pendiente,
 *   2. existe al menos una cuota pendiente donde aplicar el pago,
 *   3. se respetan las políticas de la empresa (pagos parciales / sobrepagos).
 *
 * Las comparaciones de monto usan el Money VO (BC Math, decimal exacto): los
 * saldos/cuotas son DECIMAL(12,2), así que la comparación exacta es correcta y
 * NO se necesita la tolerancia mágica `±0.01` que existía antes (regla "no floats").
 *
 * El flujo offline por lotes (PaymentSyncService) NO usa este validador a
 * propósito: los pagos ya fueron capturados en campo y rechazarlos al
 * sincronizar sería perder rastro de plata cobrada. Lo que el lote no puede
 * aplicar con seguridad (saldo excedido, crédito que cambió o sin cuotas
 * pendientes, llegada tardía) queda retenido en `held_payments` para que un
 * admin decida.
 */
class PaymentPolicyValidator
{
    /**
     * @throws PaymentPolicyException si el pago viola alguna política.
     */
    public function validate(Credit $credit, float $amount, ?CompanyFinancialSettings $settings = null): void
    {
        $settings ??= CompanyFinancialSettings::forCompany($credit->company_id);

        $amountMoney = Money::of($amount);

        // 1. El monto no puede exceder el saldo pendiente.
        $remaining = Money::of($credit->remaining_balance);
        if ($amountMoney->greaterThan($remaining)) {
            throw new PaymentPolicyException(
                'El monto excede el saldo pendiente.',
                ['amount' => ["El monto no puede ser mayor a \${$remaining->value()}."]],
            );
        }

        // 2. Debe existir una cuota pendiente donde aplicar el pago.
        $nextInstallment = $credit->installments()
            ->whereIn('status', ['pending', 'partial_paid', 'overdue'])
            ->orderBy('due_date')
            ->first();

        if (! $nextInstallment) {
            throw new PaymentPolicyException(
                'Este crédito no tiene cuotas pendientes de pago.',
                ['credit_id' => ['No hay cuotas pendientes para aplicar el pago.']],
            );
        }

        /** @var Installment $nextInstallment */

        // 3. Políticas configuradas por la empresa.
        if ($settings->allow_partial_payments && $settings->allow_overpayments) {
            return;
        }

        $installmentDue = Money::of($nextInstallment->remaining_amount);

        // Pagos parciales no permitidos: el monto debe cubrir la cuota completa.
        if (! $settings->allow_partial_payments && $amountMoney->lessThan($installmentDue)) {
            throw new PaymentPolicyException(
                'Esta empresa no permite pagos parciales.',
                ['amount' => ["Debe pagar la cuota completa: \${$installmentDue->value()}."]],
            );
        }

        // Sobrepagos no permitidos: el monto no puede superar el valor de la cuota.
        if (! $settings->allow_overpayments && $amountMoney->greaterThan($installmentDue)) {
            throw new PaymentPolicyException(
                'Esta empresa no permite pagos que excedan el valor de la cuota.',
                ['amount' => ["El monto no puede superar la cuota actual: \${$installmentDue->value()}."]],
            );
        }
    }
}
