<?php

namespace App\Services;

use App\Models\Account;
use App\Models\FeePayment;
use App\Models\JournalEntry;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

/**
 * Double-entry accounting engine. Every transaction is a balanced JournalEntry
 * (Σ debit = Σ credit). Income/expense vouchers and fee collections all funnel
 * through postEntry().
 */
class AccountingService
{
    /**
     * Post a balanced journal entry.
     *
     * @param  array<int,array{account_id:int,debit?:float,credit?:float,narration?:string}>  $lines
     */
    public function postEntry(int $branchId, string $date, string $description, array $lines, string $type = 'journal', array $opts = []): JournalEntry
    {
        $debit = round(array_sum(array_map(fn ($l) => (float) ($l['debit'] ?? 0), $lines)), 2);
        $credit = round(array_sum(array_map(fn ($l) => (float) ($l['credit'] ?? 0), $lines)), 2);

        if ($debit <= 0 || $debit !== $credit) {
            throw ValidationException::withMessages([
                'lines' => "Entry is not balanced (debit {$debit} ≠ credit {$credit}).",
            ]);
        }

        return DB::transaction(function () use ($branchId, $date, $description, $lines, $type, $opts) {
            $entry = JournalEntry::create([
                'branch_id' => $branchId,
                'entry_number' => JournalEntry::nextNumber($branchId),
                'date' => $date,
                'type' => $type,
                'reference' => $opts['reference'] ?? null,
                'description' => $description,
                'source_type' => $opts['source_type'] ?? null,
                'source_id' => $opts['source_id'] ?? null,
            ]);

            foreach ($lines as $line) {
                if ((float) ($line['debit'] ?? 0) <= 0 && (float) ($line['credit'] ?? 0) <= 0) {
                    continue;
                }
                $entry->lines()->create([
                    'account_id' => $line['account_id'],
                    'debit' => $line['debit'] ?? 0,
                    'credit' => $line['credit'] ?? 0,
                    'narration' => $line['narration'] ?? null,
                ]);
            }

            return $entry;
        });
    }

    /** Income voucher: Debit a cash/bank account, Credit an income account. */
    public function recordIncome(int $branchId, int $incomeAccountId, int $depositAccountId, float $amount, string $date, string $description, ?string $reference = null): JournalEntry
    {
        return $this->postEntry($branchId, $date, $description, [
            ['account_id' => $depositAccountId, 'debit' => $amount, 'narration' => $description],
            ['account_id' => $incomeAccountId, 'credit' => $amount, 'narration' => $description],
        ], 'income', ['reference' => $reference]);
    }

    /** Expense voucher: Debit an expense account, Credit a cash/bank account. */
    public function recordExpense(int $branchId, int $expenseAccountId, int $payAccountId, float $amount, string $date, string $description, ?string $reference = null): JournalEntry
    {
        return $this->postEntry($branchId, $date, $description, [
            ['account_id' => $expenseAccountId, 'debit' => $amount, 'narration' => $description],
            ['account_id' => $payAccountId, 'credit' => $amount, 'narration' => $description],
        ], 'expense', ['reference' => $reference]);
    }

    /** Auto-post a fee collection: Debit Cash/Bank, Credit Fee Income. */
    public function postFeePayment(FeePayment $payment): ?JournalEntry
    {
        $branchId = $payment->branch_id;

        // 1. Prefer the payment method's mapped ledger account (per-channel tracking).
        $deposit = null;
        if ($payment->payment_method_id) {
            $pm = \App\Models\PaymentMethod::withoutGlobalScopes()->find($payment->payment_method_id);
            if ($pm && $pm->account_id) {
                $deposit = Account::withoutGlobalScopes()->find($pm->account_id);
            }
        }
        // 2. Fall back to channel: cash → Cash account, everything else → Bank.
        if (! $deposit) {
            $cashBank = in_array($payment->method, ['cash'], true) ? 'cash' : 'bank';
            $deposit = Account::withoutGlobalScopes()->where('branch_id', $branchId)->where('cash_bank', $cashBank)->first();
        }

        $feeIncome = Account::withoutGlobalScopes()->where('branch_id', $branchId)->where('code', '4001')->first();

        if (! $deposit || ! $feeIncome) {
            return null; // chart of accounts not set up for this branch
        }

        return $this->postEntry(
            $branchId,
            (string) $payment->payment_date->toDateString(),
            'Fee collection — '.($payment->receipt_number ?? '').' (challan #'.$payment->fee_challan_id.')',
            [
                ['account_id' => $deposit->id, 'debit' => $payment->amount, 'narration' => 'Fee received'],
                ['account_id' => $feeIncome->id, 'credit' => $payment->amount, 'narration' => 'Fee income'],
            ],
            'fee',
            ['source_type' => FeePayment::class, 'source_id' => $payment->id, 'reference' => $payment->receipt_number]
        );
    }

    /** Remove any journal entries auto-generated from a source model. */
    public function reverseSource(Model $source): void
    {
        JournalEntry::withoutGlobalScopes()
            ->where('source_type', $source::class)
            ->where('source_id', $source->getKey())
            ->each(fn ($e) => $e->delete());
    }
}
