<?php

namespace App\Domain\Shared\Context;

use App\Domain\Identity\Models\Branch;
use App\Domain\Identity\Services\PermissionService;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;

/**
 * The branch the current request is working in.
 *
 * This lives in Context rather than Services on purpose. Domain services must
 * stay callable from queues and console commands, so they may not touch
 * session/auth and instead receive an explicit ?int $branchId. This class is
 * the adapter that produces that value — the branch counterpart to the
 * `current_academy` instance SetCurrentAcademy binds. It is bound scoped, so
 * one instance serves one request (and one Livewire round-trip).
 *
 * Session state has three meanings, and they must not be confused:
 *
 *   key absent          -> not yet resolved; fall back to the user's own branch
 *   key present, null   -> "all branches" (only meaningful for multi-branch users)
 *   key present, int    -> that branch
 *
 * Everything here tests presence with session()->exists(). Session::has() is
 * `! is_null(get($key))`, so it reports false for a key holding null — which is
 * exactly how "all branches" is stored. Using has() is what made that mode
 * impossible to stay in.
 */
class BranchContext
{
    public const KEY = 'active_branch_id';

    private ?bool $canViewAll = null;
    private ?Collection $activeBranches = null;

    public function __construct(private PermissionService $permissions)
    {
    }

    // ---------------------------------------------------------------- state

    public function isResolved(): bool
    {
        return app()->bound('session') && session()->exists(self::KEY);
    }

    /**
     * True only when the user has deliberately chosen to see every branch AND
     * is entitled to. A stale null left by a since-downgraded user is repaired
     * by branchId() rather than honoured.
     */
    public function isAllBranches(): bool
    {
        return $this->isResolved()
            && session(self::KEY) === null
            && $this->canViewAllBranches();
    }

    /**
     * The branch to filter queries by. Null means "do not filter".
     */
    public function branchId(): ?int
    {
        if (! $this->isResolved()) {
            return $this->resolveForUser(auth()->user());
        }

        $value = session(self::KEY);

        if ($value === null) {
            // Entitled: genuine all-branches. Not entitled: a stale null from a
            // revoked permission or an academy that dropped to one branch —
            // repair it rather than stranding them with an empty app.
            return $this->canViewAllBranches()
                ? null
                : $this->resolveForUser(auth()->user());
        }

        return (int) $value;
    }

    /**
     * For operations that genuinely cannot run across branches — opening a cash
     * session, taking a payment at a desk. Aborts rather than silently picking
     * one, which would quietly unlock a locked user into an arbitrary branch.
     */
    public function requireBranchId(): int
    {
        abort_if(
            $this->isAllBranches(),
            403,
            __('هذه الصفحة تحتاج فرعاً محدداً. اختر فرعاً من القائمة العلوية.')
        );

        $id = $this->branchId();

        if (! $id) {
            $branch = $this->activeBranches()->first();

            if (! $branch) {
                abort(403, __('لا يوجد فرع نشط. يرجى إنشاء فرع أولاً.'));
            }

            $id = (int) $branch->id;
            $this->set($id);
        }

        return $id;
    }

    // ---------------------------------------------------------- entitlement

    /**
     * A single-branch academy is never locked and never gets the cross-branch
     * view: comparing one branch to itself is noise, and locking its owner out
     * of their own sidebar would be a pure regression.
     */
    public function canViewAllBranches(?User $user = null): bool
    {
        // The common case — the logged-in user — is asked on every sidebar item
        // and every middleware pass, so memoise it for the request.
        if ($user === null) {
            return $this->canViewAll ??= $this->computeCanViewAll(auth()->user());
        }

        return $this->computeCanViewAll($user);
    }

    private function computeCanViewAll(?User $user): bool
    {
        if (! $user) {
            return false;
        }

        return $this->activeBranches()->count() >= 2
            && $this->permissions->can($user, config('branch_lock.permission'));
    }

