<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;

use Illuminate\Support\Facades\Log;

class JournalEntry extends Model
{
    use HasFactory, \App\Traits\HotelScopeTrait, \App\Traits\GeneratesUniqueNumbers;

    protected function getNumberPrefix(): string
    {
        return 'JE';
    }

    protected $table = 'journal_entries';

    protected $fillable = [
        'hotel_id',
        'date',
        'entry_number',
        'je_id_reference',
        'reference',
        'description',
        'total_amount',
        'currency_code',
        'exchange_rate',
        'status',
        'module_source',
        'source_id',
        'posted_at',
        'company_id',
        'fiscal_year_id',
        'created_by'
    ];

    protected $casts = [
        'date' => 'date',
        'total_amount' => 'decimal:2',
        'exchange_rate' => 'decimal:4',
        'posted_at' => 'datetime',
        'created_at' => 'datetime',
        'updated_at' => 'datetime'
    ];

    protected $appends = [
        'formatted_date',
        'formatted_total',
        'status_badge',
        'total_debit',
        'total_credit',
        'is_balanced',
        'balance_difference',
        'source_reference',
        'is_global'
    ];

    // ==================== Constants ====================

    const STATUS_DRAFT = 'draft';
    const STATUS_POSTED = 'posted';
    const STATUS_PENDING = 'pending';
    const STATUS_CANCELLED = 'cancelled';

    // ==================== Relationships ====================

    /**
     * Get the hotel that owns this journal entry
     */
    public function hotel(): BelongsTo
    {
        return $this->belongsTo(Hotel::class);
    }

    /**
     * Get the items for this journal entry
     */
    public function items(): HasMany
    {
        return $this->hasMany(JournalEntryItem::class, 'journal_entry_id');
    }

    /**
     * Get the transactions for this journal entry (for backward compatibility)
     */
    public function transactions(): HasMany
    {
        return $this->hasMany(JournalTransaction::class, 'journal_entry_id');
    }

