<?php

namespace Velis\Mvc\Controller;

use Application\Application;
use Application\Facade\MagicLinks;
use Exception;
use InvalidArgumentException;
use Laminas\Paginator\Paginator;
use Phalcon\Mvc\Controller;
use Phalcon\Mvc\Dispatcher;
use ReflectionException;
use User\User;
use Velis\App;
use Velis\App\PhalconVersion;
use Velis\App\User as AppUser;
use Velis\Arrays;
use Velis\Config\Ini as IniConfig;
use Velis\Dto\Exceptions\ValidationException;
use Velis\Exception as VelisException;
use Velis\Filesystem\FilesystemFactory;
use Velis\Filter;
use Velis\Http\Request;
use Velis\Http\Response;
use Velis\Http\Response\CachedFileResponseBuilder;
use Velis\Lang;
use Velis\Model\DataObject;
use Velis\Model\DataObject\NoColumnsException;
use Velis\Model\File as FileModel;
use Velis\Mvc\Controller\Utils\DtoRequestTransformer;
use Velis\Mvc\Controller\Utils\FrontendRouteUrlConverter;
use Velis\Mvc\FrontendRouting;
use Velis\Mvc\Router;
use Velis\Mvc\View;
use Velis\Mvc\View\Helper\Profiler;
use Velis\Output;
use Velis\Paginator\Adapter\ArrayAdapter;

/**
 * Base for MVC Action controller
 * @author Olek Procki <olo@velis.pl>
 *
 * @property Request $request
 * @property Response $response
 * @property View $view
 */
abstract class BaseController extends Controller
{
    /**
     * Actions allowed without authorization
     * @var array
     */
    protected static $_publicActions = [];


    /**
     * Actions allowed from an iframe
     * @var array
     */
    protected static $_frameAllowedActions = [];


    /**
     * Privs for each controller action
     * @var array
     */
    private $_requiredPrivs = [];


    /**
     * Request input params
     * @var Filter
     */
    protected $_filter;


    /**
     * @var string
     */
    protected $_moduleName;


    /**
     * @var string
     */
    protected $_controllerName;


    /**
     * @var string
     */
    protected $_actionName;


    /**
     * @var string
     */
    protected $_context;


    /**
     * True when profiler attached manually
     * @var bool
     */
    protected $_attachProfiler = false;

    private bool $skipFrontendRedirect = false;

    /**
     * Override this if needed
     * @return mixed (returning response will cancel action execution)
     */
    public function init()
    {
        return null;
    }


    /**
     * Controller initialization
     */
    public function onConstruct()
    {
        $this->_filter = App::$registry['filter'];

        App::$registry['moduleName']     = strtolower(Filter::filterToDash($this->dispatcher->getModuleName()));
        App::$registry['controllerName'] = strtolower(Filter::filterToDash($this->dispatcher->getControllerName()));
        App::$registry['actionName']     = strtolower(Filter::filterToDash($this->dispatcher->getActionName()));

        $this->_moduleName     = $this->view->_module     = App::$registry['moduleName'];
        $this->_controllerName = $this->view->_controller = App::$registry['controllerName'];
        $this->_actionName     = $this->view->_action     = App::$registry['actionName'];


        // load public actions from config
        if (App::$config->settings->publicActions) {
            foreach (App::$config->settings->publicActions as $publicAction) {
                list($module, $controller, $action) = explode(',', $publicAction);
                self::_declareActionAccess(trim($module), trim($controller), trim($action), Request::ACCESS_PUBLIC);
            }
        }

        if (session_id() && is_array(App::$session->additionalPublicActions)) {
            foreach (App::$session->additionalPublicActions as $publicAction) {
                list($module, $controller, $action) = explode(',', $publicAction);
                // declare additional public action
                self::_declareActionAccess(trim($module), trim($controller), trim($action), Request::ACCESS_PUBLIC);
                // make additional public actions available for iframe as well
                self::_declareActionAccess(trim($module), trim($controller), trim($action), Request::ACCESS_IFRAME);
            }
        }

        if (App::$config->settings->iFrameAllowedActions) {
            foreach (App::$config->settings->iFrameAllowedActions as $allowedAction) {
                list($module, $controller, $action) = explode(',', $allowedAction);
                self::_declareActionAccess(trim($module), trim($controller), trim($action), Request::ACCESS_IFRAME);
            }
        }

        if (App::$config->requestThrottling) {
            App::initThrottling($this->isPublicAction());
        }

        if (App::$user->isLogged() && !App::$session->unsupported_browser_info && App::isIE() && !App::isIE('Edge')) {
            if (!App::$user->hasCompany() || App::isIE() < 10) {
                $this->view->unsupported_browser_info = 1;
            }
        }
    }

