<?php

declare(strict_types=1);

namespace App\Services;

use App\Enums\ExpenseCategory;
use App\Models\Credit;
use App\Models\CreditRestructureLog;
use App\Models\Expense;
use App\Models\Installment;
use App\Services\Dashboard\DashboardCacheService;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Servicio de Cierre Anticipado con Condonación de Saldo.
 *
 * CASO DE USO:
 *   Un crédito con saldo pendiente se cierra por decisión comercial: se "perdona"
 *   (condona) el saldo remanente sin que salga dinero de caja.
 *   Ejemplo: Se prestó $1.000.000 para cobrar $1.200.000. La clienta pagó $1.100.000.
 *   Se condona los $100.000 restantes y el crédito queda como PAGADO.
 *
 * IMPACTO CONTABLE:
 *   ┌──────────────────────────────────┬────────┬──────────┬────────────────┐
 *   │ Operación                        │ Caja   │ Utilidad │ Capital        │
 *   ├──────────────────────────────────┼────────┼──────────┼────────────────┤
 *   │ Condonar $100.000 de intereses   │   —    │   -      │   —            │
 *   └──────────────────────────────────┴────────┴──────────┴────────────────┘
 *   • Caja NO se toca (affects_cash = false) — no sale dinero.
 *   • Utilidad SE REDUCE (affects_profit = true) — el negocio reconoce que
 *     perdonó $100.000 de ganancia esperada (nunca realizada).
 *   • Capital de trabajo NO cambia (reduces_capital = false) — el capital
 *     prestado ya regresó vía los $1.100.000 cobrados.
 *
 * COMPATIBILIDAD CON SmartCreditStateResolver:
 *   isFullyPaid() → recorre installments y verifica amount_paid >= total_amount.
 *   La condonación eleva amount_paid = total_amount en cada cuota pendiente,
 *   garantizando que el resolver retorne STATUS_PAID de forma permanente y
 *   que el CRON de reconciliación no revierta el cierre.
 *
 * REGLAS DE NEGOCIO:
 *   - Solo créditos activos (active / delayed / overdue) pueden condonarse.
 *   - Los créditos padre (con hijos derivados) NO pueden condonarse directamente.
 *   - El monto a condonar debe ser positivo (debe existir saldo pendiente).
 *   - Se requiere una razón / motivo de negocio.
 */
class CreditSettlementService
{
    public function __construct(
        private DashboardCacheService $dashboardCache
    ) {}

    /**
     * Cierra el crédito condonando el saldo pendiente.
     *
     * @param  Credit  $credit  Crédito a cerrar (debe estar en ACTIVE_STATUSES)
     * @param  string  $reason  Motivo de la condonación (requerido para auditoría)
     * @return Money Monto total condonado
     *
     * @throws RuntimeException Si el crédito no es elegible o no hay saldo que condonar
     */
    public function settle(Credit $credit, string $reason): Money
    {
        // ── Validaciones pre-transacción ──────────────────────────────────────
        if ($credit->isParent()) {
            throw new RuntimeException(
                'No se puede condonar un crédito padre (con créditos hijos derivados). '
                .'Los créditos padre no son operacionales.'
            );
        }

        if ($credit->isClosed()) {
            throw new RuntimeException(
                "El crédito #{$credit->id} ya está cerrado (estado: {$credit->status}). "
                .'Solo se pueden condonar créditos activos.'
            );
        }

        if (! in_array($credit->status, Credit::ACTIVE_STATUSES, true)) {
            throw new RuntimeException(
                "El crédito #{$credit->id} tiene un estado no elegible para condonación: {$credit->status}."
            );
        }

        $totalForgiven = DB::transaction(function () use ($credit, $reason) {

            // Lock del crédito para prevenir race conditions
            $credit = Credit::where('id', $credit->id)->lockForUpdate()->firstOrFail();

            // ── 1. Calcular y condonar cuotas pendientes ──────────────────────
            $pendingInstallments = Installment::where('credit_id', $credit->id)
                ->whereIn('status', [
                    Installment::STATUS_PENDING,
                    Installment::STATUS_PARTIAL_PAID,
                    Installment::STATUS_OVERDUE,
                ])
                ->lockForUpdate()
                ->get();

            $totalForgiven = Money::zero();

            foreach ($pendingInstallments as $installment) {
                $remaining = Money::of($installment->total_amount)
                    ->subtract($installment->amount_paid)
                    ->max(0);

                if ($remaining->isZero()) {
                    continue;
                }

                $totalForgiven = $totalForgiven->add($remaining);

                // Elevar amount_paid al total: el resolver verá la cuota como pagada.
                // forgiven_amount registra el monto real perdonado para auditoría.
                $installment->update([
                    'amount_paid' => $installment->total_amount,
                    'forgiven_amount' => $remaining->toString(),
                    'status' => Installment::STATUS_PAID,
                    'paid_date' => Carbon::today(),
                ]);
            }

            if ($totalForgiven->isZero()) {
                throw new RuntimeException(
                    "El crédito #{$credit->id} no tiene saldo pendiente que condonar."
                );
            }

            // ── 2. Registro contable no-caja ──────────────────────────────────
            // Crea un Expense de tipo INTEREST_WAIVER:
            //   affects_cash   = false → la caja no cambia
            //   affects_profit = true  → reduce la utilidad reportada del período
            //   reduces_capital= false → el capital no se afecta
            Expense::query()->create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'amount' => $totalForgiven->toString(),
                'category' => ExpenseCategory::INTEREST_WAIVER->value,
                'expense_type' => 'interest_waiver',
                'operation_date' => Carbon::today(),
                'notes' => "Condonación — Crédito #{$credit->id} ({$credit->client?->name}): {$reason}",
                'metadata' => [
                    'credit_id' => $credit->id,
                    'client_id' => $credit->client_id,
                    'client_name' => $credit->client?->name,
                    'forgiven_amount' => $totalForgiven->toString(),
                    'reason' => $reason,
                    'settled_by' => auth()->id(),
                ],
                'user_id' => auth()->id(),
                'affects_cash' => false,
                'affects_profit' => true,
                'reduces_capital' => false,
                'requires_approval' => false,
                'approval_status' => 'approved',
            ]);

            // ── 3. Registro de auditoría en restructure log ───────────────────
            CreditRestructureLog::create([
                'original_credit_id' => $credit->id,
                'new_credit_id' => null,   // no se crea crédito hijo
                'company_id' => $credit->company_id,
                'performed_by_user_id' => auth()->id(),
                'type' => CreditRestructureLog::TYPE_CONDONATION,
                'previous_balance' => $totalForgiven->toString(),
                'new_principal' => '0.00', // sin nuevo capital
                'new_interest_rate' => null,
                'new_installment_count' => null,
                'new_periodicity' => null,
                'additional_capital' => '0.00',
                'capitalized_interest' => '0.00',
                'reason' => $reason,
                'metadata' => [
                    'forgiven_amount' => $totalForgiven->toString(),
                    'installments_condoned' => $pendingInstallments->count(),
                    'settled_at' => now()->toIso8601String(),
                ],
            ]);

            // ── 4. Forzar estado PAID en el crédito ───────────────────────────
            // Usamos update() directo para evitar que el Observer dispare
            // SyncCreditStatusJob (el estado ya está definido, no hay ambigüedad).
            $credit->update(['status' => Credit::STATUS_PAID]);

            return $totalForgiven;
        });

        // Invalidar cachés del dashboard (Caja Actual, Saldo por Cobrar, etc.)
        $this->dashboardCache->invalidateFinancialRelated($credit->company_id);

        return $totalForgiven;
    }
}
