<?php

namespace App\Services\Payments;

use App\Models\CompanyFinancialSettings;
use App\Models\Credit;
use App\Models\Installment;
use App\Models\Payment;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

/**
 * Distribuye pagos entre las cuotas de un crédito.
 *
 * Usa Money Value Object con BC Math para precisión financiera exacta.
 */
class PaymentDistributor
{
    /**
     * Distribuye el monto del pago entre las cuotas del crédito.
     * Prioriza la cuota preferida si se proporciona.
     */
    public function distribute(
        Payment $payment,
        Credit $credit,
        float $amount,
        Carbon $paymentDate,
        ?Installment $preferredInstallment = null
    ): void {
        $payment->installments()->detach();

        if ($amount <= 0) {
            return;
        }

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

        // Respetar la política de orden configurada por la empresa.
        // Por defecto (oldest_first = true) aplica a las cuotas más antiguas primero.
        $settings = CompanyFinancialSettings::forCompany($credit->company_id);
        $direction = ($settings->apply_payment_to_oldest_first ?? true) ? 'asc' : 'desc';

        $installments = Installment::where('credit_id', $credit->id)
            ->when($preferredInstallment, fn ($q) => $q->orderByRaw('id = ? DESC', [$preferredInstallment->id]))
            ->orderBy('due_date', $direction)
            ->lockForUpdate()
            ->get();

        // Pre-cargar todos los saldos aplicados para evitar N+1
        $appliedByInstallment = DB::table('payment_installment as pi')
            ->join('payments as p', 'p.id', '=', 'pi.payment_id')
            ->where('p.voided', 0)
            ->whereIn('pi.installment_id', $installments->pluck('id'))
            ->groupBy('pi.installment_id')
            ->selectRaw('pi.installment_id, SUM(pi.applied_amount) as sum_applied')
            ->pluck('sum_applied', 'installment_id')
            ->toArray();

        foreach ($installments as $inst) {
            if ($remaining->isZero() || $remaining->isNegative()) {
                break;
            }

            $appliedSoFar = Money::of($appliedByInstallment[$inst->id] ?? 0);
            $totalAmount = Money::of($inst->total_amount);
            $due = $totalAmount->subtract($appliedSoFar);

            if ($due->isZero() || $due->isNegative()) {
                continue;
            }

            $toApply = $remaining->min($due);
            $payment->installments()->attach($inst->id, ['applied_amount' => $toApply->value()]);
            $remaining = $remaining->subtract($toApply);
        }
    }
}