    /**
     * This is executed before every found action
     * @param Dispatcher $dispatcher
     * @return bool|void
     * @throws AccessException
     * @throws NoColumnsException
     * @throws VelisException
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws ReflectionException
     */
    final public function beforeExecuteRoute(Dispatcher $dispatcher)
    {
        if (($this->request->isApi() || in_array(JsonAwareTrait::class, class_uses($this))) && !App::$user->isLogged()) {
            try {
                $authChecker = new AuthenticationChecker($dispatcher);
                $authChecker->handle();
                App::$registry['authMethod'] = $authChecker->getAuthMethod();
            } catch (Exception $e) {
                // do nothing - user is not authenticated
            }
        }

        if (MagicLinks::isMagicRequest($this->request) && App::$user->isLogged()) {
            return true;
        }

        App::$registry['needActivationRedirect'] = $this->_needActivationRedirect(true);

        // check access
        if (!App::$user->isLogged() && !$this->isPublicAction() && !App::isConsole() && !$this->dispatcher->wasForwarded()) {
            $this->checkPriv(false);

            if ($this->request->isApi() || in_array(JsonAwareTrait::class, class_uses($this))) {
                $this->response
                    ->setStatusCode(401, 'Unauthorized')
                    ->setJsonContent([
                        'error' => 'Unauthorized',
                    ])
                ;

                $this->noRender();

                return false;
            } elseif (!$this->isAjax()) {
                // register post login redirect url only when session was initialized
                if (session_id()) {
                    App::$session->postLoginRedirect = $_SERVER['REQUEST_URI'];

                    // Redirect to HTTP_REFERER when logged out of post request
                    if (!$this->request->isGet() && isset($_SERVER['HTTP_REFERER'])) {
                        App::$session->postLoginRedirect = $_SERVER['HTTP_REFERER'];
                    }
                }

                $startRoute = empty(App::$config->settings->startRoute) || App::$config->auth->preferDirectLogin ? 'login' : App::$config->settings->startRoute;
                $this->go($startRoute);

                return false;
            } else {
                $this->response->setStatusCode(401, Lang::get('GENERAL_SESSION_ENDED'));

                if ($this->_filter['json']) {
                    $this->response->setContent(
                        Output::jsonEncode([
                            'error' => Lang::get('GENERAL_SESSION_ENDED'),
                        ])
                    );
                } else {
                    $this->response->setContent(Lang::get('GENERAL_SESSION_ENDED'));
                }

                $this->noRender();

                return false;
            }
        } elseif ($this->_needActivationRedirect()) {
            // need to confirm new password for full account activation
            $this->checkPriv(false);
            $this->go(App::$user->getActivationRoute());

            return false;
        } elseif (
            method_exists('Application\Application', 'additionalRedirections')
                    && !($this->_moduleName == 'user'
                        && $this->_controllerName == 'index'
                        && $this->_actionName == 'activation')
                    && !($this->_moduleName == 'application'
                        && $this->_controllerName == 'layout'
                        && $this->_actionName == 'logo')
        ) {
            $route = Application::additionalRedirections();
            if (is_string($route)) {
                $this->checkPriv(false);

                if (!$this->isAjax()) {
                    $this->go($route);
                } else {
                    $this->noRender();
                    $this->response
                        ->setJsonContent([
                            'redirectUrl' =>  App::getRouteUrl($route),
                        ])
                        ->setStatusCode(403, 'Forbidden')
                    ;
                }
            }

            if ($route) {
                return false;
            }
        } elseif (!$this->isPublicAction() && !$this->_checkRestrictions()) {
            $this->checkPriv(false);
            $this->error(Lang::get('GENERAL_PAGE_ERROR'), '/');

            return false;
        }

        // append filter with route params
        $this->_filter = new Filter(
            Arrays::merge(
                $this->_filter->getRawCopy(),
                $dispatcher->getParams()
            ),
            App::$config->settings->autoFiltering
        );

        $this->view->_filter = $this->_filter;
        App::$registry['filter'] = $this->_filter;

        $this->_checkCSRF();
        $initResult = $this->init();

        if ($this->redirectToNewFrontend() === true) {
            return false;
        }

        if (App::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
            // Transform request data into specified DTO, and append it as a param
            $requestTransformer = new DtoRequestTransformer($this->getRequest());
            try {
                $requestTransformer->transformRequest($dispatcher);
            } catch (ValidationException $e) {
                $this->error($e->getMessage());
                return false;
            };
        }

        if ($initResult instanceof Response) {
            $initResult->send();

            return false;
        }
    }


