<?php

declare(strict_types=1);

namespace App\Models;

use App\Enums\ExpenseCategory;
use App\Traits\MultiTenantScope;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use RuntimeException;

/**
 * Modelo de Egresos - Enhanced for Accounting Integrity.
 *
 * REGLAS DE INTEGRIDAD FINANCIERA:
 *
 * 1. DESEMBOLSOS (credit_disbursement):
 *    - Requieren credit_id
 *    - Siempre afectan caja (affects_cash = true)
 *    - Reducen capital de trabajo (reduces_capital = true)
 *    - NO afectan utilidades (affects_profit = false)
 *    - NO pueden ser editados ni eliminados manualmente
 *
 * 2. AJUSTES FINANCIEROS (financial_adjustment):
 *    - Requieren financial_operation_id
 *    - NO pueden ser eliminados
 *
 * 3. TRANSACCIONES DE CAPITAL (profit_withdrawal, capital_return, dividend_payment):
 *    - Afectan patrimonio, no gastos operativos
 *    - Restricciones de edición/eliminación basadas en tiempo
 *    - Requieren aprobación
 *
 * 4. GASTOS OPERATIVOS (salary_wages, rent, utilities, etc.):
 *    - NO pueden estar ligados a un crédito
 *    - Afectan utilidades del período
 *    - Pueden ser editados y eliminados libremente
 *
 * 5. PROVISIONES Y CASTIGOS (bad_debt_provision, bad_debt_writeoff):
 *    - Provisiones no afectan caja
 *    - Castigos reducen capital
 *
 * CONSISTENCIA DE CAJA:
 *    Cash = Opening Balance + Σ(incomes WHERE affects_cash) - Σ(expenses WHERE affects_cash)
 *
 * @property int $id
 * @property int $company_id
 * @property int|null $credit_id
 * @property int|null $financial_operation_id
 * @property int $user_id
 * @property int|null $collector_user_id
 * @property string $amount
 * @property ExpenseCategory $category
 * @property string $expense_type
 * @property bool $affects_cash
 * @property bool $affects_profit
 * @property bool $reduces_capital
 * @property string|null $method
 * @property Carbon|null $operation_date
 * @property string|null $reference
 * @property string|null $notes
 * @property array|null $metadata
 * @property bool $is_locked
 * @property Carbon|null $locked_at
 * @property string|null $lock_reason
 * @property bool $requires_approval
 * @property string $approval_status
 * @property int|null $approved_by_user_id
 * @property Carbon|null $approved_at
 * @property string|null $approval_notes
 * @property Carbon $created_at
 * @property Carbon $updated_at
 * @property-read Company $company
 * @property-read Credit|null $credit
 * @property-read FinancialOperation|null $financialOperation
 * @property-read User $user
 * @property-read User|null $collector
 * @property-read User|null $approvedBy
 */
class Expense extends Model
{
    use MultiTenantScope;

    protected $fillable = [
        'company_id',
        'credit_id',
        'financial_operation_id',
        'user_id',
        'collector_user_id',
        'amount',
        'category',
        'expense_type',
        'affects_cash',
        'affects_profit',
        'reduces_capital',
        'method',
        'operation_date',
        'reference',
        'notes',
        'metadata',
        'is_locked',
        'locked_at',
        'lock_reason',
        // approval_status y requires_approval NO son mass-assignable: se fijan vía los
        // métodos del modelo (applyApprovalPolicyForCreator / approve / reject) o por
        // los defaults de $attributes. Así ningún request puede auto-aprobar un egreso.
        // (approved_by_user_id sí es fillable: lo setean servicios de confianza —
        // PartnerInvestmentService, etc.; sin approval_status fillable no es palanca.)
        'approved_by_user_id',
        'approved_at',
        'idempotency_key',
        'approval_notes',
    ];

    protected $casts = [
        'operation_date' => 'date',
        'amount' => 'decimal:2',
        'affects_cash' => 'boolean',
        'affects_profit' => 'boolean',
        'reduces_capital' => 'boolean',
        'metadata' => 'array',
        'category' => ExpenseCategory::class,
        'is_locked' => 'boolean',
        'locked_at' => 'datetime',
        'requires_approval' => 'boolean',
        'approved_at' => 'datetime',
    ];

