<?php

namespace Velis\Mvc;

use Application\Application;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Mvc\Router\Group;
use Phalcon\Mvc\Router\Route;
use Velis\App;
use Velis\Mvc\UISwitch\UISwitchService;
use Velis\Output;

abstract class FrontendRouting implements ModuleRoutingInterface
{
    use AnnotationRoutingTrait;

    public const REDIRECT_ROUTE_SUFFIX = '-frontend-redirect';
    public const FRONTEND_ROUTE_SUFFIX = '-frontend';
    public const FRONTEND_ROUTE_PREFIX = 'frontend-';

    public function __construct(protected Router $router)
    {
    }

    public function isEnabled(): bool
    {
        return true;
    }

    public function registerRoutes(): void
    {
        if ($this->isSwitchable()) {
            $this->router->registerSwitchableModule(
                $this->getSwitchedName(),
                $this->getName()
            );
        }

        if ($this->isSwitched()) {
            $this->router->registerSwitchedModule(
                $this->getSwitchedName(),
                $this->getName()
            );
        }

        if ($this->hasFrontendApp() && ($this->isSwitched() || !$this->isReplacingLegacyModule())) {
            $prefix = $this->getFrontendUrlPrefix();

            $this->router->disableInstantLinks($prefix);

            foreach ($this->getRedirectUrlPrefixes() as $redirectPrefix) {
                $this->router->disableInstantLinks($redirectPrefix);
            }
        }

        $this->addModuleDefaultRoutes();
        $this->addAnnotationRoutes();
    }

    /**
     * Adds default routes for $module
     *
     * @param string $module
     * @return $this
     */
    protected function addModuleDefaultRoutes(): self
    {
        $module = $this->getName();
        $moduleName = Router::moduleToName($module);
        $moduleClass = "{$module}\Module";

        $this->addCrudRoutes();
        $this->addActionsRoutes();
        $this->addNestedRoutes();
        $this->addNestedActionsRoutes();
        $this->addFileRoutes();
        $this->addPrintRoutes();

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

        if ($this->isReplacingLegacyModule() && !$this->isSwitched()) {
            return $this;
        }

        $this->addFrontendRoutes($moduleClass, $moduleName);

        $routesMap = $this->getRoutesMap();
        if ($routesMap) {
            $this->handleRedirects($routesMap, $moduleName);
        }

        return $this;
    }

    private function addFrontendRoutes(string $moduleClass, string $moduleName): void
    {
        $moduleUrlPrefix = $this->getFrontendUrlPrefix();
        $frontendAppDirectoryName = $this->getFrontendAppDirectoryName();
        $routeMatchPattern = "/$moduleUrlPrefix(?!-)/?(.*)";
        $this->router
            ->add($routeMatchPattern, [])
            ->match(function () use ($frontendAppDirectoryName, $moduleClass) {
                $appPath = ROOT_PATH . 'public/app/' . $frontendAppDirectoryName . '/index.html';

                $module = $this->get($moduleClass);
                $module->registerServices($this);

                $view = $this->getShared('view');
                $response = $this->getShared('response');

                if (!App::$user->isLogged()) {
                    return $response->redirect(['for' => 'login']);
                }
                if (method_exists(Application::class, 'needsActivation') && Application::needsActivation()) {
                    return $response->redirect(App::getBaseUrl());
                }
                if ($additionalRedirectRoute = Application::additionalRedirections()) {
                    if (is_string($additionalRedirectRoute)) {
                        return $response->redirect(App::getRouteUrl($additionalRedirectRoute));
                    }
                    return;
                }

                $view->start();
                $view->disableLevel(View::LEVEL_ACTION_VIEW);

                echo('<div id="app-root" class="frontend-app">');
                echo(file_get_contents($appPath));
                echo('</div>');

                $view->render('', '');
                $view->finish();

                return $response->setContent(
                    $view->getContent()
                );
            })
            ->setName($moduleName . self::FRONTEND_ROUTE_SUFFIX);

        $routesWithoutLayout = $this->getFrontendRoutesWithoutLayout();

        foreach ($routesWithoutLayout as $pattern) {
            $this->router
                ->add($pattern, [])
                ->match(function () use ($moduleClass) {
                    $appPath = ROOT_PATH . 'public/app/bid/index.html';

                    $module = $this->get($moduleClass);
                    $module->registerServices($this);

                    $view = $this->getShared('view');
                    $response = $this->getShared('response');

                    if (!App::$user->isLogged()) {
                        return $response->redirect(['for' => 'login']);
                    }
                    if (method_exists(Application::class, 'needsActivation') && Application::needsActivation()) {
                        return $response->redirect(App::getBaseUrl());
                    }
                    if ($additionalRedirectRoute = Application::additionalRedirections()) {
                        if (is_string($additionalRedirectRoute)) {
                            return $response->redirect(App::getRouteUrl($additionalRedirectRoute));
                        }
                        return;
                    }

                    $view->start();
                    $view->disableLevel(View::LEVEL_LAYOUT);

                    echo('<div id="app-root" class="frontend-app">');
                    echo(file_get_contents($appPath));
                    echo('</div>');

                    $view->render('', '');
                    $view->finish();

                    return $response->setContent(
                        $view->getContent()
                    );
                });
        }
    }