    /**
     * Executed after every found action
     * @throws VelisException
     */
    public function afterExecuteRoute(Dispatcher $dispatcher)
    {
        $returnedValue = $dispatcher->getReturnedValue();

        if ($returnedValue && App::$config->settings->voidResponseCheck) {
            VelisException::raise('Controller action should not return any value.', null, null, [
                'module' => $this->_moduleName,
                'controller' => $this->_controllerName,
                'action' => $this->_actionName,
            ]);
        }

        if (App::$config->settings->actionPrivCheck && !$this->isPublicAction() && App::$user->isLogged()) {
            // post dispatch validation - did we check any priv during this action?
            if ($this->_requiredPrivs !== false) {
                if (empty($this->_requiredPrivs)) {
                    VelisException::raise(
                        'Required privs undefined for action '
                        . $this->_moduleName . '/'
                        . $this->_controllerName . '/'
                        . $this->_actionName
                    );
                }
            }
        }

        if ($this->_attachProfiler) {
            $profiler = new Profiler();
            echo $profiler(true);
        }

        /** Clear flag if full page reload occurred */
        if (App::$session->forceReload && !$this->isAjax()) {
            App::$session->forceReload = false;
        }
        $this->addMaintenanceAlert();

        if (App::$registry['authMethod'] === 'token') {
            App::$session->destroy(App::$session->getId());
        }
    }

    private function addMaintenanceAlert()
    {
        if ($this->isAjax() || $this->view->isDisabled()) {
            return;
        }

        $this->view->maintenanceAlert = App::getMaintenanceAlert();
    }

    /**
     * Tells controller to attach profiler after executing action
     * @return $this
     */
    public function attachProfiler()
    {
        $this->_attachProfiler = true;

        return $this;
    }


    /**
     * Sets required privs for action
     * @return $this
     * @throws AccessException
     */
    public function checkPriv()
    {
        if (func_num_args() == 1) {
            // priv passed as array (module & priv pair)
            if (is_array(func_get_arg(0))) {
                list($module, $priv) = func_get_arg(0);

                // allow action access without any priv
            } elseif (func_get_arg(0) === false) {
                $this->_requiredPrivs = false;
                return $this;
            }
        } else {
            // classic priv check with 2 arguments (module, priv)
            list($module, $priv) = func_get_args();
        }

        $this->_requiredPrivs[] = array($module, $priv);

        if (!App::$user->hasPriv($module, $priv)) {
            throw new AccessException(Lang::get('GENERAL_PERM_DENIED'));
        }

        return $this;
    }


    /**
     * Checks CSRF token for restricted actions
     * @throws AccessException
     */
    protected function _checkCSRF()
    {
        if ($this->_filter['csrf_token']) {
            $this->checkCSRF();
        } elseif (App::$config->settings->csrfCheckActions) {
            $csrfRestricted = Arrays::toArray(App::$config->settings->csrfCheckActions);
            $requestAction = [
                $this->_moduleName,
                $this->_controllerName,
                $this->_actionName,
            ];
            foreach ($csrfRestricted as $restricted) {
                if ($requestAction == explode(',', $restricted)) {
                    $this->checkCSRF();
                }
            }
        }
    }


    /**
     * Validates CSRF token
     *
     * @param string $tokenField
     * @throws AccessException
     */
    public function checkCSRF($tokenField = 'csrf_token')
    {
        if (App::$session->csrfToken != $this->_filter->getRaw($tokenField)) {
            throw new AccessException(Lang::get('GENERAL_INVALID_TOKEN'));
        }
    }


