<?php

namespace Velis\Mvc;

use Exception;
use Phalcon\Mvc\Router\Annotations as PhalconRouter;
use Velis\App;
use Velis\Config\Ini as IniConfig;
use Velis\Filter;
use Velis\Http\Request;
use Velis\Mvc\UISwitch\UISwitch;
use Velis\Output;

/**
 * MVC Router
 * @author Olek Procki <olo@velis.pl>
 */
class Router extends PhalconRouter
{
    protected array $switchableModules = [];
    protected array $switchedModules = [];
    protected array $disabledInstantLinks = [];

    protected ?UISwitch $uiSwitch = null;

    /**
     * Startup & initialize routes
     * @return $this
     * @throws Exception
     */
    public function init()
    {
        $this->setDefaultModule("Application");
        /** @var Request $request */
        $request = App::getService("request");

        if ($request->isApi()) {
            $section = 'api';
        } elseif ($request->isMobile() && App::hasModule('Mobile')) {
            $section = 'mobile';
        } else {
            $section = App::$config->settings->defaultLanguage ?? '';
        }

        $this->uiSwitch = new UISwitch();
        $this->registerModuleRoutes($section);

        return $this;
    }



    /**
     * Register module routes from config files
     * @param string $section
     * @throws Exception
     */
    protected function registerModuleRoutes(string $section): void
    {
        /** @var Request $request */
        $request = App::getService('request');

        foreach (App::getModuleDirs() as $module) {
            //Remove trailing slashes automatically
            $this->removeExtraSlashes(true);

            if ($request->isApi()) {
                $this
                    ->addApiOptionsRoute()
                    ->addModuleApiRoutes($module)
                ;
            } elseif ($request->isMobile() && App::hasModule('Mobile')) {
                $this->setDefaultModule("Mobile");
                $this->add('/m', [
                    'module' => 'Mobile',
                    'controller' => 'Index',
                    'action' => 'Index'
                ]);
            } else {
                $this->addModuleDefaultRoutes($module);
            }

            // For some React requests we return urls to legacy modules so it's
            // good to have their default routes registered too
            $this->addIniRoutes($request, 'default', $module);
            $this->addIniRoutes($request, $section, $module);

            $moduleRoutingClass = $module . '\Router';
            if (class_exists($moduleRoutingClass)) {
                $moduleRouting = new $moduleRoutingClass($this);
                if ($moduleRouting instanceof ModuleRoutingInterface && $moduleRouting->isEnabled()) {
                    $moduleRouting->registerRoutes();
                }
            }

            if ($request->isApi()) {
                $class = "{$module}\Router";
                if (class_exists($class) && method_exists($class, 'create')) {
                    $appRouting = new $class($this);
                    $appRouting->create();
                }
            }
        }
    }

    /**
     * Register API route for OPTIONS method
     * @return Router
     */
    protected function addApiOptionsRoute(): self
    {
        $this->addOptions('/api/([a-zA-Z0-9\-\/]+)', [
            'module' => 'Api',
            'controller' => 'rest',
            'action' => 'options'
        ]);

        $this->addOptions('/rest/(/v([0-9]+))?([a-zA-Z0-9\-\/]+)', [
            'module' => 'Api',
            'controller' => 'login',
            'action' => 'options'
        ]);

        return $this;
    }

    /**
     * Registers default API routes for $module
     * @param string $module
     * @return $this
     */
    private function addModuleApiRoutes(string $module): self
    {
        $this->addGet(
            '/api/' . Filter::filterToDash($module),
            [
                'module' => $module,
                'controller' => 'rest',
                'action' => 'list',
                'version' => null,
            ]
        );

        $this->addGet(
            '/api/v([0-9]+)/' . Filter::filterToDash($module),
            [
                'module' => $module,
                'controller' => 'rest',
                'action' => 'list',
                'version' => 1,
            ]
        );

        $this->addPost(
            '/api/' . Filter::filterToDash($module),
            [
                'module' => $module,
                'controller' => 'rest',
                'action' => 'create',
                'version' => null,
            ]
        );

        $this->addPost(
            '/api/v([0-9]+)/' . Filter::filterToDash($module),
            [
                'module' => $module,
                'controller' => 'rest',
                'action' => 'create',
                'version' => 1,
            ]
        );

        $prefix = '/api(/v([0-9]+))?/' . Filter::filterToDash($module);

        $this->addGet(
            $prefix . '/([0-9]+)',
            [
                'module' => $module,
                'controller' => 'rest',
                'action' => 'get',
                'version' => 2,
                'id' => 3,
            ]
        );

        $this->addDelete(
            $prefix . '/([0-9]+)',
            [
                'module' => $module,
                'controller' => 'rest',
                'action' => 'delete',
                'version' => 2,
                'id' => 3,
            ]
        );

        $this->addPut(
            $prefix . '/([0-9]+)',
            [
                'module' => $module,
                'controller' => 'rest',
                'action' => 'update',
                'version' => 2,
                'id' => 3,
            ]
        );

        $this->add(
            $prefix . '/([a-zA-Z\-]+)',
            [
                'module' => $module,
                'controller' => 'rest',
                'version' => 2,
                'action' => 3,
            ]
        )->convert('action', function ($action) {
            return Output::toPascalCase($action);
        });

        return $this;
    }


    private function addModuleDefaultRoutes(string $module): self
    {
        $moduleName = self::moduleToName($module);
        $moduleClass = "{$module}\Module";

        $this->add("/$moduleName/:controller/:action", [
            'module' => $module,
            'controller' => 1,
            'action' => 2,
        ])->convert('action', function ($action) {
            return Output::toPascalCase($action);
        });

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

        return $this;
    }

