<?php

namespace App\Services;

use App\Models\AccountingEntry;
use App\Models\Branch;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\FinancialAccount;
use App\Models\FinancialAccountTransaction;
use App\Models\RecurringExpense;
use App\Models\User;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;

class ExpenseService
{
    public function __construct(private MoneyMath $money) {}

    public function createPending(array $data, User $user, ?RecurringExpense $recurring = null, ?CarbonInterface $occurrenceDate = null): Expense
    {
        $amountMinor = $this->money->toMinor((string) $data['amount']);
        if ($amountMinor <= 0) {
            throw ValidationException::withMessages(['amount' => 'Expense amount must be greater than zero.']);
        }

        return DB::transaction(function () use ($data, $user, $recurring, $occurrenceDate, $amountMinor): Expense {
            $branch = Branch::with('business')->findOrFail($data['branch_id']);
            $category = ExpenseCategory::findOrFail($data['expense_category_id']);
            $account = FinancialAccount::findOrFail($data['financial_account_id']);

            if ($category->business_id !== $branch->business_id) {
                throw ValidationException::withMessages(['expense_category_id' => 'The selected expense category does not belong to this business.']);
            }
            if ($category->status !== 'active') {
                throw ValidationException::withMessages(['expense_category_id' => 'The selected expense category is inactive.']);
            }
            if ($account->branch_id !== $branch->id) {
                throw ValidationException::withMessages(['financial_account_id' => 'The paid-from account must belong to the selected branch.']);
            }
            if ($account->status !== 'active') {
                throw ValidationException::withMessages(['financial_account_id' => 'The paid-from account must be active.']);
            }
            if ($recurring && ($recurring->branch_id !== $branch->id || $recurring->business_id !== $branch->business_id)) {
                throw ValidationException::withMessages(['recurring_expense_id' => 'The recurring expense does not belong to this branch.']);
            }

            $date = $data['expense_date'] instanceof CarbonInterface
                ? $data['expense_date']
                : \Carbon\CarbonImmutable::parse((string) $data['expense_date']);

            return Expense::create([
                'expense_number' => $this->nextExpenseNumber($branch, $date),
                'branch_id' => $branch->id,
                'expense_category_id' => $category->id,
                'financial_account_id' => $account->id,
                'recurring_expense_id' => $recurring?->id,
                'occurrence_date' => $occurrenceDate?->toDateString(),
                'description' => $data['description'],
                'amount' => $this->money->fromMinor($amountMinor),
                'expense_date' => $date->toDateString(),
                'supplier' => $data['supplier'] ?? null,
                'notes' => $data['notes'] ?? null,
                'status' => 'pending',
                'entered_by' => $user->id,
            ]);
        });
    }

    public function approve(Expense $expense, User $approver, ?string $comments = null): Expense
    {
        Gate::forUser($approver)->authorize('decide', $expense);

        return DB::transaction(function () use ($expense, $approver, $comments): Expense {
            $locked = Expense::query()->lockForUpdate()->findOrFail($expense->id);
            if ($locked->status !== 'pending') {
                throw ValidationException::withMessages(['expense' => 'Only a pending expense can be approved.']);
            }

            $locked->loadMissing('branch.business', 'category');
            if ($locked->branch->business->maker_checker_enabled && $locked->entered_by === $approver->id) {
                throw ValidationException::withMessages(['expense' => 'Maker-checker is enabled. The employee who entered this expense cannot approve it.']);
            }

            $account = FinancialAccount::query()->lockForUpdate()->findOrFail($locked->financial_account_id);
            if ($account->branch_id !== $locked->branch_id) {
                throw ValidationException::withMessages(['financial_account_id' => 'The paid-from account no longer belongs to this branch.']);
            }
            if ($account->status !== 'active') {
                throw ValidationException::withMessages(['financial_account_id' => 'The paid-from account must be active when the expense is approved.']);
            }

            $amountMinor = $this->money->toMinor((string) $locked->amount);
            $balanceMinor = $this->money->toMinor((string) $account->current_balance);
            if ($balanceMinor < $amountMinor) {
                throw ValidationException::withMessages(['amount' => 'The selected account has insufficient funds for this expense.']);
            }

            $amount = $this->money->fromMinor($amountMinor);
            $newBalance = $this->money->fromMinor($balanceMinor - $amountMinor);
            $transactionNumber = $this->nextFinancialTransactionNumber($account->branch_id);
            $reference = $locked->expense_number;
            $description = 'Expense '.$locked->expense_number.' — '.$locked->description;

            $account->update(['current_balance' => $newBalance, 'updated_by' => $approver->id]);
            $transaction = FinancialAccountTransaction::create([
                'transaction_number' => $transactionNumber,
                'account_id' => $account->id,
                'branch_id' => $account->branch_id,
                'transaction_date' => $locked->expense_date,
                'reference' => $reference,
                'type' => 'expense',
                'description' => $description,
                'money_in' => '0.00',
                'money_out' => $amount,
                'running_balance' => $newBalance,
                'user_id' => $approver->id,
                'related_type' => Expense::class,
                'related_id' => $locked->id,
            ]);

            $journal = 'JRN-'.$locked->expense_number;
            AccountingEntry::create([
                'journal_number' => $journal,
                'branch_id' => $locked->branch_id,
                'financial_account_id' => $account->id,
                'entry_date' => $locked->expense_date,
                'reference' => $reference,
                'category' => 'assets',
                'event_type' => 'expense_payment',
                'description' => $description,
                'debit' => '0.00',
                'credit' => $amount,
                'user_id' => $approver->id,
                'related_type' => Expense::class,
                'related_id' => $locked->id,
            ]);
            AccountingEntry::create([
                'journal_number' => $journal,
                'branch_id' => $locked->branch_id,
                'financial_account_id' => null,
                'entry_date' => $locked->expense_date,
                'reference' => $reference,
                'category' => 'expenses',
                'event_type' => 'expense_payment',
                'description' => $locked->category->name.' — '.$locked->description,
                'debit' => $amount,
                'credit' => '0.00',
                'user_id' => $approver->id,
                'related_type' => Expense::class,
                'related_id' => $locked->id,
            ]);

            $locked->update([
                'status' => 'approved',
                'approved_by' => $approver->id,
                'approved_at' => now(),
                'decision_comments' => $comments,
                'financial_account_transaction_id' => $transaction->id,
                'accounting_journal_number' => $journal,
            ]);

            return $locked->fresh(['branch','category','financialAccount','enteredBy','approver','accountTransaction']);
        });
    }

