<?php

namespace App\Domain\Identity\Services;

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

/**
 * Resolves which participants an account speaks for.
 *
 * Ten places did their own version of this, all shaped
 * `Guardian::where('person_id', …)->first()`, and every one of them was wrong
 * in the same two ways:
 *
 *  1. `->first()` on a non-unique column. No UNIQUE(academy_id, person_id)
 *     exists on `guardians`, so a guardian with two rows sees one set of
 *     children and is 403'd on the rest — silently, with no error anywhere.
 *
 *  2. No answer for an adult with no guardian row. All eleven
 *     app/Livewire/Parent/* components end in `->firstOrFail()`, so a player
 *     account hard-fails on every screen. That is why a player given the
 *     `parent` role sees empty lists: the domain has no path from a user to
 *     his own participant row.
 *
 * Both are fixed the same way: return the union of every guardian row this
 * account holds, plus the account's own participant row when it has one.
 * That makes `own_children` mean "the participants this account speaks for",
 * which for a guardian is the children and for a player is himself.
 */
class GuardianResolver
{
    /** @var array<int, Collection<int, Guardian>> */
    private array $guardianCache = [];

    /** @var array<int, array<int, int>> */
    private array $participantCache = [];

    /** @var array<int, array<int, int>> */
    private array $payableCache = [];

    /**
     * Every guardian row this user holds — not the first one.
     *
     * @return Collection<int, Guardian>
     */
    public function guardiansFor(User $user): Collection
    {
        if (isset($this->guardianCache[$user->id])) {
            return $this->guardianCache[$user->id];
        }

        // PermissionService wrote this as
        //     ->where('person_id', $x)->orWhere('user_id', $y)
        // which, with the tenant global scope appended, compiles to
        //     person_id = ? OR (user_id = ? AND academy_id = ?)
        // so the person_id branch escapes the tenant filter entirely. The
        // closure is what keeps both branches inside it.
        $guardians = Guardian::query()
            ->where(function ($q) use ($user) {
                if ($user->person_id) {
                    $q->where('person_id', $user->person_id);
                }

                $q->orWhere('user_id', $user->id);
            })
            ->get();

        return $this->guardianCache[$user->id] = $guardians;
    }

    /**
     * The participant ids this account may see: every linked child, plus the
     * account holder's own participant row if he is a member himself.
     *
     * @return array<int, int>
     */
    public function participantIdsFor(User $user): array
    {
        if (isset($this->participantCache[$user->id])) {
            return $this->participantCache[$user->id];
        }

        $ids = [];

        foreach ($this->guardiansFor($user) as $guardian) {
            foreach ($guardian->participants()->pluck('participants.id') as $id) {
                $ids[] = (int) $id;
            }
        }

        // The adult-member case. The mobile API already solved this by
        // comparing $participant->person_id to the user's person_id; this is
        // that answer, in one place, rather than a third resolution path.
        if ($user->person_id) {
            foreach (Participant::where('person_id', $user->person_id)->pluck('id') as $id) {
                $ids[] = (int) $id;
            }
        }

        return $this->participantCache[$user->id] = array_values(array_unique($ids));
    }

    /**
     * The subset this account may act on financially.
     *
     * `guardian_participant.can_authorize_payment` has existed since
     * 2024_01_01_000018 with zero readers, so every guardian currently has full
     * financial access to every linked child. It is the natural gate for
     * submitting a transfer proof or starting a payment, so the portal reads it.
     * A member acting for himself always qualifies.
     *
     * @return array<int, int>
     */
    public function payableParticipantIdsFor(User $user): array
    {
        if (isset($this->payableCache[$user->id])) {
            return $this->payableCache[$user->id];
        }

        $ids = [];

        foreach ($this->guardiansFor($user) as $guardian) {
            $rows = $guardian->participants()
                ->wherePivot('can_authorize_payment', true)
                ->pluck('participants.id');

            foreach ($rows as $id) {
                $ids[] = (int) $id;
            }
        }

        if ($user->person_id) {
            foreach (Participant::where('person_id', $user->person_id)->pluck('id') as $id) {
                $ids[] = (int) $id;
            }
        }

        return $this->payableCache[$user->id] = array_values(array_unique($ids));
    }

    public function mayView(User $user, int $participantId): bool
    {
        return in_array($participantId, $this->participantIdsFor($user), true);
    }

    public function mayPayFor(User $user, int $participantId): bool
    {
        return in_array($participantId, $this->payableParticipantIdsFor($user), true);
    }

    /**
     * The participants themselves, ordered so a switcher renders predictably.
     *
     * @return Collection<int, Participant>
     */
    public function participantsFor(User $user): Collection
    {
        $ids = $this->participantIdsFor($user);

        if ($ids === []) {
            return collect();
        }

        return Participant::whereIn('id', $ids)
            ->with('person')
            ->get()
            ->sortBy(fn (Participant $p) => $p->person?->name_ar ?? '')
            ->values();
    }

    /** Drop the memo — for a long-running process, or after a link changes. */
    public function forget(User $user): void
    {
        unset(
            $this->guardianCache[$user->id],
            $this->participantCache[$user->id],
            $this->payableCache[$user->id],
        );
    }
}