    public function addIniRoutes(Request $request, string $section, string $module): void
    {
        $routesConfigPath = MODULE_PATH . $module . '/config/routes.ini';
        $routesListPath   = MODULE_PATH . $module . '/config/routes-' . $section . '.local.php';

        if (!file_exists($routesConfigPath)) {
            return;
        }

        // create or recreate routes php file
        if (!file_exists($routesListPath) || filemtime($routesListPath) < filemtime($routesConfigPath)) {
            $routes = [];

            $config = new IniConfig($routesConfigPath);

            if ($section && $config->{$section}) {
                $confNode = $config->{$section};
            } elseif (!$request->isApi() && (!$request->isMobile() || !App::hasModule('Mobile'))) {
                if ($config->default) {
                    $confNode = $config->default;
                } else {
                    $confNode = $config;
                }
            } else {
                $confNode = [];
            }

            foreach ($confNode as $name => $info) {
                $params = $info->toArray();
                $routes[$name] = $this->addRouteByConfig($module, $params, $name);
            }

            file_put_contents($routesListPath, '<?php return ' . var_export($routes, true) . ';');
        } else {
            // This internal config file has its extension - it's declared with variable declaration.
            // @phpcs:ignore
            foreach ((include $routesListPath) as $name => $params) {
                $route = $this
                ->add($params['route'], $params['defaults'])
                ->setName($name)
                ;

                if (isset($params['via'])) {
                    $route->via($params['via']);
                }
            }
        }
    }

    /**
     * Adds route by config params
     *
     * @param string $module
     * @param array $params
     * @param string $name
     *
     * @return array
     */
    protected function addRouteByConfig(string $module, array $params, string $name): array
    {
        if (!isset($params['defaults']['module'])) {
            $params['defaults']['module'] = $module;
        } else {
            $params['defaults']['module'] = Output::toPascalCase($params['defaults']['module']);
        }

        if (isset($params['defaults']['controller'])) {
            $params['defaults']['controller'] = Output::toPascalCase($params['defaults']['controller']);
        }

        if (isset($params['defaults']['action'])) {
            $params['defaults']['action'] = Output::toPascalCase($params['defaults']['action']);
        }

        $routeDef = [
            'route'     => $params['route'],
            'defaults'  => $params['defaults'],
        ];

        if (isset($params['via'])) {
            $routeDef['via'] = $params['via'];
        }

        $route = $this
            ->add($params['route'], $params['defaults'])
            ->setName($name)
        ;

        if (isset($params['via'])) {
            $route->via($params['via']);
        }

        return $routeDef;
    }

    /**
     * Checks if given $url is valid for current application
     * @param string $incomingUrl
     * @return bool
     */
    public function validateUrl(string $incomingUrl): bool
    {
        $router = clone $this;
        $url = parse_url($incomingUrl, PHP_URL_PATH);
        $router->handle($url);

        if ($matched = $router->getMatchedRoute()) {
            $paths = $matched->getPaths();
            if (array_key_exists('action', $paths) && is_string($paths['action'])) {
                return true;
            }

            if (empty($paths) && $matched->getMatch() instanceof \Closure) {
                return true;
            }

            return $this->checkControllerActionExists($router);
        }

        return false;
    }

    /**
     * Check if controller and action exist when router/dispatcher working in generic mode
     * @param Router $selectedRouter
     * @return bool
     */
    public function checkControllerActionExists(?Router $selectedRouter): bool
    {
        $router = $selectedRouter ?? $this;

        $module = ucfirst($router->getModuleName());
        $controller = ucfirst($router->getControllerName()) . "Controller";
        $action = lcfirst($router->getActionName()) . "Action";
        $controllerClass = $module . "\\Controller\\" . $controller;

        return class_exists($controllerClass) && method_exists($controllerClass, $action);
    }


    /**
     * Check is module replacement is enabled
     */
    public function isModuleSwitched(string $module): bool
    {
        return array_key_exists($module, $this->switchedModules);
    }

    /**
     * Returns true if a given frontend module is enabled and the interface is switched to the new look
     */
    public function isFrontendModuleSwitched(string $frontendModuleName): bool
    {
        return in_array($frontendModuleName, $this->switchedModules);
    }

    public function registerSwitchedModule(string $sourceModule, string $module): void
    {
        $this->switchedModules[$sourceModule] = $module;
    }

    public function registerSwitchableModule(string $sourceModule, string $module): void
    {
        $this->switchableModules[$sourceModule] = $module;
    }

    /**
     * Get module name
     * @return array<string, string>
     */
    public function getSwitchedModules(): array
    {
        return $this->switchedModules;
    }

    public function getSwitchableModules(): array
    {
        return $this->switchableModules;
    }

    /**
     * Converts module to module name
     * @param string $module
     * @return string
     */
    public static function moduleToName(string $module): string
    {
        return Filter::filterToDash($module);
    }

    public function disableInstantLinks(string $module): void
    {
        $this->disabledInstantLinks[] = $module;
    }

    public function getSwitchedModulePrefixes(): array
    {
        return $this->disabledInstantLinks;
    }

    public function isNewUIEnabled(): bool
    {
        return (bool) $this->uiSwitch?->isNewUIEnabled();
    }

    public function canSwitch(): bool
    {
        return (bool) $this->uiSwitch?->canSwitch();
    }

    /**
     * Check if module is a destination module for replacement
     * @param string $module
     * @return bool
     */
    private static function isModuleSwitchedTo(string $module): bool
    {
        return Filter::filterToDash($module);
    }
}