    /**
     * Where the branch lock sends a user.
     *
     * The configured target is the cross-branch view, which is not built yet.
     * route() throws on an undefined name rather than returning null, so every
     * caller that *navigates* — the switcher, the login redirect, the
     * middleware, the dashboard — must take the name from here instead of
     * reading the config key raw. isLocked() guards the callers that *gate*;
     * it cannot guard the ones that merely need somewhere to send a user.
     *
     * Falling back to the dashboard is correct while the view is unbuilt: the
     * lock is dormant, so the dashboard is already unfiltered and showing
     * exactly the every-branch numbers the user asked for.
     */
    public function redirectRouteName(): string
    {
        $configured = config('branch_lock.redirect_route');

        return $this->redirectRouteExists() ? $configured : 'dashboard';
    }

    /**
     * Whether the cross-branch view the lock depends on has been built.
     */
    public function redirectRouteExists(): bool
    {
        return Route::has(config('branch_lock.redirect_route'));
    }

    /**
     * Viewing every branch, and therefore restricted to the cross-branch pages.
     */
    public function isLocked(): bool
    {
        // Fail safe: the lock redirects everything to the executive dashboard,
        // so until that route exists locking would 500 every page including the
        // redirect target. Keeps this dormant rather than dangerous while the
        // feature is only partly built.
        if (! $this->redirectRouteExists()) {
            return false;
        }

        return $this->isAllBranches();
    }

    public function routeIsUnlocked(?string $routeName): bool
    {
        if (! $routeName) {
            return false;
        }

        return Str::is(config('branch_lock.unlocked', []), $routeName);
    }

    // --------------------------------------------------------------- writes

    public function set(?int $branchId): void
    {
        session([self::KEY => $branchId]);
        $this->canViewAll = null;
    }

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

    /**
     * Seed the session for a user who has no branch resolved yet, and return
     * the resulting branch id (null meaning all-branches).
     *
     * Order: their pinned branch, then their role's branch, then all-branches
     * if entitled, then the sole/main active branch.
     */
    public function resolveForUser(?User $user): ?int
    {
        if (! $user) {
            return null;
        }

        $active = $this->activeBranches();

        $pinned = $user->preferred_branch_id;
        if ($pinned && $active->contains('id', $pinned)) {
            $this->set((int) $pinned);

            return (int) $pinned;
        }

        $pivot = $user->branch_id;
        if ($pivot && $active->contains('id', $pivot)) {
            $this->set((int) $pivot);

            return (int) $pivot;
        }

        if ($this->canViewAllBranches($user)) {
            // Persist the null so the next request reads "all branches" rather
            // than falling through this chain again and pinning a branch.
            $this->set(null);

            return null;
        }

        $fallback = $active->firstWhere('is_main', true) ?? $active->first();

        if (! $fallback) {
            return null;
        }

        $this->set((int) $fallback->id);

        return (int) $fallback->id;
    }

    // -------------------------------------------------------------- lookups

    public function activeBranches(): Collection
    {
        // The id tiebreaker is not cosmetic. resolveForUser() falls back to
        // `firstWhere('is_main') ?? first()`, and OC-Sport has nine branches
        // with is_main false on every one of them — so without a deterministic
        // second key Postgres was free to return them in a different order per
        // request, and a user with no pinned branch could land in a different
        // branch on each page load.
        return $this->activeBranches ??= Branch::query()
            ->where('is_active', true)
            ->orderByDesc('is_main')
            ->orderBy('id')
            ->get(['id', 'name', 'name_ar', 'code', 'is_main']);
    }

    public function currentBranch(): ?Branch
    {
        $id = $this->branchId();

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

    /**
     * The branch to stamp onto a new record.
     *
     * Deliberately NOT branchId(). API routes sit outside the web middleware
     * and queued listeners run outside the request entirely; if this returned
     * null in all-branches mode, records created there would be filed against
     * no branch and vanish from every per-branch total permanently.
     */
    public function branchIdForStamping(): ?int
    {
        if (app()->runningInConsole()) {
            return null;
        }

        if ($this->isResolved()) {
            $value = session(self::KEY);

            if ($value) {
                return (int) $value;
            }
        }

        return auth()->user()?->branch_id;
    }
}
