<?php

namespace App\Filament\Resources\Concerns;

use App\Filament\Forms\Components\PermissionOverridePicker;
use App\Models\Role;
use App\Services\AuditLog;
use Closure;
use Illuminate\Support\Facades\Auth;

/**
 * Reproduces UserManagementUserController::assertNoEscalation() /
 * RoleController::assertActorHoldsPermissions() from leolanka-inertia as
 * Filament field-level validation rules: a non-full-admin actor can only
 * grant permissions/roles they hold themselves, and never a full-admin role.
 * Shared by RoleResource (its own permission set) and UserResource (role
 * assignment + the effective permission-override matrix).
 */
trait ChecksPermissionEscalation
{
    /**
     * Rule for a `permissions` CheckboxList: every selected permission name
     * must be held by the acting user, unless the actor is a full admin.
     */
    protected static function permissionEscalationRule(): Closure
    {
        return function (): Closure {
            return function (string $attribute, mixed $value, Closure $fail): void {
                $actor = Auth::user();
                if (! $actor || $actor->isFullAdmin()) {
                    return;
                }

                $actorPermissions = $actor->cachedPermissionNames();

                // Livewire syncs an all-unchecked checkbox group back as
                // `false`, not `[]` — (array) false is [false], not [],
                // which would otherwise fail every name check below.
                foreach (is_array($value) ? $value : [] as $name) {
                    if (! $actorPermissions->contains($name)) {
                        AuditLog::record('escalation.blocked', $actor, ['attempted_permission' => $name]);
                        $fail("You cannot grant permission \"{$name}\" — you do not hold it yourself.");

                        return;
                    }
                }
            };
        };
    }

    /**
     * Rule for the {@see PermissionOverridePicker}
     * state (`['grant' => [...], 'revoke' => [...]]`): a non-full-admin actor may
     * only *grant* permissions they hold themselves. Revoking needs no permission
     * held — taking access away from someone else is never escalation.
     */
    protected static function permissionOverrideEscalationRule(): Closure
    {
        return function (): Closure {
            return function (string $attribute, mixed $value, Closure $fail): void {
                $actor = Auth::user();
                if (! $actor || $actor->isFullAdmin()) {
                    return;
                }

                $actorPermissions = $actor->cachedPermissionNames();

                foreach (PermissionOverridePicker::normalize($value)['grant'] as $name) {
                    if (! $actorPermissions->contains($name)) {
                        AuditLog::record('escalation.blocked', $actor, ['attempted_permission' => $name]);
                        $fail("You cannot grant permission \"{$name}\" — you do not hold it yourself.");

                        return;
                    }
                }
            };
        };
    }

    /**
     * Rule for a role multi-select: every selected role must be active, the actor
     * must hold every permission the role carries, and a non-full-admin,
     * non-all-branch actor may never assign a full-admin or all-branch role —
     * otherwise the direct {@see allBranchAccessEscalationRule()} toggle guard
     * could be sidestepped by assigning a role that carries the grant instead of
     * setting it directly.
     *
     * The "actor holds every permission the role carries" check is
     * RoleController::assertActorHoldsPermissions() applied to role *assignment*.
     * It used to be enforced only as a side effect: the user form submitted the
     * target's whole effective permission set, so {@see permissionOverrideEscalationRule()}'s
     * predecessor happened to reject any role bringing in a permission the actor
     * lacked. Now that the matrix submits only deliberate overrides, that
     * incidental cover is gone and the rule has to be stated here — without it a
     * limited administrator could hand out a role far stronger than their own.
     */
    protected static function roleEscalationRule(): Closure
    {
        return function (): Closure {
            return function (string $attribute, mixed $value, Closure $fail): void {
                $actor = Auth::user();
                $roles = Role::query()->with('permissions:id,name')->whereIn('id', (array) $value)->get();

                foreach ($roles as $role) {
                    if (! $role->is_active) {
                        $fail("Role \"{$role->name}\" is inactive and cannot be assigned.");

                        return;
                    }
                }

                if (! $actor || $actor->isFullAdmin()) {
                    return;
                }

                // Full-admin/all-branch checks run first: a role flagged either way
                // is rejected for that reason specifically, regardless of what
                // explicit permission rows it happens to also carry (typically
                // none, but nothing enforces that) — otherwise the "beyond" check
                // below could report a technically-true but misleading reason.
                foreach ($roles as $role) {
                    if ($role->is_full_admin) {
                        AuditLog::record('escalation.blocked', $actor, [
                            'attempted_role_id' => $role->id,
                            'reason' => 'full_admin_role',
                        ]);
                        $fail('You cannot assign a full-admin role.');

                        return;
                    }
                }

                if (! $actor->hasAllBranchAccess()) {
                    foreach ($roles as $role) {
                        if ($role->is_all_branches) {
                            AuditLog::record('escalation.blocked', $actor, [
                                'attempted_role_id' => $role->id,
                                'reason' => 'all_branches_role',
                            ]);
                            $fail('You cannot assign a role that grants All-Branch Access.');

                            return;
                        }
                    }
                }

                $actorPermissions = $actor->cachedPermissionNames();

                foreach ($roles as $role) {
                    $beyond = $role->permissions
                        ->pluck('name')
                        ->reject(fn (string $name): bool => $actorPermissions->contains($name));

                    if ($beyond->isNotEmpty()) {
                        AuditLog::record('escalation.blocked', $actor, [
                            'attempted_role_id' => $role->id,
                            'reason' => 'role_exceeds_actor_permissions',
                            'attempted_permission' => $beyond->first(),
                        ]);
                        $fail("You cannot assign role \"{$role->name}\" — it grants \"{$beyond->first()}\", which you do not hold yourself.");

                        return;
                    }
                }
            };
        };
    }

    /**
     * Rule for the `is_all_branches` toggle: only an actor who already holds
     * unrestricted branch access (full admin or the grant itself) may extend
     * it to someone else. Turning it off, or leaving it off, is never
     * escalation and always passes.
     */
    protected static function allBranchAccessEscalationRule(): Closure
    {
        return function (): Closure {
            return function (string $attribute, mixed $value, Closure $fail): void {
                if (! $value) {
                    return;
                }

                $actor = Auth::user();
                if (! $actor || $actor->isFullAdmin() || $actor->hasAllBranchAccess()) {
                    return;
                }

                AuditLog::record('escalation.blocked', $actor, ['attempted_grant' => 'is_all_branches']);
                $fail('You cannot grant All-Branch Access — you do not hold it yourself.');
            };
        };
    }
}