    protected $attributes = [
        'affects_cash' => true,
        'affects_profit' => true,
        'reduces_capital' => false,
        'is_locked' => false,
        'requires_approval' => false,
        'approval_status' => 'approved',
    ];

    // Approval status constants
    public const APPROVAL_PENDING = 'pending';

    public const APPROVAL_APPROVED = 'approved';

    public const APPROVAL_REJECTED = 'rejected';

    /**
     * Categorías con las que el sistema registra la salida de capital al
     * desembolsar un crédito. `disbursement` es el valor legado —y el que sigue
     * escribiendo `CreditOperationService`—; `credit_disbursement`, el actual.
     *
     * No son gastos: son capital prestado que vuelve con las cuotas. Mismo
     * criterio que ya aplica el panel del admin.
     */
    public const DISBURSEMENT_CATEGORIES = ['disbursement', 'credit_disbursement'];

    /**
     * Transient flag: when true, the creating hook skips the approval requirement check.
     * Use this when the approval already happened at a higher level (e.g. PartnerWithdrawal approval).
     */
    public bool $preApproved = false;

    protected static function booted(): void
    {
        /*=========================================================
        | VALIDACIONES DE DOMINIO
        |
        | NOTA: Estas validaciones aplican a operaciones Eloquent.
        | Las operaciones del sistema vía query builder las omiten
        | intencionalmente para permitir actualizaciones controladas.
        =========================================================*/
        static::creating(function (Expense $expense) {
            self::validateExpenseIntegrity($expense);
            self::setFinancialImpactFlags($expense);
            self::checkApprovalRequirements($expense);
        });

        // 🔒 Bloqueo de desembolsos y registros bloqueados
        static::updating(function (Expense $expense) {
            // Bloqueados no se pueden editar
            if ($expense->is_locked && ! $expense->isDirty('is_locked')) {
                throw new RuntimeException(
                    'No se puede modificar un egreso bloqueado.'
                );
            }

            // Desembolsos totalmente bloqueados
            if ($expense->expense_type === 'disbursement') {
                throw new RuntimeException(
                    'No se permite modificar egresos de desembolso.'
                );
            }

            // Verificar permisos por categoría
            $category = $expense->getCategoryEnum();
            if ($category && ! $category->canBeEdited()) {
                $allowedFields = ['notes', 'metadata', 'is_locked', 'locked_at', 'lock_reason',
                    'approval_status', 'approved_by_user_id', 'approved_at', 'approval_notes'];
                $dirty = array_keys($expense->getDirty());
                $disallowedChanges = array_diff($dirty, $allowedFields);

                if (! empty($disallowedChanges)) {
                    throw new RuntimeException(
                        "No se pueden modificar campos en un egreso de tipo '{$category->getLabel()}'."
                    );
                }
            }
        });

        static::deleting(function (Expense $expense) {
            if ($expense->is_locked) {
                throw new RuntimeException(
                    'No se puede eliminar un egreso bloqueado.'
                );
            }

            if (in_array($expense->expense_type, ['disbursement', 'financial_adjustment'])) {
                throw new RuntimeException(
                    'No se puede eliminar un egreso financiero.'
                );
            }

            $category = $expense->getCategoryEnum();
            if ($category && ! $category->canBeDeleted()) {
                throw new RuntimeException(
                    "No se puede eliminar un egreso de tipo '{$category->getLabel()}'."
                );
            }
        });
    }

    /**
     * Validate expense integrity based on type and category.
     */
    protected static function validateExpenseIntegrity(Expense $expense): void
    {
        $category = $expense->getCategoryEnum();

        // Credit disbursement validation
        if ($expense->expense_type === 'disbursement' ||
            $category === ExpenseCategory::CREDIT_DISBURSEMENT ||
            $category === ExpenseCategory::DISBURSEMENT) { // Legacy

            if (! $expense->credit_id) {
                throw new RuntimeException(
                    'Un desembolso debe estar asociado a un crédito.'
                );
            }

            if (! $expense->affects_cash) {
                throw new RuntimeException(
                    'Un desembolso siempre debe afectar caja.'
                );
            }
        }

        // Financial adjustment validation
        if ($expense->expense_type === 'financial_adjustment' ||
            $category === ExpenseCategory::FINANCIAL_ADJUSTMENT) {

            if (! $expense->financial_operation_id) {
                throw new RuntimeException(
                    'Un ajuste financiero debe pertenecer a una operación financiera.'
                );
            }
        }

        // Bad debt write-off validation
        if ($category === ExpenseCategory::BAD_DEBT_WRITEOFF ||
            $category === ExpenseCategory::BAD_DEBT) {

            if (! $expense->credit_id) {
                throw new RuntimeException(
                    'Un castigo de cartera debe estar asociado a un crédito.'
                );
            }
        }

        // Operational expenses cannot have credit_id
        if ($category && $category->isOperational() && $expense->credit_id) {
            throw new RuntimeException(
                "Un gasto operativo '{$category->getLabel()}' no puede estar ligado a un crédito."
            );
        }

        // Capital transactions cannot have credit_id
        if ($category && $category->isCapitalTransaction() && $expense->credit_id) {
            throw new RuntimeException(
                "Una transacción de capital '{$category->getLabel()}' no puede estar ligada a un crédito."
            );
        }
    }