    private function addCrudRoutes(): void
    {
        $this->router->mount($this->crudGroup());
    }

    /**
     * @return array<string>
     */
    protected function getFrontendRoutesWithoutLayout(): array
    {
        return [];
    }

    private function crudGroup(): Group
    {
        $module = $this->getName();
        $moduleName = Router::moduleToName($module);
        $slugPattern = '([\w%]+)';
        $rest = new Group(['module' => $module]);
        $rest->setPrefix("/$moduleName/api");

        $pattern = '/:controller';
        $paths = ['controller' => 1];

        $rest->addGet("$pattern/$slugPattern", $paths + ['action' => 'index', 'id' => 2]);
        $rest->addGet($pattern, $paths + ['action' => 'list']);
        $rest->addPost($pattern, $paths + ['action' => 'create']);
        $rest->addPut("$pattern/$slugPattern", $paths + ['action' => 'update', 'id' => 2]);
        $rest->addPatch("$pattern/$slugPattern", $paths + ['action' => 'patch', 'id' => 2]);
        $rest->addDelete("$pattern/$slugPattern", $paths + ['action' => 'delete', 'id' => 2]);

        // allow unicode in slug parameter
        array_walk($rest->getRoutes(), function ($route): void {
            $route->convert('id', function ($slug) {
                return urldecode($slug);
            });
        });

        $rest->beforeMatch(function ($uri, $route) use ($module, $moduleName) {
            $pattern = $route->getPattern();
            $paths = $route->getPaths();
            preg_match($route->getCompiledPattern(), $uri, $matches);
            $controller = $matches[$paths['controller']];
            $namespace = $matches[$paths['namespace']];

            $dispatcher = new Dispatcher();
            $dispatcher->setControllerName($controller);
            if ($namespace) {
                $dispatcher->setNamespaceName("$module\\" . $dispatcher->getHandlerSuffix() . "\\" . $namespace);
            } else {
                $dispatcher->setNamespaceName("$module\\" . $dispatcher->getHandlerSuffix());
            }

            $controllerClass = $dispatcher->getHandlerClass();

            if (!App::$di->has($controllerClass) && !class_exists($controllerClass)) {
                $paths['action'] = "{$paths['action']}-{$controller}";
                $paths['controller'] = $moduleName;

                $route->reConfigure($pattern, $paths);
            }

            return true;
        });

        return $rest;
    }

    private function addActionsRoutes(): void
    {
        $module = $this->getName();
        $moduleName = Router::moduleToName($module);

        $this->router->add("/$moduleName/api/:controller/actions/:action", [
            'module' => $module,
            'controller' => 1,
            'action' => 2,
        ]);

        $this->router->add("/$moduleName/api/:controller/:int/:action", [
            'module' => $module,
            'controller' => 1,
            'id' => 2,
            'action' => 3,
        ]);
    }

    private function addNestedRoutes(): void
    {
        $this->router->mount($this->nestedCrudGroup());
    }

    private function nestedCrudGroup(): Group
    {
        $module = $this->getName();
        $moduleName = Router::moduleToName($module);
        $slugPattern = '([\w%]+)';
        $rest = new Group(['module' => $module]);
        $rest->setPrefix("/$moduleName/api");

        $rest->addGet("/:namespace/$slugPattern/:controller", [
            'namespace' => 1,
            'parentId' => 2,
            'controller' => 3,
            'action' => 'list',
        ]);
        $rest->addGet("/:namespace/$slugPattern/:controller/$slugPattern", [
            'namespace' => 1,
            'parentId' => 2,
            'controller' => 3,
            'id' => 4,
            'action' => 'index',
        ]);
        $rest->addPost("/:namespace/$slugPattern/:controller", [
            'namespace' => 1,
            'parentId' => 2,
            'controller' => 3,
            'action' => 'create',
        ]);
        $rest->addPut("/:namespace/$slugPattern/:controller/$slugPattern", [
            'namespace' => 1,
            'parentId' => 2,
            'controller' => 3,
            'id' => 4,
            'action' => 'update',
        ]);
        $rest->addPatch("/:namespace/$slugPattern/:controller/$slugPattern", [
            'namespace' => 1,
            'parentId' => 2,
            'controller' => 3,
            'id' => 4,
            'action' => 'patch',
        ]);
        $rest->addDelete("/:namespace/$slugPattern/:controller/$slugPattern", [
            'namespace' => 1,
            'parentId' => 2,
            'controller' => 3,
            'id' => 4,
            'action' => 'delete',
        ]);

        $this->convertNestedGroup($rest, $module);

        $rest->beforeMatch(function ($uri, $route) use ($module, $moduleName) {
            return $this->checkIfNamespacedRouteHasMatches($uri, $route, $module);
        });

        return $rest;
    }

    private function addFileRoutes(): void
    {
        $module = $this->getName();
        $moduleName = Router::moduleToName($module);
        $this->router->addGet("/$moduleName/api/:controller/files/:action", [
            'module' => $module,
            'controller' => 1,
            'action' => 2,
        ])->convert('action', function ($action) {
            return 'file' . Output::toPascalCase($action);
        });
    }

