<?php

namespace App\Services\Payments;

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

/**
 * Materializa (recalcula y persiste) los totales de pagos y cuotas.
 *
 * Usa Money Value Object con BC Math para precisión financiera exacta.
 */
class PaymentMaterializer
{
    /**
     * Recalcula y materializa, desde pivotes:
     * - installments.amount_paid (+ status + paid_date)
     * - payments.applied_amount
     * - payments.extra_amount
     *
     * Requisitos:
     * - Debe ejecutarse dentro de una transacción
     * - Debe operarse por crédito (aislamiento claro y eficiente)
     */
    public function materializeCredit(Credit $credit): void
    {
        $creditId = $credit->id;
        $companyId = $credit->company_id;

        // 1) Recalcular amount_paid por cuota desde pivotes válidos (payments.voided=0)
        $installments = Installment::query()
            ->where('credit_id', $creditId)
            ->lockForUpdate()
            ->get();

        // Mapa combinado: installment_id => [sum_applied, last_payment_date]
        $installmentStats = DB::table('payment_installment as pi')
            ->join('payments as p', 'p.id', '=', 'pi.payment_id')
            ->where('p.company_id', $companyId)
            ->where('p.credit_id', $creditId)
            ->where('p.voided', 0)
            ->groupBy('pi.installment_id')
            ->select(
                'pi.installment_id',
                DB::raw('COALESCE(SUM(pi.applied_amount), 0) as sum_applied'),
                DB::raw('MAX(p.payment_date) as last_payment_date')
            )
            ->get()
            ->keyBy('installment_id');

        $paidMap = $installmentStats->pluck('sum_applied', 'installment_id')->toArray();
        $lastPaymentDateByInstallment = $installmentStats->pluck('last_payment_date', 'installment_id')->toArray();

        foreach ($installments as $inst) {
            $sumApplied = Money::of($paidMap[$inst->id] ?? 0);
            $totalAmount = Money::of($inst->total_amount);

            // El saldo condonado (forgiven_amount, p. ej. por pronto pago) cuenta
            // como satisfecho igual que el dinero cobrado. Incluirlo evita que una
            // re-materialización borre una condonación previa (la cuota volvería a
            // 'partial_paid' y reabriría el saldo).
            $forgiven = Money::of($inst->forgiven_amount ?? 0);
            $effectivePaid = $sumApplied->add($forgiven);

            // materializar amount_paid (derivado de pivotes + condonación)
            $inst->amount_paid = $effectivePaid->value();

            // status/paid_date derivados
            if ($effectivePaid->isZero() || $effectivePaid->isNegative()) {
                $inst->status = Installment::STATUS_PENDING;
                $inst->paid_date = null;
            } elseif ($effectivePaid->lessThan($totalAmount)) {
                $inst->status = Installment::STATUS_PARTIAL_PAID;
                $inst->paid_date = null;
            } else {
                $inst->status = Installment::STATUS_PAID;

                if (is_null($inst->paid_date)) {
                    $inst->paid_date = $lastPaymentDateByInstallment[$inst->id] ?? null;
                }
            }

            $inst->save();
        }

        // 2) Recalcular applied_amount por payment desde pivotes
        $payments = Payment::query()
            ->where('company_id', $companyId)
            ->where('credit_id', $creditId)
            ->lockForUpdate()
            ->get();

        $pivotSumByPayment = DB::table('payment_installment as pi')
            ->join('payments as p', 'p.id', '=', 'pi.payment_id')
            ->where('p.company_id', $companyId)
            ->where('p.credit_id', $creditId)
            ->where('p.voided', 0)
            ->groupBy('pi.payment_id')
            ->pluck(DB::raw('COALESCE(SUM(pi.applied_amount),0) as sum_applied'), 'pi.payment_id')
            ->toArray();

        foreach ($payments as $payment) {
            if ((bool) $payment->voided) {
                // Para voided: normalizamos aplicados y extra a 0 (evita confusión operativa).
                $payment->applied_amount = 0;
                $payment->extra_amount = 0;
                $payment->save();

                continue;
            }

            $sumApplied = Money::of($pivotSumByPayment[$payment->id] ?? 0);
            $paymentAmount = Money::of($payment->amount);

            $payment->applied_amount = $sumApplied->value();

            if ($paymentAmount->isPositive()) {
                $extra = $paymentAmount->subtract($sumApplied)->max(0);
                $payment->extra_amount = $extra->value();
            } else {
                // pagos no positivos: no deberían aplicar
                $payment->extra_amount = 0;
            }

            $payment->save();
        }
    }
}