    /**
     * Disables/Enables view rendering
     *
     * @param bool $renderingOff
     * @return $this
     */
    public function noRender($renderingOff = true)
    {
        if ($renderingOff) {
            $this->view->disable();
        } else {
            $this->view->enable();
        }

        return $this;
    }


    /**
     * Returns true if request comes from ajax call
     *
     * @param bool $ignoreInstant pass true to handle instant request like regular one
     * @return bool
     */
    public function isAjax($ignoreInstant = false)
    {
        $request = App::getService('request');

        return $request->isAjax($ignoreInstant);
    }


    /**
     * Sets rendering context
     *
     * @param string $context
     * @return $this
     */
    public function setContext($context)
    {
        $this->_context = $context;
        $this->view->setContext($context);

        return $this;
    }


    /**
     * Sets context to ajax & disables layout
     * @return $this
     */
    public function ajaxContext()
    {
        $this->view->setBlank();

        return $this->setContext('ajax');
    }


    /**
     * Adds notice message on stack
     *
     * @param string $message
     * @param string $redirectUrl
     * @return $this|Response
     */
    public function notice($message, $redirectUrl = null)
    {
        $this->flash->success($message);

        if ($redirectUrl != null) {
            return $this->_redirect($redirectUrl);
        }

        return $this;
    }


    /**
     * Adds error message on stack
     *
     * @param string $message
     * @param string $redirectUrl
     * @return $this|Response
     */
    public function error($message, $redirectUrl = null)
    {
        $this->flash->error($message);

        if ($redirectUrl != null) {
            return $this->_redirect($redirectUrl);
        }

        return $this;
    }


    /**
     * Adds warning message on stack
     *
     * @param string $message
     * @param string $redirectUrl
     * @return $this|Response
     */
    public function warning($message, $redirectUrl = null)
    {
        $this->flash->warning($message);

        if ($redirectUrl != null) {
            return $this->_redirect($redirectUrl);
        }

        return $this;
    }


    /**
     * Redirects to $url
     * @param string $url
     * @return Response
     */
    protected function _redirect($url)
    {
        $this->noRender();

        if (strpos($url, 'http') === false) {
            $url = App::getBaseUrl(true) . $url;
        }

        if ($this->response->canRedirect()) {
            $this->response->increaseRedirectCalls();
            return $this->response->redirect($url);
        } else {
            $this->response->resetRedirectCalls();

            if (App::$user->isLogged() || App::isConsole() || $this->request->isMobile() || $this->request->isApi()) {
                return $this->response->redirect(App::getBaseUrl(true));
            } else {
                return $this->response->redirect(App::getBaseUrl(true) . App::getRouteUrl('login'));
            }
        }
    }


    /**
     * Forwards to another action within single request
     * @param array $params
     */
    protected function _forward($params)
    {
        if ($this->request->isInstant() && $params['url']) {
            $this->response->setHeader(
                'x-instant-forward-url',
                $params['url']
            );
        }

        if ($params['module']) {
            $this->dispatcher->setModuleName($params['module']);
        }

        return $this->dispatcher->forward($params);
    }


    /**
     * Forward to previous action
     * @param array $alternativeParams
     */
    protected function _rewind($alternativeParams = null)
    {
        if ($_SERVER['HTTP_REFERER']) {
            $url         = parse_url($_SERVER['HTTP_REFERER']);
            $href        = $url['path'];
            $queryParams = [];

            $this->router->handle($href);
            parse_str($url['query'], $queryParams);

            $forwardParams = array_merge(
                $this->router->getParams(),
                $queryParams
            );

            $this->_forward([
                'module'     => $this->router->getModuleName(),
                'controller' => $this->router->getControllerName(),
                'action'     => $this->router->getActionName(),
                'params'     => $forwardParams,
                'url'        => $href . ($url['query'] ? '?' . $url['query'] : '')
            ]);
        } elseif ($alternativeParams) {
            $this->_forward($alternativeParams);
        } else {
            $this->_forward([
                'module'     => 'application',
                'controller' => 'index',
                'action'     => 'index',
                'url'        => '/'
            ]);
        }
    }


