<?php

namespace App\Models\Concerns;

use App\Filament\Forms\Components\PermissionOverridePicker;
use App\Models\Central\Tenant;
use App\Models\Permission;
use App\Services\PermissionCatalog;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

/**
 * Layered RBAC: a user's effective permissions are COMPUTED, never copied.
 *
 *   Effective = (permissions of every active assigned role) + allow overrides − deny overrides
 *
 * Roles are the bulk control; per-user allow/deny overrides are the exception
 * layer. Editing a role flows to its users immediately (no re-sync).
 */
trait HasPermissions
{
    /**
     * Per-user exception layer (allow/deny). Not the source of truth on its own.
     */
    public function permissionOverrides(): BelongsToMany
    {
        return $this->belongsToMany(Permission::class, 'user_permission_overrides')
            ->withPivot(['type', 'granted_by', 'granted_at']);
    }

    public function cachedPermissionNames(): Collection
    {
        return Cache::remember(
            $this->permissionCacheKey(),
            now()->addHour(),
            fn () => $this->computeEffectivePermissionNames(),
        );
    }

    /**
     * Resolve the live effective permission set from roles + overrides.
     */
    public function computeEffectivePermissionNames(): Collection
    {
        $rolePermissions = DB::table('role_permissions as rp')
            ->join('user_roles as ur', 'ur.role_id', '=', 'rp.role_id')
            ->join('roles as r', 'r.id', '=', 'rp.role_id')
            ->join('permissions as p', 'p.id', '=', 'rp.permission_id')
            ->where('ur.user_id', $this->id)
            ->where('r.is_active', true)
            ->whereNull('r.deleted_at')
            ->pluck('p.name');

        [$allows, $denies] = $this->overrideNames();

        return $rolePermissions
            ->merge($allows)
            ->unique()
            ->diff($denies)
            ->values();
    }

    /**
     * The stored exception layer, in the shape the per-user permission editor
     * binds to ({@see PermissionOverridePicker}).
     * Anything absent from both lists is inherited from the user's roles.
     *
     * @return array{grant: array<int, string>, revoke: array<int, string>}
     */
    public function permissionOverrideLists(): array
    {
        [$allows, $denies] = $this->overrideNames();

        return [
            'grant' => $allows->values()->all(),
            'revoke' => $denies->values()->all(),
        ];
    }

    /**
     * The permissions this user's active roles grant, before the override layer.
     * This is the set that moves on its own when a role is edited.
     *
     * @return Collection<int, string>
     */
    public function inheritedPermissionNames(): Collection
    {
        return DB::table('role_permissions as rp')
            ->join('user_roles as ur', 'ur.role_id', '=', 'rp.role_id')
            ->join('roles as r', 'r.id', '=', 'rp.role_id')
            ->join('permissions as p', 'p.id', '=', 'rp.permission_id')
            ->where('ur.user_id', $this->id)
            ->where('r.is_active', true)
            ->whereNull('r.deleted_at')
            ->distinct()
            ->pluck('p.name');
    }

    /**
     * Per-permission provenance for auditing in the UI.
     *
     * @return array{roles: array<string, array<int, string>>, allow: array<int, string>, deny: array<int, string>, inherited: array<int, string>, effective: array<int, string>}
     */
    public function permissionSources(): array
    {
        $byRole = DB::table('role_permissions as rp')
            ->join('user_roles as ur', 'ur.role_id', '=', 'rp.role_id')
            ->join('roles as r', 'r.id', '=', 'rp.role_id')
            ->join('permissions as p', 'p.id', '=', 'rp.permission_id')
            ->where('ur.user_id', $this->id)
            ->where('r.is_active', true)
            ->whereNull('r.deleted_at')
            ->get(['p.name as permission', 'r.name as role']);

        $roles = [];
        foreach ($byRole as $row) {
            $roles[$row->permission][] = $row->role;
        }

        [$allows, $denies] = $this->overrideNames();

        return [
            'roles' => $roles,
            'allow' => $allows->values()->all(),
            'deny' => $denies->values()->all(),
            // Held purely through the roles, with no override of its own — the
            // part of the set that follows a role edit.
            'inherited' => array_values(array_diff(array_keys($roles), $allows->all(), $denies->all())),
            'effective' => $this->computeEffectivePermissionNames()->all(),
        ];
    }

    public function hasPermissionTo(string $permission): bool
    {
        // The tenant entitlement ceiling caps everyone — even a tenant full admin
        // cannot use a module the landlord did not grant.
        if (! $this->tenantAllows($permission)) {
            return false;
        }

        if ($this->isFullAdmin()) {
            return true;
        }

        return $this->cachedPermissionNames()->contains($permission);
    }

    /**
     * @param  array<int, string>  $permissions
     */
    public function hasAnyPermission(array $permissions): bool
    {
        $isFullAdmin = $this->isFullAdmin();
        $cached = $isFullAdmin ? null : $this->cachedPermissionNames();

        foreach ($permissions as $permission) {
            if (! $this->tenantAllows($permission)) {
                continue;
            }

            if ($isFullAdmin || $cached->contains($permission)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Whether the user's tenant is entitled to use this permission. No
     * entitlement rows at all means no ceiling has been configured yet —
     * unrestricted, consistent with {@see PermissionCatalog::grantableFor()}.
     * The ceiling only skips entirely when the user has no tenant (CLI/test context).
     */
    private function tenantAllows(string $permission): bool
    {
        $tenantId = $this->tenant_id;
        if (! $tenantId) {
            return true;
        }

        $entitled = Tenant::entitledPermissionNamesFor((int) $tenantId);
        if ($entitled === []) {
            return true;
        }

        return in_array($permission, $entitled, true);
    }

    public function flushPermissionCache(): void
    {
        Cache::forget($this->permissionCacheKey());
        Cache::forget($this->fullAdminCacheKey());
        Cache::forget($this->allBranchAccessCacheKey());
    }

    /**
     * @return array{0: Collection<int, string>, 1: Collection<int, string>}
     */
    private function overrideNames(): array
    {
        $rows = DB::table('user_permission_overrides as o')
            ->join('permissions as p', 'p.id', '=', 'o.permission_id')
            ->where('o.user_id', $this->id)
            ->get(['p.name', 'o.type']);

        return [
            $rows->where('type', 'allow')->pluck('name'),
            $rows->where('type', 'deny')->pluck('name'),
        ];
    }

    private function permissionCacheKey(): string
    {
        return "user:{$this->id}:permissions";
    }

    private function fullAdminCacheKey(): string
    {
        return "user:{$this->id}:full_admin";
    }

    private function allBranchAccessCacheKey(): string
    {
        return "user:{$this->id}:all_branches";
    }
}
