<?php

namespace App\Domain\Identity\Services;

use App\Domain\Attendance\Models\AttendanceRecord;
use App\Domain\Financial\Models\Invoice;
use App\Domain\Identity\Models\Guardian;
use App\Domain\Identity\Services\GuardianResolver;
use App\Domain\Participant\Models\Participant;
use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Shared\Scopes\BranchScope;
use App\Domain\Shared\Scopes\BranchScopeThrough;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSession;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Schema;

class PermissionService
{
    private array $cachedGroupIds = [];
    private array $cachedChildIds = [];

    /** table => whether it has a branch_id column. */
    private static array $branchColumnCache = [];

    public function can(User $user, string $permission, ?Model $record = null): bool
    {
        if ($user->is_super_admin) {
            return true;
        }

        $role = $user->primaryRole;
        if (!$role) {
            $role = $user->roles->first();
            if (!$role) {
                return false;
            }
            if (!$role->relationLoaded('permissions')) {
                $role->load('permissions');
            }
        }

        $rolePermission = $role->permissions->firstWhere('name', $permission);
        if (!$rolePermission) {
            return false;
        }

        if ($record === null) {
            return true;
        }

        $scope = $rolePermission->pivot->scope;

        return match ($scope) {
            'all' => true,
            'academy' => true,
            'branch' => $this->checkBranchScope($user, $record),
            'own' => $this->checkOwnScope($user, $record),
            'own_groups' => $this->checkOwnGroupsScope($user, $record),
            'own_children' => $this->checkOwnChildrenScope($user, $record),
            default => false,
        };
    }

    public function applyScope(Builder $query, User $user, string $permission): Builder
    {
        if ($user->is_super_admin) {
            return $query;
        }

        $role = $user->primaryRole;
        if (!$role) {
            $role = $user->roles->first();
            if (!$role) {
                return $query->whereRaw('1 = 0');
            }
            if (!$role->relationLoaded('permissions')) {
                $role->load('permissions');
            }
        }

        $rolePermission = $role->permissions->firstWhere('name', $permission);
        if (!$rolePermission) {
            return $query->whereRaw('1 = 0');
        }

        $scope = $rolePermission->pivot->scope;
        $table = $query->getModel()->getTable();

        return match ($scope) {
            'all', 'academy' => $query,
            'branch' => $this->applyBranchScope($query, $user, $table),
            'own' => $query->where("{$table}.created_by", $user->id),
            'own_groups' => $this->applyOwnGroupsScope($query, $user, $table),
            'own_children' => $this->applyOwnChildrenScope($query, $user, $table),
            default => $query,
        };
    }

    // --- Branch scope ---

    /**
     * Narrow a list to the reader's branch — for the models the branch scope
     * does not already narrow.
     *
     * This used to be one line, `where("{$table}.branch_id", $user->branch_id)`,
     * and it was wrong three ways. It pinned the user's HOME branch even when
     * they were working in another one; on a shared table (employees, trainers,
     * products) it hid every academy-wide row, because those carry branch_id
     * NULL meaning "every branch"; and on a table with no branch_id at all it
     * was a SQL error rather than a filter.
     *
     * A model carrying BranchScope or BranchScopeThrough is already narrowed to
     * the ACTIVE branch, with the correct shared/strict reading, so the honest
     * thing here is to add nothing. What is left is the handful of tables that
     * have the column but stay outside the branch model — users, most of all —
     * and those keep the old behaviour.
     */
    private function applyBranchScope(Builder $query, User $user, string $table): Builder
    {
        $model = $query->getModel();

        if ($model::hasGlobalScope(BranchScope::class)
            || $model::hasGlobalScope(BranchScopeThrough::class)) {
            return $query;
        }

        if (! $this->tableHasBranchColumn($model, $table)) {
            return $query;
        }

        return $query->where("{$table}.branch_id", $user->branch_id);
    }

    /**
     * Does this table carry a branch_id at all? Memoised per process — a
     * missing column has to be answered without throwing, and asking the
     * information schema on every list query would not be free.
     */
    private function tableHasBranchColumn(Model $model, string $table): bool
    {
        $connection = $model->getConnectionName();
        $key = ($connection ?? 'default') . ':' . $table;

        if (! array_key_exists($key, self::$branchColumnCache)) {
            self::$branchColumnCache[$key] = Schema::connection($connection)
                ->hasColumn($table, 'branch_id');
        }

        return self::$branchColumnCache[$key];
    }

    /**
     * A record-level branch check.
     *
     * The record was, in every UI path, already read through a branch-scoped
     * query, so this is a second line rather than the first. It stays open for
     * records that carry no branch — roles, settings, the chart of accounts and
     * the line-item tables, none of which belong to a branch — because denying
     * there would 403 people out of pages that have nothing to do with branch.
     */
    private function checkBranchScope(User $user, Model $record): bool
    {
        $recordBranchId = $record->getAttribute('branch_id');

        if ($recordBranchId === null) {
            // Either the model has no branch column, or it is a shared
            // catalogue row that every branch is entitled to use.
            return true;
        }

        return (int) $recordBranchId === (int) $user->branch_id;
    }

    // --- Own scope ---

    private function checkOwnScope(User $user, Model $record): bool
    {
        if (!isset($record->created_by)) {
            return true;
        }
        return $record->created_by === $user->id;
    }

    // --- Own Groups scope (trainers) ---