    /**
     * Set financial impact flags based on category.
     */
    protected static function setFinancialImpactFlags(Expense $expense): void
    {
        $category = $expense->getCategoryEnum();

        if ($category instanceof ExpenseCategory) {
            if (! $expense->isDirty('affects_cash')) {
                $expense->affects_cash = $category->affectsCash();
            }

            if (! $expense->isDirty('affects_profit')) {
                $expense->affects_profit = $category->affectsProfit();
            }

            if (! $expense->isDirty('reduces_capital')) {
                $expense->reduces_capital = $category->reducesCapital();
            }
        }
    }

    /**
     * Check if expense requires approval and set status accordingly.
     */
    protected static function checkApprovalRequirements(Expense $expense): void
    {
        // Skip when the approval was already handled at a higher level (e.g. PartnerWithdrawal).
        if ($expense->preApproved) {
            return;
        }

        $category = $expense->getCategoryEnum();

        if ($category && $category->requiresApproval()) {
            $expense->requires_approval = true;

            // Check amount threshold
            $settings = CompanyFinancialSettings::forCompany($expense->company_id);
            if ($settings->requiresApprovalForAmount($expense->amount)) {
                $expense->approval_status = self::APPROVAL_PENDING;
            }
        }
    }

    // ═══════════════════════════════════════════════════════════════════════
    // CATEGORY HELPERS
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Get category as enum instance.
     */
    public function getCategoryEnum(): ?ExpenseCategory
    {
        $category = $this->category;

        if ($category instanceof ExpenseCategory) {
            return $category;
        }

        if (is_string($category)) {
            return ExpenseCategory::tryFrom($category);
        }

        return null;
    }

