<?php

namespace App\Services;

use App\Models\Branch;
use App\Models\Central\Tenant;
use App\Models\Concerns\BranchScoped;
use App\Models\Sales\Till;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

/**
 * Single point of "who/where/what tenant am I" for services.
 *
 * Branch resolution order:
 *   1. "All branches" mode (aggregate views) — branchId() returns null but
 *      isAllBranches() is true, so BranchScope passes through instead of
 *      scoping to "no rows".
 *   2. Explicit override (set by ResolveBranchContext middleware from the
 *      session, X-Branch-Id header, or `branch_id` query param).
 *   3. Authenticated user's default branch (users.default_warehouse_id, if present).
 *   4. The single active branch when exactly one exists — this is what makes a
 *      single-branch deployment "just work" with no selector.
 *   5. The first branch the user has explicit access to.
 *   6. null — caller decides what to do.
 */
class CurrentContext
{
    protected ?int $branchOverride = null;

    protected bool $explicit = false;

    protected bool $allBranches = false;

    protected ?int $tenantId = null;

    protected ?Tenant $tenantModel = null;

    /**
     * Landlord ("central") context — tenant scoping is bypassed. Set only by
     * the central guard middleware or {@see runWithoutTenant()}.
     */
    protected bool $central = false;

    /**
     * Per-request cache for {@see branches()} — every form with a branch
     * select, every table filter, and the top-bar switcher all call this
     * on the same request. Without caching, a single page with 3 branch
     * selects would issue 3 identical Branch queries.
     */
    protected ?Collection $branchesCache = null;

    protected ?int $soleBranchIdCache = null;

    protected bool $soleBranchIdComputed = false;

    /**
     * Per-user memo for {@see accessibleBranchIds()}, whose answer is read by
     * BranchScope on effectively every query.
     *
     * @var array<int, SupportCollection<int, int>|null>
     */
    protected array $accessibleBranchIdsCache = [];

    public function user(): ?User
    {
        return Auth::user();
    }

    public function userId(): ?int
    {
        return Auth::id();
    }

    /**
     * Bind the active tenant for this request/job. Drives {@see TenantScope}.
     */
    public function setTenant(Tenant|int $tenant): void
    {
        if ($tenant instanceof Tenant) {
            $this->tenantModel = $tenant;
            $this->tenantId = $tenant->id;
        } else {
            $this->tenantModel = null;
            $this->tenantId = $tenant;
        }

        $this->central = false;
    }

    public function forgetTenant(): void
    {
        $this->tenantId = null;
        $this->tenantModel = null;
    }

    /**
     * The current tenant id, or null in central/CLI context.
     *
     * Deliberately does NOT fall back to Auth::user()->tenant_id: resolving the
     * authenticated user runs a tenant-scoped User query, which would re-enter
     * this method and recurse. The tenant is bound up-front by IdentifyTenant.
     */
    public function tenantId(): ?int
    {
        return $this->tenantId;
    }

    public function tenant(): ?Tenant
    {
        if ($this->tenantModel !== null) {
            return $this->tenantModel;
        }

        $id = $this->tenantId();
        if ($id === null) {
            return null;
        }

        return $this->tenantModel = Tenant::query()->find($id);
    }

    /**
     * The tenant to brand the current page for.
     *
     * On a tenant host this is simply the bound tenant ({@see Tenant()}). On a
     * universal host — where no tenant is bound yet, e.g. the platform's own
     * bare domain — it is the workspace named by the `slug` / `tenant` query
     * parameter, so the login page can show that tenant's logo and name before
     * the visitor has authenticated.
     */
    public function brandingTenant(): ?Tenant
    {
        $tenant = $this->tenant();

        if ($tenant !== null) {
            return $tenant;
        }

        $slug = request()->query('slug') ?? request()->query('tenant');
        if (! is_string($slug) || trim($slug) === '') {
            return null;
        }

        return Tenant::query()->where('slug', Str::lower(trim($slug)))->first();
    }

    public function hasTenant(): bool
    {
        return $this->tenantId() !== null;
    }

    /**
     * The tenant's ISO 4217 currency code (Master Control "Locale" tab),
     * falling back to LKR — the currency the whole tenant base traded in
     * before this was configurable, and what every legacy money column is
     * still stored as. Used wherever a currency code drives formatting (e.g.
     * every `->money()` column) instead of a hardcoded literal.
     */
    public function currencyCode(): string
    {
        $code = $this->brandingTenant()?->setting('locale.currency');

        return is_string($code) && trim($code) !== '' ? strtoupper(trim($code)) : 'LKR';
    }

