<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\CompanyFinancialSettings;
use App\Models\Credit;
use App\Models\Installment;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Illuminate\Support\Collection;

class SmartCreditStateResolver
{
    /**
     * Caché de settings por company_id para evitar N+1 en resolveMany().
     *
     * @var array<int, CompanyFinancialSettings>
     */
    private array $settingsCache = [];

    /**
     * Resuelve estados para una colección de créditos en modo batch (N+1 free).
     *
     * Pre-requisito de eager loading (OBLIGATORIO para evitar N+1):
     *   Credit::with(['installments', 'children'])->...->get()
     *
     * Con las relaciones pre-cargadas:
     * - isParent()          → usa $credit->children (relation-aware), sin query
     * - loadMissing('installments') → no-op (ya cargadas)
     * - isCreditOverdue()   → itera colección en memoria, sin query
     * - hasDelayedBehavior()→ itera colección en memoria, sin query
     *
     * Complejidad: O(C × I) en memoria, O(2) queries SQL por chunk (vs O(N) sin batch).
     *
     * @param  Collection<int, Credit>  $credits  Créditos con 'installments' y 'children' cargados
     * @return array<int, string> Mapa [credit_id => expected_status]
     */
    public function resolveMany(Collection $credits): array
    {
        $result = [];

        foreach ($credits as $credit) {
            $result[$credit->id] = $this->resolve($credit);
        }

        return $result;
    }

    /**
     * Determina el estado exacto del crédito usando reglas de negocio.
     *
     * IMPORTANTE: Los créditos padre (con hijos derivados) NO deben
     * cambiar de estado automáticamente. Solo los créditos hoja mutan.
     *
     * Para procesamiento masivo, usar resolveMany() con eager loading de
     * 'installments' y 'children' para evitar N+1 queries.
     */
    public function resolve(Credit $credit): string
    {
        /*=========================================================
        | GUARD: CRÉDITOS PADRE - NO MUTAN ESTADO
        | Un crédito con hijos derivados debe mantener su estado
        | original (EXTENDED, REFINANCED, RESTRUCTURED, etc.)
        =========================================================*/
        if ($credit->isParent()) {
            return $credit->status;
        }

        /*=========================================================
        | GUARD: ESTADOS INMUTABLES (LEGACY/CERRADOS)
        | Estados de operaciones de crédito que no deben cambiar
        =========================================================*/
        $immutableStates = [
            Credit::STATUS_CANCELED,
            Credit::STATUS_EXTENDED,
            Credit::STATUS_REFINANCED,
            Credit::STATUS_RESTRUCTURED,
            Credit::STATUS_RENEWED,
            Credit::STATUS_INTEREST_CAPITALIZED,
            Credit::STATUS_DEFAULTED,
            Credit::STATUS_ARCHIVED,
        ];

        if (in_array($credit->status, $immutableStates, true)) {
            return $credit->status;
        }

        $credit->loadMissing('installments');

        $today = Carbon::today();

        if ($this->isFullyPaid($credit)) {
            return Credit::STATUS_PAID;
        }

        /*=========================================================
        | 1. OVERDUE  → el crédito completo está vencido
        |    credit.due_date < today  AND remaining balance > 0
        =========================================================*/
        if ($this->isCreditOverdue($credit, $today)) {
            return Credit::STATUS_OVERDUE;
        }

        /*=========================================================
        | 2. DEFAULTED → cuota vencida más allá de days_until_default
        |    Usa el umbral configurado en CompanyFinancialSettings.
        =========================================================*/
        if ($this->isDefaulted($credit, $today)) {
            return Credit::STATUS_DEFAULTED;
        }

        /*=========================================================
        | 3. DELAYED → cuotas vencidas más allá de days_until_delayed
        |    Usa el umbral configurado en CompanyFinancialSettings.
        =========================================================*/
        if ($this->hasDelayedBehavior($credit, $today)) {
            return Credit::STATUS_DELAYED;
        }

        /*=========================================================
        | 3. ACTIVE → todo al día
        =========================================================*/
        return Credit::STATUS_ACTIVE;
    }

    /*=========================================================
    | REGLA 0 — FULLY PAID
    =========================================================*/
    private function isFullyPaid(Credit $credit): bool
    {
        /** @var Installment $i */
        foreach ($credit->installments as $i) {
            if (Money::of($i->amount_paid)->lessThan($i->total_amount)) {
                return false;
            }
        }

        return true;
    }

    /*=========================================================
    | REGLA 1 — CRÉDITO VENCIDO (OVERDUE)
    | credit.due_date < hoy  AND saldo pendiente > 0
    =========================================================*/
    private function isCreditOverdue(Credit $credit, Carbon $today): bool
    {
        if (! $credit->due_date) {
            return false;
        }

        $dueDate = Carbon::parse($credit->due_date)->startOfDay();

        if ($dueDate->gte($today)) {
            return false;
        }

        // saldo pendiente basado en cuotas
        /** @var Collection<int, Installment> $installments */
        $installments = $credit->installments;
        $remaining = $installments->reduce(
            fn (Money $carry, Installment $i) => $carry->add(
                Money::of($i->total_amount)->subtract($i->amount_paid)->max(0)
            ),
            Money::zero()
        );

        return $remaining->isPositive();
    }

    /*=========================================================
    | REGLA 2 — DEFAULTED
    | Una cuota lleva más de days_until_default días sin pagarse.
    | Umbral configurado en CompanyFinancialSettings.
    =========================================================*/
    private function isDefaulted(Credit $credit, Carbon $today): bool
    {
        $daysUntilDefault = (int) ($this->getSettingsFor($credit)->days_until_default ?? 90);

        /** @var Installment $i */
        foreach ($credit->installments as $i) {
            if (
                $i->due_date &&
                Carbon::parse($i->due_date)->addDays($daysUntilDefault)->lte($today) &&
                Money::of($i->amount_paid)->lessThan($i->total_amount)
            ) {
                return true;
            }
        }

        return false;
    }

    /*=========================================================
    | REGLA 3 — DELAYED
    | Una cuota lleva más de days_until_delayed días sin pagarse.
    | Umbral configurado en CompanyFinancialSettings (default: 1 día).
    =========================================================*/
    private function hasDelayedBehavior(Credit $credit, Carbon $today): bool
    {
        $daysUntilDelayed = (int) ($this->getSettingsFor($credit)->days_until_delayed ?? 1);

        /** @var Installment $i */
        foreach ($credit->installments as $i) {
            if (
                $i->due_date &&
                Carbon::parse($i->due_date)->addDays($daysUntilDelayed)->lte($today) &&
                Money::of($i->amount_paid)->lessThan($i->total_amount)
            ) {
                return true;
            }
        }

        return false;
    }

    /*=========================================================
    | HELPER — Carga settings con caché por company_id
    =========================================================*/
    private function getSettingsFor(Credit $credit): CompanyFinancialSettings
    {
        $companyId = $credit->company_id;

        if (! isset($this->settingsCache[$companyId])) {
            $this->settingsCache[$companyId] = CompanyFinancialSettings::forCompany($companyId);
        }

        return $this->settingsCache[$companyId];
    }
}
