<?php

namespace App\Domain\Shared\Context;

/**
 * The switch that makes branch a real isolation boundary rather than a filter
 * every screen has to remember to apply.
 *
 * BranchContext answers "which branch is this request in?". This class answers
 * the separate question "should Eloquent enforce it?", and it is deliberately
 * the only thing BranchScope consults. Keeping the two apart matters because
 * the answers differ: a console command has no branch and must read every one;
 * a member in the portal has a branch resolved for them and must still read
 * their own child wherever that child trains.
 *
 * Three states, and every one of them is a real case:
 *
 *   inactive          -> no filtering at all. Console, queues, unauthenticated
 *                        requests, the public website, and member portal
 *                        accounts. Nothing here has a back-office branch.
 *   active, id = null -> "all branches". A user holding branches.view_all who
 *                        deliberately chose the cross-branch view.
 *   active, id = int  -> that branch, and only that branch.
 *
 * Suppression is a counter rather than a flag so nested calls compose: a report
 * that suppresses the scope and calls a service that suppresses it again must
 * not come back scoped halfway through.
 *
 * Bound as a scoped singleton, so one instance serves one request — and a
 * Livewire round-trip is its own request.
 */
final class BranchScopeState
{
    private bool $active = false;

    private ?int $branchId = null;

    private int $suppressed = 0;

    /**
     * Turn enforcement on for this request.
     *
     * $branchId null means the user is entitled to and has chosen every branch;
     * the scope then adds nothing, which is the correct reading of "all".
     */
    public function activate(?int $branchId): void
    {
        $this->active = true;
        $this->branchId = $branchId;
    }

    public function deactivate(): void
    {
        $this->active = false;
        $this->branchId = null;
    }

    /**
     * Should a query be filtered right now?
     *
     * False in every ambiguous case. A scope that guesses would hide rows from
     * a cron job or a parent, and silently-missing rows are far worse than
     * un-narrowed ones in the surfaces where this returns false.
     */
    public function shouldFilter(): bool
    {
        return $this->active
            && $this->suppressed === 0
            && $this->branchId !== null;
    }

    public function branchId(): ?int
    {
        return $this->branchId;
    }

    public function isActive(): bool
    {
        return $this->active;
    }

    /**
     * Run a callback with branch enforcement off, then restore it.
     *
     * For the handful of operations that are cross-branch by definition:
     * transferring a player between branches, the executive dashboard, a
     * consolidated export. Restoring in a finally block is what keeps one
     * throwing report from unscoping the rest of the request.
     */
    public function withoutScope(callable $callback): mixed
    {
        $this->suppressed++;

        try {
            return $callback();
        } finally {
            $this->suppressed--;
        }
    }

    /**
     * Same, but for callers that cannot wrap a closure (a Livewire mount that
     * has to build several queries). Must be paired with restore().
     */
    public function suppress(): void
    {
        $this->suppressed++;
    }

    public function restore(): void
    {
        if ($this->suppressed > 0) {
            $this->suppressed--;
        }
    }
}