    /**
     * Redirects to route
     *
     * @param string $route RouteInterface name
     * @param array $params Parameters to use in url generation, if any
     * @return Response
     */
    public function go($route = null, array $params = array())
    {
        $this->noRender();

        if ($this->response->canRedirect()) {
            $this->response->increaseRedirectCalls();
            $params['for'] = $route;
            return $this->response->redirect($params);
        } else {
            $this->response->resetRedirectCalls();

            if (App::$user->isLogged() || App::isConsole() || $this->request->isMobile() || $this->request->isApi()) {
                return $this->response->redirect(App::getBaseUrl(true));
            } else {
                return $this->response->redirect(App::getBaseUrl(true) . App::getRouteUrl('login'));
            }
        }
    }


    /**
     * Generates a URL based on a route
     *
     * @deprecated preffered to use url() function
     *
     * @param  string $route RouteInterface name
     * @param  array $params Parameters to use in url generation, if any
     *
     * @return string
     */
    public function getRouteUrl($route = null, array $params = array())
    {
        return App::getRouteUrl($route, $params);
    }


    /**
     * Returns request object
     * @return \Phalcon\Http\Request
     */
    public function getRequest()
    {
        return $this->request;
    }


    /**
     * Returns response object
     * @return Response
     */
    public function getResponse()
    {
        return $this->response;
    }


    /**
     * Generates a URL based on a route
     *
     * @param  string $route RouteInterface name
     * @param  array $params Parameters to use in url generation, if any
     *
     * @return string
     */
    public function url($route = null, array $params = array())
    {
        return App::getRouteUrl($route, $params);
    }


    /**
     * If referer exists - redirects back to referer
     * or $alternativeUrl otherwise (if not passed, redirects to root path)
     * @param string|null $alternativeUrl
     * @return Response
     */
    public function back($alternativeUrl = null)
    {
        if ($_SERVER['HTTP_REFERER']) {
            return $this->_redirect($_SERVER['HTTP_REFERER']);
        } else {
            if ($alternativeUrl != null) {
                return $this->_redirect($alternativeUrl);
            } else {
                return $this->_redirect('/');
            }
        }
    }


    /**
     * Deny access (redirect) ans set error message
     *
     * @param string $message
     * @return void
     * @throws AccessException
     */
    public function deny($message = null)
    {
        if ($message == null) {
            $message = Lang::get('GENERAL_PERM_DENIED');
        }

        $this->noRender();
        $this->checkPriv(false);

        throw new AccessException($message);
    }


    /**
     * Shortcut function for retriving page number from filter
     *
     * @param string $pageVarName
     * @return int
     */
    public function page($pageVarName = 'page')
    {
        return $this->_filter->getInt($pageVarName) ? $this->_filter->getInt($pageVarName) : 1;
    }


    /**
     * Shortcut function for retrieving order field from filter
     *
     * @param string $default
     * @param string $orderVarName
     * @param string $sortVarName
     *
     * @return string
     */
    public function order($default = null, $orderVarName = 'order', $sortVarName = 'sort')
    {
        $orderOption = trim($this->_filter[$orderVarName]);
        $sortOption = trim($this->_filter[$sortVarName]);

        // remove order query if RestController-like sort param is present
        if (preg_match('/.*:(asc|desc)/', $sortOption)) {
            unset($this->_filter[$orderVarName]);
            unset($this->_filter[$sortVarName]);
            return $default;
        }

        if (strlen($sortOption)) {
            $orderParts = preg_split('/,(?![^\(]*\))/', $orderOption);
            array_walk($orderParts, function (&$part, $i, $sortOption) {
                $part = $part . ' ' . $sortOption;
            }, $sortOption);

            $orderOption = implode(',', $orderParts);
        }
        return strlen($orderOption) ? $orderOption : $default;
    }


    /**
     * Returns new instance of paginator object
     *
     * @param array $array result array
     * @param int $total total rows count
     * @param int $page current page number
     * @param int $itemsPerPage items displayed per page
     *
     * @return Paginator
     */
    public function paginator($array, $total = null, $page = null, $itemsPerPage = DataObject::ITEMS_PER_PAGE)
    {
        $this->view->paginator = new Paginator(new ArrayAdapter($array, $total));

        if ($page != null) {
            $this->view->paginator->setCurrentPageNumber($page);
        } elseif ($this->page()) {
            $this->view->paginator->setCurrentPageNumber($this->page());
        }

        $this->view->paginator->setItemCountPerPage($itemsPerPage);
        $this->view->paginator->startItemNo = (($page - 1) * $itemsPerPage) + 1;

        return $this->view->paginator;
    }


