<?php

namespace Velis;

use Application\Application as SinguApplication;
use ArrayObject;
use BadMethodCallException;
use DateTimeZone;
use Exception;
use Gaufrette\Exception\FileNotFound as FileNotFoundException;
use GuzzleHttp\Client as GuzzleClient;
use InvalidArgumentException;
use Phalcon\Cli\Console as ConsoleApp;
use Phalcon\Config\Exception as ConfigException;
use Phalcon\Events\ManagerInterface;
use Phalcon\Flash\Session as SessionFlash;
use Phalcon\Http\ResponseInterface;
use Phalcon\Mvc\Application;
use Phalcon\Mvc\Dispatcher\Exception as DispatcherException;
use RuntimeException;
use SessionHandler;
use SplFixedArray;
use Velis\Api\HttpExceptionHandler\ExceptionHandler;
use Velis\Api\HttpExceptionHandler\HttpException;
use Velis\App\Config;
use Velis\App\LoaderFactory;
use Velis\App\PhalconVersion;
use Velis\App\Setting;
use Velis\App\User as AppUser;
use Velis\Bpm\Ticket\Service as TicketService;
use Velis\Bpm\Workflow;
use Velis\Cache\CacheFactory;
use Velis\Cache\CacheFactoryInterface;
use Velis\Cache\CacheInterface;
use Velis\Command\CommandBus;
use Velis\Config\Ini as IniConfig;
use Velis\Crypt\CryptServiceProvider;
use Velis\Db\DbServiceProvider;
use Velis\Debugger\Debugger;
use Velis\Debugger\DebuggerServiceProvider;
use Velis\Debugger\Renderer\ArrayRenderer;
use Velis\Debugger\Renderer\WhoopsHtmlRenderer;
use Velis\Di\CliFactoryDefault;
use Velis\Di\FactoryDefault;
use Velis\Exception as VelisException;
use Velis\Filesystem\Av\AvServiceProvider;
use Velis\Filesystem\FilesystemFactory;
use Velis\Filesystem\FilesystemInterface;
use Velis\Filesystem\FilesystemServiceProvider;
use Velis\Filter\Validation;
use Velis\Filter\ValidatorFactory;
use Velis\Http\Client;
use Velis\Http\ClientInterface;
use Velis\Http\Request;
use Velis\Http\Response;
use Velis\Lang\LangServiceProvider;
use Velis\Lang\LangVariant;
use Velis\Lang\Services\ClearLangsService;
use Velis\Log\LoggerServiceProvider;
use Velis\Log\Sentry\ScopeConfiguration;
use Velis\MaintenanceAlert\MaintenanceAlertFormatter;
use Velis\MaintenanceAlert\MaintenanceAlertFormatterInterface;
use Velis\MaintenanceAlert\MaintenanceAlertGenerator;
use Velis\MaintenanceAlert\MaintenanceAlertGeneratorInterface;
use Velis\Microsoft\TokenProvider;
use Velis\Microsoft\TokenService;
use Velis\Model\BaseModel;
use Velis\Model\DataObject\NoColumnsException;
use Velis\Mvc\Controller\AbstractRestController;
use Velis\Mvc\Controller\AccessException;
use Velis\Mvc\Module\BaseModule;
use Velis\Mvc\Router;
use Velis\Mvc\View\Engine\EngineFactory;
use Velis\Queue\Queue;
use Velis\RateLimiter\RateLimiterServiceProvider;
use Velis\RateLimiter\TooManyAttemptsException;
use Velis\Session\DummySession;
use Velis\Session\SessionFactory;
use Velis\Session\SessionFactoryInterface;
use Velis\Session\SessionInterface;
use Velis\Throttling\LeakyBucket as Throttling;
use Velis\Timezone\DateTimeZoneProvider;
use Velis\User\TrackerEntry;
use Velis\User\UserProvider;

/**
 * Application class
 * @author Olek Procki <olo@velis.pl>
 *
 * @property Response $response
 */
class App extends Application
{
    /**
     * Environment types
     */
    public const ENV_DEVELOPMENT = 'development';
    public const ENV_TESTING       = 'testing';
    public const ENV_RELEASE       = 'release';
    public const ENV_PRODUCTION    = 'production';
    public const ENV_DEMO          = 'demo';

    /**
     * @deprecated left here for backward compatibility, use ENV_DEVELOPMENT instead
     */
    public const ENV_DEVELOPLMENT = 'development';


    /**
     * Cache common intervals
     */
    public const CACHE_3M          = 180;
    public const CACHE_5M          = 300;
    public const CACHE_15M         = 900;
    public const CACHE_30M         = 1800;
    public const CACHE_1H          = 3600;
    public const CACHE_12H         = 43200;
    public const CACHE_24H         = 86400;


    /**
     * Error reporting masks
     */
    public const ERROR_MASK_E_ERROR              = 1;
    public const ERROR_MASK_E_WARNING            = 2;
    public const ERROR_MASK_E_PARSE              = 4;
    public const ERROR_MASK_E_NOTICE             = 8;
    public const ERROR_MASK_E_CORE_ERROR         = 16;
    public const ERROR_MASK_E_CORE_WARNING       = 32;
    public const ERROR_MASK_E_COMPILE_ERROR      = 64;
    public const ERROR_MASK_E_USER_ERROR         = 256;
    public const ERROR_MASK_E_USER_WARNING       = 512;
    public const ERROR_MASK_E_USER_NOTICE        = 1024;
    public const ERROR_MASK_E_STRICT             = 2048;
    public const ERROR_MASK_E_RECOVERABLE_ERROR  = 4096;
    public const ERROR_MASK_E_DEPRECATED         = 8192;
    public const ERROR_MASK_E_USER_DEPRECATED    = 16384;