    /**
     * The tenant's currency symbol, derived from the seeded {@see
     * \App\Models\Reference\Currency} the landlord picked — never typed by
     * hand. Falls back to "Rs." (LKR's symbol), matching {@see currencyCode()}.
     */
    public function currencySymbol(): string
    {
        $symbol = $this->brandingTenant()?->setting('locale.currency_symbol');

        return is_string($symbol) && trim($symbol) !== '' ? trim($symbol) : 'Rs.';
    }

    /**
     * The tenant's IANA timezone (Master Control "Locale" tab), falling back
     * to the application default. Drives the header clock and, per-request,
     * `date_default_timezone_set()` (see IdentifyTenant) so `now()`/Carbon
     * agree with what the tenant sees on screen.
     */
    public function timezone(): string
    {
        $tz = $this->brandingTenant()?->setting('locale.timezone');

        return is_string($tz) && trim($tz) !== '' ? trim($tz) : (string) config('app.timezone');
    }

    /**
     * The tenant's BCP-47 locale tag (e.g. "en-LK"), falling back to the
     * application default. Drives number/date formatting (e.g. the header
     * clock and any `Number::currency()`/`Intl` call that takes a locale).
     */
    public function localeTag(): string
    {
        $locale = $this->brandingTenant()?->setting('locale.locale');

        return is_string($locale) && trim($locale) !== '' ? trim($locale) : str_replace('_', '-', (string) config('app.locale'));
    }

    /**
     * Mark the request as landlord/central context (no tenant scoping).
     */
    public function markCentral(bool $central = true): void
    {
        $this->central = $central;
    }

    public function isCentral(): bool
    {
        return $this->central;
    }

    /**
     * Run a callback bound to a specific tenant, restoring the previous context
     * afterwards. Used by provisioning, queued jobs, and landlord impersonation.
     */
    public function runForTenant(Tenant|int $tenant, callable $callback): mixed
    {
        $previousId = $this->tenantId;
        $previousModel = $this->tenantModel;
        $previousCentral = $this->central;

        $this->setTenant($tenant);

        try {
            return $callback();
        } finally {
            $this->tenantId = $previousId;
            $this->tenantModel = $previousModel;
            $this->central = $previousCentral;
        }
    }

    /**
     * Run a callback with tenant scoping disabled (landlord/global operations).
     */
    public function runWithoutTenant(callable $callback): mixed
    {
        $previousId = $this->tenantId;
        $previousModel = $this->tenantModel;
        $previousCentral = $this->central;

        $this->forgetTenant();
        $this->central = true;

        try {
            return $callback();
        } finally {
            $this->tenantId = $previousId;
            $this->tenantModel = $previousModel;
            $this->central = $previousCentral;
        }
    }

    public function setBranch(int $branchId, bool $explicit = true): void
    {
        $this->branchOverride = $branchId;
        $this->explicit = $explicit;
        $this->allBranches = false;
    }

    /**
     * Aggregate ("All branches") mode for dashboards and reports.
     */
    public function setAllBranches(): void
    {
        $this->allBranches = true;
        $this->explicit = true;
        $this->branchOverride = null;
    }

    public function isAllBranches(): bool
    {
        return $this->allBranches;
    }

    public function hasExplicitBranch(): bool
    {
        return $this->explicit;
    }

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

        if ($this->branchOverride !== null) {
            return $this->branchOverride;
        }

        $user = Auth::user();
        if (! $user) {
            return null;
        }

        // Default branch on the user, if the column exists.
        $default = $user->default_warehouse_id ?? null;
        if ($default) {
            return (int) $default;
        }

        // Single-branch deployment: exactly one active branch → use it implicitly.
        $active = Branch::query()->active()->limit(2)->pluck('id');
        if ($active->count() === 1) {
            return (int) $active->first();
        }

        // Otherwise fall back to the first branch the user can access.
        $accessible = DB::table('user_warehouse_access')
            ->where('user_id', $user->id)
            ->pluck('warehouse_id');

        if ($accessible->isNotEmpty()) {
            return (int) $accessible->first();
        }

