<?php

namespace App\Domain\Financial\Services;

use App\Domain\Financial\Enums\TransactionType;
use App\Domain\Financial\Models\Expense;
use App\Domain\Financial\Models\FacilityRentPayment;
use App\Domain\Financial\Models\FinancialAccount;
use App\Domain\Financial\Models\Transaction;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

class ExpenseService
{
    public function recordRentPayment(array $data, User $actor): FacilityRentPayment
    {
        return DB::transaction(function () use ($data, $actor) {
            $academyId = app('current_academy')?->id ?? $actor->academy_id;

            $rentPayment = FacilityRentPayment::create([
                'academy_id' => $academyId,
                'branch_id' => $data['branch_id'] ?? null,
                'facility_id' => $data['facility_id'],
                'period' => $data['period'],
                'amount' => $data['amount'],
                'payment_method' => $data['payment_method'],
                'cheque_number' => $data['cheque_number'] ?? null,
                'cheque_date' => $data['cheque_date'] ?? null,
                'bank_name' => $data['bank_name'] ?? null,
                'recipient_name' => $data['recipient_name'] ?? null,
                'payment_date' => $data['payment_date'],
                'notes' => $data['notes'] ?? null,
                'created_by' => $actor->id,
            ]);

            $this->createExpenseTransaction(
                academyId: $academyId,
                expenseAccountCode: '5010',
                paymentMethod: $data['payment_method'],
                amount: $data['amount'],
                description: "إيجار منشأة: {$data['period']}",
                referenceModel: $rentPayment,
                actor: $actor,
                transactionDate: $data['payment_date'],
                branchId: $rentPayment->branch_id,
            );

            return $rentPayment;
        });
    }

    public function recordExpense(array $data, User $actor): Expense
    {
        return DB::transaction(function () use ($data, $actor) {
            $academyId = app('current_academy')?->id ?? $actor->academy_id;

            $expense = Expense::create([
                'academy_id' => $academyId,
                'branch_id' => $data['branch_id'] ?? null,
                'category' => $data['category'],
                'amount' => $data['amount'],
                'description' => $data['description'],
                'recipient_name' => $data['recipient_name'] ?? null,
                'payment_method' => $data['payment_method'],
                'receipt_reference' => $data['receipt_reference'] ?? null,
                'expense_date' => $data['expense_date'],
                'notes' => $data['notes'] ?? null,
                'created_by' => $actor->id,
                ...$this->attachmentAttributes($data, $actor),
            ]);

            $expenseAccountCode = $this->resolveExpenseAccountCode($data['category']);

            $this->createExpenseTransaction(
                academyId: $academyId,
                expenseAccountCode: $expenseAccountCode,
                paymentMethod: $data['payment_method'],
                amount: $data['amount'],
                description: $data['description'],
                referenceModel: $expense,
                actor: $actor,
                transactionDate: $data['expense_date'],
                branchId: $expense->branch_id,
            );

            return $expense;
        });
    }

