<?php

namespace App\Livewire\Portal;

use App\Domain\Identity\Models\Person;
use App\Domain\Identity\Models\PortalInvitation;
use App\Domain\Identity\Services\PortalInvitationService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\WhatsApp\Services\WhatsAppService;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;

/**
 * Staff issue portal invitations from here.
 *
 * The raw token exists exactly once, in the response to the click that created
 * it — after that only its SHA-256 is stored and nobody, staff included, can
 * recover the link. Re-inviting revokes the previous one rather than leaving
 * two working links in the wild.
 *
 * This is a **staff** screen that happens to live in the Portal namespace, so
 * unlike everything else here it is branch-enforced: its route is
 * `portal-invitations.index`, which does not match the `portal.*` pattern in
 * config/branch_scope.php. That enforcement reaches it through exactly one
 * model — Guardian, which carries BranchScope in shared mode — and every query
 * on this screen is routed through `guardians()` so that it actually applies.
 * `people` and `portal_invitations` have no branch_id of their own, so a
 * findOrFail() on an id out of a wire:click was narrowed by nothing at all:
 * staff at one branch could mint, WhatsApp and revoke live portal credentials
 * for another branch's guardians.
 *
 * Shared mode is the right reading here and not a weakening. A guardian whose
 * children train at two branches keeps `branch_id NULL` by design (see the
 * backfill in 2026_09_13_000001), because both desks legitimately serve that
 * family — hard-filtering would leave them invitable from neither.
 */
#[Layout('layouts.app')]
class PortalInvitationManager extends Component
{
    use WithPagination;

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

    /**
     * The one moment the raw link exists. Held for this render only and never
     * written anywhere; a refresh loses it, which is the correct behaviour for
     * a single-use credential.
     */
    public ?string $freshLink = null;

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

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

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    /**
     * The guardians this screen may act on.
     *
     * `whereHas('guardian')` is doing two jobs: it is what makes this a
     * guardian list rather than a list of every person in the academy, and it
     * is what applies Guardian's BranchScope — `whereHas` runs the related
     * model's global scopes, so the narrowing is the scope's, not a hand-rolled
     * `where('branch_id', …)` that would have to be repeated and would hide the
     * shared, null-branch rows.
     */
    private function guardians(): Builder
    {
        return Person::query()->whereHas('guardian');
    }

    public function invite(int $personId, PortalInvitationService $invitations): void
    {
        $this->authorize('users.create');

        // Re-read through the same query that rendered the button. A wire:click
        // parameter is a browser input, and this one ends in a live credential.
        $person = $this->guardians()->findOrFail($personId);

        try {
            ['invitation' => $invitation, 'token' => $token] = $invitations->issue(
                $person,
                auth()->user(),
                null,
                'link',
                request()->ip(),
            );
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());

            return;
        }

        $this->freshLink = route('portal.activate', $token);
        $this->freshPersonId = $person->id;

        session()->flash('success', __('تم إنشاء رابط التفعيل — انسخه الآن، لن يظهر مرة أخرى'));
    }

    public function sendViaWhatsApp(int $personId, PortalInvitationService $invitations, WhatsAppService $whatsapp): void
    {
        $this->authorize('users.create');

        // Same lookup as invite(), and it matters more here: this one does not
        // just show the link on screen, it sends it to a phone number.
        $person = $this->guardians()->findOrFail($personId);

        if (! $person->phone) {
            session()->flash('error', __('لا يوجد رقم هاتف لهذا الشخص'));

            return;
        }

        try {
            ['token' => $token] = $invitations->issue($person, auth()->user(), null, 'whatsapp', request()->ip());
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());

            return;
        }

        $link = route('portal.activate', $token);
        $academy = app('current_academy');

        $whatsapp->sendText(
            $person->phone,
            __('مرحباً') . ' ' . ($person->name_ar ?: $person->name) . "\n\n"
            . __('فعّل حسابك في بوابة') . ' ' . ($academy->name_ar ?: $academy->name) . ":\n"
            . $link . "\n\n"
            . __('الرابط صالح لمدة ٧٢ ساعة ويُستخدم مرة واحدة.')
        );

        session()->flash('success', __('أُرسل الرابط عبر واتساب'));
    }

    public function revoke(int $invitationId, PortalInvitationService $invitations): void
    {
        $this->authorize('users.create');

        // portal_invitations has no branch of its own, so it is narrowed
        // through the invited person's guardian — the same boundary the list
        // below is drawn on. Without it, staff at one branch could kill a
        // pending activation link belonging to another, and the member's link
        // would simply stop working with nothing on either screen to say why.
        $invitations->revoke(
            PortalInvitation::whereHas('person.guardian')->findOrFail($invitationId)
        );

        session()->flash('success', __('تم إلغاء الدعوة'));
    }

    public function render()
    {
        $people = $this->guardians()
            ->when($this->search !== '', function ($q) {
                $term = '%' . $this->search . '%';
                // The closure keeps both branches inside the tenant scope.
                $q->where(function ($inner) use ($term) {
                    $inner->where('name_ar', 'like', $term)
                        ->orWhere('name', 'like', $term)
                        ->orWhere('phone', 'like', $term);
                });
            })
            ->orderBy('name_ar')
            ->paginate(15);

        // Narrowed the same way as the list above, so "آخر الدعوات" and the
        // "دعوة سارية" badge derived from it below describe the same set of
        // people the table shows. Left unnarrowed, a branch's screen computed
        // its own already-invited state from another branch's rows.
        $invitations = PortalInvitation::whereHas('person.guardian')
            ->with(['person', 'creator'])
            ->orderByDesc('created_at')
            ->limit(20)
            ->get();

        return view('livewire.portal.portal-invitation-manager', [
            'people' => $people,
            'invitations' => $invitations,
            'activeByPerson' => $invitations->whereNull('consumed_at')->whereNull('revoked_at')
                ->where('expires_at', '>', now())->keyBy('person_id'),
        // #[Title] is a PHP attribute and cannot call term(), so the branch's
        // word reaches the browser tab from here instead.
        ])->title(__('دعوات بوابة') . ' ' . term('members'));
    }
}
