<?php

namespace Filament\Actions\Concerns;

use BackedEnum;
use Closure;
use Illuminate\Auth\Access\Response;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Gate;
use LogicException;
use UnitEnum;

trait CanBeAuthorized
{
    // Security: Actions do not have automatic policy-based authorization.
    // Authorization defaults to `null` (allowed for all users).
    // You must explicitly use `authorize()`, `visible()`, or
    // `hidden()` to restrict access to custom actions.

    protected mixed $authorization = null;

    protected string | Closure | null $authorizationMessage = null;

    protected bool | Closure $hasAuthorizationTooltip = false;

    protected bool | Closure $hasAuthorizationNotification = false;

    protected bool | string | UnitEnum | Closure | null $authorizeIndividualRecords = null;

    /**
     * @param  Model | class-string | array<mixed> | null  $arguments
     */
    public function authorize(mixed $abilities, Model | string | array | null $arguments = null): static
    {
        $abilities = match (true) {
            $abilities instanceof BackedEnum => $abilities->value,
            $abilities instanceof UnitEnum => $abilities->name,
            default => $abilities,
        };

        if (is_string($abilities) || is_array($abilities)) {
            $this->authorization = [
                'type' => 'all',
                'abilities' => Arr::wrap($abilities),
                'arguments' => Arr::wrap($arguments),
            ];
        } else {
            $this->authorization = $abilities;
        }

        return $this;
    }

    /**
     * @param  string | UnitEnum | array<string | UnitEnum>  $abilities
     * @param  Model | array<mixed> | null  $arguments
     */
    public function authorizeAny(string | UnitEnum | array $abilities, Model | array | null $arguments = null): static
    {
        $abilities = match (true) {
            $abilities instanceof BackedEnum => $abilities->value,
            $abilities instanceof UnitEnum => $abilities->name,
            default => $abilities,
        };

        $this->authorization = [
            'type' => 'any',
            'abilities' => Arr::wrap($abilities),
            'arguments' => Arr::wrap($arguments),
        ];

        return $this;
    }

    /**
     * @param  array<mixed>  $arguments
     * @return array<mixed>
     */
    protected function parseAuthorizationArguments(array $arguments): array
    {
        if ($record = $this->getRecord()) {
            array_unshift($arguments, $record);
        } elseif ($model = $this->getModel()) {
            array_unshift($arguments, $model);
        }

        return $arguments;
    }

    public function isAuthorized(): bool
    {
        if (! $this->hasTable()) {
            return $this->resolveIsAuthorized();
        }

        if (! $this->prepareVisibilityCache()) {
            return $this->resolveIsAuthorized();
        }

        return $this->cachedIsAuthorized ??= $this->resolveIsAuthorized();
    }

    protected function resolveIsAuthorized(): bool
    {
        if ($this->authorization === null) {
            return $this->getHasActionsLivewire()?->getDefaultActionAuthorizationResponse($this)?->allowed() ?? true;
        }

        if (! is_array($this->authorization)) {
            $response = $this->evaluate($this->authorization);

            return match (true) {
                $response instanceof Response => $response->allowed(),
                default => (bool) $response,
            };
        }

        $abilities = $this->authorization['abilities'] ?? [];
        $arguments = $this->parseAuthorizationArguments($this->authorization['arguments'] ?? []);
        $type = $this->authorization['type'] ?? null;

        return match ($type) {
            'all' => Gate::check($abilities, $arguments),
            'any' => Gate::any($abilities, $arguments),
            default => false,
        };
    }

    public function getAuthorizationResponse(): Response
    {
        if ($this->authorization === null) {
            return $this->getHasActionsLivewire()->getDefaultActionAuthorizationResponse($this) ?? Response::allow();
        }

        if (! is_array($this->authorization)) {
            $response = $this->evaluate($this->authorization);

            return match (true) {
                $response instanceof Response => $response,
                (bool) $response => Response::allow(),
                default => Response::deny(),
            };
        }

        $abilities = $this->authorization['abilities'] ?? [];
        $arguments = $this->parseAuthorizationArguments($this->authorization['arguments'] ?? []);
        $type = $this->authorization['type'] ?? null;

        foreach ($abilities as $ability) {
            $response = Gate::inspect($ability, Arr::wrap($arguments));

            if (($type === 'any') && $response->allowed()) {
                return $response;
            }

            if (($type === 'all') && $response->denied()) {
                return $response;
            }
        }

        return Response::allow();
    }

