<?php

namespace App\Domain\Shared\Context;

use App\Domain\Identity\Services\GuardianResolver;
use App\Domain\Participant\Models\Participant;
use App\Models\User;
use Illuminate\Support\Collection;

/**
 * Which member the portal is currently showing.
 *
 * Modelled on BranchContext deliberately, including the three-state session
 * key, because it is the same shape of problem: a selection that scopes some
 * surfaces and not others.
 *
 *   key absent          -> not resolved; fall back to the account's sole or
 *                          first member
 *   key present, null   -> "the whole family"
 *   key present, int    -> that member
 *
 * The distinction that makes this a scope rule rather than a screen: **training
 * is member-scoped and money is family-scoped.** A guardian looking at the
 * schedule is asking about one child; a guardian looking at what is owed is
 * asking about the household. The current `/parent` components each re-read
 * `session('active_child_id')` independently and ParentFinances ignores it and
 * aggregates everyone — which is the domain saying this out loud. Deciding it
 * here means the eleven screens do not each re-implement it.
 *
 * The id is validated against GuardianResolver on every read, so a value put
 * into the session — or left there after a withdrawal — cannot widen what the
 * account can see.
 */
class PortalContext
{
    public const KEY = 'portal_active_participant_id';

    private ?array $allowed = null;

    public function __construct(private readonly GuardianResolver $guardians) {}

    /** @return array<int, int> */
    public function allowedParticipantIds(): array
    {
        if ($this->allowed !== null) {
            return $this->allowed;
        }

        $user = auth()->user();

        return $this->allowed = $user ? $this->guardians->participantIdsFor($user) : [];
    }

    public function isResolved(): bool
    {
        // session()->exists(), not has(): has() is `! is_null(get($key))`, so it
        // reports false for the key holding null — which is exactly how "the
        // whole family" is stored.
        return app()->bound('session') && session()->exists(self::KEY);
    }

    public function isWholeFamily(): bool
    {
        return $this->isResolved() && session(self::KEY) === null && count($this->allowedParticipantIds()) > 1;
    }

    /**
     * The member a member-scoped surface should show. Null only when the
     * account speaks for nobody.
     */
    public function participantId(): ?int
    {
        $allowed = $this->allowedParticipantIds();

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

        if (! $this->isResolved()) {
            return $this->set($allowed[0]);
        }

        $value = session(self::KEY);

        // "Whole family" is not a valid answer for a member-scoped surface, and
        // neither is an id the account no longer speaks for — a withdrawal
        // between requests leaves exactly that.
        if ($value === null || ! in_array((int) $value, $allowed, true)) {
            return $allowed[0];
        }

        return (int) $value;
    }

    /**
     * The ids a family-scoped surface should cover: everyone when the whole
     * family is selected, otherwise just the active member.
     *
     * @return array<int, int>
     */
    public function familyParticipantIds(): array
    {
        $allowed = $this->allowedParticipantIds();

        if ($allowed === []) {
            return [];
        }

        if ($this->isWholeFamily()) {
            return $allowed;
        }

        $active = $this->participantId();

        return $active ? [$active] : [];
    }

    /**
     * Money is family-scoped by default, so an unresolved or whole-family
     * selection covers the household rather than one child.
     *
     * @return array<int, int>
     */
    public function moneyParticipantIds(): array
    {
        return $this->allowedParticipantIds();
    }

    public function set(?int $participantId): ?int
    {
        if ($participantId !== null && ! in_array($participantId, $this->allowedParticipantIds(), true)) {
            return $this->participantId();
        }

        session([self::KEY => $participantId]);

        return $participantId;
    }

    public function clear(): void
    {
        session()->forget(self::KEY);
        $this->allowed = null;
    }

    /** @return Collection<int, Participant> */
    public function switchableProfiles(): Collection
    {
        $user = auth()->user();

        return $user ? $this->guardians->participantsFor($user) : collect();
    }

    public function activeParticipant(): ?Participant
    {
        $id = $this->participantId();

        return $id ? $this->switchableProfiles()->firstWhere('id', $id) : null;
    }

    public function speaksForAnyone(): bool
    {
        return $this->allowedParticipantIds() !== [];
    }

    public function mayPayFor(int $participantId): bool
    {
        $user = auth()->user();

        return $user !== null && $this->guardians->mayPayFor($user, $participantId);
    }

    /** @return array<int, int> */
    public function payableParticipantIds(): array
    {
        $user = auth()->user();

        return $user ? $this->guardians->payableParticipantIdsFor($user) : [];
    }

    public function forget(?User $user = null): void
    {
        $this->allowed = null;

        if ($user) {
            $this->guardians->forget($user);
        }
    }
}
