<?php

namespace App\Livewire\Financial;

use App\Domain\Financial\Enums\AccountType;
use App\Domain\Financial\Enums\AllocationType;
use App\Domain\Financial\Enums\RevenueSource;
use App\Domain\Financial\Enums\TaxMode;
use App\Domain\Financial\Models\FinancialAccount;
use App\Domain\Financial\Models\RevenueMapping;
use App\Domain\Financial\Models\RevenueMappingLine;
use App\Domain\Financial\Services\RevenueRouter;
use App\Domain\Financial\Services\RevenueRoutingService;
use App\Domain\Shared\Exceptions\DomainException;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Component;

/**
 * Revenue routing — the screen that answers "which account does this money go
 * into", for every kind of money the system takes.
 *
 * Everything an accountant can say here is said in one place: the destination
 * accounts and their shares, the tax treatment, and a preview that runs the
 * rule being typed through the very same engine that will post tonight's
 * collections. The preview matters more than it looks — a routing rule is
 * abstract until you watch a real number split, and a rule nobody can check is
 * a rule nobody will trust.
 */
#[Layout('layouts.app')]
#[Title('توجيه الإيرادات')]
class RevenueRouting extends Component
{
    /** Which source's rule is open in the editor. */
    #[Locked]
    public ?string $editingSource = null;

    /** Set only by chooseOverride()/editRule(); never from the browser. */
    #[Locked]
    public ?string $scopeType = null;

    #[Locked]
    public ?int $scopeId = null;

    public string $scopeLabel = '';

    public bool $isActive = true;
    public string $taxMode = 'none';
    public string $taxRateDisplay = '14';
    public ?int $taxAccountId = null;
    public string $notes = '';

    /** @var list<array{account_id: ?int, allocation_type: string, value: string, label: string}> */
    public array $lines = [];

    public string $previewAmount = '1000';

    public bool $showAccountForm = false;
    public string $newAccountCode = '';
    public string $newAccountName = '';
    public string $newAccountType = 'revenue';

    public bool $showOverridePicker = false;
    public string $overrideSearch = '';

    public function mount(): void
    {
        $this->authorize('revenue_routing.view');

        // `?source=product` opens that rule directly, so a link in a message
        // lands on the rule being argued about rather than on a list of nine.
        // editRule() re-validates the key; an unknown one simply opens nothing.
        if ($source = request()->query('source')) {
            $this->editRule((string) $source);
        }
    }

    /* ---------------------------------------------------------------- edit */

    /**
     * Open a rule for editing.
     *
     * The scope class is derived from the source rather than accepted from the
     * caller: this method is reachable from the browser, and a class name that
     * arrives over the wire is not something to hand to `::withoutGlobalScopes()`.
     * An id is all the caller gets to choose, and only for sources that can be
     * narrowed at all.
     */
    public function editRule(string $sourceKey, ?int $scopeId = null): void
    {
        $this->authorize('revenue_routing.view');

        $source = RevenueSource::tryFrom($sourceKey);

        if (! $source) {
            return;
        }

        $scopeType = $scopeId !== null ? $source->scopeModel() : null;

        if ($scopeId !== null && ! $scopeType) {
            return;
        }

        $this->resetValidation();
        $this->editingSource = $source->value;
        $this->scopeType = $scopeType;
        $this->scopeId = $scopeType ? $scopeId : null;
        $this->showOverridePicker = false;
        $this->showAccountForm = false;

        $mapping = $this->findMapping($source, $scopeType, $scopeId);

        $this->scopeLabel = $this->labelForScope($scopeType, $scopeId);
        $this->isActive = $mapping?->is_active ?? true;
        $this->taxMode = $mapping?->tax_mode->value ?? TaxMode::None->value;
        $this->taxRateDisplay = $mapping && $mapping->tax_rate_bp > 0
            ? rtrim(rtrim(number_format($mapping->taxRatePercent(), 2, '.', ''), '0'), '.')
            : '14';
        $this->taxAccountId = $mapping?->tax_account_id;
        $this->notes = (string) ($mapping?->notes ?? '');

        $this->lines = $mapping
            ? $mapping->lines->map(fn (RevenueMappingLine $line) => [
                'account_id' => $line->account_id,
                'allocation_type' => $line->allocation_type->value,
                'value' => match ($line->allocation_type) {
                    AllocationType::Percentage => rtrim(rtrim(number_format($line->percent(), 2, '.', ''), '0'), '.'),
                    AllocationType::Fixed => number_format($line->fixed_amount / 100, 2, '.', ''),
                    AllocationType::Remainder => '',
                },
                'label' => (string) $line->label,
            ])->values()->all()
            : [];

        // An override starts as a copy of the default it is overriding, so the
        // accountant edits a working rule instead of building one from nothing.
        if ($this->lines === []) {
            $this->lines = $this->linesFromDefault($source);
        }
    }