        return null;
    }

    /**
     * Active branches this user may view — drives the branch selector. A user
     * with no explicit access list, no full-admin role, and no all-branch
     * grant sees none — {@see accessibleBranchIds()} for the access rule.
     *
     * @return Collection<int, Branch>
     */
    public function branches(): Collection
    {
        if ($this->branchesCache !== null) {
            return $this->branchesCache;
        }

        $user = Auth::user();
        if (! $user) {
            return $this->branchesCache = Branch::query()->whereRaw('1 = 0')->get();
        }

        $query = Branch::query()->active()->orderBy('name');

        $accessible = $this->accessibleBranchIds();
        if ($accessible !== null) {
            $query->whereIn('id', $accessible);
        }

        return $this->branchesCache = $query->get();
    }

    /**
     * Clear per-request branch caches — used in tests that impersonate
     * different users within the same process, and after a branch
     * selector POST that changes session state mid-request.
     */
    public function clearBranchCache(): void
    {
        $this->branchesCache = null;
        $this->soleBranchIdCache = null;
        $this->soleBranchIdComputed = false;
        $this->accessibleBranchIdsCache = [];
    }

    /**
     * Branch IDs the current user is restricted to, or null when unrestricted
     * (full admin, or an explicit {@see User::hasAllBranchAccess()}
     * grant). An empty (non-null) collection means the user has zero branch
     * access — no implicit "empty list = unrestricted" fallback.
     *
     * @return SupportCollection<int, int>|null
     */
    public function accessibleBranchIds(): ?SupportCollection
    {
        $user = Auth::user();
        if (! $user) {
            return collect();
        }

        if (method_exists($user, 'isFullAdmin') && $user->isFullAdmin()) {
            return null;
        }

        if (method_exists($user, 'hasAllBranchAccess') && $user->hasAllBranchAccess()) {
            return null;
        }

        // Memoised per user for the life of the request. BranchScope and every
        // branch-restricted Select call this, so a single form render used to
        // hit user_warehouse_access a dozen times over for an answer that
        // cannot change mid-request. Keyed by user id so an actingAs() switch
        // inside one process (tests, queue workers) still reads its own access.
        if (array_key_exists($user->id, $this->accessibleBranchIdsCache)) {
            return $this->accessibleBranchIdsCache[$user->id];
        }

        return $this->accessibleBranchIdsCache[$user->id] = DB::table('user_warehouse_access')
            ->where('user_id', $user->id)
            ->pluck('warehouse_id')
            ->map(fn ($id) => (int) $id);
    }

    /**
     * Whether the current user is unrestricted by branch (full admin or an
     * explicit all-branch grant) — the shared decision both {@see BranchScope}
     * and any manual branch-restriction code (e.g. raw-query dashboard
     * widgets that can't rely on a model's global scope) should use, so the
     * "who bypasses branch scoping" rule lives in exactly one place.
     */
    public function hasUnrestrictedBranchAccess(): bool
    {
        $user = Auth::user();

        return $user !== null
            && ((method_exists($user, 'isFullAdmin') && $user->isFullAdmin())
                || (method_exists($user, 'hasAllBranchAccess') && $user->hasAllBranchAccess()));
    }

    /**
     * Restricts a query builder to the current user's accessible branches on
     * the given column, applying the exact same decision tree as
     * {@see \App\Models\Scopes\BranchScope} — for raw/manual queries that
     * can't pick up a model's global scope (e.g. joins that start from a
     * different base model, like an invoice-items report joined to
     * invoices). Prefer {@see BranchScoped} whenever the
     * query is a plain Eloquent query on the scoped model itself; reach for
     * this only when that isn't possible.
     *
     * @param  \Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Database\Query\Builder  $query
     */
    public function restrictToAccessibleBranches(mixed $query, string $column): void
    {
        if (! Auth::check()) {
            return;
        }

        // Landlord/central context is never branch-restricted: there is no
        // tenant branch context on the central domain, and trying to resolve
        // one would lazily pick a cross-tenant branch (Branch::active() is not
        // scoped per-tenant here) or none — collapsing every branch-scoped
        // landlord grid (e.g. the tenant's tills) to a single arbitrary branch
        // or to no rows at all. TenantScope makes the same central pass-through,
        // so BranchScope must mirror it. Tenant users — real tenant requests or
        // CurrentContext::runForTenant() — still scope below.
        if ($this->isCentral()) {
            return;
        }

        if ($this->isAllBranches()) {
            $accessible = $this->accessibleBranchIds();
            if ($accessible !== null) {
                $query->whereIn($column, $accessible);
            }

            return;
        }

        if ($this->hasUnrestrictedBranchAccess() && ! $this->hasExplicitBranch()) {
            return;
        }

        $branchId = $this->branchId();
        if ($branchId === null) {
            $query->whereRaw('1 = 0');

            return;
        }

        $query->where($column, $branchId);
    }

    /**
     * Till IDs the current user is restricted to, or null when unrestricted
     * (full admin — sees every till). An empty (non-null) collection means the
     * user has zero till access — strict: no rows means no tills until access
     * is assigned. This mirrors {@see accessibleBranchIds()} where null means
     * unrestricted; the previous implementation returned an empty collection for
     * full admins which incorrectly locked them to zero tills (see
     * restrictToAccessibleTills / tillsForBranch).
     *
     * @return SupportCollection<int, int>|null
     */
    public function accessibleTillIds(): ?SupportCollection
    {
        $user = Auth::user();
        if (! $user) {
            return collect();
        }

        if (method_exists($user, 'isFullAdmin') && $user->isFullAdmin()) {
            return null;
        }

        return DB::table('user_till_access')
            ->where('user_id', $user->id)
            ->pluck('till_id')
            ->map(fn ($id) => (int) $id);
    }

    /**
     * Restricts a query builder to the current user's accessible tills on
     * the given column, applying the same decision tree as
     * {@see accessibleTillIds()} — for raw/manual queries that can't pick
     * up a model's global scope. No-op for guests, in the landlord/
     * central context, for unrestricted users (full admin), and for users
     * with no explicit till rows yet (empty) — so master-created tills are
     * immediately visible without a manual assignment step. Once explicit
     * assignments exist, the query is filtered to those tills only.
     *
     * @param  \Illuminate\Database\Eloquent\Builder<*>|\Illuminate\Database\Query\Builder  $query
     */
    public function restrictToAccessibleTills(mixed $query, string $column): void
    {
        if (! Auth::check()) {
            return;
        }

        if ($this->isCentral()) {
            return;
        }

        $accessible = $this->accessibleTillIds();

        // Null (full admin) or empty (no explicit restriction yet) → no filtering,
        // so master-created tills appear immediately.
        if ($accessible === null || $accessible->isEmpty()) {
            return;
        }

        $query->whereIn($column, $accessible);
    }

    /**
     * Active tills in the given branch. Full admins (null) see every active
     * till; users with explicit `user_till_access` rows see only their assigned
     * tills; users with no rows yet (empty) see every active till in the branch
     * — so a till created in Master Control appears in POS immediately without
     * a manual assignment step. This keeps Till master (which now shows all
     * branch-accessible tills) and POS in sync.
     *
     * @return Collection<int, Till>
     */
    public function tillsForBranch(int $branchId): Collection
    {
        $query = Till::query()->where('branch_id', $branchId)->where('is_active', true)->orderBy('name');

        $accessible = $this->accessibleTillIds();

        // Null = unrestricted (full admin) or empty = no explicit restriction yet
        // — show every active till so master-created tills are immediately visible.
        if ($accessible === null || $accessible->isEmpty()) {
            return $query->get();
        }

        $query->whereIn('id', $accessible);

        return $query->get();
    }

    /**
     * Whether the current user may operate in the given branch.
     */
    public function canAccessBranch(int $branchId): bool
    {
        return $this->branches()->pluck('id')->contains($branchId);
    }

    public function hasMultipleBranches(): bool
    {
        return $this->branches()->count() > 1;
    }

    /**
     * The single branch id the current user may access, or null when they
     * have zero or multiple. When this returns a non-null id, every required
     * branch select should auto-fill that id and lock itself — the user has
     * no meaningful choice to make, so the form should not ask them to make
     * one. The filter dropdowns should hide entirely for the same reason.
     */
    public function soleBranchId(): ?int
    {
        if ($this->soleBranchIdComputed) {
            return $this->soleBranchIdCache;
        }

        $this->soleBranchIdComputed = true;

        $branches = $this->branches();

        if ($branches->count() !== 1) {
            return $this->soleBranchIdCache = null;
        }

        return $this->soleBranchIdCache = (int) $branches->first()->id;
    }

    /**
     * Best-effort branch resolution from a request — used by middleware.
     * Silently ignores branches the current user has no access to, so a
     * crafted X-Branch-Id header can never widen visibility.
     */
    public function resolveFromRequest(Request $request): void
    {
        $explicit = $request->header('X-Branch-Id') ?? $request->query('branch_id');
        if ($explicit && $this->canAccessBranch((int) $explicit)) {
            $this->setBranch((int) $explicit, explicit: true);
        }
    }
}