    public function recordExternalRevenue(array $data, User $actor): Expense
    {
        return DB::transaction(function () use ($data, $actor) {
            $academyId = app('current_academy')?->id ?? $actor->academy_id;

            $expense = Expense::create([
                'academy_id' => $academyId,
                'branch_id' => $data['branch_id'] ?? null,
                'category' => 'external_revenue',
                'amount' => $data['amount'],
                'description' => $data['description'],
                'recipient_name' => $data['source'] ?? null,
                'payment_method' => $data['payment_method'],
                'receipt_reference' => $data['reference_number'] ?? null,
                'expense_date' => $data['revenue_date'],
                'notes' => $data['notes'] ?? null,
                'created_by' => $actor->id,
                ...$this->attachmentAttributes($data, $actor),
            ]);

            $revenueAccount = FinancialAccount::where('academy_id', $academyId)
                ->where('code', '4060')
                ->first();

            if (!$revenueAccount) {
                throw new DomainException('حساب الإيرادات الخارجية غير موجود — يرجى إعداد شجرة الحسابات');
            }

            $cashAccountCode = match ($data['payment_method']) {
                'bank_transfer', 'cheque' => '1010',
                default => '1000',
            };

            $cashAccount = FinancialAccount::where('academy_id', $academyId)
                ->where('code', $cashAccountCode)
                ->first();

            if (!$cashAccount) {
                throw new DomainException('حساب النقدية/البنك غير موجود — يرجى إعداد شجرة الحسابات');
            }

            Transaction::create([
                'academy_id' => $academyId,
                // The ledger row belongs to the branch of the document it
                // records, not to whatever branch the writer happened to have
                // selected — and on a queued or console write there is no
                // selected branch at all, which would leave the entry filed
                // against none and missing from every branch's P&L.
                'branch_id' => $expense->branch_id,
                'debit_account_id' => $cashAccount->id,
                'credit_account_id' => $revenueAccount->id,
                'reference_type' => get_class($expense),
                'reference_id' => $expense->id,
                'amount' => $data['amount'],
                'currency' => 'EGP',
                'type' => TransactionType::PaymentReceived,
                'description' => $data['description'],
                'transaction_date' => $data['revenue_date'],
                'created_by' => $actor->id,
            ]);

            return $expense;
        });
    }

    /**
     * Attach (or replace) the scan of the paper receipt on an expense that is
     * already in the ledger.
     *
     * The caller has already moved the upload onto a disk — this records where
     * it went and who put it there, and cleans up the file it displaced.
     *
     * @param  array{path: string, name: string, disk: string, mime: ?string, size: ?int}  $file
     */
    public function attachReceipt(Expense $expense, array $file, User $actor): Expense
    {
        if ($expense->isCancelled()) {
            throw new DomainException('لا يمكن إرفاق مستند بمصروف ملغى');
        }

        $previousPath = $expense->attachment_path;
        $previousDisk = $expense->attachment_path ? $expense->attachmentDiskName() : null;

        $expense->update([
            'attachment_path' => $file['path'],
            'attachment_name' => $file['name'],
            'attachment_disk' => $file['disk'],
            'attachment_mime' => $file['mime'] ?? null,
            'attachment_size' => $file['size'] ?? null,
            'attachment_uploaded_by' => $actor->id,
            'attachment_uploaded_at' => now(),
        ]);

        if ($previousPath && $previousPath !== $file['path']) {
            $this->deleteFile($previousDisk, $previousPath);
        }

        return $expense->refresh();
    }

    public function removeReceipt(Expense $expense, User $actor): Expense
    {
        if (!$expense->hasAttachment()) {
            throw new DomainException('لا يوجد مستند مرفق بهذا المصروف');
        }

        if ($expense->isCancelled()) {
            throw new DomainException('لا يمكن تعديل مرفقات مصروف ملغى');
        }

        $path = $expense->attachment_path;
        $disk = $expense->attachmentDiskName();

        $expense->update([
            'attachment_path' => null,
            'attachment_name' => null,
            'attachment_disk' => null,
            'attachment_mime' => null,
            'attachment_size' => null,
            'attachment_uploaded_by' => null,
            'attachment_uploaded_at' => null,
        ]);

        $this->deleteFile($disk, $path);

        return $expense->refresh();
    }