    /**
     * Sets file headers & data
     *
     * @param string $name
     * @param string $type
     * @param FileModel|mixed $file
     * @return Response
     */
    public function setFile($name, $type, $file)
    {
        $this->noRender();

        $this->response
            ->setHeader('Pragma', 'public')
            ->setHeader('Cache-Control', 'private, must-revalidate, post-check=0, pre-check=0')
            ->setHeader('Content-Type', $type ?: 'application/octet-stream')
            ->setHeader('Content-Disposition', 'attachment; filename="' . $name . '"')
            ->setHeader('Content-Transfer-Encoding', 'binary')
        ;

        if ($file) {
            if ($file instanceof FileModel) {
                $this->response->setContent($file->getContents());
            } else {
                $this->response->setContent($file);
            }
        }
    }


    /**
     * Checks if module is enabled
     * @param string $module
     * @return bool
     */
    public function checkModuleEnabled($module = null)
    {
        if ($module == null) {
            $module = Output::toPascalCase(ucfirst($this->_moduleName));
        }

        if (!App::hasModule($module)) {
            if (App::isSuper()) {
                $this->error('Module unavailable');
            }
            $this->back();

            return false;
        }

        return true;
    }


    /**
     * Registers extra public action
     *
     * @param string $module
     * @param string $controller
     * @param string $action
     * @param string $accessType
     */
    private static function _declareActionAccess($module, $controller, $action, $accessType)
    {
        if ($accessType == 'iframe') {
            $actionsArray = &static::$_frameAllowedActions;
        } else {
            $actionsArray = &static::$_publicActions;
        }

        if ($actionsArray[$module][$controller] != '*') {
            if ($action != '*') {
                if (!is_array($actionsArray[$module][$controller])) {
                    $actionsArray[$module][$controller] = [];
                }
                if (is_array($action)) {
                    $actionsArray[$module][$controller] = array_merge(
                        $actionsArray[$module][$controller],
                        $action
                    );
                } else {
                    $actionsArray[$module][$controller][] = $action;
                }
            } else {
                $actionsArray[$module][$controller] = $action;
            }
        }
    }


    /**
     * Checks if activation redirect is required
     * @return bool|void
     */
    private function _needActivationRedirect($ignoreActionVisibility = false)
    {
        $currentRequest    = array($this->_moduleName, $this->_controllerName, $this->_actionName);

        $activationRequest = array('user', 'index', 'activation');
        $logoutRequest     = array('application', 'login', 'logout');
        $langRequest       = array('application', 'index', 'switch-lang');
        $switchRequest     = array('user', 'index', 'switch');

        if ($this->isPublicAction() && !$ignoreActionVisibility) {
            return false;
        }

        if (App::$user->isLogged()) {
            if (App::$user['activated'] != 1 && !App::$session->ldapAuthorize) {
                if (App::isSuper(true)) {
                    return false;
                }

                if (
                    !in_array($currentRequest, array($activationRequest, $logoutRequest, $langRequest))
                    && !(App::isSuper(true) && $currentRequest == $switchRequest)
                ) {
                    return true;
                }
            }
        }
    }


    /**
     * Checks if action is public
     * @return bool
     */
    public function isPublicAction()
    {
        return $this->checkActionAccess(Request::ACCESS_PUBLIC);
    }


    /**
     * Checks if last invoked action can be extecuted within an iframe
     * @return bool
     */
    public function isFrameAllowedAction()
    {
        return $this->checkActionAccess(Request::ACCESS_IFRAME);
    }

    /**
     * Checks action access type (public/iframe)
     *
     * @param string $accessType
     * @return bool
     * @throws InvalidArgumentException
     */
    public function checkActionAccess($accessType): bool
    {
        $module     = $this->_moduleName;
        $controller = $this->_controllerName;
        $action     = $this->_actionName;

        if ($accessType == Request::ACCESS_PUBLIC) {
            $actionsArray = static::$_publicActions;
        } elseif ($accessType == Request::ACCESS_IFRAME) {
            $actionsArray = static::$_frameAllowedActions;
        } else {
            throw new InvalidArgumentException('Invalid access type: ' . $accessType);
        }

        if (array_key_exists($module, $actionsArray)) {
            if (array_key_exists($controller, $actionsArray[$module])) {
                if (
                    (is_array($actionsArray[$module][$controller]) && in_array($action, $actionsArray[$module][$controller]))
                    || '*' == $actionsArray[$module][$controller]
                ) {
                    return true;
                }
            }
        }

        return false;
    }


