<?php

declare(strict_types=1);

namespace App\Models;

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

/**
 * Modelo de Ingresos - Enhanced for Accounting Integrity.
 *
 * REGLAS DE INTEGRIDAD FINANCIERA:
 *
 * 1. INGRESOS VINCULADOS A PAGOS (payment, loan_payment_principal, loan_payment_interest, overpayment):
 *    - Requieren payment_id Y credit_id
 *    - NO pueden ser eliminados manualmente (UI bloqueada)
 *    - NO pueden editarse campos críticos (amount, category, credit_id, payment_id)
 *    - El SISTEMA puede eliminarlos vía query builder (bypass de eventos)
 *      cuando se reversa un pago - Ver PaymentReverser, PaymentRegistrar
 *
 * 2. TRANSACCIONES DE CAPITAL (capital_injection, investor_contribution, opening_balance):
 *    - Afectan capital de trabajo, NO utilidades
 *    - Restricciones de edición/eliminación basadas en tiempo
 *    - Opening balance solo puede crearse una vez
 *
 * 3. INGRESOS OPERATIVOS (late_fee, penalty_interest, service_fee, etc.):
 *    - NO pueden tener payment_id ni credit_id
 *    - Afectan utilidades del período
 *    - Pueden ser creados/editados/eliminados libremente
 *
 * CONSISTENCIA DE CAJA:
 *    Cash = Opening Balance + Σ(incomes WHERE affects_cash) - Σ(expenses WHERE affects_cash)
 *
 * IMPACTO FINANCIERO:
 *    - affects_capital: TRUE si incrementa el capital de trabajo
 *    - affects_profit: TRUE si incrementa las utilidades del período
 *
 * @property int $id
 * @property int $company_id
 * @property int|null $credit_id
 * @property int|null $payment_id
 * @property int $user_id
 * @property int|null $collector_user_id
 * @property string $amount
 * @property IncomeCategory $category
 * @property string|null $method
 * @property Carbon|null $operation_date
 * @property string|null $reference
 * @property string|null $notes
 * @property array|null $metadata
 * @property bool $affects_capital
 * @property bool $affects_profit
 * @property bool $is_locked
 * @property Carbon|null $locked_at
 * @property string|null $lock_reason
 * @property Carbon $created_at
 * @property Carbon $updated_at
 * @property-read Company $company
 * @property-read Credit|null $credit
 * @property-read Payment|null $payment
 * @property-read User $user
 * @property-read User|null $collector
 */
class Income extends Model
{
    use MultiTenantScope;

    protected $fillable = [
        'company_id',
        'credit_id',
        'payment_id',
        'user_id',
        'collector_user_id',
        'amount',
        'category',
        'method',
        'operation_date',
        'reference',
        'notes',
        'metadata',
        'affects_capital',
        'affects_profit',
        'is_locked',
        'locked_at',
        'lock_reason',
    ];

    protected $casts = [
        'operation_date' => 'date',
        'amount' => 'decimal:2',
        'metadata' => 'array',
        'category' => IncomeCategory::class,
        'affects_capital' => 'boolean',
        'affects_profit' => 'boolean',
        'is_locked' => 'boolean',
        'locked_at' => 'datetime',
    ];

    protected $attributes = [
        'affects_capital' => false,
        'affects_profit' => true,
        'is_locked' => false,
    ];