    /**
     * Is this a credit operation (disbursement, adjustment)?
     */
    public function isCreditOperation(): bool
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory && $category->isCreditOperation();
    }

    /**
     * Is this a capital transaction?
     */
    public function isCapitalTransaction(): bool
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory && $category->isCapitalTransaction();
    }

    /**
     * Is this an operational expense?
     */
    public function isOperational(): bool
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory && $category->isOperational();
    }

    /**
     * Is this a provision or write-off?
     */
    public function isProvision(): bool
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory && $category->isProvision();
    }

    /**
     * Get category label.
     */
    public function getCategoryLabel(): string
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory
            ? $category->getLabel()
            : ucfirst($this->getRawOriginal('category') ?? 'Desconocido');
    }

    /**
     * Get category color for badges.
     */
    public function getCategoryColor(): string
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory
            ? $category->getColor()
            : 'gray';
    }

    /**
     * Get category icon.
     */
    public function getCategoryIcon(): string
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory
            ? $category->getIcon()
            : 'heroicon-o-banknotes';
    }

    /**
     * Get category description.
     */
    public function getCategoryDescription(): string
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory
            ? $category->getDescription()
            : '';
    }

    /**
     * Get financial impact summary.
     */
    public function getFinancialImpactSummary(): string
    {
        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory
            ? $category->getFinancialImpactSummary()
            : '';
    }

    // ═══════════════════════════════════════════════════════════════════════
    // MONEY HELPERS
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Get amount as Money object.
     */
    public function getAmountMoney(): Money
    {
        return Money::of($this->amount);
    }

    // ═══════════════════════════════════════════════════════════════════════
    // LOCKING METHODS
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Check if this expense can be edited.
     */
    public function canBeEdited(): bool
    {
        if ($this->is_locked) {
            return false;
        }

        if ($this->approval_status === self::APPROVAL_PENDING) {
            return false;
        }

        $category = $this->getCategoryEnum();
        if (! $category) {
            return false;
        }

        if (! $category->canBeEdited()) {
            return false;
        }

        // Check time-based lock for capital transactions
        if ($category->isCapitalTransaction()) {
            $settings = CompanyFinancialSettings::forCompany($this->company_id);
            if ($settings->isCapitalTransactionLocked($this->created_at)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if this expense can be deleted.
     */
    public function canBeDeleted(): bool
    {
        if ($this->is_locked) {
            return false;
        }

        $category = $this->getCategoryEnum();

        return $category instanceof ExpenseCategory && $category->canBeDeleted();
    }

    /**
     * Lock this expense record.
     */
    public function lock(string $reason = 'Manual lock'): bool
    {
        if ($this->is_locked) {
            return false;
        }

        $this->is_locked = true;
        $this->locked_at = now();
        $this->lock_reason = $reason;

        return $this->save();
    }

    /**
     * Unlock this expense record (admin only).
     */
    public function unlock(): bool
    {
        if (! $this->is_locked) {
            return false;
        }

        $this->is_locked = false;
        $this->locked_at = null;
        $this->lock_reason = null;

        return $this->save();
    }

    // ═══════════════════════════════════════════════════════════════════════
    // APPROVAL METHODS
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Check if expense is pending approval.
     */
    public function isPendingApproval(): bool
    {
        return $this->approval_status === self::APPROVAL_PENDING;
    }

    /**
     * Check if expense is approved.
     */
    public function isApproved(): bool
    {
        return $this->approval_status === self::APPROVAL_APPROVED;
    }

    /**
     * Check if expense was rejected.
     */
    public function isRejected(): bool
    {
        return $this->approval_status === self::APPROVAL_REJECTED;
    }

    /**
     * Approve this expense.
     */
    public function approve(?string $notes = null): bool
    {
        if (! $this->isPendingApproval()) {
            return false;
        }

        $this->approval_status = self::APPROVAL_APPROVED;
        $this->approved_by_user_id = auth()->id();
        $this->approved_at = now();
        $this->approval_notes = $notes;

        return $this->save();
    }

    /**
     * Reject this expense.
     */
    public function reject(string $reason): bool
    {
        if (! $this->isPendingApproval()) {
            return false;
        }

        $this->approval_status = self::APPROVAL_REJECTED;
        $this->approved_by_user_id = auth()->id();
        $this->approved_at = now();
        $this->approval_notes = $reason;

        return $this->save();
    }

    /**
     * Aplica la política de aprobación de egresos según el ROL del creador.
     *
     * FUENTE ÚNICA DE VERDAD para "¿quién necesita aprobación?":
     *   - admin / super_admin → es el mismo que aprueba → auto-aprobado.
     *   - supervisor / collector → requiere que el admin lo apruebe → queda PENDING.
     *
     * Un egreso pendiente NO descuenta caja hasta que el admin lo apruebe
     * (ver scopeEffective). Debe llamarse desde flujos iniciados por un usuario
     * (PWA, panel Filament). NO aplica a operaciones de sistema (desembolsos,
     * ajustes de crédito) ni al flujo de capital de socios, que tienen su propia
     * autorización.
     *
     * Marca preApproved=true para que checkApprovalRequirements (política por
     * monto/categoría) no reescriba la decisión basada en rol.
     */
    public function applyApprovalPolicyForCreator(User $creator): void
    {
        $isApprover = $creator->hasAnyRole(['admin', 'super_admin']);

        $this->requires_approval = ! $isApprover;
        $this->approval_status = $isApprover
            ? self::APPROVAL_APPROVED
            : self::APPROVAL_PENDING;

        if ($isApprover) {
            $this->approved_by_user_id = $creator->id;
            $this->approved_at = now();
        } else {
            $this->approved_by_user_id = null;
            $this->approved_at = null;
        }

        // La decisión por rol es definitiva: evita que el hook la sobreescriba.
        $this->preApproved = true;
    }

    /**
     * Deja el gasto pendiente de aprobación sin importar el rol de quien lo sube.
     * Lo usa la cola offline cuando el gasto lo capturó OTRO usuario en el mismo
     * teléfono: quien sube (aunque sea admin) no lo aprobó.
     */
    public function markPendingApproval(): void
    {
        $this->requires_approval = true;
        $this->approval_status = self::APPROVAL_PENDING;
        $this->approved_by_user_id = null;
        $this->approved_at = null;
        $this->preApproved = true;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // RELATIONSHIPS
    // ═══════════════════════════════════════════════════════════════════════

    public function credit(): BelongsTo
    {
        return $this->belongsTo(Credit::class);
    }

    public function financialOperation(): BelongsTo
    {
        return $this->belongsTo(FinancialOperation::class);
    }

    public function company(): BelongsTo
    {
        return $this->belongsTo(Company::class);
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

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

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

    /**
     * Audit logs for this expense.
     */
    public function auditLogs(): MorphMany
    {
        return $this->morphMany(FinancialAuditLog::class, 'auditable');
    }

    // ═══════════════════════════════════════════════════════════════════════
    // SCOPES
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Scope to filter by cash-affecting expenses.
     */
    public function scopeAffectsCash($query)
    {
        return $query->where('affects_cash', true);
    }

    /**
     * Scope to filter by profit-affecting expenses.
     */
    public function scopeAffectsProfit($query)
    {
        return $query->where('affects_profit', true);
    }

    /**
     * Scope to filter by capital-reducing expenses.
     */
    public function scopeReducesCapital($query)
    {
        return $query->where('reduces_capital', true);
    }

    /**
     * Scope to filter by locked records.
     */
    public function scopeLocked($query)
    {
        return $query->where('is_locked', true);
    }

    /**
     * Scope to filter by unlocked records.
     */
    public function scopeUnlocked($query)
    {
        return $query->where('is_locked', false);
    }

    /**
     * Scope to filter by approval status.
     */
    public function scopeWithApprovalStatus($query, string $status)
    {
        return $query->where('approval_status', $status);
    }

    /**
     * Scope to filter by pending approval.
     */
    public function scopePendingApproval($query)
    {
        return $query->where('approval_status', self::APPROVAL_PENDING);
    }

    /**
     * Scope: egresos "efectivos" para caja, saldos y reportes financieros.
     *
     * Un egreso solo impacta caja/contabilidad cuando está APROBADO, o cuando NO
     * requiere aprobación (lo creó un admin, o es una operación de sistema).
     * Excluye los PENDIENTES de aprobación y los RECHAZADOS: el dinero solo se
     * descuenta de caja cuando el admin aprueba el egreso.
     *
     * Es la regla canónica usada por getCashPosition, getCashBase, getNetProfit,
     * CashFlowService y FinancialController para que TODOS los cálculos de saldo
     * sean consistentes.
     */
    public function scopeEffective($query)
    {
        return $query->where(function ($q) {
            $q->where('approval_status', self::APPROVAL_APPROVED)
                ->orWhere('requires_approval', false);
        });
    }

    /**
     * Scope to filter by category.
     */
    public function scopeOfCategory($query, ExpenseCategory $category)
    {
        return $query->where('category', $category->value);
    }

    /**
     * Excluye los desembolsos de crédito, que el sistema registra como gasto.
     *
     * Sin esto, contar "los gastos de alguien" incluye cada crédito que
     * entregó: el panel del cobrador enseñaba el desembolso en dos tarjetas a la
     * vez, una como "Desembolsado" y otra como "Gastos".
     *
     * @param  Builder<Expense>  $query
     * @return Builder<Expense>
     */
    public function scopeWithoutDisbursements(Builder $query): Builder
    {
        return $query->whereNotIn('category', self::DISBURSEMENT_CATEGORIES);
    }

    /**
     * Scope for credit operations.
     */
    public function scopeCreditOperations($query)
    {
        return $query->whereIn('category', array_map(
            fn ($c) => $c->value,
            ExpenseCategory::creditOperationCategories()
        ));
    }

    /**
     * Scope for capital transactions.
     */
    public function scopeCapitalTransactions($query)
    {
        return $query->whereIn('category', array_map(
            fn ($c) => $c->value,
            ExpenseCategory::capitalCategories()
        ));
    }

    /**
     * Scope for operational expenses.
     */
    public function scopeOperational($query)
    {
        return $query->whereIn('category', array_map(
            fn ($c) => $c->value,
            ExpenseCategory::operationalCategories()
        ));
    }
}
