<?php

namespace App\Livewire\Portal\Concerns;

use App\Domain\Shared\Context\BranchScopeState;
use App\Domain\Shared\Context\PortalContext;

/**
 * What every portal screen shares: it authorises in mount(), and it reads its
 * scope from PortalContext rather than from a route parameter or a public
 * property.
 *
 * The `#[Locked]` distinction is not optional here. This project runs Livewire
 * v4, where a plain `public` property is settable from the browser — so
 * validating an id in mount() and then filtering queries on it in render() is
 * an IDOR, not a check. Nothing in this namespace holds a participant id in a
 * public property at all: the active member comes from the session through
 * PortalContext, which re-validates it against GuardianResolver on every read.
 */
trait PortalScreen
{
    /**
     * The member portal is branch-unscoped, and it has to be unscoped on every
     * request that touches it — not only on the first one.
     *
     * `ResolveBranchContext` decides that from the route name, and the initial
     * GET does arrive on `portal.*`, which `config('branch_scope.unscoped_routes')`
     * exempts. Every subsequent Livewire round-trip does not: it arrives on
     * Livewire's own `default-livewire.update` route, which matches nothing in
     * that list. For an account whose roles are all member roles that changes
     * nothing — `isMemberAccount()` keeps enforcement off regardless of route —
     * but a staff account that is also a parent is not a member account, so the
     * first wire:click on a portal screen would start filtering their own
     * family by the branch they happen to be working in. A child at another
     * branch would vanish from the switcher, their invoices would drop out of
     * the household total, and `activeParticipantId()` would 403 the parent out
     * of the portal entirely.
     *
     * So the exemption is asserted here, by the screen, where it is a fact
     * about the surface rather than a guess from a route name. `boot` rather
     * than `mount` because mount runs once and this has to hold on hydrate too.
     *
     * It widens what Eloquent returns; it never widens what the member may see.
     * That boundary is membership — `PortalContext` re-validates every
     * participant id against `GuardianResolver` on each read — and this does
     * not touch it.
     */
    public function bootPortalScreen(): void
    {
        app(BranchScopeState::class)->deactivate();
    }

    protected function portal(): PortalContext
    {
        return app(PortalContext::class);
    }

    /**
     * Gate the screen. Called from every mount().
     *
     * Two separate questions, and both have to be asked: does this account hold
     * the permission, and does it speak for anybody at all. A staff account
     * with portal.access but no children must not land in an empty portal
     * pretending to be a member.
     */
    protected function authorizePortal(?string $permission = null): void
    {
        $this->authorize($permission ?? 'portal.access');

        if (! $this->portal()->speaksForAnyone()) {
            abort(403, __('هذا الحساب غير مرتبط بأي مشترك في الأكاديمية'));
        }
    }

    /** The member a member-scoped surface is showing. */
    protected function activeParticipantId(): int
    {
        $id = $this->portal()->participantId();

        abort_if($id === null, 403, __('هذا الحساب غير مرتبط بأي مشترك في الأكاديمية'));

        return $id;
    }

    /**
     * Every participant this account speaks for.
     *
     * Money is family-scoped and training is member-scoped — that split is the
     * switcher's whole meaning, and it is decided here rather than eleven times
     * over.
     *
     * @return array<int, int>
     */
    protected function familyParticipantIds(): array
    {
        return $this->portal()->moneyParticipantIds();
    }
}