    /**
     * DI instance
     * @var FactoryDefault
     */
    public static $di;

    /**
     * Service user
     */
    public static ?AppUser $user = null;

    /**
     * Cache handler
     * @var CacheInterface
     */
    public static $cache;


    /**
     * Registry
     * @var ArrayObject
     */
    public static $registry;

    public static ?SessionInterface $session = null;

    /**
     * Application config
     * @var Config
     */
    public static $config;


    /**
     * Translator instance
     * @var Lang
     */
    public static $lang;


    /**
     * Application settings
     * @var array
     */
    protected static $_settings;


    /**
     * Application instance
     * @var App
     */
    protected static $_instance;


    /**
     * Application domains
     * @var array
     */
    protected static $_domains = [];


    /**
     * Super users
     * @var array
     */
    protected static $_superUsers = [];


    /**
     * Support users
     * @var array
     */
    protected static $_supportUsers = [];


    /**
     * Available modules' directories
     * @var array
     */
    protected static $_moduleDirs;


    /**
     * Array of dispatch-end triggers
     * @var array<callable>
     */
    protected static array $_finishTriggers = [];
    private static ?string $correlationId;


    /**
     * Console application handler
     * @var ConsoleApp
     */
    protected $_console;


    /**
     * Throttling component
     * @var Throttling
     */
    public static $throttling;

    protected static bool $isLocalFileStorageForced = false;


    /**
     * Initialize application
     * @param array $configuration
     * @return App
     * @throws RuntimeException
     * @throws Exception
     */
    public static function init(array $configuration = [])
    {
        if (isset(self::$_instance)) {
            throw new RuntimeException('Application already initialized');
        }

        if (isset($configuration['domains'])) {
            self::$_domains = $configuration['domains'];
        } else {
            self::$_domains = [];
        }

        if (isset($configuration['autoloader']) && is_array($configuration['autoloader'])) {
            $configuration['autoloader'][] = ROOT_PATH . 'lib';
        } else {
            $configuration['autoloader'] = [ROOT_PATH . 'lib'];
        }
        if (self::isConsole()) {
            $configuration['autoloader'][] = MODULE_PATH . 'Admin/src/Console';
        }

        $loaderFactory = new LoaderFactory();
        $loader = $loaderFactory->createLoader();
        $namespaces = [];

        foreach (self::getModuleDirs() as $module) {
            $namespaces[$module] = MODULE_PATH . $module . '/src';
        }

        $autoloaderClassmaps = [];
        foreach ($configuration['autoloader'] as $key => $elem) {
            if (is_array($elem)) {
                $autoloaderClassmaps += $elem;
                unset($configuration['autoloader'][$key]);
            }
        }

        $loader
            ->setDirectories($configuration['autoloader'])
            ->setNamespaces($namespaces)
        ;

        if ($autoloaderClassmaps) {
            $loader->setClasses($autoloaderClassmaps);
        }

        $loader->register();

        if (
            self::isConsole()
            && isset($configuration['runParameters'])
            && is_array($configuration['runParameters'])
        ) {
            $parser = new ParametersParser();
            $runParams = $parser->parse($configuration['runParameters']);

            if (isset($runParams['forceFileStorage']) && $runParams['forceFileStorage'] == 1) {
                self::forceLocalFileStorage(true);
            }
        }


        self::_initShutdownHandler();
        self::registerExceptionHandler();
        self::initHa();
        self::_initEnv();
        self::_initConfig();
        self::_updateErrorReportingLevel();

        self::$_instance = new self();

        return self::$_instance;
    }

    /**
     * Get Phalcon major version number
     * @return int
     */
    public static function getPhalconMajorVersion(): int
    {
        $version = new PhalconVersion();

        return $version->getMajorVersion();
    }

    /**
     * PHP shutdown handler initialization (saves additional error info to error log)
     */
    protected static function _initShutdownHandler()
    {
        register_shutdown_function(['\Velis\Debug', 'shutdownHandler']);
    }

    public static function registerExceptionHandler(): void
    {
        set_exception_handler(Debug::exceptionHandler(...));
    }

    /**
     * @return void
     */
    private static function initHa()
    {
        $defaultLogPath = DATA_PATH . 'log' . DIRECTORY_SEPARATOR;
        $haConfigPath = CONFIG_PATH . 'ha.ini';

        try {
            if (!file_exists($haConfigPath)) {
                throw new Exception();
            }

            $haConfig = new IniConfig($haConfigPath);
            $logPath = $haConfig->get('logPath');
            $logPath = $logPath ? ROOT_PATH . $logPath : $defaultLogPath;

            if (DIRECTORY_SEPARATOR != substr($logPath, -1)) {
                $logPath .= DIRECTORY_SEPARATOR;
            }

            define('LOG_PATH', $logPath);
        } catch (Exception $e) {
            define('LOG_PATH', $defaultLogPath);
        }
    }