    /**
     * Checks role restrictions
     * @return bool
     * @throws Exception
     */
    protected function _checkRestrictions()
    {
        if ($_SERVER['CLI_MODE']) {
            return true;
        } else {
            $restrictionsConfig = new IniConfig(ROOT_PATH . 'config' . DIRECTORY_SEPARATOR . 'role-restrictions.ini');

            $allowedRequests = array();

            foreach (App::$session->userRoles as $roleAcro => $role) {
                if ($restrictionsConfig->{strtolower($roleAcro)}) {
                    foreach ($restrictionsConfig->{strtolower($roleAcro)}->restrictions as $allowedRequest) {
                        list($module, $controller, $action) = explode(',', $allowedRequest);

                        if ($action == '*') {
                            $allowedRequests[$module][$controller] = $action;
                        } else {
                            $allowedRequests[$module][$controller][] = $action;
                        }
                    }
                }
            }

            $module     = $this->_moduleName;
            $controller = $this->_controllerName;
            $action     = $this->_actionName;

            if (count($allowedRequests)) {
                if (array_key_exists($module, $allowedRequests)) {
                    if (array_key_exists($controller, $allowedRequests[$module])) {
                        if (
                            (is_array($allowedRequests[$module][$controller]) && in_array($action, $allowedRequests[$module][$controller]))
                            || '*' == $allowedRequests[$module][$controller]
                        ) {
                            return true;
                        }
                    }
                }
                return false;
            } else {
                return true;
            }
        }
    }


    /**
     * Function add note identifier to session
     * @param string $noteIdentifier
     * @return bool
     */
    public function cleanCachedNote($noteIdentifier)
    {
        if (!is_array(App::$session->noteCache)) {
            App::$session->noteCache = array();
        }
        App::$session->noteCache[] = 'cached-note-' . $noteIdentifier;

        return true;
    }


    /**
     * @param array $rawContent
     * @return \Phalcon\Http\Response
     *
     * @deprecated use Response::setJsonContent() instead
     */
    protected function sendJsonResponse($rawContent)
    {
        return $this->response
            ->setContentType('application/json')
            ->setContent(Output::jsonEncode($rawContent))
        ;
    }

    /**
     * @param string $path
     * @param bool $useCache
     * @param string $filesystemType
     * @param string $expireAfter
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @throws ReflectionException
     */
    protected function setCachedFileResponse(string $path, bool $useCache = true, string $filesystemType = FilesystemFactory::TYPE_UPLOAD, string $expireAfter = '1 year'): void
    {
        /** @var CachedFileResponseBuilder $builder */
        $builder = App::$di->get('response.cachedFileResponseBuilder', [
            'filesystem' => $filesystemType,
        ]);

        $ifModifiedSince = $this->request->getHeader('If-Modified-Since') ?: null;

        $this->response = $builder->getResponse($path, $useCache, $ifModifiedSince, $expireAfter);
    }

    protected function getFrontendModuleName(): string
    {
        return '';
    }

    protected function skipFrontendRedirect(): void
    {
        $this->skipFrontendRedirect = true;
    }

    /**
     * Redirects to new frontend if module is switched and the view is not rendered inside a tab or a widget
     * Returns true if redirect is happening (to short circuit the given action)
     * Use $this->skipFrontendRedirect() in a child controller init to forcibly skip the redirect (e.g. for xls downloads)
     */
    private function redirectToNewFrontend(): bool
    {
        if ($this->isAjax(true) || $this->skipFrontendRedirect) {
            return false;
        }

        $router = $this->router;

        if (!$router instanceof Router) {
            return false;
        }

        if (
            $router->isModuleSwitched(Output::toPascalCase($this->_moduleName))
            || $router->isFrontendModuleSwitched($this->getFrontendModuleName())
        ) {
            $routeConverter = new FrontendRouteUrlConverter(
                router: $this->router,
                filter: $this->_filter,
                dispatcher: $this->dispatcher,
                referer: $_SERVER['REQUEST_URI']
            );

            $url = $routeConverter->getRedirectUrl();

            if ($url) {
                $this->_redirect($url);
                return true;
            }
        }

        return false;
    }
}
