<?php

namespace App\Livewire\Admin;

use App\Domain\Financial\Services\AccountAnomalyScanner;
use App\Domain\Shared\Support\ArabicSearch;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;

/**
 * Every account that needs a human decision, worst first — and what to do
 * about each one.
 *
 * The alternative — and what actually happened on the first academy to go live
 * — is that nobody knows a file is wrong until a parent argues at the desk. By
 * then the money is months old and the person who took it has gone home. This
 * screen reads the same books the roster reads and says: these are the players
 * whose file does not add up, this is why, and this is how it is handled.
 *
 * The guidance is on the screen rather than in a manual because the operator
 * here is a receptionist mid-shift, not an accountant with time to read. A case
 * they do not understand is a case they skip, and a skipped case is money.
 *
 * Read-only. Every row links into the settlement wizard, where a person decides
 * what actually happened before anything is written.
 */
#[Layout('layouts.app')]
#[Title('حالات تحتاج تسوية')]
class SettlementWorklist extends Component
{
    #[Url(as: 'case')]
    public string $caseFilter = '';

    #[Url(as: 'q')]
    public string $search = '';

    /**
     * The row whose full detail is open.
     *
     * Locked: it only ever names a row this render already produced, and the
     * detail it reveals is the participant's own money. Letting the browser set
     * it would be handing it an id to point at.
     */
    #[Locked]
    public ?int $expanded = null;

    public function mount(): void
    {
        $this->authorize('settlements.manage');
    }

    public function clearFilter(): void
    {
        $this->caseFilter = '';
        $this->search = '';
        $this->expanded = null;
    }

    public function selectCase(string $case): void
    {
        // Selecting the open case again closes it. A filter you can only escape
        // by finding the "all" button is a filter operators leave switched on.
        $this->caseFilter = $this->caseFilter === $case ? '' : $case;
        $this->expanded = null;
    }

    public function toggleRow(int $participantId): void
    {
        $this->expanded = $this->expanded === $participantId ? null : $participantId;
    }

    /**
     * The scan is a handful of grouped queries over the branch's own rows, not
     * a query per participant — an academy of a few hundred players is one
     * page-load, and the branch scope on Participant is what keeps it to this
     * branch.
     *
     * Scanned unfiltered, then narrowed here. The scanner's own `only` argument
     * would drop the other cases from the result entirely, and the case rail
     * would then show a count of zero beside every case except the selected one
     * — which reads as "these are fixed" rather than "these are hidden".
     *
     * @return array{all:array<int,array>, shown:array<int,array>, counts:array<string,int>}
     */
    private function scan(): array
    {
        $all = app(AccountAnomalyScanner::class)->scan(branchId: null);

        $counts = [];
        foreach ($all as $row) {
            foreach ($row['cases'] as $case) {
                $counts[$case] = ($counts[$case] ?? 0) + 1;
            }
        }

        $term = trim($this->search);
        $shown = $all;

        if ($this->caseFilter !== '') {
            $shown = array_values(array_filter(
                $shown,
                fn ($row) => in_array($this->caseFilter, $row['cases'], true)
            ));
        }

        if ($term !== '') {
            $shown = array_values(array_filter($shown, function ($row) use ($term) {
                $person = $row['participant']->person;

                // Same folding the wizard's search uses, so a name found there
                // is found here too.
                return ArabicSearch::matches($person?->name_ar, $term)
                    || ArabicSearch::matches($person?->name, $term)
                    || str_contains((string) $person?->phone, ArabicSearch::normalise($term));
            }));
        }

        return ['all' => $all, 'shown' => $shown, 'counts' => $counts];
    }

    /** The worklist as a spreadsheet, for the people who work off paper. */
    public function export()
    {
        $this->authorize('settlements.manage');

        $rows = $this->scan()['shown'];
        $cases = AccountAnomalyScanner::CASES;

        return response()->streamDownload(function () use ($rows, $cases) {
            $out = fopen('php://output', 'w');
            // BOM so Excel opens Arabic correctly instead of as mojibake.
            fwrite($out, "\xEF\xBB\xBF");
            fputcsv($out, ['رقم المشترك', 'الاسم', 'الهاتف', 'البرنامج', 'الحالات', 'فواتير غير مسددة', 'المستحق (ج.م)', 'المحصَّل (ج.م)', 'زيادة محاسَبة (ج.م)']);

            foreach ($rows as $row) {
                $participant = $row['participant'];

                // Money the books are asking for twice. It belongs in the export
                // for the same reason it belongs on the screen: it is the one
                // figure on the row that is not a debt.
                $overBilled = array_sum(array_column($row['detail']['over_billed_bundle'] ?? [], 'over_billed'));

                fputcsv($out, [
                    $participant->id,
                    $participant->person?->name_ar,
                    $participant->person?->phone,
                    // A player whose enrolment was destroyed has no programme to
                    // read off the relation; his last invoice line is the only
                    // surviving record of which one it was.
                    $participant->enrollments->first()?->program?->name_ar
                        ?? ($row['detail']['no_enrollment']['last_program'] ?? null),
                    implode(' / ', array_map(fn ($c) => $cases[$c]['label'], $row['cases'])),
                    $row['unpaid_invoices'],
                    number_format($row['owed'] / 100, 2, '.', ''),
                    number_format($row['paid'] / 100, 2, '.', ''),
                    $overBilled > 0 ? number_format($overBilled / 100, 2, '.', '') : '',
                ]);
            }

            fclose($out);
        }, 'settlement-worklist-' . now()->format('Y-m-d') . '.csv', [
            'Content-Type' => 'text/csv; charset=UTF-8',
        ]);
    }

    public function render()
    {
        $scan = $this->scan();

        // Money the books are asking for twice, across every flagged account.
        // It sits beside the debt in the header because it is the one headline
        // figure that is owed BY the academy, and averaging it into "المستحق"
        // is how an over-billing goes unnoticed for a season.
        $overBilled = 0;
        foreach ($scan['all'] as $row) {
            $overBilled += array_sum(array_column($row['detail']['over_billed_bundle'] ?? [], 'over_billed'));
        }

        return view('livewire.admin.settlement-worklist', [
            'rows' => $scan['shown'],
            'total' => count($scan['all']),
            'counts' => $scan['counts'],
            'cases' => AccountAnomalyScanner::CASES,
            'guide' => AccountAnomalyScanner::CASES[$this->caseFilter] ?? null,
            'totalOwed' => array_sum(array_column($scan['all'], 'owed')),
            'overBilled' => $overBilled,
        ]);
    }
}