    protected static function booted(): void
    {
        /*=========================================================
        | PROTECCIÓN: No eliminar/editar ingresos de pagos o bloqueados
        |
        | NOTA: Estas validaciones aplican a operaciones Eloquent.
        | Las operaciones del sistema (reversión de pagos) usan
        | query builder para omitir estas protecciones.
        =========================================================*/
        static::deleting(function (Income $income) {
            if ($income->is_locked) {
                throw new \RuntimeException(
                    'No se puede eliminar un ingreso bloqueado.'
                );
            }

            if ($income->payment_id !== null) {
                throw new \RuntimeException(
                    'No se puede eliminar un ingreso generado por un pago.'
                );
            }

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

        static::updating(function (Income $income) {
            // Bloqueados no se pueden editar
            if ($income->is_locked && ! $income->isDirty('is_locked')) {
                throw new \RuntimeException(
                    'No se puede modificar un ingreso bloqueado.'
                );
            }

            // Solo proteger campos críticos si está vinculado a un pago
            if ($income->payment_id !== null) {
                $protectedFields = ['amount', 'category', 'credit_id', 'payment_id', 'company_id'];
                $dirty = $income->getDirty();

                foreach ($protectedFields as $field) {
                    if (array_key_exists($field, $dirty)) {
                        throw new \RuntimeException(
                            "No se puede modificar '{$field}' en un ingreso generado por un pago."
                        );
                    }
                }
            }

            // Verificar permisos de edición por categoría
            $category = $income->getCategoryEnum();
            if ($category && ! $category->canBeEdited()) {
                // Allow only metadata and notes changes for non-editable categories
                $allowedFields = ['notes', 'metadata', 'is_locked', 'locked_at', 'lock_reason'];
                $dirty = array_keys($income->getDirty());
                $disallowedChanges = array_diff($dirty, $allowedFields);

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

        /*=========================================================
        | VALIDACIÓN DE INTEGRIDAD AL CREAR
        =========================================================*/
        static::creating(function (Income $income) {
            self::validateCategoryIntegrity($income);
            self::setFinancialImpactFlags($income);
        });
    }

    /**
     * Valida la integridad de la categoría y sus relaciones.
     *
     * REGLAS:
     * - payment-linked: REQUIEREN payment_id Y credit_id
     * - capital: Validaciones específicas
     * - operational: NO PUEDEN tener payment_id NI credit_id
     */
    protected static function validateCategoryIntegrity(Income $income): void
    {
        $category = $income->getCategoryEnum();

        if (! $category instanceof IncomeCategory) {
            throw new \RuntimeException(
                "Categoría de ingreso inválida: '{$income->getRawOriginal('category')}'."
            );
        }

        // Categorías vinculadas a pagos
        if ($category->isPaymentLinked()) {
            if (empty($income->payment_id)) {
                throw new \RuntimeException(
                    "La categoría '{$category->getLabel()}' requiere payment_id."
                );
            }

            if (empty($income->credit_id)) {
                throw new \RuntimeException(
                    "La categoría '{$category->getLabel()}' requiere credit_id para trazabilidad."
                );
            }
        }

        // Transacciones de capital - validaciones específicas
        if ($category->isCapitalTransaction()) {
            // Opening balance: verificar que no exista otro para esta compañía
            if ($category === IncomeCategory::OPENING_BALANCE) {
                $existingBalance = Income::where('company_id', $income->company_id)
                    ->where('category', IncomeCategory::OPENING_BALANCE->value)
                    ->exists();

                if ($existingBalance) {
                    throw new \RuntimeException(
                        'Ya existe un balance inicial para esta empresa. No se puede crear otro.'
                    );
                }
            }

            // Capital transactions cannot have payment_id
            if (! empty($income->payment_id)) {
                throw new \RuntimeException(
                    "La categoría de capital '{$category->getLabel()}' no puede tener payment_id."
                );
            }
        }

        // Categorías operativas (NO pueden tener payment_id ni credit_id)
        if ($category->isOperational()) {
            if (! empty($income->credit_id)) {
                throw new \RuntimeException(
                    "La categoría operativa '{$category->getLabel()}' no puede tener credit_id."
                );
            }

            if (! empty($income->payment_id)) {
                throw new \RuntimeException(
                    "La categoría operativa '{$category->getLabel()}' no puede tener payment_id."
                );
            }
        }
    }

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

        if ($category instanceof IncomeCategory) {
            // Only set if not explicitly provided
            if (! $income->isDirty('affects_capital')) {
                $income->affects_capital = $category->affectsCapital();
            }

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

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

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

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

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

        return null;
    }

    /**
     * ¿Es un ingreso vinculado a un pago?
     */
    public function isPaymentLinked(): bool
    {
        $category = $this->getCategoryEnum();

        return $category instanceof IncomeCategory && $category->isPaymentLinked();
    }

    /**
     * ¿Es una transacción de capital?
     */
    public function isCapitalTransaction(): bool
    {
        $category = $this->getCategoryEnum();

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

    /**
     * ¿Es un ingreso operativo?
     */
    public function isOperational(): bool
    {
        $category = $this->getCategoryEnum();

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

    /**
     * ¿Es un overpayment (saldo a favor)?
     */
    public function isOverpayment(): bool
    {
        return $this->getCategoryEnum() === IncomeCategory::OVERPAYMENT;
    }

    /**
     * ¿Es el balance inicial?
     */
    public function isOpeningBalance(): bool
    {
        return $this->getCategoryEnum() === IncomeCategory::OPENING_BALANCE;
    }

    /**
     * Obtener etiqueta de la categoría.
     */
    public function getCategoryLabel(): string
    {
        $category = $this->getCategoryEnum();

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

    /**
     * Obtener color de la categoría para badges.
     */
    public function getCategoryColor(): string
    {
        $category = $this->getCategoryEnum();

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

    /**
     * Obtener icono de la categoría.
     */
    public function getCategoryIcon(): string
    {
        $category = $this->getCategoryEnum();

        return $category instanceof IncomeCategory
            ? $category->getIcon()
            : 'heroicon-o-currency-dollar';
    }

    /**
     * Obtener descripción de la categoría.
     */
    public function getCategoryDescription(): string
    {
        $category = $this->getCategoryEnum();

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

    /**
     * Obtener resumen de impacto financiero.
     */
    public function getFinancialImpactSummary(): string
    {
        $category = $this->getCategoryEnum();

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

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

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

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

    /**
     * Check if this income can be edited.
     */
    public function canBeEdited(): bool
    {
        if ($this->is_locked) {
            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 income can be deleted.
     */
    public function canBeDeleted(): bool
    {
        if ($this->is_locked) {
            return false;
        }

        $category = $this->getCategoryEnum();

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

    /**
     * Lock this income 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 income 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();
    }

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

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

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

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

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

    public function payment(): BelongsTo
    {
        return $this->belongsTo(Payment::class, 'payment_id');
    }

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

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

    /**
     * Scope to filter by capital-affecting incomes.
     */
    public function scopeAffectsCapital($query)
    {
        return $query->where('affects_capital', true);
    }

    /**
     * Scope to filter by profit-affecting incomes.
     */
    public function scopeAffectsProfit($query)
    {
        return $query->where('affects_profit', 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 category type.
     */
    public function scopeOfCategory($query, IncomeCategory $category)
    {
        return $query->where('category', $category->value);
    }

    /**
     * Scope for payment-linked incomes.
     */
    public function scopePaymentLinked($query)
    {
        return $query->whereIn('category', array_map(
            fn ($c) => $c->value,
            IncomeCategory::paymentLinkedCategories()
        ));
    }

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

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