<?php

declare(strict_types=1);

namespace App\Support\Rbac;

use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\Auth\UiTooltips as AuthUiTooltips;
use Closure;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use ReflectionObject;
use Throwable;

/**
 * Central RBAC UI Enforcement Helper for Filament Actions.
 *
 * Enforces constitution RBAC-UX rules:
 * - Non-member → hidden UI + 404 server-side
 * - Member without capability → visible-but-disabled + tooltip + 403 server-side
 * - Member with capability → enabled
 * - Destructive actions → requiresConfirmation()
 *
 * @see \App\Support\Rbac\UiTooltips
 * @see \App\Support\Rbac\TenantAccessContext
 */
final class UiEnforcement
{
    private Action|BulkAction $action;

    private bool $requireMembership = true;

    private ?string $capability = null;

    private bool $isDestructive = false;

    private ?string $customTooltip = null;

    private Model|Closure|null $record = null;

    private ?Collection $records = null;

    private bool $isBulk = false;

    private bool $preserveExistingVisibility = false;

    private bool $preserveExistingDisabled = false;

    private function __construct(Action|BulkAction $action)
    {
        $this->action = $action;
    }

    /**
     * Create enforcement for a header/page action.
     *
     * @param  Action  $action  The Filament action to wrap
     */
    public static function forAction(Action $action): self
    {
        return new self($action);
    }

    /**
     * Create enforcement for a table row action.
     *
     * @param  Action  $action  The Filament action to wrap
     * @param  Model|Closure  $record  The record or a closure that returns the record
     */
    public static function forTableAction(Action $action, Model|Closure $record): self
    {
        $instance = new self($action);
        $instance->record = $record;

        return $instance;
    }

    /**
     * Create enforcement for a bulk action with all-or-nothing semantics.
     *
     * If any selected record fails the capability check for a member,
     * the action is disabled entirely.
     *
     * @param  BulkAction  $action  The Filament bulk action to wrap
     */
    public static function forBulkAction(BulkAction $action): self
    {
        $instance = new self($action);
        $instance->isBulk = true;

        return $instance;
    }

    /**
     * Require managed-environment access for this action.
     *
     * @param  bool  $require  Whether membership is required (default: true)
     */
    public function requireMembership(bool $require = true): self
    {
        $this->requireMembership = $require;

        return $this;
    }

    /**
     * Require a specific capability for this action.
     *
     * @param  string  $capability  A capability constant from Capabilities class
     *
     * @throws \InvalidArgumentException If capability is not in the canonical registry
     */
    public function requireCapability(string $capability): self
    {
        if (! Capabilities::isKnown($capability)) {
            throw new \InvalidArgumentException(
                "Unknown capability: {$capability}. Use constants from ".Capabilities::class
            );
        }

        $this->capability = $capability;

        return $this;
    }

    /**
     * Mark this action as destructive (requires confirmation modal).
     */
    public function destructive(): self
    {
        $this->isDestructive = true;

        return $this;
    }

    /**
     * Override the default tooltip for disabled actions.
     *
     * @param  string  $message  Custom tooltip message
     */
    public function tooltip(string $message): self
    {
        $this->customTooltip = $message;

        return $this;
    }

    /**
     * Preserve the action's existing visibility logic.
     *
     * Use this when the action already has business-logic visibility
     * (e.g., `->visible(fn ($record) => $record->trashed())`) that should be kept.
     *
     * UiEnforcement will combine the existing visibility condition with tenant
     * membership visibility, instead of overwriting it.
     *
     * @return $this
     */
    public function preserveVisibility(): self
    {
        $this->preserveExistingVisibility = true;

        return $this;
    }

    /**
     * Preserve the action's existing disabled logic.
     *
     * Use this when the action is disabled for business reasons (e.g. operation
     * state) and you still want UiEnforcement to add capability gating on top.
     *
     * @return $this
     */
    public function preserveDisabled(): self
    {
        $this->preserveExistingDisabled = true;

        return $this;
    }

