<?php

namespace App\Domain\Shared\Traits;

use App\Domain\Identity\Models\Branch;
use App\Domain\Shared\Context\BranchScopeState;
use App\Domain\Shared\Scopes\BranchScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

/**
 * A record that belongs to one branch — stamped on create, enforced on read.
 *
 * This trait used to stamp only, with a comment saying branch was "a reporting
 * lens, not an isolation boundary". That was the design, and it is what made
 * every screen individually responsible for remembering to filter. Roughly
 * eighty components did remember for their main list query; almost none did
 * for the <select> they rendered beside it, so picking a branch narrowed the
 * table and left the dropdowns offering every other branch's groups, players
 * and facilities. Branch is now an isolation boundary, on the same footing as
 * academy, and the enforcement lives here so no screen can forget it.
 *
 * Two shapes, chosen per model by $branchScopeAllowsShared:
 *
 *   false (default) — strict. Operational records owned by exactly one branch.
 *   true            — shared. Catalogue and configuration where branch_id NULL
 *                     means "every branch uses this row". Hard-filtering these
 *                     would hide the academy-wide base price, and a missing
 *                     base price is a hard failure that stops a sale.
 *
 * The scope is off entirely outside a branch-scoped request — console, queues,
 * the public site, the member portal — see BranchScopeState.
 */
trait BelongsToBranch
{
    public static function bootBelongsToBranch(): void
    {
        static::creating(function ($model) {
            $column = $model->getBranchColumn();

            if (! $model->{$column}) {
                $model->{$column} = static::resolveActiveBranchId();
            }
        });

        static::addGlobalScope(static::branchScope());
    }

    /**
     * The scope this model is isolated by.
     *
     * Overridable for the one model whose branch is many-valued: a pricing rule
     * targets a list of branches through a pivot, not a column, so it supplies
     * its own. Everything else wants the column.
     */
    public static function branchScope(): \Illuminate\Database\Eloquent\Scope
    {
        return new BranchScope(static::branchScopeAllowsShared());
    }

    /**
     * Does branch_id NULL mean "shared with every branch" for this model?
     *
     * Overridden by a `protected static bool $branchScopeAllowsShared = true;`
     * on the model. A static property rather than a method so a model reads as
     * a declaration, and so the value is visible next to $fillable.
     */
    public static function branchScopeAllowsShared(): bool
    {
        return property_exists(static::class, 'branchScopeAllowsShared')
            && static::$branchScopeAllowsShared === true;
    }

    /**
     * The column holding the branch. Overridable for the one or two tables
     * that name it differently.
     */
    public function getBranchColumn(): string
    {
        return 'branch_id';
    }

    /**
     * The branch the current actor is working in.
     *
     * Returns null rather than guessing when there is no request context —
     * queued jobs and scheduled commands run for the whole academy, and
     * inventing a branch there would misfile their records.
     */
    public static function resolveActiveBranchId(): ?int
    {
        return app(\App\Domain\Shared\Context\BranchContext::class)
            ->branchIdForStamping();
    }

    /**
     * Read across every branch for one query.
     *
     * For the operations that are cross-branch by definition — transferring a
     * player to another branch, an executive roll-up, a consolidated export.
     * Reach for it deliberately; every use is a hole in the isolation the rest
     * of the system now depends on.
     */
    public function scopeWithoutBranchScope(Builder $query): Builder
    {
        return $query->withoutGlobalScope(static::branchScope()::class);
    }

    /**
     * Read one specific other branch.
     */
    public function scopeForBranch(Builder $query, ?int $branchId): Builder
    {
        $query = $query->withoutGlobalScope(static::branchScope()::class);

        return $branchId === null
            ? $query
            : $query->where($this->getTable().'.'.$this->getBranchColumn(), $branchId);
    }

    /**
     * Run a callback with branch enforcement lifted for every model, not just
     * this one. For a report that touches a dozen tables.
     */
    public static function acrossBranches(callable $callback): mixed
    {
        return app(BranchScopeState::class)->withoutScope($callback);
    }

    public function branch(): BelongsTo
    {
        return $this->belongsTo(Branch::class, $this->getBranchColumn());
    }
}