    /**
     * Get the user who created this entry
     */
    public function createdBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'created_by');
    }

    /**
     * Get the source model (polymorphic relationship)
     */
    public function source(): MorphTo
    {
        return $this->morphTo('source', 'module_source', 'source_id');
    }

    /**
     * Get the invoice if source is invoice
     */
    public function invoice()
    {
        return $this->belongsTo(Invoice::class, 'source_id')
            ->where('module_source', 'invoice');
    }

    /**
     * Get the payment if source is payment
     */
    public function payment()
    {
        return $this->belongsTo(Payment::class, 'source_id')
            ->where('module_source', 'payment');
    }

    /**
     * Get the receipt if source is receipt
     */
    public function receipt()
    {
        return $this->belongsTo(Receipt::class, 'source_id')
            ->where('module_source', 'receipt');
    }

    /**
     * Get the reservation if source is reservation
     */
    public function reservation()
    {
        return $this->belongsTo(Reservation::class, 'source_id')
            ->where('module_source', 'reservation');
    }

    // ==================== Scopes ====================

    /**
     * Scope to filter by hotel including global entries
     */
    public function scopeForHotel($query, $hotelId)
    {
        return $query->where(function($q) use ($hotelId) {
            $q->where('hotel_id', $hotelId)
              ->orWhereNull('hotel_id'); // Global entries
        });
    }

    /**
     * Scope to get only global entries
     */
    public function scopeGlobal($query)
    {
        return $query->whereNull('hotel_id');
    }

    /**
     * Scope to get only hotel-specific entries
     */
    public function scopeHotelSpecific($query, $hotelId)
    {
        return $query->where('hotel_id', $hotelId);
    }

    /**
     * Scope to get only posted entries
     */
    public function scopePosted($query)
    {
        return $query->where('status', self::STATUS_POSTED);
    }

    /**
     * Scope to get only draft entries
     */
    public function scopeDraft($query)
    {
        return $query->where('status', self::STATUS_DRAFT);
    }

    /**
     * Scope to get entries by date range
     */
    public function scopeDateRange($query, $startDate, $endDate)
    {
        return $query->whereBetween('date', [$startDate, $endDate]);
    }

    /**
     * Scope to get entries for a specific month
     */
    public function scopeForMonth($query, $year, $month)
    {
        return $query->whereYear('date', $year)
            ->whereMonth('date', $month);
    }

    /**
     * Scope to get entries by module source
     */
    public function scopeByModule($query, $module, $sourceId = null)
    {
        $query = $query->where('module_source', $module);
        
        if ($sourceId) {
            $query->where('source_id', $sourceId);
        }
        
        return $query;
    }

    /**
     * Scope to get unbalanced entries
     */
    public function scopeUnbalanced($query)
    {
        return $query->whereHas('items', function($q) {
            $q->selectRaw('journal_entry_id, ABS(SUM(debit) - SUM(credit)) as diff')
                ->having('diff', '>', 0.01)
                ->groupBy('journal_entry_id');
        });
    }

    // ==================== Accessors ====================

    /**
     * Check if journal is global
     */
    public function getIsGlobalAttribute(): bool
    {
        return is_null($this->hotel_id);
    }

    /**
     * Get formatted date
     */
    public function getFormattedDateAttribute(): string
    {
        return $this->date->format('M d, Y');
    }

    /**
     * Get formatted total amount
     */
    public function getFormattedTotalAttribute(): string
    {
        $symbol = $this->currency_code === 'USD' ? '$' : 'UGX ';
        return $symbol . number_format($this->total_amount, 2);
    }

    /**
     * Get status badge class
     */
    public function getStatusBadgeAttribute(): string
    {
        return match($this->status) {
            self::STATUS_POSTED => 'bg-success',
            self::STATUS_DRAFT => 'bg-warning',
            self::STATUS_PENDING => 'bg-info',
            self::STATUS_CANCELLED => 'bg-danger',
            default => 'bg-secondary'
        };
    }

    /**
     * Get total debit amount from items
     */
    public function getTotalDebitAttribute(): float
    {
        return (float) $this->items()->sum('debit');
    }

    /**
     * Get total credit amount from items
     */
    public function getTotalCreditAttribute(): float
    {
        return (float) $this->items()->sum('credit');
    }

    /**
     * Check if entry is balanced
     */
    public function getIsBalancedAttribute(): bool
    {
        return abs($this->total_debit - $this->total_credit) < 0.01;
    }

    /**
     * Get balance difference
     */
    public function getBalanceDifferenceAttribute(): float
    {
        return abs($this->total_debit - $this->total_credit);
    }

    /**
     * Get source reference
     */
    public function getSourceReferenceAttribute(): ?string
    {
        if (!$this->module_source || !$this->source_id) {
            return null;
        }

        return strtoupper($this->module_source) . '-' . $this->source_id;
    }

    // ==================== Methods ====================

    /**
     * Check if entry can be posted
     */
    public function canBePosted(): bool
    {
        return $this->status === self::STATUS_DRAFT 
            && $this->items()->count() >= 2 
            && $this->is_balanced;
    }

    /**
     * Check if entry can be edited
     */
    public function canBeEdited(): bool
    {
        return $this->status === self::STATUS_DRAFT;
    }

    /**
     * Check if entry can be deleted
     */
    public function canBeDeleted(): bool
    {
        return $this->status === self::STATUS_DRAFT;
    }

    /**
     * Post the journal entry
     */
    public function post(): bool
    {
        if (!$this->canBePosted()) {
            return false;
        }

        return $this->update([
            'status' => self::STATUS_POSTED,
            'posted_at' => now()
        ]);
    }

    /**
     * Cancel the journal entry
     */
    public function cancel(): bool
    {
        if (!$this->canBeDeleted()) {
            return false;
        }

        return $this->update(['status' => self::STATUS_CANCELLED]);
    }

    /**
     * Reverse the journal entry
     */
    public function reverse($reversalDate = null): self
    {
        if ($this->status !== self::STATUS_POSTED) {
            throw new \Exception('Only posted entries can be reversed.');
        }

        $reversalDate = $reversalDate ?? now();

        // Create reversal entry
        $reversal = self::create([
            'hotel_id' => $this->hotel_id,
            'date' => $reversalDate,
            'entry_number' => self::generateEntryNumber($this->hotel_id),
            'description' => "Reversal of {$this->entry_number}: {$this->description}",
            'reference' => $this->reference,
            'total_amount' => $this->total_amount,
            'status' => self::STATUS_DRAFT,
            'created_by' => auth()->id(),
        ]);

        // Copy items with reversed debits/credits
        foreach ($this->items as $item) {
            JournalEntryItem::create([
                'hotel_id' => $reversal->hotel_id,
                'journal_entry_id' => $reversal->id,
                'account_id' => $item->account_id,
                'debit' => $item->credit,
                'credit' => $item->debit,
                'description' => "Reversal: {$item->description}",
            ]);
        }

        return $reversal;
    }

    /**
     * Validate that all items are valid
     */
    public function validateItems(): array
    {
        $errors = [];

        if ($this->items()->count() < 2) {
            $errors[] = 'At least two journal items are required.';
        }

        if (!$this->is_balanced) {
            $errors[] = 'Debits and credits must be equal. Difference: ' . $this->balance_difference;
        }

        foreach ($this->items as $item) {
            if ($item->debit > 0 && $item->credit > 0) {
                $errors[] = "Item #{$item->id} cannot have both debit and credit amounts.";
            }

            if ($item->debit == 0 && $item->credit == 0) {
                $errors[] = "Item #{$item->id} must have either debit or credit amount.";
            }

            $account = $item->account;
            if (!$account || !$account->is_active) {
                $errors[] = "Account for item #{$item->id} is invalid or inactive.";
            }

            if ($account && $account->is_header) {
                $errors[] = "Cannot post to header account: {$account->full_path}";
            }
        }

        return $errors;
    }

    /**
     * Boot method for model events
     */
    protected static function boot()
    {
        parent::boot();

        static::creating(function ($journalEntry) {
            if (empty($journalEntry->currency_code)) {
                $journalEntry->currency_code = 'UGX';
            }
            
            if (empty($journalEntry->exchange_rate)) {
                $journalEntry->exchange_rate = 1.0000;
            }
            
            if (empty($journalEntry->status)) {
                $journalEntry->status = self::STATUS_DRAFT;
            }
        });

        static::updating(function ($journalEntry) {
            // Prevent editing of posted entries
            if ($journalEntry->getOriginal('status') === self::STATUS_POSTED) {
                // Allow ONLY if we are NOT changing any financial data
                // In some cases, reversing might update a reference, but usually, it's safer to block all.
                // If we want to allow reversing status change specifically, we could check that.
                // But generally, once posted, it's locked.
                if ($journalEntry->isDirty(['total_amount', 'date', 'entry_number', 'hotel_id'])) {
                    Log::warning('Unauthorized attempt to modify posted journal entry financials', [
                        'entry_id' => $journalEntry->id,
                        'user_id' => auth()->id()
                    ]);
                    throw new \Exception('Financial data of a posted journal entry cannot be modified.');
                }
                
                // If status is being changed FROM posted to something else, block it.
                if ($journalEntry->isDirty('status') && $journalEntry->status !== self::STATUS_POSTED) {
                    throw new \Exception('A posted journal entry cannot be moved back to draft or cancelled. Use reversal instead.');
                }
            }
        });

        static::updated(function ($journalEntry) {
            // When shifting status to POSTED, recalculate balances for all items
            if ($journalEntry->isDirty('status') && $journalEntry->status === self::STATUS_POSTED) {
                foreach ($journalEntry->items as $item) {
                    if ($item->account) {
                        $item->account->updateBalanceFromItem($item);
                    }
                }
            }
        });

        static::deleting(function ($journalEntry) {
            if ($journalEntry->getOriginal('status') === self::STATUS_POSTED) {
                Log::error('Unauthorized attempt to delete posted journal entry', [
                    'entry_id' => $journalEntry->id,
                    'user_id' => auth()->id()
                ]);
                throw new \Exception('A posted journal entry cannot be deleted. It must be reversed.');
            }
            
            // Delete related items
            $journalEntry->items()->delete();
        });
    }
}