    private function addPrintRoutes(): void
    {
        $module = $this->getName();
        $moduleName = Router::moduleToName($module);
        $slugPattern = '([0-9a-zA-Z]+)';
        $this->router->addGet("/$moduleName/api/:controller/print/$slugPattern", [
            'module' => $module,
            'controller' => 1,
            'action' => 'print',
            'id' => 2,
        ]);
    }

    private function addNestedActionsRoutes(): void
    {
        $this->router->mount($this->nestedActionsGroup());
    }

    private function nestedActionsGroup(): Group
    {
        $module = $this->getName();
        $moduleName = Router::moduleToName($module);
        $slugPattern = '([\w%]+)';
        $group = new Group(['module' => $module]);
        $group->setPrefix("/$moduleName/api");

        $group->add("/:namespace/$slugPattern/:controller/$slugPattern/:action", [
            'namespace' => 1,
            'parentId' => 2,
            'controller' => 3,
            'id' => 4,
            'action' => 5,
        ]);

        $this->convertNestedGroup($group, $module);

        $group->beforeMatch(function ($uri, $route) use ($module) {
            return $this->checkIfNamespacedRouteHasMatches($uri, $route, $module);
        });

        return $group;
    }

    /**
     * URL decodes nested group parameters
     */
    private function convertNestedGroup(Group $group, string $module): void
    {
        array_walk($group->getRoutes(), function ($route) use ($module): void {
            $route->convert('id', function ($slug) {
                return urldecode($slug);
            });

            $route->convert('parentId', function ($slug) {
                return urldecode($slug);
            });

            $route->convert('namespace', function ($namespace) use ($module) {
                return "$module\\Controller\\" . Output::toPascalCase($namespace);
            });
        });
    }

    /**
     * Checks if a route matched by parent namespace exists by trying to match via virtual dispatcher
     */
    private function checkIfNamespacedRouteHasMatches(string $uri, Route $route, string $module): bool
    {
        $paths = $route->getPaths();
        preg_match($route->getCompiledPattern(), $uri, $matches);
        $controller = $matches[$paths['controller']];
        $namespace = $matches[$paths['namespace']];
        $action = is_integer($paths['action']) ? $matches[$paths['action']] : $paths['action'];

        $dispatcher = new Dispatcher();
        $dispatcher->setControllerName($controller);
        $dispatcher->setNamespaceName("$module\\" . $dispatcher->getHandlerSuffix() . "\\" . Output::toPascalCase($namespace));

        $controllerClass = $dispatcher->getHandlerClass();
        $actionMethod = Output::toCamelCase($action) . 'Action';

        if (!class_exists($controllerClass) || !method_exists($controllerClass, $actionMethod)) {
            return false;
        }

        return true;
    }

    /**
     * @param array<string, string> $routes
     * @param string $moduleClass
     */
    private function handleRedirects(array $routes, string $moduleName): void
    {
        $router = $this->router;
        foreach ($routes as $path => $redirect) {
            $this->router
                ->add($path, [])
                ->match(function () use ($redirect, $router) {
                    $params = $router->getParams();

                    // parametrized redirects
                    $redirect = preg_replace_callback(
                        '/{([\w-]+)}/',
                        function ($matches) use ($params) {
                            [$raw, $placeholder] = $matches;
                            $placeholder = strtolower($placeholder);
                            return array_key_exists($placeholder, $params) ? $params[$placeholder] : $raw;
                        },
                        $redirect
                    );

                    // keep query string if present
                    if ($queryString = parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY)) {
                        $redirect .= '?' . $queryString;
                    }

                    $response = App::getService('response');

                    return $response->redirect($redirect, true, 302);
                })
                ->setName("$moduleName-$path" . self::REDIRECT_ROUTE_SUFFIX);
        }
    }

    protected function isSwitchable(): bool
    {
        return $this->isReplacingLegacyModule() && App::hasModule($this->getName());
    }

    protected function isSwitched(): bool
    {
        return $this->isSwitchable() && $this->router->isNewUIEnabled();
    }

    protected function isReplacingLegacyModule(): bool
    {
        return $this->getSwitchedName() !== null;
    }

    protected function hasFrontendApp(): bool
    {
        return $this->getFrontendUrlPrefix() !== null;
    }

    protected function getFrontendAppDirectoryName(): ?string
    {
        if (!$this->hasFrontendApp()) {
            return null;
        }

        $name = $this->isSwitchable() ? $this->getSwitchedName() : $this->getName();
        $dirName = strtolower(Router::moduleToName($name));

        return $dirName;
    }

    /**
     * @return array<string>
     */
    protected function getRedirectUrlPrefixes(): array
    {
        return [];
    }

    abstract protected function getName(): string;
    abstract protected function getSwitchedName(): ?string;
    abstract protected function getFrontendUrlPrefix(): ?string;

    /**
     * @return array<string, string>|null
     */
    abstract protected function getRoutesMap(): ?array;
}