    /**
     * Apply all enforcement rules to the action and return it.
     *
     * This sets up:
     * - UI visibility (hidden for non-members)
     * - UI disabled state + tooltip (for members without capability)
     * - Destructive confirmation (if marked)
     * - Server-side guards (404/403)
     *
     * @return Action|BulkAction The configured action
     */
    public function apply(): Action|BulkAction
    {
        $this->applyVisibility();
        $this->applyDisabledState();
        $this->applyDestructiveConfirmation();
        $this->applyServerSideGuard();

        return $this->action;
    }

    /**
     * Evaluate whether a bulk selection is authorized (all-or-nothing).
     *
     * - If any selected tenant is not a membership: false.
     * - If all are memberships but any lacks capability: false.
     */
    public function bulkSelectionIsAuthorized(User $user, Collection $records): bool
    {
        $tenantIds = $this->resolveTenantIdsFromRecords($records);

        if ($tenantIds === []) {
            return false;
        }

        /** @var CapabilityResolver $resolver */
        $resolver = app(CapabilityResolver::class);
        $resolver->primeMemberships($user, $tenantIds);

        foreach ($tenantIds as $tenantId) {
            $tenant = $this->makeTenantStub($tenantId);

            if (! $resolver->isMember($user, $tenant)) {
                return false;
            }

            if ($this->capability !== null && ! $resolver->can($user, $tenant, $this->capability)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Hide action for non-members.
     *
     * Skipped if preserveVisibility() was called.
     */
    private function applyVisibility(): void
    {
        if (! $this->requireMembership) {
            return;
        }

        $existingVisibility = $this->preserveExistingVisibility
            ? $this->getExistingVisibilityCondition()
            : null;

        $this->action->visible(function (?Model $record = null) use ($existingVisibility) {
            $context = $this->resolveContextWithRecord($record);

            if (! $context->isMember) {
                return false;
            }

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

            return $this->evaluateVisibilityCondition($existingVisibility, $record);
        });
    }

    /**
     * Attempt to retrieve the existing visibility condition from the action.
     *
     * Filament stores this as the protected property `$isVisible` (bool|Closure)
     * on actions via the CanBeHidden concern.
     */
    private function getExistingVisibilityCondition(): bool|Closure|null
    {
        try {
            $ref = new ReflectionObject($this->action);
            if (! $ref->hasProperty('isVisible')) {
                return null;
            }

            $property = $ref->getProperty('isVisible');
            $property->setAccessible(true);

            /** @var bool|Closure $value */
            $value = $property->getValue($this->action);

            return $value;
        } catch (Throwable) {
            return null;
        }
    }

    /**
     * Evaluate an existing bool|Closure visibility condition.
     *
     * This is a best-effort evaluator for business visibility closures.
     * If the closure cannot be evaluated safely, we fail closed (return false).
     */
    private function evaluateVisibilityCondition(bool|Closure $condition, ?Model $record): bool
    {
        if (is_bool($condition)) {
            return $condition;
        }

        try {
            $reflection = new \ReflectionFunction($condition);
            $parameters = $reflection->getParameters();

            if ($parameters === []) {
                return (bool) $condition();
            }

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

            return (bool) $condition($record);
        } catch (Throwable) {
            return false;
        }
    }

    /**
     * Disable action for members without capability.
     */
    private function applyDisabledState(): void
    {
        if ($this->capability === null) {
            return;
        }

        $existingDisabled = $this->preserveExistingDisabled
            ? $this->getExistingDisabledCondition()
            : null;

        $tooltip = $this->customTooltip ?? AuthUiTooltips::insufficientPermission();

        $this->action->disabled(function (?Model $record = null) use ($existingDisabled) {
            if ($existingDisabled !== null && $this->evaluateDisabledCondition($existingDisabled, $record)) {
                return true;
            }

            if ($this->isBulk && $this->action instanceof BulkAction) {
                $user = auth()->user();

                if (! $user instanceof User) {
                    return true;
                }

                /** @var Collection<int, Model> $records */
                $records = collect($this->action->getSelectedRecords());

                return ! $this->bulkSelectionIsAuthorized($user, $records);
            }

            $context = $this->resolveContextWithRecord($record);

            // Non-members are hidden, so this only affects members
            if (! $context->isMember) {
                return true;
            }

            return ! $context->hasCapability;
        });

        // Only show tooltip when actually disabled
        $this->action->tooltip(function (?Model $record = null) use ($tooltip) {
            if ($this->isBulk && $this->action instanceof BulkAction) {
                $user = auth()->user();

                if (! $user instanceof User) {
                    return $tooltip;
                }

                /** @var Collection<int, Model> $records */
                $records = collect($this->action->getSelectedRecords());

                if (! $this->bulkSelectionIsAuthorized($user, $records)) {
                    return $tooltip;
                }

                return null;
            }

            $context = $this->resolveContextWithRecord($record);

            if ($context->isMember && ! $context->hasCapability) {
                return $tooltip;
            }

            return null;
        });
    }

    /**
     * Attempt to retrieve the existing disabled condition from the action.
     *
     * Filament stores this as the protected property `$isDisabled` (bool|Closure)
     * on actions via the CanBeDisabled concern.
     */
    private function getExistingDisabledCondition(): bool|Closure|null
    {
        try {
            $ref = new ReflectionObject($this->action);
            if (! $ref->hasProperty('isDisabled')) {
                return null;
            }

            $property = $ref->getProperty('isDisabled');
            $property->setAccessible(true);

            /** @var bool|Closure $value */
            $value = $property->getValue($this->action);

            return $value;
        } catch (Throwable) {
            return null;
        }
    }

    /**
     * Evaluate an existing bool|Closure disabled condition.
     *
     * This is a best-effort evaluator for business disabled closures.
     * If the closure cannot be evaluated safely, we fail closed (return true).
     */
    private function evaluateDisabledCondition(bool|Closure $condition, ?Model $record): bool
    {
        if (is_bool($condition)) {
            return $condition;
        }

        try {
            $reflection = new \ReflectionFunction($condition);
            $parameters = $reflection->getParameters();

            if ($parameters === []) {
                return (bool) $condition();
            }

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

            return (bool) $condition($record);
        } catch (Throwable) {
            return true;
        }
    }

    /**
     * Add confirmation modal for destructive actions.
     */
    private function applyDestructiveConfirmation(): void
    {
        if (! $this->isDestructive) {
            return;
        }

        $this->action->requiresConfirmation();
        $this->action->modalHeading(UiTooltips::DESTRUCTIVE_CONFIRM_TITLE);
        $this->action->modalDescription(UiTooltips::DESTRUCTIVE_CONFIRM_DESCRIPTION);
    }

    /**
     * Wrap the action handler with server-side authorization guard.
     *
     * This is a defense-in-depth measure. In normal operation, Filament's
     * isDisabled() check prevents execution. This guard catches edge cases
     * where the disabled check might be bypassed.
     */
    private function applyServerSideGuard(): void
    {
        $this->action->before(function (?Model $record = null): void {
            if ($this->isBulk && $this->action instanceof BulkAction) {
                $user = auth()->user();

                if (! $user instanceof User) {
                    abort(403);
                }

                /** @var Collection<int, Model> $records */
                $records = collect($this->action->getSelectedRecords());

                $this->authorizeBulkSelectionOrAbort($user, $records);

                return;
            }

            $context = $this->resolveContextWithRecord($record);

            // Non-member → 404 (deny-as-not-found)
            if ($context->shouldDenyAsNotFound()) {
                abort(404);
            }

            // Member without capability → 403 (forbidden)
            if ($context->shouldDenyAsForbidden()) {
                abort(403);
            }
        });
    }

    /**
     * Server-side enforcement for bulk selections.
     *
     * - If any selected tenant is not a membership: 404 (deny-as-not-found).
     * - If all are memberships but any lacks capability: 403.
     */
    private function authorizeBulkSelectionOrAbort(User $user, Collection $records): void
    {
        $tenantIds = $this->resolveTenantIdsFromRecords($records);

        if ($tenantIds === []) {
            abort(403);
        }

        /** @var CapabilityResolver $resolver */
        $resolver = app(CapabilityResolver::class);
        $resolver->primeMemberships($user, $tenantIds);

        foreach ($tenantIds as $tenantId) {
            $tenant = $this->makeTenantStub($tenantId);

            if (! $resolver->isMember($user, $tenant)) {
                abort(404);
            }
        }

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

        foreach ($tenantIds as $tenantId) {
            $tenant = $this->makeTenantStub($tenantId);

            if (! $resolver->can($user, $tenant, $this->capability)) {
                abort(403);
            }
        }
    }

    /**
     * @param  Collection<int, Model>  $records
     * @return array<int, int>
     */
    private function resolveTenantIdsFromRecords(Collection $records): array
    {
        $tenantIds = [];

        foreach ($records as $record) {
            if ($record instanceof ManagedEnvironment) {
                $tenantIds[] = (int) $record->getKey();

                continue;
            }

            if ($record instanceof Model) {
                $tenantId = $record->getAttribute('managed_environment_id');
                if ($tenantId !== null) {
                    $tenantIds[] = (int) $tenantId;

                    continue;
                }

                if (method_exists($record, 'relationLoaded') && $record->relationLoaded('tenant')) {
                    $relatedTenant = $record->getRelation('tenant');

                    if ($relatedTenant instanceof ManagedEnvironment) {
                        $tenantIds[] = (int) $relatedTenant->getKey();

                        continue;
                    }
                }
            }
        }

        if ($tenantIds === []) {
            $tenant = Filament::getTenant();
            if ($tenant instanceof ManagedEnvironment) {
                $tenantIds[] = (int) $tenant->getKey();
            }
        }

        return array_values(array_unique($tenantIds));
    }

    private function makeTenantStub(int $tenantId): ManagedEnvironment
    {
        $tenant = new ManagedEnvironment;
        $tenant->forceFill(['id' => $tenantId]);
        $tenant->exists = true;

        return $tenant;
    }

    /**
     * Resolve the current access context with an optional record.
     */
    private function resolveContextWithRecord(?Model $record = null): TenantAccessContext
    {
        $user = auth()->user();

        // For table actions, resolve the record and use it as tenant if it's a ManagedEnvironment
        $tenant = $this->resolveTenantWithRecord($record);

        if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment) {
            return new TenantAccessContext(
                user: null,
                tenant: null,
                isMember: false,
                hasCapability: false,
            );
        }

        /** @var CapabilityResolver $resolver */
        $resolver = app(CapabilityResolver::class);

        $isMember = $resolver->isMember($user, $tenant);

        $hasCapability = true;
        if ($this->capability !== null && $isMember) {
            $hasCapability = $resolver->can($user, $tenant, $this->capability);
        }

        return new TenantAccessContext(
            user: $user,
            tenant: $tenant,
            isMember: $isMember,
            hasCapability: $hasCapability,
        );
    }

    /**
     * Resolve the tenant for this action with an optional record.
     *
     * Priority:
     * 1. If $record is passed and is a ManagedEnvironment, use it
     * 2. If $this->record is set (for forTableAction), resolve it
     * 3. Fall back to Filament::getTenant()
     */
    private function resolveTenantWithRecord(?Model $record = null): ?ManagedEnvironment
    {
        // If a record is passed directly (from closure parameter), check if it's a ManagedEnvironment
        if ($record instanceof ManagedEnvironment) {
            return $record;
        }

        // If a record has an eagerly-loaded `tenant` relation, prefer it.
        // This avoids relying on Filament::getTenant() for list pages that are not tenant-scoped.
        if ($record instanceof Model && method_exists($record, 'relationLoaded') && $record->relationLoaded('tenant')) {
            $relatedTenant = $record->getRelation('tenant');

            if ($relatedTenant instanceof ManagedEnvironment) {
                return $relatedTenant;
            }
        }

        if ($this->action instanceof Action) {
            $actionRecord = $this->action->getRecord(withDefault: false);

            if ($actionRecord instanceof ManagedEnvironment) {
                return $actionRecord;
            }

            if ($actionRecord instanceof Model && method_exists($actionRecord, 'relationLoaded') && $actionRecord->relationLoaded('tenant')) {
                $relatedTenant = $actionRecord->getRelation('tenant');

                if ($relatedTenant instanceof ManagedEnvironment) {
                    return $relatedTenant;
                }
            }
        }

        // If a record is set from forTableAction, try to resolve it
        if ($this->record !== null) {
            $resolved = $this->record instanceof Closure
                ? ($this->record)()
                : $this->record;

            if ($resolved instanceof ManagedEnvironment) {
                return $resolved;
            }
        }

        // Default: use Filament's current tenant
        return Filament::getTenant();
    }
}
