<?php

namespace App\Models;

use App\Services\CreditStatusSyncService;
use App\Support\CreditRules;
use App\Traits\MultiTenantScope;
use App\ValueObjects\Money;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

/**
 * @property-read string $code Código visible del crédito (company_credit_number con padding, o el id como fallback).
 *
 * @method static \Illuminate\Database\Eloquent\Builder<static> leafCredits()
 */
class Credit extends Model
{
    use HasFactory, MultiTenantScope;

    /*=========================================================
    | ESTADOS
    =========================================================*/
    public const STATUS_ACTIVE = 'active';

    public const STATUS_DELAYED = 'delayed';

    public const STATUS_OVERDUE = 'overdue';

    public const STATUS_PAID = 'paid';

    public const STATUS_CANCELED = 'canceled';

    // Legacy
    public const STATUS_RESTRUCTURED = 'restructured';

    public const STATUS_REFINANCED = 'refinanced';

    public const STATUS_EXTENDED = 'extended';

    public const STATUS_RENEWED = 'renewed';

    public const STATUS_INTEREST_CAPITALIZED = 'interest_capitalized';

    public const STATUS_DEFAULTED = 'defaulted';

    public const STATUS_ARCHIVED = 'archived';

    public const CLOSED_STATES = [
        self::STATUS_PAID,
        self::STATUS_CANCELED,
        self::STATUS_RESTRUCTURED,
        self::STATUS_REFINANCED,
        self::STATUS_EXTENDED,
        self::STATUS_RENEWED,
        self::STATUS_INTEREST_CAPITALIZED,
        self::STATUS_DEFAULTED,
        self::STATUS_ARCHIVED,
    ];

    /** Estados activos donde se pueden registrar pagos */
    public const ACTIVE_STATUSES = [
        self::STATUS_ACTIVE,
        self::STATUS_DELAYED,
        self::STATUS_OVERDUE,
    ];

    protected $fillable = [
        'company_id',
        'company_credit_number',
        'client_id',
        'collector_user_id',
        'created_by_user_id',
        'parent_credit_id',
        'status',
        'amount',
        'interest_rate',
        'installments_count',
        'periodicity',
        'start_date',
        'first_due_date_override',
        'due_date',
        'due_day_1',
        'due_day_2',
        'idempotency_key',
    ];

    protected $casts = [
        'start_date' => 'date',
        'first_due_date_override' => 'date',
        'due_date' => 'date',
        'amount' => 'decimal:2',
        'interest_rate' => 'decimal:2',
    ];

    /**
     * Código legible del crédito: company_credit_number con padding de 3
     * dígitos (001). Fallback al id si aún no fue numerado.
     */
    public function getCodeAttribute(): string
    {
        return $this->company_credit_number !== null
            ? str_pad((string) $this->company_credit_number, 3, '0', STR_PAD_LEFT)
            : (string) $this->id;
    }

    protected static function booted(): void
    {
        // Asigna el siguiente número secuencial por empresa (1, 2, 3…) en
        // creación. Cubre TODOS los paths de Credit::create (nuevo, refinancia,
        // renovación, reestructuración, clonado) porque es un hook de modelo,
        // no lógica en un controller/servicio puntual.
        //
        // lockForUpdate() serializa la lectura del MAX() entre transacciones
        // concurrentes para la misma empresa; el índice único
        // `credits_company_number_unique` es la red de seguridad final si,
        // por lo que sea, el hook corriera fuera de una transacción.
        static::creating(function (Credit $credit): void {
            if (! empty($credit->company_id) && empty($credit->company_credit_number)) {
                $max = static::withoutGlobalScopes()
                    ->where('company_id', $credit->company_id)
                    ->lockForUpdate()
                    ->max('company_credit_number');
                $credit->company_credit_number = ((int) $max) + 1;
            }
        });
    }

    /*=========================================================
    | RELACIONES
    =========================================================*/
    /** @return BelongsTo<Company, $this> */
    public function company(): BelongsTo
    {
        return $this->belongsTo(Company::class);
    }

    /**
     * El genérico es necesario: sin él PHPStan ve `BelongsTo<Model>` y no
     * reconoce los accessors de Client (p. ej. `formatted_address`).
     *
     * @return BelongsTo<Client, $this>
     */
    public function client(): BelongsTo
    {
        return $this->belongsTo(Client::class);
    }

    /** @return BelongsTo<User, $this> */
    public function collector(): BelongsTo
    {
        return $this->belongsTo(User::class, 'collector_user_id');
    }

    public function creator(): BelongsTo
    {
        return $this->belongsTo(User::class, 'created_by_user_id');
    }

    public function parent(): BelongsTo
    {
        return $this->belongsTo(Credit::class, 'parent_credit_id');
    }

    public function children(): HasMany
    {
        return $this->hasMany(Credit::class, 'parent_credit_id');
    }

    public function installments(): HasMany
    {
        return $this->hasMany(Installment::class)->orderBy('installment_number');
    }

