<?php

namespace App\Services\Payments;

use App\Events\PaymentRegistered;
use App\Models\Credit;
use App\Models\Income;
use App\Models\Installment;
use App\Models\Payment;
use App\Models\PaymentAuditLog;
use App\Services\FinanceAutoLogger;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

class PaymentRegistrar
{
    public function __construct(
        protected PaymentDistributor $distributor,
        protected PaymentMaterializer $materializer,
        protected PaymentInvariantGuard $guard
    ) {}

    /**
     * Registrar pago (dispatcher principal).
     */
    public function register(
        float $amount,
        Carbon $paymentDate,
        int $registeredByUserId,
        ?Installment $installment = null,
        ?Credit $credit = null,
        ?string $paymentMethod = null,
    ): Payment {
        if ($installment) {
            return $this->forInstallment(
                $installment,
                $amount,
                $paymentDate,
                $registeredByUserId,
                $paymentMethod,
            );
        }

        if (! $credit) {
            throw new InvalidArgumentException('Debe proporcionar un crédito si no se proporciona una cuota.');
        }

        return $this->forCredit(
            $credit,
            $amount,
            $paymentDate,
            $registeredByUserId,
            $paymentMethod,
        );
    }

    /**
     * Registrar pago a nivel de crédito completo.
     */
    public function forCredit(
        Credit $credit,
        float $amount,
        Carbon $paymentDate,
        int $registeredByUserId,
        ?string $paymentMethod = null,
    ): Payment {
        return DB::transaction(function () use ($credit, $amount, $paymentDate, $registeredByUserId, $paymentMethod) {
            $credit = Credit::where('id', $credit->id)
                ->where('company_id', $credit->company_id)
                ->lockForUpdate()
                ->firstOrFail();

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

            $payment = Payment::create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'installment_id' => null,
                'amount' => Money::of($amount)->value(),
                'applied_amount' => 0,
                'extra_amount' => 0,
                'payment_date' => $paymentDate->toDateString(),
                'payment_method' => $paymentMethod,
                'registered_by_user_id' => $registeredByUserId,
            ]);

            $this->distributor->distribute($payment, $credit, $payment->amount, $paymentDate);
            $this->materializer->materializeCredit($credit->fresh());
            $payment->refresh();

            $this->syncIncomesForPayment($payment);
            $this->guard->assertCreditIntegrity($credit->fresh());

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

            $this->logAuditEvent(
                'created',
                $payment,
                $credit,
                null,
                ['mode' => 'credit_cascade'],
                $before,
                $after,
                $registeredByUserId
            );

            // Disparar evento para actualizar estado del crédito
            PaymentRegistered::dispatch($payment, $credit->fresh());

            return $payment;
        });
    }

    /**
     * Registrar pago priorizando una cuota específica.
     */
    public function forInstallment(
        Installment $installment,
        float $amount,
        Carbon $paymentDate,
        int $registeredByUserId,
        ?string $paymentMethod = null,
    ): Payment {
        $credit = $installment->credit;

        return DB::transaction(function () use ($credit, $installment, $amount, $paymentDate, $registeredByUserId, $paymentMethod) {
            $installment = Installment::where('id', $installment->id)->lockForUpdate()->firstOrFail();
            $credit = Credit::where('id', $credit->id)
                ->where('company_id', $credit->company_id)
                ->lockForUpdate()
                ->firstOrFail();

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

            $payment = Payment::create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'installment_id' => $installment->id,
                'amount' => Money::of($amount)->value(),
                'applied_amount' => 0,
                'extra_amount' => 0,
                'payment_date' => $paymentDate->toDateString(),
                'payment_method' => $paymentMethod,
                'registered_by_user_id' => $registeredByUserId,
            ]);

            $this->distributor->distribute(
                $payment,
                $credit,
                $payment->amount,
                $paymentDate,
                $installment
            );

            $this->materializer->materializeCredit($credit->fresh());
            $payment->refresh();

            $this->syncIncomesForPayment($payment);
            $this->guard->assertCreditIntegrity($credit->fresh());

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

            $this->logAuditEvent(
                'created',
                $payment,
                $credit,
                $installment,
                ['mode' => 'installment_first'],
                $before,
                $after,
                $registeredByUserId
            );

            // Disparar evento para actualizar estado del crédito
            PaymentRegistered::dispatch($payment, $credit->fresh());

            return $payment;
        });
    }

    /*==============================================================
     | MÉTODOS AUXILIARES
     ===============================================================*/
    protected function syncIncomesForPayment(Payment $payment): void
    {
        Income::where('payment_id', $payment->id)
            ->where('company_id', $payment->company_id)
            ->delete();

        if (! $payment->voided && $payment->applied_amount > 0) {
            app(FinanceAutoLogger::class)->logIncome([
                'company_id' => $payment->company_id,
                'user_id' => $payment->registered_by_user_id,
                'amount' => $payment->applied_amount,
                'category' => 'payment',
                'credit_id' => $payment->credit_id,
                'payment_id' => $payment->id,
                'operation_date' => $payment->payment_date,
            ]);
        }

        // Overpayment: excedente de pago que genera saldo a favor
        // DEBE tener credit_id para trazabilidad completa
        if (! $payment->voided && $payment->extra_amount > 0) {
            app(FinanceAutoLogger::class)->logIncome([
                'company_id' => $payment->company_id,
                'user_id' => $payment->registered_by_user_id,
                'amount' => $payment->extra_amount,
                'category' => 'overpayment',
                'credit_id' => $payment->credit_id, // Trazabilidad: crédito origen del excedente
                'payment_id' => $payment->id,
                'operation_date' => $payment->payment_date,
                'metadata' => [
                    'type' => 'overpayment',
                    'source' => 'payment_excess',
                    'original_payment_amount' => $payment->amount,
                    'applied_amount' => $payment->applied_amount,
                ],
            ]);
        }
    }

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

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