    private function getAssignedGroupIds(User $user): array
    {
        if (isset($this->cachedGroupIds[$user->id])) {
            return $this->cachedGroupIds[$user->id];
        }

        // Groups via Assignment records
        $assignmentIds = Assignment::active()
            ->forUser($user->id)
            ->where('assignable_type', TrainingGroup::class)
            ->pluck('assignable_id')
            ->toArray();

        // Groups where user is head_trainer_id
        $headTrainerIds = TrainingGroup::where('head_trainer_id', $user->id)
            ->pluck('id')
            ->toArray();

        $ids = array_unique(array_merge($assignmentIds, $headTrainerIds));

        $this->cachedGroupIds[$user->id] = $ids;
        return $ids;
    }

    private function checkOwnGroupsScope(User $user, Model $record): bool
    {
        $groupIds = $this->getAssignedGroupIds($user);

        if (empty($groupIds)) {
            return false;
        }

        if ($record instanceof TrainingGroup) {
            return in_array($record->id, $groupIds);
        }

        if ($record instanceof TrainingSession) {
            return in_array($record->training_group_id, $groupIds);
        }

        if (isset($record->training_group_id)) {
            return in_array($record->training_group_id, $groupIds);
        }

        if ($record instanceof Participant) {
            return Enrollment::where('participant_id', $record->id)
                ->whereIn('training_group_id', $groupIds)
                ->whereIn('status', ['active', 'pending'])
                ->exists();
        }

        if ($record instanceof AttendanceRecord) {
            $session = $record->session;
            return $session && in_array($session->training_group_id, $groupIds);
        }

        return false;
    }

    private function applyOwnGroupsScope(Builder $query, User $user, string $table): Builder
    {
        $groupIds = $this->getAssignedGroupIds($user);

        if (empty($groupIds)) {
            return $query->whereRaw('1 = 0');
        }

        return match ($table) {
            'training_groups' => $query->whereIn("{$table}.id", $groupIds),
            'training_sessions' => $query->whereIn("{$table}.training_group_id", $groupIds),
            'enrollments' => $query->whereIn("{$table}.training_group_id", $groupIds),
            'evaluations' => $query->whereIn("{$table}.training_group_id", $groupIds),
            'training_programs' => $query->whereIn("{$table}.id", function ($sub) use ($groupIds) {
                $sub->select('training_program_id')
                    ->from('training_groups')
                    ->whereIn('id', $groupIds);
            }),
            'participants' => $query->whereIn("{$table}.id", function ($sub) use ($groupIds) {
                $sub->select('participant_id')
                    ->from('enrollments')
                    ->whereIn('training_group_id', $groupIds)
                    ->whereIn('status', ['active', 'pending']);
            }),
            'attendance_records' => $query->whereIn("{$table}.training_session_id", function ($sub) use ($groupIds) {
                $sub->select('id')
                    ->from('training_sessions')
                    ->whereIn('training_group_id', $groupIds);
            }),
            'invoices' => $query->whereIn("{$table}.billable_id", function ($sub) use ($groupIds) {
                $sub->select('participant_id')
                    ->from('enrollments')
                    ->whereIn('training_group_id', $groupIds)
                    ->whereIn('status', ['active', 'pending']);
            })->where("{$table}.billable_type", Participant::class),
            default => $query->whereIn("{$table}.training_group_id", $groupIds),
        };
    }

    // --- Own Children scope (parents/guardians) ---

    private function getChildParticipantIds(User $user): array
    {
        if (isset($this->cachedChildIds[$user->id])) {
            return $this->cachedChildIds[$user->id];
        }

        // This was `->where('person_id', …)->orWhere('user_id', …)->first()`.
        // Two bugs in one line: the orWhere let the person_id branch escape the
        // tenant global scope, and ->first() on a non-unique column meant a
        // guardian with two rows saw one set of children and was 403'd on the
        // rest. GuardianResolver returns the union of every row, and includes
        // the account's own participant so a player is not permanently empty.
        $ids = app(GuardianResolver::class)->participantIdsFor($user);

        $this->cachedChildIds[$user->id] = $ids;

        return $ids;
    }

    private function checkOwnChildrenScope(User $user, Model $record): bool
    {
        $childIds = $this->getChildParticipantIds($user);

        if (empty($childIds)) {
            return false;
        }

        if ($record instanceof Participant) {
            return in_array($record->id, $childIds);
        }

        if (isset($record->participant_id)) {
            return in_array($record->participant_id, $childIds);
        }

        if ($record instanceof Invoice && $record->billable_type === Participant::class) {
            return in_array($record->billable_id, $childIds);
        }

        if ($record instanceof AttendanceRecord && $record->subject_type === Participant::class) {
            return in_array($record->subject_id, $childIds);
        }

        return false;
    }

    private function applyOwnChildrenScope(Builder $query, User $user, string $table): Builder
    {
        $childIds = $this->getChildParticipantIds($user);

        if (empty($childIds)) {
            return $query->whereRaw('1 = 0');
        }

        return match ($table) {
            'participants' => $query->whereIn("{$table}.id", $childIds),
            'enrollments' => $query->whereIn("{$table}.participant_id", $childIds),
            'evaluations' => $query->whereIn("{$table}.participant_id", $childIds),
            'invoices' => $query->where("{$table}.billable_type", Participant::class)
                ->whereIn("{$table}.billable_id", $childIds),
            'payments' => $query->whereIn("{$table}.invoice_id", function ($sub) use ($childIds) {
                $sub->select('id')
                    ->from('invoices')
                    ->where('billable_type', Participant::class)
                    ->whereIn('billable_id', $childIds);
            }),
            'attendance_records' => $query->where("{$table}.subject_type", Participant::class)
                ->whereIn("{$table}.subject_id", $childIds),
            default => $query->whereIn("{$table}.participant_id", $childIds),
        };
    }
}