    /**
     * Sets application environment
     */
    protected static function _initEnv()
    {
        if (!defined('APP_ENV') && isset($_SERVER['APP_ENV'])) {
            define('APP_ENV', $_SERVER['APP_ENV']);
        }

        if (!defined('APP_ENV')) {
            foreach (self::$_domains as $env => $envDomains) {
                foreach ($envDomains as $envDomain) {
                    if ($envDomain == $_SERVER['SERVER_NAME']) {
                        define('APP_ENV', $env);
                        break(2);
                    } elseif (strpos($envDomain, '*') === 0 && trim($envDomain, '*') == substr($_SERVER['SERVER_NAME'], strpos($_SERVER['SERVER_NAME'], '.'))) {
                        define('APP_ENV', $env);
                        break(2);
                    }
                }
            }
            if (!defined('APP_ENV')) {
                if (isset($_SERVER['REMOTE_ADDR'])) {
                    define('APP_ENV', in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1']) ? self::ENV_DEVELOPMENT : self::ENV_PRODUCTION);
                } else {
                    define('APP_ENV', self::ENV_PRODUCTION);
                }
            }
        }

        if (!isset($_SERVER['ON_DEV'])) {
            $_SERVER['ON_DEV'] = in_array(
                APP_ENV,
                [
                    self::ENV_DEVELOPMENT,
                    self::ENV_TESTING,
                ]
            );
        }

        $_SERVER['CLI_MODE'] = self::isConsole();

        ini_set('display_errors', $_SERVER['ON_DEV'] ? 1 : 0);
        ini_set('error_log', LOG_PATH . 'php/php_errors_' . date('Y-m-d') . '.log');
    }


    /**
     * Config initialization
     * @throws Exception
     */
    protected static function _initConfig()
    {
        self::$config = Config::load();

        if (count(self::$config->admin->superUsers)) {
            self::$_superUsers = Arrays::toArray(self::$config->admin->superUsers);
        }

        if (self::$config->admin->supportUsers && count(self::$config->admin->supportUsers)) {
            self::$_supportUsers = Arrays::toArray(self::$config->admin->supportUsers);
        }
    }

    public static function getTimezone(): DateTimeZone
    {
        $timeZoneProvider = new DateTimeZoneProvider();

        if (self::$config->hasTimezoneSupport()) {
            return $timeZoneProvider->get(self::$user->getTimezone());
        } elseif (self::$config->settings->timeZone) {
            return $timeZoneProvider->get(self::$config->settings->timeZone);
        }

        return new DateTimeZone('Europe/Warsaw');
    }

    /**
     * Bootstrap & run application
     * @throws NoColumnsException
     * @throws ConfigException
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    public function run()
    {
        // Reserve memory to handle OOP errors. Allocate 1 MB of memory.
        $GLOBALS['reserved_memory'] = new SplFixedArray(1024 * 1024 / 16);

        self::$registry = new ArrayObject();

        // register DI services first
        $this->_registerServices();

        BaseModel::init();

        self::$config
            ->registerEnabledModules()
            ->append(self::settings())
        ;

        $userData = null;
        if (session_id() && App::$session) {
            $userData = self::$session->get('userData');
        }
        self::$user = new AppUser($userData);

        $scopeConfiguration = App::getService(ScopeConfiguration::class)
            ->setUser(self::$user)
            ->setTag('correlationId', $this->getCorrelationId());

        // configure language & localization settings
        if (isset($_COOKIE['lang']) && $_COOKIE['lang']) {
            Lang::switchLanguage($_COOKIE['lang'], false);
        } elseif (isset(self::$user['lang_id']) && self::$user['lang_id']) {
            Lang::switchLanguage(self::$user['lang_id'], false);
        } elseif (!self::isConsole() && Lang::getByDomain()) {
            Lang::switchLanguage(Lang::getByDomain(), false);
        } elseif (!self::isConsole() && Lang::getByBrowser()) {
            Lang::switchLanguage(Lang::getByBrowser(), false);
        } elseif (self::$config->settings->defaultLanguage) {
            Lang::switchLanguage(self::$config->settings->defaultLanguage, false);
        }

        if (!App::isConsole()) {
            date_default_timezone_set(self::getTimezone()->getName());
        }

        Lang::setupLocale(false);

        Lang::$onEmpty = Lang::BHVR_RETURN_EN;

        // register enabled event handlers
        if (self::hasModule('Workflow')) {
            Workflow::setup();
        }

        if (!self::isConsole()) {
            $modules = self::getModuleDefinitions();
            $this->registerModules($modules);

            if (self::isHttpRequest()) {
                $this->handle($_SERVER['REQUEST_URI']);
            }

            $this->_finish();
        } else {
            global $argv;

            $arguments = [];
            foreach ($argv as $k => $arg) {
                if ($k == 1) {
                    $arguments['task'] = $arg;
                } elseif ($k == 2) {
                    $arguments['action'] = Output::toPascalCase($arg);
                } elseif ($k >= 3) {
                    $arguments['params'][] = $arg;
                }
            }

            // define global constants for the current task and action
            define('CURRENT_TASK', ($argv[1] ?? null));
            define('CURRENT_ACTION', ($argv[2] ?? null));

            App::$session = new DummySession();

            try {
                // handle incoming arguments
                $this->_console->handle($arguments);
                $this->_finish();
            } catch (Exception $e) {
                echo $e->getMessage();
                exit(255);
            }
        }
    }


    /**
     * Handles a MVC request
     * @param string $uri
     * @return bool|\Phalcon\Http\Response|ResponseInterface
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws Exception
     */
    public function handle(string $uri)
    {
        try {
            $response = parent::handle($uri);

            if (self::$config->db->duplicatedQueryCheck) {
                if ($duplicates = self::getService('db')->getDuplicatedQueries()) {
                    $path = join(
                        ' | ',
                        [App::$registry['moduleName'], App::$registry['controllerName'], App::$registry['actionName']],
                    );

                    VelisException::raise(
                        'Duplicated queries ' . $path,
                        null,
                        null,
                        [
                            'module'     => App::$registry['moduleName'],
                            'controller' => App::$registry['controllerName'],
                            'action'     => App::$registry['actionName'],
                            'url'        => $_SERVER['REQUEST_URI'],
                            'user_id'    => App::$user->id(),
                            'session_id' => session_id(),
                            'queries'    => $duplicates,
                        ]
                    );
                }
            }

            if (!$response->isPostponed()) {
                return $response->sendForce();
            }
            return $response;
        } catch (Exception $e) {
            if (!self::$di->has('view')) {
                throw $e;
            } else {
                $this->view->enable();
            }

            $response = $this->response;

            if ($e instanceof AccessException && !$this->request->isApi()) {
                $message = $e->getMessage() ?: Lang::get('GENERAL_PERM_DENIED');

                if (self::isConsole()) {
                    echo $message . "\n\n";
                    exit;
                }

                if ($this->request->isAjax()) {
                    $response
                        ->setContent('')
                        ->setStatusCode('403', $message)
                    ;
                } else {
                    if (!$e->isSilent()) {
                        $this->flash->error($message);
                    }

                    if (method_exists(SinguApplication::class, 'handlePermissionDenied')) {
                        SinguApplication::handlePermissionDenied($response);
                    } elseif ($_SERVER['HTTP_REFERER'] && $response->canRedirect()) {
                        $response->increaseRedirectCalls();
                        $response->redirect($_SERVER['HTTP_REFERER']);
                    } else {
                        $response->resetRedirectCalls();
                        $response->redirect(self::getBaseUrl());
                    }

                    if (!$e->isSilent()) {
                        $response->setContent('');
                    }
                }
            } else {
                $view = $this->view;
                $view->setViewsDir(MODULE_PATH . 'Application/view/');

                $template = 'index';

                $view->errorLayout = true;

                $notFound = $e instanceof DispatcherException;

                if ($notFound || $e instanceof FileNotFoundException) {
                    if (method_exists(SinguApplication::class, 'handleNotFound')) {
                        SinguApplication::handleNotFound();
                    } else {
                        $response->setStatusCode(404, 'Not found');
                    }
                    $template = '404';
                } elseif (!$this->request->isApi() && $e instanceof TooManyAttemptsException) {
                    $response->setStatusCode(419, 'Too many requests');
                } elseif (
                    self::$config->errorReporting->reportExceptions
                    && !$e instanceof HttpException
                    && (
                        !$this->request->isApi()
                        || !$this->dispatcher->getActiveController() instanceof AbstractRestController
                    )
                ) {
                    $response->setStatusCode(500, 'Internal server error');

                    Debug::reportException($e);
                }

                if (
                    $this->request->isApi()
                    || $this->dispatcher->getActiveController() instanceof AbstractRestController
                ) {
                    $handler = ExceptionHandler::parse($e);
                    $bestAccept = $this->request->getBestAccept();
                    $response->setContentType($bestAccept);
                    $response->setStatusCode($handler->getHttpException()->getHttpCode());

                    if ($bestAccept !== 'application/json' && App::$config->debugMode) {
                        $response->setContent($handler->toHtml());
                    } else {
                        $response->setContent($handler->toJson());
                    }
                } else {
                    // Phalcon requires relative layout path
                    $layoutPath = '../view/layout/';
                    $view->setLayoutsDir($layoutPath);

                    $view->selectMenu('home');
                    $view->exception = $e;

                    if ($v = $view->render('error', $template)) {
                        $response
                            ->resetHeaders()
                            ->setContent($v->getContent())
                        ;
                    }
                }
            }

            return $response->sendForce();
        }
    }


    /**
     * Finishes application
     */
    protected function _finish()
    {
        if (!self::isConsole() && self::$config->settings->trackUsers) {
            $request = self::getService('request');

            if ((self::$user->isLogged() || $request->isLogin() || $request->isPublicAction()) && !$request->isHidden()) {
                TrackerEntry::log();
            }
        }

        if (self::$config->settings->clearLogs) {
            Debug::clearLogs();
        }

        foreach (self::$_finishTriggers as $callback) {
            call_user_func($callback);
        }
    }


    /**
     * Attaches function at the end of dispatch loop
     * @param callable $callback
     */
    public static function onFinish($callback)
    {
        if (!is_callable($callback)) {
            throw new InvalidArgumentException('Argument must be a valid callback');
        }
        self::$_finishTriggers[] = $callback;
    }


    /**
     * Registers application services
     * @return $this
     */
    protected function _registerServices()
    {
        if (self::isConsole()) {
            self::$di = new CliFactoryDefault();

            $this->_registerSessionFactory();
            self::$di->register(new DbServiceProvider());
            $this->_registerCache();

            $this->_console = new ConsoleApp();
            $this->_console->setDI(self::$di);
        } else {
            self::$di = new FactoryDefault();

            //Register http request & response classes
            self::$di->set('request', Request::class, true);
            self::$di->set('response', Response::class, true);

            self::$di->set('response.cachedFileResponseBuilder', function ($filesystemType) {
                $response = App::$di['response'];

                $serviceName = match ($filesystemType) {
                    FilesystemFactory::TYPE_UPLOAD => 'filesystem.upload',
                    default => 'filesystem.app',
                };

                /** @var FilesystemInterface $filesystem */
                $filesystem = App::$di->get($serviceName);

                return new Response\CachedFileResponseBuilder($response, $filesystem);
            });

            // Register common services
            $this
                ->_registerSessionFactory()
                ->_registerSession()
                ->_registerRouter()
                ->_registerFilter()
            ;

            self::$di->register(new DbServiceProvider());

            $this
                ->_registerCache()
                ->_registerFlash()
                ->registerModuleServices()
                ->registerDebugger()
            ;

            $this->setDI(self::$di);
        }

        self::$di->set("commandBus", CommandBus::class, true);
        self::$di->set("ticketService", TicketService::class, true);
        self::$di->set("privsLogger", PrivsLogger::class, true);
        self::$di->set("allowedIdsFilter", AllowedIdsFilter::class);

        // Register application specific services
        if (self::$config->settings->serviceManager) {
            foreach (self::$config->settings->serviceManager as $service => $class) {
                self::$di->set($service, $class);
            }
        }

        self::$di->set(ClientInterface::class, function () {
            return new Client(new GuzzleClient());
        });

        self::$di->set('httpClient', function () {
            return self::$di->get(ClientInterface::class);
        });

        self::$di->set('templatingEngineFactory', new EngineFactory(self::$di));

        self::$di->set('queue', function () {
            $queue = new Queue();

            if (self::$config->resque->redis) {
                $port = self::$config->resque->redis->port ?: '6379';
                $redisServer = self::$config->resque->redis->server . ':' . $port;

                $queue->setBackend($redisServer);
            }

            return $queue;
        });

        self::$di->set('validation', function () {
            if (self::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
                $wrapped = new \Phalcon\Filter\Validation();
            } else {
                $wrapped = new \Phalcon\Validation();
            }

            $validatorFactory = new ValidatorFactory();

            return new Validation($wrapped, $validatorFactory);
        });

        self::$di->set('userProvider', UserProvider::class);

        if (self::$config->microsoft && self::$config->microsoft->tenant) {
            self::$di->set('msTokenService', function () {
                $tokenProvider = new TokenProvider(self::$config->microsoft->tenant);
                $cache = self::$di->get('cache');
                return new TokenService($tokenProvider, $cache);
            });
        }

        self::$di->set(
            'maintenanceAlertFormatter',
            fn (): MaintenanceAlertFormatterInterface => new MaintenanceAlertFormatter(
                alertTemplate: Lang::get('GENERAL_MAINTENANCE_ALERT'),
                fromTimePlaceholder: 'XXX',
                toTimePlaceholder: 'YYY',
            )
        );

        self::$di->set('maintenanceAlertGenerator', function (): MaintenanceAlertGeneratorInterface {
            /** @var MaintenanceAlertFormatterInterface $alertFormatter */
            $alertFormatter = self::$di['maintenanceAlertFormatter'];

            return new MaintenanceAlertGenerator(
                alertFormatter: $alertFormatter,
                userTimeZone: App::$user->getTimezone(),
                applicationDefaultTimeZone: Timezone::getDefaultTimezone()->getName(),
            );
        });

        self::$di->register(new RateLimiterServiceProvider());

        self::$di->register(new FilesystemServiceProvider());
        self::$di->register(new AvServiceProvider());
        self::$di->register(new LoggerServiceProvider());
        self::$di->register(new LangServiceProvider());
        self::$di->register(new DebuggerServiceProvider());
        self::$di->register(new CryptServiceProvider());

        return $this;
    }


    /**
     * Registers session factory
     * @return $this
     */
    private function _registerSessionFactory()
    {
        self::$di->set('sessionFactory', SessionFactory::class);

        return $this;
    }


    /**
     * Registers session service
     * @return $this
     */
    protected function _registerSession()
    {
        //Start the session the first time when some component request the session service
        self::$di->setShared('session', function () {
            $config = [];

            /** @var Request $request */
            $request = App::getService('request');

            if (App::$config->session) {
                $config = Arrays::toArray(App::$config->session);
            }
            if ($request->isMobile() && App::hasModule('Mobile')) {
                $config['use_cookies'] = false;
                ini_set('session.use_cookies', false);
            } else {
                session_set_cookie_params(0, '/;SameSite=None', null, $config['cookie_secure'], $config['cookie_httponly']);
            }

            /** @var SessionFactoryInterface $factory */
            $factory = App::$di->get('sessionFactory');

            try {
                if (
                    class_exists('Redis')
                    && $config['adapter'] == 'Redis'
                    && !self::isLocalFileStorage()
                ) {
                    if (!isset($config['prefix'])) {
                        $config['prefix'] = md5(App::$config->db->database ?: APP_PATH);
                    }

                    if (!isset($config['lifetime'])) {
                        $config['lifetime'] = ini_get('session.gc_maxlifetime');
                    }

                    $config['lifetime'] = (int) $config['lifetime'];

                    $session = $factory->createRedisSession($config);
                }
            } catch (Exception $e) {
                VelisException::raise($e->getMessage(), $e->getCode(), $e);
                session_set_save_handler(new SessionHandler(), true);
                unset($session);
            }

            if (!isset($session)) {
                $session = $factory->createFileSession($config);
            }

            // Using through vintage GUI
            if (!$request->isApi() && (!$request->isMobile() || !App::hasModule('Mobile'))) {
                $session->start();
                return $session;
            }

            // Using API with Cookie as auth method
            if ($request->isApi() && !is_string($request->authorization())) {
                $session->start();

                return $session;
            }

            // Using API with `Authorization token` as auth method
            if ($token = $request->authorization()) {
                session_id($token);
                $session->setId($token);
                $session->start();

                return $session;
            }

            return $session;
        });

        if (self::isHttpRequest()) {
            self::$session = self::$di['session'];
        }

        return $this;
    }


    /**
     * Registering a router
     * @return App
     */
    protected function _registerRouter()
    {
        self::$di->set('router', function () {
            $router = new Router();
            return $router->init();
        });

        return $this;
    }


    /**
     * Global input filter initialization
     * @return App
     */
    protected function _registerFilter()
    {
        $filter = new Filter(
            array_merge($_POST, $_GET),
            App::$config->settings->autoFiltering
        );

        unset($filter['_url']);
        self::$registry['filter'] = $filter;

        return $this;
    }

    /**
     * Registers cache service
     * @return $this
     */
    protected function _registerCache()
    {
        self::$di->set('cacheFactory', CacheFactory::class);

        if (self::$config->cache_lang) {
            self::$di->set('lang_cache', function () {
                /** @var CacheFactoryInterface $factory */
                $factory = self::$di->get('cacheFactory');
                $options = Arrays::toArray(self::$config->cache_lang->options);

                return $factory->createFileCache($options);
            });
        }

        self::$di->set('cache', function () {
            /** @var CacheFactoryInterface $factory */
            $factory = App::$di->get('cacheFactory');
            $options = Arrays::toArray(App::$config->cache->options);

            try {
                if (
                    App::$config->cache->adapter == 'Redis'
                    && !self::isLocalFileStorage()
                ) {
                    if (App::$config->settings->instanceAcro) {
                        $options['prefix'] = App::$config->settings->instanceAcro;
                    } elseif (App::$config->cache->options->prefix) {
                        $options['prefix'] = App::$config->cache->options->prefix;
                    } else {
                        $options['prefix'] = Filter::filterAlnum(App::$config->settings->domain ?: $_SERVER['SERVER_NAME']);
                    }

                    return $factory->createRedisCache($options);
                }
            } catch (Exception $e) {
                VelisException::raise($e->getMessage(), $e->getCode(), $e);
            }

            return $factory->createFileCache($options);
        });

        self::$cache = self::$di['cache'];

        if (App::$config->getDbHasChanged()) {
            App::$cache->clear();
        }

        return $this;
    }


    /**
     * Registers flash messenger
     * @return $this
     */
    protected function _registerFlash()
    {
        //Register the flash service with custom CSS classes
        self::$di->set('flash', function () {
            $flash = new SessionFlash();
            $flash->setCssClasses([
                'error'     => 'alert alert-danger',
                'success'   => 'alert alert-success',
                'notice'    => 'alert alert-info',
                'warning'   => 'alert alert-warning',
            ]);

            return $flash;
        });

        return $this;
    }


    public function registerDebugger(): static
    {
        self::$di->set(
            'debugger',
            fn () => new Debugger(
                htmlRenderer: new WhoopsHtmlRenderer(),
                arrayRenderer: new ArrayRenderer()
            )
        );

        return $this;
    }


    /**
     * Returns true if on devel server or service viewed from trusted IPs
     *
     * used when generating profiler table and in ErrorController
     * when displaying exception info
     *
     * @see smarty_function_profiler()
     * @see ErrorController::errorAction()
     *
     * @return bool
     */
    public static function devMode()
    {
        if (self::$config->settings->debugMode) {
            return true;
        }

        if (self::isConsole()) {
            return false;
        }

        $request = self::getService('request');

        $shouldStartSession = (
            $request
            && (!$request->isMobile() || !self::hasModule('Mobile'))
            && !$request->isApi()
            && self::isHttpRequest()
        );

        $sess = null;
        if ($shouldStartSession || self::isSessionStarted()) {
            $sess = self::$di['session'];
        }

        if ($sess && $sess->debugMode) {
            return true;
        }

        return false;
    }


    /**
     * Returns true, when application is in demo version
     * @return bool
     */
    public static function demoMode(): bool
    {
        return ($_SERVER['DEMO'] ?? false) || APP_ENV == self::ENV_DEMO;
    }


    /**
     * Returns instance of MVC application
     * @return App
     */
    public static function getInstance()
    {
        return self::$_instance;
    }


    /**
     * Returns MVC application event manager
     * @return ManagerInterface
     */
    public static function getEvents()
    {
        return self::$di['eventsManager'];
    }


    /**
     * Returns service from ServiceManager
     * @template T
     * @param string|class-string<T> $serviceName
     * @param array<mixed> $params
     * @return ($serviceName is class-string<T> ? T : object|null)
     */
    public static function getService(string $serviceName, array $params = [])
    {
        $obj = self::$di[$serviceName];

        if (is_callable($obj)) {
            return $obj(...$params);
        }

        return $obj;
    }


    /**
     * Checks if user is super priviledged
     *
     * @param bool $checkSourceUser
     * @return bool
     */
    public static function isSuper($checkSourceUser = false): bool
    {
        if (!isset(self::$user)) {
            return false;
        }

        $isSuper = self::$user->isSuper();

        if (!$isSuper && $checkSourceUser && self::$user->isSwitched()) {
            return self::$user->getSourceUser()->isSuper();
        }

        return $isSuper;
    }


    /**
     * Checks if user is suppoort user
     *
     * @param bool $checkSourceUser
     * @return bool
     */
    public static function isSupport($checkSourceUser = false)
    {
        if (!isset(self::$user)) {
            return false;
        }

        $isSupport = self::$user->isSupport();

        if (!$isSupport && $checkSourceUser && self::$user->isSwitched()) {
            return self::$user->getSourceUser()->isSupport();
        }

        return $isSupport;
    }


    /**
     * Checks if user is tests master user
     *
     * @param bool $checkSourceUser
     * @return bool
     */
    public static function isQa($checkSourceUser = false): bool
    {
        if (
            !isset(self::$user) ||
            !App::$config->settings->isQaEnv ||
            !App::$config->settings->qaUserId
        ) {
            return false;
        }

        $isQa = self::$user->id() == App::$config->settings->qaUserId;

        if (!$isQa && $checkSourceUser && self::$user->isSwitched()) {
            return self::$user->getSourceUser()->id() == App::$config->settings->qaUserId;
        }

        return $isQa;
    }



    /**
     * Returns application setting(s)
     *
     * @param string $setting
     * @param mixed $default
     * @param string|null $section
     * @return mixed
     */
    public static function settings($setting = null, $default = null, $section = null)
    {
        if ($setting && self::$config->settings->aliases[$setting]) {
            $setting = self::$config->settings->aliases[$setting];
        }

        if (!isset(self::$_settings)) {
            if ((self::$cache['settings']) === null) {
                unset(self::$cache['settings']);
                self::$cache['settings'] = Setting::getAppSettings();
            }
            self::$_settings = self::$cache['settings'];
        }

        if ($setting != null) {
            if ($section) {
                $value = self::$_settings[$section][$setting];
            } elseif (isset(self::$_settings['Settings'])) {
                $settings = Arrays::filterKeepZeros([self::$_settings[$setting], self::$_settings['Settings'][$setting]]);
                $value = Arrays::getFirst($settings);
            } else {
                $value = self::$_settings[$setting];
            }

            if (!strlen(trim($value)) && isset($default)) {
                return $default;
            } else {
                return $value;
            }
        } else {
            return self::$_settings;
        }
    }


    /**
     * Returns available modules directories
     * @return array
     */
    public static function getModuleDirs()
    {
        if (!isset(self::$_moduleDirs)) {
            self::$_moduleDirs = array_slice(scandir(MODULE_PATH), 2);
        }
        return self::$_moduleDirs;
    }


    /**
     * @return array
     */
    public static function getModuleDefinitions()
    {
        $modules = [];

        // register the installed modules
        foreach (self::getModuleDirs() as $module) {
            if (file_exists(MODULE_PATH . $module . '/Module.php')) {
                $modules[$module] = [
                    'className' => "$module\Module",
                    'path' => MODULE_PATH . "$module/Module.php",
                ];
            }
        }

        return $modules;
    }


    /**
     * Registers services defined in specified modules
     * @return $this
     */
    public function registerModuleServices()
    {
        $modules = self::getModuleDefinitions();

        foreach ($modules as $moduleDefinition) {
            if (!class_exists($moduleDefinition['className'])) {
                // This internal config file has its extension - it's declared with variable declaration.
                // @phpcs:ignore
                include_once $moduleDefinition['path'];
            }

            /** @var BaseModule $module */
            $module = new $moduleDefinition['className']();
            $module->registerCommonServices(self::$di);
        }

        return $this;
    }


    /**
     * Checks if module is enabled in application
     *
     * @param string $module
     * @return bool
     */
    public static function hasModule($module)
    {
        return self::$config->isModuleEnabled($module);
    }


    /**
     * Returns true if at least one of modules is enabled
     *
     * @param $module, $module, ...
     * @return bool
     */
    public static function hasAnyModule()
    {
        foreach (func_get_args() as $module) {
            if (self::hasModule($module)) {
                return true;
            }
        }

        return false;
    }


    /**
     * Generates an url given the name of a route.
     *
     * @param  string  $name               Name of the route
     * @param  array   $params             Parameters for the link
     *
     * @return string Url                  For the link href attribute
     */
    public static function getRouteUrl($name, array $params = [])
    {
        $url = self::$di['url'];

        $params['for'] = $name;
        return trim($url->get($params), '?');
    }


    /**
     * Returns application base url
     * @param bool $ignoreFixedDomain
     * @return string
     */
    public static function getBaseUrl($ignoreFixedDomain = false)
    {
        $httpMethod = App::$config->settings->httpMethod ?: 'http';

        if (App::$config->settings->fixedDomain && !$ignoreFixedDomain) {
            $domain = App::$config->settings->fixedDomain;
        } else {
            $domain = $_SERVER['SERVER_NAME'] ?: App::$config->settings->domain;
        }

        if (App::$config->settings->allowCustomUrlPort) {
            if ($_SERVER["SERVER_PORT"] && !in_array($_SERVER["SERVER_PORT"], [443,80])) {
                $domain .= ":" . $_SERVER["SERVER_PORT"];
            }
        }

        return $httpMethod . '://' . $domain;
    }


    /**
     * Outputs message if console mode
     * @param string $message
     * @param bool $newLine
     */
    public static function consoleLog($message, $newLine = false)
    {
        if (self::isConsole()) {
            print($message . ($newLine ? "\n" : ""));
        }
    }


    /**
     * Returns true if console request
     * @return bool
     */
    public static function isConsole()
    {
        return PHP_SAPI == 'cli' && !defined('PHPUNIT_INITIALIZED');
    }


    /**
     * Returns true if http request
     * @return bool
     */
    public static function isHttpRequest()
    {
        return PHP_SAPI != 'cli';
    }


    /**
     * Returns true if current script is a Resque task
     * @return bool
     */
    public static function isQueue()
    {
        return defined('RESQUE_TASK');
    }


    /**
     * Returns superUsers ids
     * @return int[]
     */
    public static function getSuperUsers()
    {
        return self::$_superUsers;
    }


    /**
     * Returns supportUsers ids
     * @return int[]
     */
    public static function getSupportUsers()
    {
        return self::$_supportUsers;
    }


    /**
     * Checks if current user is super privileged
     *
     * @param int $userId
     * @return bool
     */
    public static function checkSuper($userId)
    {
        return in_array($userId, self::$_superUsers);
    }


    /**
     * Returns true if session is active
     *
     * @return bool
     */
    public static function isSessionStarted()
    {
        if (php_sapi_name() !== 'cli') {
            return session_status() === PHP_SESSION_ACTIVE;
        }

        return false;
    }


    /**
     * Cleans cache & all temporary files
     * @param array|null $params
     */
    public static function clean($params = null)
    {
        self::consoleLog("Clearing:", true);
        self::consoleLog("* Cache...");

        self::$cache->clear();

        self::consoleLog("[done]", true);
        self::consoleLog("* Smarty...");

        array_map('unlink', glob(DATA_PATH . 'smarty-compiled/*'));

        self::consoleLog("[done]", true);

        if ($params['langs']) {
            $langVariants = Lang::getLangVariants();
            // To include langs overloaded for Prologis
            $langVariants[] = new LangVariant('PROLOGIS');
            $clearLangsService = new ClearLangsService($langVariants);
            $clearLangsService->refresh($params['langs']);
        }

        self::consoleLog("* Router module configs...");
        foreach (self::getModuleDirs() as $module) {
            array_map('unlink', glob(MODULE_PATH . $module . '/config/*routes*.local.php'));
        }
        self::consoleLog("[done]", true);

        self::consoleLog("\nFinished.\n\n");
    }


    /**
     * Clears the language cache if there is separate one for this purpose
     * @return void
     * @throws Exception
     */
    public static function cleanLangCache(): void
    {
        $variant = Lang::getLangVariant();
        if ($variant) {
            $variant->clearOverloadedLangs();
        }
        if (self::$di->has('lang_cache')) {
            self::consoleLog("Clearing:", true);
            self::consoleLog("* Generating langs...");
            Lang::loadAll();
            self::consoleLog("[done]\n\n");
        }
    }



    /**
     * Initialize request throttling component
     * @param bool $public
     */
    public static function initThrottling($public = false)
    {
        self::$throttling = new Throttling($public);
    }


    /**
     * Static wrapper for request methods
     *
     * @param string $name
     * @param array $arguments
     *
     * @return mixed
     *
     * @throws BadMethodCallException
     */
    public static function __callStatic($name, $arguments)
    {
        $request = self::getService('request');
        $methods = [
            $name,
            str_replace('Request', '', $name)
        ];


        foreach ($methods as $method) {
            if ($request && method_exists($request, $method)) {
                return call_user_func_array([$request, $method], $arguments);
            }
        }

        throw new BadMethodCallException("Method $name not found in class App");
    }


    /**
     * Check if app runs in testing environment
     * @return bool
     */
    public static function isTesting(): bool
    {
        return APP_ENV == self::ENV_TESTING;
    }

    /**
     * Update error log level depending on app settings
     */
    protected static function _updateErrorReportingLevel()
    {
        if (
            self::$config->error
            && self::$config->error->reportingLevel
            && count(Arrays::toArray(self::$config->error->reportingLevel))
        ) {
            $reportingLevel = ini_get('error_reporting');

            foreach (self::$config->error->reportingLevel as $acro => $enabled) {
                $currentStatus = error_reporting() & constant($acro);
                $maskAcro = 'ERROR_MASK_' . $acro;
                $maskValue = constant("self::$maskAcro");

                if (!$currentStatus && $enabled) {
                    $reportingLevel += $maskValue;
                } elseif ($currentStatus && !$enabled) {
                    $reportingLevel -= $maskValue;
                }
            }

            ini_set('error_reporting', $reportingLevel);
        }
    }

    /**
     * @throws Exception
     */
    public static function getMaintenanceAlert(): ?string
    {
        $maintenanceDateStart = App::settings('MaintenanceDateStart');
        $maintenanceDateStop = App::settings('MaintenanceDateStop');
        $maintenanceTimeZone = App::settings('MaintenanceTimeZone');
        $multipleTimezoneSupport = App::settings('MultipleTimezoneSupport');

        if (!$maintenanceDateStart || !$maintenanceDateStop || !$maintenanceTimeZone) {
            return null;
        }

        /** @var MaintenanceAlertGeneratorInterface $maintenanceAlertGenerator */
        $maintenanceAlertGenerator = self::$di['maintenanceAlertGenerator'];

        return $maintenanceAlertGenerator->getAlert(
            maintenanceDateStart: $maintenanceDateStart,
            maintenanceDateStop: $maintenanceDateStop,
            maintenanceTimeZone: $maintenanceTimeZone,
            multipleTimeZoneSupport: $multipleTimezoneSupport,
        );
    }


    private static function isLocalFileStorage(): bool
    {
        if (self::$isLocalFileStorageForced) {
            return true;
        }

        return self::$config->forceDisableRedis == 1;
    }


    public static function forceLocalFileStorage(bool $enabled = false): void
    {
        self::$isLocalFileStorageForced = $enabled;
    }

    public function getCorrelationId(): string
    {
        if (!isset(self::$correlationId)) {
            self::$correlationId = uniqid();
        }

        return self::$correlationId;
    }

    public static function getAppName(): string
    {
        return App::$config->appName ?? $_SERVER['APP_NAME'] ?? '';
    }

    public static function getAppEnv(): string
    {
        return App::$config->appEnv ?? APP_ENV ?? '';
    }

    public static function getAppUser(): AppUser
    {
        return App::$user ?? throw new RuntimeException("User not found");
    }
}