    /**
     * El generico es necesario: sin el, PHPStan ve `HasMany<Model>` y no resuelve
     * los scopes de Payment (p. ej. `collections()`) ni sus propiedades.
     *
     * @return HasMany<Payment, $this>
     */
    public function payments(): HasMany
    {
        return $this->hasMany(Payment::class);
    }

    /**
     * Último pago válido (no anulado) del crédito.
     *
     * Usa ofMany con subconsulta → una sola query extra en batch (sin N+1).
     * Criterio: MAX(payment_date), MAX(id) como desempate.
     */
    public function latestPayment(): HasOne
    {
        return $this->hasOne(Payment::class)->ofMany(
            ['payment_date' => 'max', 'id' => 'max'],
            // `collections()` y no solo `voided`: si no, al anular el ultimo pago
            // el "ultimo pago del cliente" pasaba a ser el asiento de reversion,
            // es decir un monto negativo.
            fn ($q) => $q->collections()
        );
    }

    public function auditLogs(): HasMany
    {
        return $this->hasMany(CreditAuditLog::class);
    }

    public function paymentAuditLogs(): HasMany
    {
        return $this->hasMany(PaymentAuditLog::class, 'credit_id');
    }

    public function incomes(): HasMany
    {
        return $this->hasMany(Income::class);
    }

    public function collectorOrder(): HasOne
    {
        return $this->hasOne(CollectorCreditOrder::class);
    }

    /*=========================================================
    | HELPERS
    =========================================================*/
    public function isClosed(): bool
    {
        return in_array($this->status, self::CLOSED_STATES, true);
    }

    /**
     * Verifica si este crédito es padre (tiene créditos hijos derivados).
     *
     * Relation-aware: usa la colección ya cargada para evitar una query adicional
     * cuando 'children' está en el eager load (e.g., reconciliación masiva, Jobs).
     * Si la relación no está cargada, ejecuta exists() normalmente.
     */
    public function isParent(): bool
    {
        if ($this->relationLoaded('children')) {
            return $this->children->isNotEmpty();
        }

        return $this->children()->exists();
    }

    /**
     * Verifica si este crédito puede mutar estado automáticamente.
     *
     * NO puede mutar si:
     * 1. Es padre (tiene hijos derivados)
     * 2. Está en estado cerrado legacy (EXTENDED, REFINANCED, etc.)
     */
    public function canAutoUpdateStatus(): bool
    {
        if ($this->isParent()) {
            return false;
        }

        $immutableStates = [
            self::STATUS_EXTENDED,
            self::STATUS_REFINANCED,
            self::STATUS_RESTRUCTURED,
            self::STATUS_RENEWED,
            self::STATUS_INTEREST_CAPITALIZED,
            self::STATUS_DEFAULTED,
            self::STATUS_ARCHIVED,
        ];

        return ! in_array($this->status, $immutableStates, true);
    }

    public function hasPayments(): bool
    {
        return CreditRules::hasPayments($this);
    }

    public function isReadOnly(): bool
    {
        return $this->hasPayments();
    }

    /**
     * Saldo pendiente del crédito = Σ(total_amount) − Σ(amount_paid) sobre cuotas.
     *
     * Fuente de verdad unificada: misma fórmula que CreditManager::getRemainingBalance().
     *
     * Relation-aware: si 'installments' ya está cargado (eager load), opera
     * sobre la colección en memoria sin ejecutar queries adicionales (evita N+1).
     * Si no está cargado, ejecuta dos queries como fallback.
     *
     * Mínimo: 0 (no puede ser negativo, consistente con CreditManager::max(0)).
     */
    public function getRemainingBalanceAttribute(): float
    {
        if ($this->relationLoaded('installments')) {
            $total = $this->installments->sum('total_amount');
            $paid = $this->installments->sum('amount_paid');

            return Money::of($total)->subtract($paid)->max(0)->value();
        }

        // Fallback: 2 queries. Ocurre solo cuando installments no están pre-cargados.
        $total = $this->installments()->sum('total_amount');
        $paid = $this->installments()->sum('amount_paid');

        return Money::of($total)->subtract($paid)->max(0)->value();
    }

    public function hasCapitalPayments(): bool
    {
        return CreditRules::hasCapitalPayments($this);
    }

    public function hasInterestOnlyPayments(): bool
    {
        return $this->hasPayments() && ! $this->hasCapitalPayments();
    }

    /*=========================================================
    | BALANCES DERIVADOS – SUMAS Y CONTADORES
    =========================================================*/

    /**
     * Suma de todas las cuotas (principal + interés).
     *
     * Relation-aware: si 'installments' ya está cargado (eager load), opera sobre
     * la colección en memoria sin queries adicionales (evita N+1 en SyncController,
     * que serializa este accessor por cada crédito). Fallback: 1 query.
     */
    public function getTotalReceivableAttribute(): float
    {
        if ($this->relationLoaded('installments')) {
            return (float) $this->installments->sum('total_amount');
        }

        return (float) $this->installments()->sum('total_amount');
    }

