<?php

    namespace Icing\Controller\Component;

    use Ali1\BruteForceShield\BruteForceShield;
    use Ali1\BruteForceShield\Configuration as BruteForceConfiguration;
    use ArrayAccess;
    use Authentication\Controller\Component\AuthenticationComponent;
    use Authentication\IdentityInterface;
    use Authorization\Controller\Component\AuthorizationComponent;
    use Authorization\Exception\ForbiddenException;
    use Cake\Cache\Cache;
    use Cake\Controller\Component;
    use Cake\Core\Configure;
    use Cake\Event\EventDispatcherTrait;
    use Cake\Http\Exception\HttpException;
    use Cake\ORM\Locator\LocatorAwareTrait;
    use Cake\Utility\Hash;
    use Exception;
    use Icing\Model\Entity\User;
    use Icing\Model\Table\PermissionsTable;
    use InvalidArgumentException;
    use RuntimeException;
    use TwoFactorAuth\Controller\Component\TwoFactorAuthComponent;

    /**
     * @property AuthenticationComponent $Authentication
     * @property AuthorizationComponent  $Authorization
     * @property TwoFactorAuthComponent  $TwoFactorAuth
     * @property PermissionsTable        $Permissions
     * @mixin AuthorizationComponent
     */
    class   AuthComponent extends Component
    {

        use LocatorAwareTrait;
        use EventDispatcherTrait;

        protected array $components = [
            'Authentication.Authentication',
            'Authorization.Authorization',
            'TwoFactorAuth.TwoFactorAuth',
        ];

        protected array $_defaultConfig = [
            'bruteForceConfig'          => [
                'timeLimit'    => 300,
                'attemptLimit' => 6,
            ],
            'requireAuthorizationCheck' => false,
        ];

        /**
         * @throws Exception
         */
        public function initialize(array $config): void
        {
            parent::initialize($config);


            //Load the other components
            $this->getController()->components()->set('Authentication', $this->Authentication);
            $this->getController()->components()->set('Authorization', $this->Authorization);

            // Complete access granted for Admins
            if (
                !$this->getConfig('requireAuthorizationCheck', false)
                || $this->getUserData('user_group_id', false) === 'admin'
            ) {
                $this->skipAuthorization();
            }

            if ($this->getUserData('dev')) {
                Configure::write('debug', true);
            }
        }

        // Authentication Related Methods

        public function getIdentity(): ?IdentityInterface
        {
            return $this->Authentication->getIdentity();
        }

        public function getIdentityData(string $path)
        {
            return $this->Authentication->getIdentityData($path);
        }

        public function getUser(): ?User
        {
            $identity = $this->Authentication->getIdentity();
            if (!$identity) {
                return null;
            }
            if ($identity instanceof User) {
                return $identity;
            }
            return null;
        }

        public function getUserData($key, $default = null)
        {
            try {
                return $this->Authentication->getIdentityData($key);
            } catch (RuntimeException) {
                return $default;
            }
        }

        // Authorization & Authentication Related Methods

        public function skipAuthorization(): static
        {
            try {
                $this->Authorization->skipAuthorization();
            } catch (InvalidArgumentException) {
            }
            return $this;
        }

        public function allow(...$actions): void
        {
            if (empty($actions)) {
                $this->allow($this->getController()->getRequest()->getParam('action'));
                return;
            }
            $actions = Hash::flatten($actions);
            $this->Authentication->addUnauthenticatedActions($actions);
            $action = $this->getController()->getRequest()->getParam('action');
            if (in_array($action, $actions)) {
                $this->skipAuthorization();
            }
        }

        // Authorization Related Methods

        public function is(int|string|iterable $user_group_id = null): bool
        {
            $id = $this->getUser()?->user_group_id;
            if (is_null($user_group_id)) {
                return $id;
            }
            if (is_iterable($user_group_id)) {
                return c($user_group_id)->some(fn($user_group_id) => $this->is($user_group_id));
            }
            return $id === $user_group_id;
        }

        public function isA(...$user_group_ids): bool
        {
            return $this->is($user_group_ids);
        }

        public function isAdmin(): bool
        {
            return $this->is('admin');
        }

        public function can($resource, $action): bool
        {
            return $this->Authorization->can($resource, $action);
        }

        public function authorize($resource = null, $action = null): void
        {
            //todo - move this check to policies
            /*$user = $this->getUser();
            if (!$user || $user->is('admin')) {
                return;
            }*/
            try {
                $this->Authorization->authorize($resource, $action);
            } catch (ForbiddenException $e) {
                $message = $e->getResult()?->getReason() ?? 'You are not authorized to access that location.';
                $this->getController()->Flash->error($message);
                throw $e;
            }
        }

        public function challenge($result = false, $errorMessage = 'Unauthorized to access that location', $then = null): void
        {
            // Admins have special permissions
            if ($this->getUser()?->is('admin')) {
                return;
            }
            if ($result !== false) {
                return;
            }
            if (is_callable($then)) {
                $then();
            }
            throw new ForbiddenException(null, $errorMessage);
        }

        public function applyScope(mixed $resource, ?string $action = null, mixed ...$optionalArgs)
        {
            return $this->Authorization->applyScope($resource, $action, ...$optionalArgs);
        }

        /**
         * Implements brute force protection by checking the number of attempts within a specified time limit.
         *
         * @param mixed        $data   Data used for the brute force protection check.
         * @param array|string $key
         * @param array        $config Optional configuration settings
         *                             - timeLimit: How long to check back for, in seconds
         *                             - attemptLimit: Number of allowed attempts before locking
         *                             - cache: Cache configuration to use, defaults to 'default'
         *
         * @return bool True if the request passes the brute force check, false if blocked.
         */
        public function bruteForceProtection(array|string $data = null, array|string $key = null, array $config = []): bool
        {
            $data   ??= $this->getController()->getRequest()->getData();
            $key    ??= $this->getController()->getRequest()->clientIp();
            $config += $this->getConfig('bruteForceConfig', []);

            if (!$data || !$key) {
                return true;
            }

            $cache = $config['cache'] ?? 'Icing.bruteForceProtection';
            if (!Cache::getConfig($cache)) {
                throw new RuntimeException("Cache configuration: '$cache' not found.");
            }

            if (is_iterable($key)) {
                return c($key)->every(fn($k) => $this->bruteForceProtection($data, $k, $config));
            }

            $bruteForceConfiguration = new BruteForceConfiguration();
            if ($config['timeLimit']) {
                $bruteForceConfiguration->setTimeWindow($config['timeLimit']);
            }
            if ($config['attemptLimit']) {
                $bruteForceConfiguration->setTotalAttemptsLimit($config['attemptLimit']);
            }

            $history = Cache::read($key, $cache) ?? [];
            if (is_string($history)) {
                $history = json_decode($history, true);
            }

            if (is_string($data)) {
                $data = compact('data');
            }

            $shield  = new BruteForceShield();
            $history = $shield->validate($history, $data, $bruteForceConfiguration);
            Cache::write($key, $history, $cache);

            $timeout = count($history['attempts'] ?? []);
            if ($timeout > 2) {
                sleep(min($timeout - 1, 10));
            }

            if ($shield->isValidated()) {
                return true;
            }

            throw new HttpException ('Too many attempts, you have been temporarily blocked. Please try again later.');
        }

        public function setIdentity(ArrayAccess $identity): static
        {
            $this->dispatchEvent('Auth.beforeSetIdentity', compact('identity'));
            $this->Authentication->setIdentity($identity);
            $this->dispatchEvent('Auth.afterSetIdentity', compact('identity'));
            return $this;
        }

        /**
         * @throws Exception
         */
        public function impersonate(ArrayAccess $identity): static
        {
            $this->dispatchEvent('Auth.beforeSetIdentity', compact('identity'));
            $this->Authentication->impersonate($identity);
            $this->dispatchEvent('Auth.afterSetIdentity', compact('identity'));
            return $this;
        }


    }
