<?php

namespace App\Services\Payments;

use App\Events\PaymentReversed;
use App\Models\Credit;
use App\Models\Income;
use App\Models\Payment;
use App\Models\PaymentAuditLog;
use App\Models\User;
use App\ValueObjects\Money;
use Exception;
use Illuminate\Support\Facades\DB;

/**
 * Servicio para reversar (anular) pagos.
 *
 * Usa Money Value Object con BC Math para precisión financiera exacta.
 */
class PaymentReverser
{
    /**
     * Revierte (anula) un pago.
     */
    public function reverse(Payment $payment, User $user, string $reason): Payment
    {
        if ($payment->voided) {
            throw new Exception('El pago ya está anulado.');
        }

        return DB::transaction(function () use ($payment, $user, $reason) {
            $payment = Payment::where('id', $payment->id)
                ->where('company_id', $payment->company_id)
                ->lockForUpdate()
                ->firstOrFail();

            /** @var Credit $credit */
            $credit = $payment->credit()->lockForUpdate()->firstOrFail();

            $before = $this->snapshotContext($payment, $credit);

            // 0) Capturar applied_amount ANTES de detach+materializar (step 2 lo pone a 0)
            $originalAppliedAmount = Money::of($payment->applied_amount);

            // 1) Revertir efectos financieros
            $this->revertPaymentEffect($payment);

            // 2) Materializar totales reales del pago original (applied_amount → 0)
            $this->materializePaymentTotals($payment);

            // 3) Eliminar ingreso asociado
            $this->deleteIncomeForPayment($payment);

            // 4) Marcar pago como voided
            $payment->update([
                'voided' => true,
                'voided_by_user_id' => $user->id,
                'voided_at' => now(),
                'void_reason' => $reason,
            ]);

            // 4b) Re-materializar cuotas del crédito tras el void (amount_paid → 0)
            app(PaymentMaterializer::class)->materializeCredit($credit);

            // 5) Crear pago compensatorio (reversal) - monto negativo
            $reversalAmount = $originalAppliedAmount->negate();

            $reversal = Payment::create([
                'company_id' => $payment->company_id,
                'credit_id' => $payment->credit_id,
                'amount' => $reversalAmount->abs()->negate()->value(),
                'applied_amount' => 0,
                'extra_amount' => 0,
                'payment_date' => now()->toDateString(),
                'payment_method' => 'reversal',
                'registered_by_user_id' => $user->id,
                'original_payment_id' => $payment->id,
            ]);

            // 6) El reversal NO lleva pivotes. Su monto es negativo y NO aplica a
            //    cuotas; clonar aquí los pivotes positivos (sobre un pago voided=0)
            //    hacía que materializeCredit y PaymentDistributor los sumaran como
            //    dinero aplicado → doble conteo del saldo en cada materialización
            //    posterior. La trazabilidad se mantiene vía original_payment_id y
            //    el log de auditoría (snapshots before/after).

            // 7) Materializar reversal desde BD (sin pivotes → applied/extra = 0)
            $this->materializePaymentTotals($reversal);

            // 7b) Verificar invariantes del crédito (defensa en profundidad):
            //     atrapa cualquier regresión que vuelva a dejar pivotes vivos en un
            //     pago de reversión y abortaría la transacción antes de corromper.
            app(PaymentInvariantGuard::class)->assertCreditIntegrity($credit);

            // 8) Disparar evento para actualizar estado del crédito
            $credit->refresh();
            PaymentReversed::dispatch($payment, $reversal, $credit);

            // 9) Auditoría compensatoria
            $after = [
                'original_payment' => $this->snapshotPayment($payment->fresh()),
                'reversal_payment' => $this->snapshotPayment($reversal),
                'credit' => $this->snapshotCredit($credit),
                'installments' => $this->snapshotInstallments($credit),
            ];

            $this->logAuditEvent(
                action: 'reversed',
                payment: $payment,
                credit: $credit,
                input: ['reason' => $reason],
                before: $before,
                after: $after,
                reason: $reason,
                userId: $user->id,
            );

            return $reversal;
        });
    }

    /*==============================================================
     | MÉTODOS AUXILIARES
     ===============================================================*/
    protected function revertPaymentEffect(Payment $payment): void
    {
        $payment->installments()->detach();
    }

    protected function materializePaymentTotals(Payment $payment): void
    {
        $appliedSum = DB::table('payment_installment as pi')
            ->join('payments as p', 'p.id', '=', 'pi.payment_id')
            ->where('pi.payment_id', $payment->id)
            ->where('p.voided', 0)
            ->sum('pi.applied_amount');

        $applied = Money::of($appliedSum ?? 0);
        $paymentAmount = Money::of($payment->amount);
        $extra = $paymentAmount->subtract($applied)->max(0);

        $payment->applied_amount = $applied->value();
        $payment->extra_amount = $extra->value();
        $payment->save();
    }

    protected function deleteIncomeForPayment(Payment $payment): void
    {
        Income::where('payment_id', $payment->id)
            ->where('company_id', $payment->company_id)
            ->delete();
    }

    protected function snapshotContext(Payment $payment, Credit $credit): array
    {
        return [
            'payment' => $payment->toArray(),
            'credit' => $credit->toArray(),
            'installments' => $credit->installments()->get()->toArray(),
        ];
    }

    protected function snapshotPayment(Payment $payment): array
    {
        return $payment->toArray();
    }

    protected function snapshotCredit(Credit $credit): array
    {
        return $credit->toArray();
    }

    protected function snapshotInstallments(Credit $credit): array
    {
        return $credit->installments->toArray();
    }

    protected function logAuditEvent(
        string $action,
        Payment $payment,
        Credit $credit,
        ?array $input,
        ?array $before,
        ?array $after,
        ?string $reason,
        int $userId
    ): void {
        PaymentAuditLog::create([
            'payment_id' => $payment->id,
            'credit_id' => $credit->id,
            'installment_id' => null,
            'company_id' => $payment->company_id,
            'user_id' => $userId,
            'action' => $action,
            'amount' => $payment->amount,
            'previous_data' => $before,
            'new_data' => $after,
        ]);
    }
}