    public function reject(Expense $expense, User $rejecter, string $comments): Expense
    {
        Gate::forUser($rejecter)->authorize('decide', $expense);

        return DB::transaction(function () use ($expense, $rejecter, $comments): Expense {
            $locked = Expense::query()->lockForUpdate()->findOrFail($expense->id);
            if ($locked->status !== 'pending') {
                throw ValidationException::withMessages(['expense' => 'Only a pending expense can be rejected.']);
            }
            $locked->update([
                'status' => 'rejected',
                'rejected_by' => $rejecter->id,
                'rejected_at' => now(),
                'decision_comments' => $comments,
            ]);
            return $locked->fresh(['branch','category','financialAccount','enteredBy','rejecter']);
        });
    }

    private function nextExpenseNumber(Branch $branch, CarbonInterface $date): string
    {
        $year = (int) $date->format('Y');
        DB::table('number_sequences')->insertOrIgnore([
            'type' => 'expense', 'branch_id' => $branch->id, 'year' => $year, 'current_value' => 0,
            'created_at' => now(), 'updated_at' => now(),
        ]);
        $row = DB::table('number_sequences')->where('type','expense')->where('branch_id',$branch->id)->where('year',$year)->lockForUpdate()->first();
        $next = ((int) $row->current_value) + 1;
        DB::table('number_sequences')->where('id',$row->id)->update(['current_value'=>$next,'updated_at'=>now()]);
        $tag = strtoupper(preg_replace('/[^A-Z0-9]/','',str_replace('BR-','',$branch->code ?? '')) ?: (string) $branch->id);
        return 'EXP-'.$tag.'-'.$year.'-'.str_pad((string) $next, 6, '0', STR_PAD_LEFT);
    }

    private function nextFinancialTransactionNumber(int $branchId): string
    {
        $year = (int) now()->format('Y');
        DB::table('number_sequences')->insertOrIgnore([
            'type'=>'financial_transaction','branch_id'=>$branchId,'year'=>$year,'current_value'=>0,'created_at'=>now(),'updated_at'=>now(),
        ]);
        $row = DB::table('number_sequences')->where('type','financial_transaction')->where('branch_id',$branchId)->where('year',$year)->lockForUpdate()->first();
        $next = ((int) $row->current_value) + 1;
        DB::table('number_sequences')->where('id',$row->id)->update(['current_value'=>$next,'updated_at'=>now()]);
        $branch = Branch::findOrFail($branchId);
        $tag = strtoupper(preg_replace('/[^A-Z0-9]/','',str_replace('BR-','',$branch->code ?? '')) ?: (string) $branchId);
        return 'FTX-'.$tag.'-'.$year.'-'.str_pad((string) $next, 6, '0', STR_PAD_LEFT);
    }
}