<?php

namespace App\Domain\Financial\Services;

use App\Domain\Financial\Models\FinancialAccount;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Shared\Exceptions\DomainException;

/**
 * Resolves chart-of-accounts rows by CODE, scoped to an academy.
 *
 * Before this existed, PaymentService::resolveDebitAccount() and
 * resolveCreditAccount() returned the literal ids 1 and 2 with a `// TODO`.
 * Seeder order made that "Dr Cash / Cr Bank" on every payment ever recorded:
 * no revenue account was ever credited, no receivable ever cleared, and
 * FinancialOverview::getRevenueBySource() — which groups transactions by
 * credit_account_id restricted to revenue accounts — therefore always
 * returned an empty array.
 *
 * Resolution is by code because ids differ per tenant (each client DB seeded
 * its own chart) and hard-fails when the account is absent, following the
 * precedent already set by PayrollService::createPayslipTransaction().
 */
class LedgerAccountResolver
{
    /** Assets — where money lands. */
    public const CASH = '1000';
    public const BANK = '1010';
    public const RECEIVABLE = '1100';
    public const WALLET = '1200';

    /** Revenue — what the money was for. */
    public const REVENUE_TRAINING = '4000';
    public const REVENUE_REGISTRATION = '4010';
    public const REVENUE_EQUIPMENT = '4020';
    public const REVENUE_FACILITY = '4030';
    public const REVENUE_PRIVATE = '4040';
    public const REVENUE_TOURNAMENT = '4050';
    public const REVENUE_OTHER = '4060';

    /** @var array<string, FinancialAccount> keyed "{academyId}:{code}" */
    private array $cache = [];

    public function byCode(int $academyId, string $code): FinancialAccount
    {
        $key = "{$academyId}:{$code}";

        if (isset($this->cache[$key])) {
            return $this->cache[$key];
        }

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

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

        return $this->cache[$key] = $account;
    }

    /**
     * The asset account a payment settles into, from its method.
     *
     * `wallet` means the academy's own internal wallet, not mobile money —
     * settling from it moves the balance held for the member, not cash.
     */
    public function assetAccountForMethod(int $academyId, ?string $method): FinancialAccount
    {
        $code = match ($method) {
            'cash' => self::CASH,
            'wallet' => self::WALLET,
            'card', 'bank_transfer', 'instapay', 'cheque', 'online' => self::BANK,
            default => self::CASH,
        };

        return $this->byCode($academyId, $code);
    }

    /**
     * Split an amount across revenue accounts in proportion to the invoice's
     * own line composition, so getRevenueBySource() reports what was sold.
     *
     * Integer arithmetic only: every share is floored with intdiv() and the
     * remainder is given to the last row, per the money rules.
     *
     * @return list<array{account: FinancialAccount, amount: int}>
     */
    public function splitRevenue(int $academyId, ?Invoice $invoice, int $amount): array
    {
        if ($amount <= 0) {
            return [];
        }

        $lines = $invoice
            ? $invoice->items()->get(['itemable_type', 'total_amount'])
            : collect();

        $byCode = [];

        foreach ($lines as $line) {
            $code = $this->revenueCodeForItemable($line->itemable_type);
            $byCode[$code] = ($byCode[$code] ?? 0) + max(0, (int) $line->total_amount);
        }

        $base = array_sum($byCode);

        // No invoice, no lines, or a zero-value invoice: everything lands in
        // one bucket rather than being silently dropped from the ledger.
        if ($base <= 0) {
            return [[
                'account' => $this->byCode($academyId, $invoice ? self::REVENUE_TRAINING : self::REVENUE_OTHER),
                'amount' => $amount,
            ]];
        }

        arsort($byCode);

        $split = [];
        $allocated = 0;
        $codes = array_keys($byCode);
        $last = array_key_last($codes);

        foreach ($codes as $i => $code) {
            $share = $i === $last
                ? $amount - $allocated          // remainder to the last row
                : intdiv($amount * $byCode[$code], $base);

            $allocated += $share;

            if ($share > 0) {
                $split[] = ['account' => $this->byCode($academyId, $code), 'amount' => $share];
            }
        }

        return $split;
    }

    /**
     * An invoice line with no itemable is a programme subscription — that is
     * the convention ParticipantBillingService and every revenue widget read.
     */
    public function revenueCodeForItemable(?string $itemableType): string
    {
        return match ($itemableType) {
            null => self::REVENUE_TRAINING,
            \App\Domain\Inventory\Models\Product::class => self::REVENUE_EQUIPMENT,
            \App\Domain\Inventory\Models\Kit::class => self::REVENUE_EQUIPMENT,
            \App\Domain\Event\Models\Event::class => self::REVENUE_TOURNAMENT,
            \App\Domain\Facility\Models\SpaceReservation::class => self::REVENUE_FACILITY,
            default => self::REVENUE_OTHER,
        };
    }
}