    public function closeEditor(): void
    {
        $this->editingSource = null;
        $this->scopeType = null;
        $this->scopeId = null;
        $this->scopeLabel = '';
        $this->lines = [];
        $this->showOverridePicker = false;
        $this->showAccountForm = false;
        $this->resetValidation();
    }

    public function addLine(): void
    {
        $this->lines[] = [
            'account_id' => null,
            'allocation_type' => AllocationType::Percentage->value,
            'value' => '',
            'label' => '',
        ];
    }

    public function removeLine(int $index): void
    {
        unset($this->lines[$index]);
        $this->lines = array_values($this->lines);
    }

    public function save(RevenueRoutingService $service): void
    {
        $this->authorize('revenue_routing.manage');

        $source = RevenueSource::tryFrom((string) $this->editingSource);

        if (! $source) {
            session()->flash('error', __('مصدر الإيراد غير معروف'));

            return;
        }

        try {
            $service->save(
                $this->academyId(),
                $source,
                [
                    'is_active' => $this->isActive,
                    'label' => $source->label(),
                    'tax_mode' => $this->taxMode,
                    'tax_rate_bp' => $this->taxRateBasisPoints(),
                    'tax_account_id' => $this->taxMode === TaxMode::None->value ? null : $this->taxAccountId,
                    'notes' => $this->notes ?: null,
                ],
                $this->linesForService(),
                auth()->id(),
                $this->scopeType,
                $this->scopeId,
            );

            session()->flash('success', __('تم حفظ قاعدة التوجيه — تسري على كل تحصيل من الآن'));
            $this->closeEditor();
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());
        }
    }

    public function deleteOverride(string $sourceKey, int $scopeId, RevenueRoutingService $service): void
    {
        $this->authorize('revenue_routing.manage');

        $source = RevenueSource::tryFrom($sourceKey);
        $scopeType = $source?->scopeModel();

        if (! $source || ! $scopeType) {
            return;
        }

        $mapping = $this->findMapping($source, $scopeType, $scopeId);

        if (! $mapping) {
            return;
        }

        try {
            $service->deleteOverride($mapping);
            session()->flash('success', __('تم حذف التخصيص — عاد الصنف للقاعدة الافتراضية'));
            $this->closeEditor();
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());
        }
    }

    /* ----------------------------------------------------------- overrides */

    public function openOverridePicker(): void
    {
        $this->authorize('revenue_routing.manage');
        $this->overrideSearch = '';
        $this->showOverridePicker = true;
    }

    public function chooseOverride(int $id): void
    {
        $this->authorize('revenue_routing.manage');

        $source = RevenueSource::tryFrom((string) $this->editingSource);
        $model = $source?->scopeModel();

        if (! $source || ! $model) {
            return;
        }

        // Confirmed against this academy before it is accepted: an id typed
        // into the request must not be able to attach a rule to another
        // tenant's product.
        $exists = $model::withoutGlobalScopes()
            ->where('academy_id', $this->academyId())
            ->whereKey($id)
            ->exists();

        if (! $exists) {
            return;
        }

        $this->editRule($source->value, $id);
    }

    /* ------------------------------------------------------- chart account */

    public function createAccount(RevenueRoutingService $service): void
    {
        $this->authorize('revenue_routing.manage');

        $this->validate([
            'newAccountCode' => 'required|string|max:20',
            'newAccountName' => 'required|string|max:120',
            'newAccountType' => 'required|in:revenue,liability,equity',
        ], [
            'newAccountCode.required' => 'رقم الحساب مطلوب',
            'newAccountName.required' => 'اسم الحساب مطلوب',
            'newAccountType.in' => 'نوع الحساب غير مسموح هنا',
        ]);

        try {
            $account = $service->createAccount($this->academyId(), [
                'code' => $this->newAccountCode,
                'name_ar' => $this->newAccountName,
                'name' => $this->newAccountName,
                'type' => $this->newAccountType,
            ]);

            $this->showAccountForm = false;
            $this->newAccountCode = '';
            $this->newAccountName = '';

            session()->flash('success', __('تم إنشاء الحساب ') . $account->code . ' — ' . $account->name_ar);
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());
        }
    }

    /* ------------------------------------------------------------- reading */

    public function render(RevenueRouter $router)
    {
        $academyId = $this->academyId();
        $accounts = $this->postableAccounts($academyId);

        return view('livewire.financial.revenue-routing', [
            'sources' => $this->sourceRows($academyId),
            'accounts' => $accounts,
            'taxAccounts' => $accounts->where('type', AccountType::Liability)->values(),
            'preview' => $this->preview($router, $academyId),
            'overrideCandidates' => $this->overrideCandidates($academyId),
            'canManage' => auth()->user()?->can('revenue_routing.manage') ?? false,
        ]);
    }

    /**
     * One row per revenue source: the rule, what it currently does, how many
     * items override it, and how much money has actually come through it.
     *
     * The money column is not decoration either. A rule with no history behind
     * it is a rule nobody needs to argue about; a rule sitting on two million
     * pounds is where the conversation starts.
     */
    private function sourceRows(int $academyId): array
    {
        $mappings = RevenueMapping::withoutGlobalScopes()
            ->with(['lines.account', 'taxAccount'])
            ->where('academy_id', $academyId)
            ->get()
            ->groupBy(fn (RevenueMapping $m) => $m->source_key->value);

        $volume = $this->volumeBySource($academyId);

        $rows = [];

        foreach (RevenueSource::catalogue() as $source) {
            $all = $mappings->get($source->value, collect());
            $overrides = $all->filter(fn (RevenueMapping $m) => $m->scope_type !== null)->values();

            $rows[] = [
                'source' => $source,
                'mapping' => $all->firstWhere('scope_type', null),
                'overrides' => $overrides,
                'scopeNames' => $this->scopeNames($source, $overrides),
                'volume' => $volume[$source->value] ?? ['count' => 0, 'total' => 0],
            ];
        }

        return $rows;
    }

    /**
     * Names for the items an override is written against, read in one query
     * and without the global scopes.
     *
     * A morphTo in the view would resolve each row separately and through the
     * branch scope, so a product from another branch would render as a rule
     * against nothing — which reads as a bug in the rule rather than in the
     * lookup.
     *
     * @return array<int, string>
     */
    private function scopeNames(RevenueSource $source, $overrides): array
    {
        $model = $source->scopeModel();

        if (! $model || $overrides->isEmpty()) {
            return [];
        }

        return $model::withoutGlobalScopes()
            ->where('academy_id', $this->academyId())
            ->whereIn('id', $overrides->pluck('scope_id'))
            ->get(['id', 'name', 'name_ar'])
            ->mapWithKeys(fn ($item) => [$item->id => (string) ($item->name_ar ?: $item->name)])
            ->all();
    }

    /**
     * What each source has actually collected, read off the invoice lines
     * themselves rather than the ledger — the ledger only started recording
     * revenue accounts after the account resolver was fixed, so it is silent
     * about everything sold before that.
     *
     * @return array<string, array{count: int, total: int}>
     */
    private function volumeBySource(int $academyId): array
    {
        $rows = DB::table('invoice_items')
            ->where('academy_id', $academyId)
            ->selectRaw('itemable_type, count(*) as line_count, coalesce(sum(total_amount), 0) as total')
            ->groupBy('itemable_type')
            ->get();

        $out = [];

        foreach ($rows as $row) {
            $key = RevenueSource::forItemable($row->itemable_type)->value;

            $out[$key] ??= ['count' => 0, 'total' => 0];
            $out[$key]['count'] += (int) $row->line_count;
            $out[$key]['total'] += (int) $row->total;
        }

        $fee = DB::table('invoices')
            ->where('academy_id', $academyId)
            ->whereNull('deleted_at')
            ->where('service_fee_amount', '>', 0)
            ->selectRaw('count(*) as line_count, coalesce(sum(service_fee_amount), 0) as total')
            ->first();

        if ($fee && (int) $fee->line_count > 0) {
            $out[RevenueSource::ServiceFee->value] = [
                'count' => (int) $fee->line_count,
                'total' => (int) $fee->total,
            ];
        }

        return $out;
    }

    /**
     * The entry the rule currently being edited would produce for the amount in
     * the preview box — built from the unsaved form state, run through the real
     * allocation engine.
     */
    private function preview(RevenueRouter $router, int $academyId): ?array
    {
        $source = RevenueSource::tryFrom((string) $this->editingSource);

        if (! $source) {
            return null;
        }

        $amount = (int) round(((float) str_replace(',', '', $this->previewAmount)) * 100);

        if ($amount <= 0) {
            return null;
        }

        $lines = $this->linesForService();

        if ($lines === []) {
            return null;
        }

        $accounts = FinancialAccount::withoutGlobalScopes()
            ->where('academy_id', $academyId)
            ->whereIn('id', array_column($lines, 'account_id'))
            ->get()
            ->keyBy('id');

        // A line pointing at nothing cannot be previewed; the save path will
        // reject it with a message that explains why.
        foreach ($lines as $line) {
            if (! $accounts->has($line['account_id'])) {
                return null;
            }
        }

        $mapping = new RevenueMapping([
            'academy_id' => $academyId,
            'source_key' => $source->value,
            'is_active' => $this->isActive,
            'tax_mode' => $this->taxMode,
            'tax_rate_bp' => $this->taxRateBasisPoints(),
            'tax_account_id' => $this->taxAccountId,
        ]);

        $mapping->setRelation('lines', collect($lines)->map(function (array $line, int $i) use ($accounts, $academyId) {
            $model = new RevenueMappingLine([
                'academy_id' => $academyId,
                'account_id' => $line['account_id'],
                'allocation_type' => $line['allocation_type'],
                'percentage_bp' => $line['percentage_bp'],
                'fixed_amount' => $line['fixed_amount'],
                'label' => $line['label'],
                'sort_order' => $i,
            ]);

            $model->setRelation('account', $accounts->get($line['account_id']));

            return $model;
        }));

        $mapping->setRelation('taxAccount', $this->taxMode === TaxMode::None->value
            ? null
            : FinancialAccount::withoutGlobalScopes()
                ->where('academy_id', $academyId)
                ->whereKey($this->taxAccountId)
                ->first());

        try {
            // Exclusive rules post the tax the invoice charged; in a preview
            // there is no invoice, so show what a correctly-taxed invoice
            // would have added on top of this amount.
            $chargedTax = $this->taxMode === TaxMode::Exclusive->value
                ? intdiv($amount * $this->taxRateBasisPoints(), 10000)
                : null;

            $allocation = $router->allocateWith($academyId, $mapping, $source, $amount, $chargedTax);
        } catch (DomainException $e) {
            return ['error' => $e->getMessage()];
        }

        return [
            'allocation' => $allocation->collapsed(),
            'balanced' => $allocation->isBalanced(),
        ];
    }

    private function overrideCandidates(int $academyId)
    {
        $source = RevenueSource::tryFrom((string) $this->editingSource);
        $model = $source?->scopeModel();

        if (! $this->showOverridePicker || ! $model) {
            return collect();
        }

        $taken = RevenueMapping::withoutGlobalScopes()
            ->where('academy_id', $academyId)
            ->where('source_key', $source->value)
            ->where('scope_type', $model)
            ->pluck('scope_id');

        $query = $model::withoutGlobalScopes()
            ->where('academy_id', $academyId)
            ->whereNotIn('id', $taken);

        if (trim($this->overrideSearch) !== '') {
            $term = '%' . trim($this->overrideSearch) . '%';
            $query->where(fn ($q) => $q->where('name_ar', 'like', $term)->orWhere('name', 'like', $term));
        }

        return $query->orderBy('name_ar')->limit(25)->get(['id', 'name', 'name_ar']);
    }

    private function postableAccounts(int $academyId)
    {
        return FinancialAccount::withoutGlobalScopes()
            ->where('academy_id', $academyId)
            ->where('is_active', true)
            ->whereIn('type', [
                AccountType::Revenue->value,
                AccountType::Liability->value,
                AccountType::Equity->value,
            ])
            ->orderBy('code')
            ->get(['id', 'code', 'name', 'name_ar', 'type']);
    }

    /* --------------------------------------------------------------- utils */

    private function findMapping(RevenueSource $source, ?string $scopeType, ?int $scopeId): ?RevenueMapping
    {
        return RevenueMapping::withoutGlobalScopes()
            ->with(['lines.account', 'taxAccount'])
            ->where('academy_id', $this->academyId())
            ->where('source_key', $source->value)
            ->when(
                $scopeType === null,
                fn ($q) => $q->whereNull('scope_type'),
                fn ($q) => $q->where('scope_type', $scopeType)->where('scope_id', $scopeId),
            )
            ->first();
    }

    /** @return list<array{account_id: ?int, allocation_type: string, value: string, label: string}> */
    private function linesFromDefault(RevenueSource $source): array
    {
        $default = $this->findMapping($source, null, null);

        if ($default && $default->lines->isNotEmpty()) {
            return $default->lines->map(fn (RevenueMappingLine $line) => [
                'account_id' => $line->account_id,
                'allocation_type' => $line->allocation_type->value,
                'value' => match ($line->allocation_type) {
                    AllocationType::Percentage => rtrim(rtrim(number_format($line->percent(), 2, '.', ''), '0'), '.'),
                    AllocationType::Fixed => number_format($line->fixed_amount / 100, 2, '.', ''),
                    AllocationType::Remainder => '',
                },
                'label' => (string) $line->label,
            ])->values()->all();
        }

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

        return [[
            'account_id' => $account?->id,
            'allocation_type' => AllocationType::Remainder->value,
            'value' => '',
            'label' => '',
        ]];
    }

    /**
     * Form state converted to what the service takes: percentages as basis
     * points, money as piasters. Nothing here decides anything — every rule
     * about what is allowed lives in the service, where the console and the
     * queue reach it too.
     *
     * @return list<array{account_id: int, allocation_type: string, percentage_bp: ?int, fixed_amount: ?int, label: ?string}>
     */
    private function linesForService(): array
    {
        $out = [];

        foreach ($this->lines as $line) {
            $type = AllocationType::tryFrom($line['allocation_type'] ?? '');
            $accountId = (int) ($line['account_id'] ?? 0);

            if (! $type || $accountId <= 0) {
                continue;
            }

            $value = (float) str_replace(',', '', (string) ($line['value'] ?? ''));

            $out[] = [
                'account_id' => $accountId,
                'allocation_type' => $type->value,
                'percentage_bp' => $type === AllocationType::Percentage ? (int) round($value * 100) : null,
                'fixed_amount' => $type === AllocationType::Fixed ? (int) round($value * 100) : null,
                'label' => ($line['label'] ?? null) ?: null,
            ];
        }

        return $out;
    }

    private function taxRateBasisPoints(): int
    {
        return (int) round(((float) str_replace(',', '', $this->taxRateDisplay)) * 100);
    }

    private function labelForScope(?string $scopeType, ?int $scopeId): string
    {
        if (! $scopeType || ! $scopeId) {
            return '';
        }

        $model = $scopeType::withoutGlobalScopes()
            ->where('academy_id', $this->academyId())
            ->whereKey($scopeId)
            ->first();

        return (string) ($model?->name_ar ?: $model?->name ?: '');
    }

    private function academyId(): int
    {
        return (int) app('current_academy')->id;
    }
}
