<?php
    declare(strict_types=1);

    namespace Icing;

    use Authentication\AuthenticationService;
    use Authentication\AuthenticationServiceInterface;
    use Authentication\AuthenticationServiceProviderInterface;
    use Authentication\Identifier\AbstractIdentifier;
    use Authentication\Middleware\AuthenticationMiddleware;
    use Authorization\AuthorizationService;
    use Authorization\AuthorizationServiceInterface;
    use Authorization\AuthorizationServiceProviderInterface;
    use Authorization\Exception\ForbiddenException;
    use Authorization\Exception\MissingIdentityException;
    use Authorization\Middleware\AuthorizationMiddleware;
    use Authorization\Policy\ResolverCollection;
    use Cake\Console\CommandCollection;
    use Cake\Core\BasePlugin;
    use Cake\Core\Configure;
    use Cake\Core\ContainerInterface;
    use Cake\Core\PluginApplicationInterface;
    use Cake\Datasource\FactoryLocator;
    use Cake\Http\Middleware\CsrfProtectionMiddleware;
    use Cake\Http\Middleware\EncryptedCookieMiddleware;
    use Cake\Http\MiddlewareQueue;
    use Cake\Routing\RouteBuilder;
    use Cake\Routing\Router;
    use Cake\Utility\Inflector;
    use Cake\Utility\Security;
    use Icing\Model\Entity\User;
    use Icing\Policy\ControllerResolver;
    use Icing\Policy\OrmResolver;
    use Icing\Policy\StringResolver;
    use Psr\Http\Message\ServerRequestInterface;
    use ReflectionClass;
    use ReflectionException;
    use Symfony\Component\Finder\Finder;
    use Symfony\Component\Finder\SplFileInfo;

    /**
     * Plugin for Icing
     */
    class IcingPlugin extends BasePlugin implements AuthenticationServiceProviderInterface, AuthorizationServiceProviderInterface
    {
        /**
         * Load all the plugin configuration and bootstrap logic.
         *
         * The host application is provided as an argument. This allows you to load
         * additional plugin dependencies, or attach events.
         *
         * @param PluginApplicationInterface $app The host application
         *
         * @return void
         */
        public function bootstrap(PluginApplicationInterface $app): void
        {
            $app->addPlugin('Authorization');
            $app->addPlugin('Authentication');
            $app->addPlugin('Muffin/Trash');
            $app->addPlugin('CakePdf');

            $configFiles = [
                $this->getConfigPath() . 'functions.php',
                $this->getConfigPath() . 'bootstrap.php',
            ];
            foreach ($configFiles as $file) {
                if (file_exists($file)) {
                    include_once $file;
                }
            }

            $this->_registerOverrides();

            if (php_sapi_name() === 'cli') {
                $this->bootstrapCli();
            }
        }

        public function bootstrapCli() {}

        /**
         * Add middleware for the plugin.
         *
         * @param MiddlewareQueue $middlewareQueue The middleware queue to update.
         *
         * @return MiddlewareQueue
         */
        public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
        {
            //$this->_overrideCsrfProtectionMiddlewareSkipCheckCallback($middlewareQueue);

            // Add your middlewares here
            $middlewareQueue
                ->add(
                    new EncryptedCookieMiddleware(
                        [
                            //'two_factor_authentication_token',
                            'keep_me_logged_in_token',
                        ],
                        Security::hash(Security::getSalt())
                    )
                )
                ->add(new AuthenticationMiddleware($this))
                ->add(
                    new AuthorizationMiddleware(
                        $this,
                        [
                            'identityDecorator'   => function ($auth, $user) {
                                if ($user instanceof User) {
                                    return $user->setAuthorization($auth);
                                }
                            },
                            'unauthorizedHandler' => [
                                'className'  => 'Authorization.Redirect',
                                'url'        => '/login',
                                'queryParam' => 'r',
                                'exceptions' => [
                                    MissingIdentityException::class,
                                    ForbiddenException::class,
                                ],
                            ],
                        ]
                    )
                )
            ;

            return $middlewareQueue;
        }

        /**
         * Add routes for the plugin.
         *
         * If your plugin has many routes, and you would like to isolate them into a separate file,
         * you can create `$plugin/config/routes.php` and delete this method.
         *
         * @param RouteBuilder $routes The route builder to update.
         *
         * @return void
         */
        public function routes(RouteBuilder $routes): void
        {
            parent::routes($routes);
        }

        /**
         * Add commands for the plugin.
         *
         * @param CommandCollection $commands The command collection to update.
         *
         * @return CommandCollection
         */
        public function console(CommandCollection $commands): CommandCollection
        {
            // Add your commands here

            return parent::console($commands);
        }

        /**
         * Register application container services.
         *
         * @param ContainerInterface $container The Container to update.
         *
         * @return void
         * @link https://book.cakephp.org/4/en/development/dependency-injection.html#dependency-injection
         */
        public function services(ContainerInterface $container): void
        {
            // Add your services here
        }

        /**
         * Returns a service provider instance.
         *
         * @param ServerRequestInterface $request Request
         *
         * @return AuthenticationServiceInterface
         */
        public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
        {
            $params = [
                'plugin'     => 'Icing',
                'controller' => 'Auth',
                'action'     => 'login',
                'prefix'     => null,
            ];
            $logins = c(Router::routes())
                ->filter(
                    fn($route) => ($route->defaults['prefix'] ?? null) === $params['prefix']
                        && ($route->defaults['plugin'] ?? null) === $params['plugin']
                        && ($route->defaults['controller'] ?? null) === $params['controller']
                        && ($route->defaults['action'] ?? false) === $params['action']

                )
                ->map(fn($route) => $route->template)
                ->toList()
            ;

            $service = new AuthenticationService();

            $fields             = [
                AbstractIdentifier::CREDENTIAL_USERNAME => 'email',
                AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
            ];
            $passwordIdentifier = [
                'Authentication.Password' => [
                    'fields'   => $fields,
                    'resolver' => [
                        'className' => 'Authentication.Orm',
                        'userModel' => 'Icing.Users',
                        'finder'    => 'all',
                    ],
                ],
            ];

            $service->setConfig(
                [
                    'unauthenticatedRedirect' => '/login',
                    'queryParam'              => '_r',
                ]
            );

            $service->loadAuthenticator(
                'Authentication.Session',
                [
                    'identifier' => $passwordIdentifier,
                ]
            );
            $service->loadAuthenticator(
                'Authentication.Cookie',
                [
                    'identifier'      => $passwordIdentifier,
                    'fields'          => $fields,
                    'rememberMeField' => 'keep_me_logged_in',
                    'cookie'          => [
                        'name'     => 'keep_me_logged_in_token',
                        'expires'  => date_create('+30 days')->getTimestamp(),
                        'secure'   => true,
                        'httponly' => true,
                    ],
                    'loginUrl'        => $logins,
                ]
            );
            $service->loadAuthenticator(
                'Icing.TwoFactorForm',
                [
                    'identifier' => $passwordIdentifier,
                    'fields'     => $fields,
                    'cookie'     => [
                        'name'     => 'two_factor_authentication_token',
                        'expires'  => date_create('+30 days')->getTimestamp(),
                        'secure'   => true,
                        'httponly' => true,
                    ],
                    'loginUrl'   => $logins,
                ]
            );

            return $service;
        }

        public function getAuthorizationService(ServerRequestInterface $request): AuthorizationServiceInterface
        {
            $ormResolver        = new OrmResolver();
            $controllerResolver = new ControllerResolver();
            $stringResolver     = new StringResolver();

            $resolver = new ResolverCollection([$controllerResolver, $ormResolver, $stringResolver]);
            $service  = new AuthorizationService($resolver);

            $service->skipAuthorization();

            return $service;
        }

        protected function _registerOverrides(): void
        {
            $tableLocator = FactoryLocator::get('Table');

            // Search the APP/Model/Table/Icing directory for overrides
            $tablePath = APP . 'Model' . DS . 'Table' . DS . 'Icing';
            if (is_dir($tablePath)) {
                $tableFinder = new Finder();
                $tableFinder->files()
                    ->in($tablePath)
                    ->name('*.php')
                ;

                // Build a list of [icing table alias => app table class] if it exists
                c(iterator_to_array($tableFinder))
                    ->indexBy(
                        function (SplFileInfo $file) use ($tablePath) {
                            return 'Icing.' . basename($file->getBasename('Table.php'));
                        }
                    )
                    ->map(
                        function (SplFileInfo $file) use ($tablePath) {
                            return "App\\Model\\Table\\Icing" . str_replace(
                                    [$tablePath, '/', '.php'],
                                    ['', '\\', ''],
                                    $file->getPathname()
                                );
                        }
                    )
                    ->filter(
                        function ($class) {
                            return class_exists($class);
                        }
                    )
                    ->each(
                        function ($appTableClass, $icingAlias) use ($tableLocator) {
                            $config = $tableLocator->getConfig($icingAlias);
                            $config += [
                                'className'   => $appTableClass,
                                'entityClass' => 'Icing\\Model\\Entity\\' . Inflector::singularize(
                                        basename($appTableClass, 'Table')
                                    ),
                            ];
                            $tableLocator->setConfig($icingAlias, $config);
                        }
                    )
                ;
            }

            // search the APP/Model/Entity/Icing directory for overrides
            $entityPath = APP . 'Model\\Entity\\Icing';
            if (is_dir($entityPath)) {
                $entityFinder = new Finder();
                $entityFinder->files()
                    ->in($entityPath)
                    ->name('*.php')
                ;

                // Build a list of [icing table alias => app entity class] if it exists
                c(iterator_to_array($entityFinder))
                    ->indexBy(
                        function (SplFileInfo $file) use ($entityPath) {
                            $table = basename($file->getBasename('.php'));
                            $table = Inflector::pluralize($table);
                            return "Icing.$table";
                        }
                    )
                    ->map(
                        function (SplFileInfo $file) use ($entityPath) {
                            return "App\\Model\\Entity\\Icing" . str_replace(
                                    [$entityPath, '/', '.php'],
                                    ['', '\\', ''],
                                    $file->getPathname()
                                );
                        }
                    )
                    ->filter(
                        function ($class) {
                            return class_exists($class);
                        }
                    )
                    ->each(
                        function ($appEntityClass, $icingAlias) use ($tableLocator) {
                            $config = $tableLocator->getConfig($icingAlias);
                            $tableLocator->setConfig($icingAlias, ['entityClass' => $appEntityClass] + $config);
                        }
                    )
                ;
            }
        }

        /**
         * @throws ReflectionException
         */
        protected function _overrideCsrfProtectionMiddlewareSkipCheckCallback(MiddlewareQueue $middlewareQueue): void
        {
            $whitelist = Configure::read('Icing.CsrfWhitelist');
            if (empty($whitelist)) {
                return;
            }

            $csrfProtectionMiddleware = c($middlewareQueue)
                ->reduce(
                    function ($csrfProtectionMiddleware, $middleware) {
                        if ($middleware instanceof CsrfProtectionMiddleware) {
                            $csrfProtectionMiddleware = $middleware;
                        }
                        return $csrfProtectionMiddleware;
                    },
                    null
                )
            ;
            if (!$csrfProtectionMiddleware) {
                return;
            }

            $reflection = new ReflectionClass($csrfProtectionMiddleware);
            $property   = $reflection->getProperty('skipCheckCallback');
            $property->setAccessible(true);

            $originalCallback = $property->getValue($csrfProtectionMiddleware) ?? (fn() => false);
            $callback         = fn($request) => $originalCallback($request)
                || in_array(
                    requestToPathRoute($request),
                    $whitelist
                );

            $csrfProtectionMiddleware->skipCheckCallback($callback);
        }


    }