    public function cancelExpense(Expense $expense, string $reason, User $actor): Expense
    {
        if ($expense->status === 'cancelled') {
            throw new DomainException('هذا المصروف ملغى بالفعل');
        }

        return DB::transaction(function () use ($expense, $reason, $actor) {
            $expense->update([
                'status' => 'cancelled',
                'cancelled_by' => $actor->id,
                'cancelled_at' => now(),
                'cancellation_reason' => $reason,
            ]);

            // Read the original without the branch scope: the expense being
            // cancelled is already proven visible, and a reversing entry that
            // cannot find its original silently leaves the ledger unbalanced —
            // which is worse than reading one row from another branch. Entries
            // written before the backfill, or from a queue, carry no branch.
            $originalTransaction = Transaction::withoutBranchScope()
                ->where('reference_type', get_class($expense))
                ->where('reference_id', $expense->id)
                ->first();

            if ($originalTransaction) {
                Transaction::create([
                    'academy_id' => $expense->academy_id,
                    'branch_id' => $originalTransaction->branch_id ?? $expense->branch_id,
                    'debit_account_id' => $originalTransaction->credit_account_id,
                    'credit_account_id' => $originalTransaction->debit_account_id,
                    'reference_type' => get_class($expense),
                    'reference_id' => $expense->id,
                    'amount' => $expense->amount,
                    'currency' => 'EGP',
                    'type' => TransactionType::Adjustment,
                    'description' => "إلغاء مصروف: {$expense->description}",
                    'transaction_date' => now()->toDateString(),
                    'created_by' => $actor->id,
                ]);
            }

            return $expense;
        });
    }

    private function createExpenseTransaction(
        int $academyId,
        string $expenseAccountCode,
        string $paymentMethod,
        int $amount,
        string $description,
        $referenceModel,
        User $actor,
        string $transactionDate,
        ?int $branchId = null,
    ): void {
        $debitAccount = FinancialAccount::where('academy_id', $academyId)
            ->where('code', $expenseAccountCode)
            ->first();

        if (!$debitAccount) {
            throw new DomainException('حساب المصروفات غير موجود — يرجى إعداد شجرة الحسابات');
        }

        $creditAccountCode = match ($paymentMethod) {
            'cash' => '1000',
            'bank_transfer', 'cheque' => '1010',
            default => '1000',
        };

        $creditAccount = FinancialAccount::where('academy_id', $academyId)
            ->where('code', $creditAccountCode)
            ->first();

        if (!$creditAccount) {
            throw new DomainException('حساب النقدية/البنك غير موجود — يرجى إعداد شجرة الحسابات');
        }

        Transaction::create([
            'academy_id' => $academyId,
            'branch_id' => $branchId,
            'debit_account_id' => $debitAccount->id,
            'credit_account_id' => $creditAccount->id,
            'reference_type' => get_class($referenceModel),
            'reference_id' => $referenceModel->id,
            'amount' => $amount,
            'currency' => 'EGP',
            'type' => TransactionType::PaymentMade,
            'description' => $description,
            'transaction_date' => $transactionDate,
            'created_by' => $actor->id,
        ]);
    }

    /**
     * Only stamp the uploader when a file actually came with the request —
     * otherwise every expense without a receipt would claim one was attached.
     *
     * @return array<string, mixed>
     */
    private function attachmentAttributes(array $data, User $actor): array
    {
        if (empty($data['attachment_path'])) {
            return [];
        }

        return [
            'attachment_path' => $data['attachment_path'],
            'attachment_name' => $data['attachment_name'] ?? basename($data['attachment_path']),
            'attachment_disk' => $data['attachment_disk'] ?? 'local',
            'attachment_mime' => $data['attachment_mime'] ?? null,
            'attachment_size' => $data['attachment_size'] ?? null,
            'attachment_uploaded_by' => $actor->id,
            'attachment_uploaded_at' => now(),
        ];
    }

    /**
     * A missing file is not an error worth failing the write for — the row is
     * already correct and the orphan is harmless.
     */
    private function deleteFile(?string $disk, ?string $path): void
    {
        if (!$disk || !$path) {
            return;
        }

        try {
            Storage::disk($disk)->delete($path);
        } catch (\Throwable $e) {
            report($e);
        }
    }

    private function resolveExpenseAccountCode(string $category): string
    {
        return match ($category) {
            'maintenance' => '5030',
            'utilities' => '5030',
            'sports_equipment' => '5020',
            'supplies' => '5020',
            default => '5050',
        };
    }
}