    public function getAuthorizationResponseWithMessage(): Response
    {
        $response = $this->getAuthorizationResponse();

        if ($response->allowed()) {
            return $response;
        }

        $message = $this->getAuthorizationMessage();

        if (filled($message)) {
            invade($response)->message = $message; /** @phpstan-ignore-line */

            return $response;
        }

        if (blank($response->message())) {
            throw new LogicException('An authorization was denied without a message.');
        }

        return $response;
    }

    public function authorizationMessage(string | Closure | null $message): static
    {
        $this->authorizationMessage = $message;

        return $this;
    }

    public function getAuthorizationMessage(): ?string
    {
        return $this->evaluate($this->authorizationMessage);
    }

    public function authorizationTooltip(bool | Closure $condition = true): static
    {
        $this->hasAuthorizationTooltip = $condition;

        return $this;
    }

    public function authorizationNotification(bool | Closure $condition = true): static
    {
        $this->hasAuthorizationNotification = $condition;

        return $this;
    }

    public function hasAuthorizationTooltip(): bool
    {
        return (bool) $this->evaluate($this->hasAuthorizationTooltip);
    }

    public function hasAuthorizationNotification(): bool
    {
        return (bool) $this->evaluate($this->hasAuthorizationNotification);
    }

    public function isAuthorizedOrNotHiddenWhenUnauthorized(): bool
    {
        if (! $this->hasTable()) {
            return $this->resolveIsAuthorizedOrNotHiddenWhenUnauthorized();
        }

        if (! $this->prepareVisibilityCache()) {
            return $this->resolveIsAuthorizedOrNotHiddenWhenUnauthorized();
        }

        return $this->cachedIsAuthorizedOrNotHiddenWhenUnauthorized ??= $this->resolveIsAuthorizedOrNotHiddenWhenUnauthorized();
    }

    protected function resolveIsAuthorizedOrNotHiddenWhenUnauthorized(): bool
    {
        if (! $this->hasAuthorizationTooltip() && ! $this->hasAuthorizationNotification()) {
            return $this->isAuthorized();
        }

        $response = $this->getAuthorizationResponse();

        if ($response->allowed()) {
            return true;
        }

        return filled($response->message()) || filled($this->getAuthorizationMessage());
    }

    public function authorizeIndividualRecords(bool | string | UnitEnum | Closure | null $callback = true): static
    {
        $this->authorizeIndividualRecords = $callback;

        return $this;
    }

    public function getIndividualRecordAuthorizationResponse(Model $record): Response
    {
        if (is_string($this->authorizeIndividualRecords) || ($this->authorizeIndividualRecords instanceof UnitEnum)) {
            return Gate::inspect($this->authorizeIndividualRecords, Arr::wrap($record));
        }

        $resolver = ($this->authorizeIndividualRecords instanceof Closure)
            ? $this->authorizeIndividualRecords
            : $this->getHasActionsLivewire()->getDefaultActionIndividualRecordAuthorizationResponseResolver($this);

        if (! $resolver) {
            throw new LogicException('No function was passed to [authorizeIndividualRecords()].');
        }

        $response = $resolver($record);

        if ($response instanceof Response) {
            return $response;
        }

        return $response ? Response::allow() : Response::deny();
    }

    public function shouldAuthorizeIndividualRecords(): bool
    {
        return filled($this->authorizeIndividualRecords) && ($this->authorizeIndividualRecords !== false);
    }

    public function hasAuthorization(): bool
    {
        return $this->authorization !== null
            || $this->authorizeIndividualRecords !== null
            || $this->hasAuthorizationNotification !== false
            || $this->hasAuthorizationTooltip !== false
            || $this->authorizationMessage !== null;
    }
}