    /**
     * Suma de todo lo pagado sobre las cuotas.
     *
     * Relation-aware: ver getTotalReceivableAttribute(). Evita N+1 en sync.
     */
    public function getTotalPaidAttribute(): float
    {
        if ($this->relationLoaded('installments')) {
            return (float) $this->installments->sum('amount_paid');
        }

        return (float) $this->installments()->sum('amount_paid');
    }

    public function getInstallmentsPaidCountAttribute(): int
    {
        return $this->installments()
            ->where('status', Installment::STATUS_PAID)
            ->count();
    }

    public function getInstallmentsPendingCountAttribute(): int
    {
        // Consideramos pendiente todo lo que no esté totalmente pagado
        return $this->installments()
            ->where('status', '!=', Installment::STATUS_PAID)
            ->count();
    }

    /**
     * Recalcula y actualiza el estado del crédito.
     *
     * Delega a CreditStatusSyncService para evitar doble sync via observer.
     */
    public function refreshStatus(): void
    {
        app(CreditStatusSyncService::class)->sync($this);
    }

    /*=========================================================
    | SCOPES
    =========================================================*/

    /**
     * Scope para excluir créditos padre (solo créditos hoja).
     * Usar en queries de dashboard, cartera, reportes.
     */
    public function scopeLeafCredits($query)
    {
        return $query->whereDoesntHave('children');
    }

    /**
     * Scope para créditos activos excluyendo padres.
     * Combina filtro de estados activos + exclusión de padres.
     */
    public function scopeActiveLeafCredits($query)
    {
        return $query->leafCredits()
            ->whereIn('status', self::ACTIVE_STATUSES);
    }

    /**
     * Créditos reemplazados: existe un hijo que los sustituye.
     *
     * Son historia, no cartera. Un crédito extendido/refinanciado/renovado/
     * reestructurado sigue existiendo como registro, pero el que manda es su
     * hijo. Mezclarlos con los vigentes hace que el mismo préstamo aparezca
     * dos veces — el motivo de que este scope exista.
     */
    public function scopeReplacedCredits($query)
    {
        return $query->whereHas('children');
    }

    /**
     * Créditos terminados de verdad: estado terminal y sin hijo que los sustituya.
     */
    public function scopeClosedLeafCredits($query)
    {
        return $query->leafCredits()
            ->whereIn('status', self::CLOSED_STATES);
    }

    /**
     * Código del crédito hijo que sustituye a este, si existe.
     *
     * Se apoya en la relación ya cargada (`CreditResource::getEloquentQuery`
     * hace eager load) para no disparar una consulta por fila en el listado.
     */
    public function replacementCode(): ?string
    {
        /** @var self|null $replacement */
        $replacement = $this->children->sortByDesc('id')->first();

        return $replacement?->code;
    }

    /*=========================================================
    | LABELS PARA FILAMENT
    =========================================================*/
    public function getStatusLabel(): string
    {
        return self::statusLabel($this->status);
    }

    /**
     * Etiqueta de un estado. Fuente única: los selects de Filament la usan para
     * no volver a escribir a mano una lista que ya se quedó corta una vez (el
     * filtro "Estado" del listado solo ofrecía 5 de los 12 estados reales).
     */
    public static function statusLabel(string $status): string
    {
        return match ($status) {
            self::STATUS_ACTIVE => 'Activo',
            self::STATUS_DELAYED => 'En mora',
            self::STATUS_OVERDUE => 'Vencido',
            self::STATUS_PAID => 'Pagado',
            self::STATUS_CANCELED => 'Cancelado',
            self::STATUS_EXTENDED => 'Extendido',
            self::STATUS_REFINANCED => 'Refinanciado',
            self::STATUS_RESTRUCTURED => 'Reestructurado',
            self::STATUS_RENEWED => 'Renovado',
            self::STATUS_INTEREST_CAPITALIZED => 'Interés Capitalizado',
            self::STATUS_DEFAULTED => 'En Castigo',
            self::STATUS_ARCHIVED => 'Archivado',
            default => ucfirst($status),
        };
    }

    /**
     * Los 12 estados con su etiqueta, para los selects de Filament.
     *
     * @return array<string, string>
     */
    public static function statusOptions(): array
    {
        $statuses = [...self::ACTIVE_STATUSES, ...self::CLOSED_STATES];

        return array_combine(
            $statuses,
            array_map(static fn (string $s): string => self::statusLabel($s), $statuses)
        );
    }

    public function getStatusBadgeColor(): string
    {
        return match ($this->status) {
            self::STATUS_ACTIVE => 'success',
            self::STATUS_DELAYED => 'warning',
            self::STATUS_OVERDUE => 'danger',
            self::STATUS_PAID => 'primary',
            self::STATUS_CANCELED => 'gray',
            // Estados de operaciones de crédito (cerrados)
            self::STATUS_EXTENDED => 'purple',
            self::STATUS_REFINANCED => 'purple',
            self::STATUS_RESTRUCTURED => 'purple',
            self::STATUS_RENEWED => 'purple',
            self::STATUS_INTEREST_CAPITALIZED => 'purple',
            self::STATUS_DEFAULTED => 'danger',
            self::STATUS_ARCHIVED => 'gray',
            default => 'secondary',
        };
    }
}
