<?php

namespace Velis\Mvc\Controller;

use Exception;
use InvalidArgumentException;
use Phalcon\Http\Request;
use Phalcon\Http\Response;
use Phalcon\Mvc\Controller;
use ReflectionException;
use Throwable;
use Velis\Api\HttpExceptionHandler\ExceptionHandler;
use Velis\Api\HttpExceptionHandler\HttpException;
use Velis\App;
use Velis\App\User as AppUser;
use Velis\Exception\TokenExpiredException;
use Velis\Exception\UnauthorizedException;
use Velis\Filesystem\FilesystemFactory;
use Velis\Filter;
use Velis\Http\Response\CachedFileResponseBuilder;
use Velis\Lang;
use Velis\Mvc\Controller\Listeners\Concrete\AbstractListener;
use Velis\Mvc\Controller\Listeners\Concrete\Listeners;
use Velis\Mvc\Controller\Utils\RouteClassifier;
use Velis\Mvc\Paginator;
use Velis\User\TrackerEntry;

abstract class AbstractRestController extends Controller
{
    use ErrorHelpersTrait;
    use ListPresentationHelpersTrait;

    protected Paginator $paginator;

    protected Filter $params;

    /**
     * @deprecated Use `$this->params` instead.
     */
    protected Filter $_filter;

    protected bool $initialized = false;

    protected AppUser $appUser;

    /**
     * Privs for each controller action
     * @var array<array<string, string>>
     * todo: rename to $requiredPrivs (remove underscore)
     */
    private array $_requiredPrivs = [];

    /**
     * Define middlewares just for this controller.
     * @return array<class-string<AbstractListener>>
     */
    protected function getListeners(): array
    {
        return [];
    }

    /**
     * Define listener groups for this controller.
     * @return array<class-string<AbstractListener>>
     */
    protected function getListenerGroups(): array
    {
        return [];
    }

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

    public function onConstruct(): void
    {
        $this->registerExceptionHandler();

        $this->initTrackerEntry();
        $this->registerNameProperties();
        $this->registerParams();
        $this->initThrottling();
        $this->disableView();

        $this->initialized = true;

        $this->boot();
        $this->registerPaginator();
        $this->setAppUser();
    }

    protected function setAppUser(): void
    {
        $this->appUser = App::$user ?? throw new \RuntimeException('App user not set');
    }

    /**
     * Initializing tracker entry for the controller.
     */
    protected function initTrackerEntry(): void
    {
        TrackerEntry::logErrorResponse(true);
    }

    protected function registerNameProperties(): void
    {
        // todo: I'm not a big fan of it :/
        // we should use $this->dispatcher instead.
        // Refactor it one day.
        // We cane remove it, when we remove manual ValidatorFactory from `init()` in Api module.
        // This initialization is performed in ValidateFormParamsListener.
        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'];
    }

    /**
     * Get and filter request params (request body and GET params).
     */
    protected function registerParams(): void
    {
        $this->params = App::$registry['filter'];
        $queryParams = new Filter($_GET);
        unset($queryParams['_url']);

        $this->params = new Filter(
            array_merge(
                $this->params->getRawCopy(),
                $queryParams->parseArrayParameters(),
                $this->request->getJsonRawBody(true) ?: [],
                $this->dispatcher->getParams(),
            ),
            App::$config->settings->autoFiltering
        );

        // todo: pls remove it one day
        // It was not removed yet because of the backward compatibility (Flutter API)
        $this->_filter = $this->params;
    }

    /**
     * Initializing request throttling.
     */
    protected function initThrottling(): void
    {
        if (App::$config->requestThrottling) {
            App::initThrottling();
        }
    }

    /**
     * Disabling Phalcon view - we want pure application/json response.
     */
    protected function disableView(): void
    {
        $this->view->disable();
    }

    /**
     * Register exception handler for the controller.
     */
    protected function registerExceptionHandler(): void
    {
        // maybe we won't need it in the future
    }

    /**
     * IMPORTANT: Do not use this method directly.
     * Exceptions should be handled by global Exception Handler.
     * It can be overridden in the controller to handle exceptions in a different way or use another Exception Map.
     */
    protected function handleException(Throwable $exception): void
    {
        $handler = ExceptionHandler::parse($exception);

        $this->response->setStatusCode($handler->getHttpException()->getHttpCode());
        $this->response->setContent($handler->toJson());
    }

    /**
     * @throws AccessException
     * @throws HttpException
     * @throws TokenExpiredException
     * @throws UnauthorizedException
     * @throws ReflectionException
     */
    protected function boot(): void
    {
        // If it's an OPTIONS request, we don't need to check the auth.
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            return;
        }

        // If it's an authorization or public request, we don't need to check the auth.
        if (
            $this->getRequest()->isAuthorization()
            || RouteClassifier::isPublic($this->dispatcher->getControllerClass(), $this->dispatcher->getActionName())
        ) {
            $this->handlePublicRoute();
        } else {
            $this->handleProtectedRoute();
        }

        if (!$this->init()) {
            throw new AccessException();
        }
    }

    /**
     * Initialize protected action logic.
     * @throws HttpException
     * @throws TokenExpiredException
     * @throws UnauthorizedException
     */
    private function handleProtectedRoute(): void
    {
        $listenerGroups = $this->getListenerGroups();

        // Authenticate request and fetch information about the auth method.
        $authChecker = new AuthenticationChecker($this->dispatcher);
        $authChecker->handle();
        $authMethod = $authChecker->getAuthMethod();

        // Register common listeners and these valid for the used auth method.
        $this->registerListeners([$authMethod] + $listenerGroups);

        $this->checkControllerPrivs();
    }

    // Initialize public action logic.
    private function handlePublicRoute(): void
    {
        $this->registerListeners(['public'] + $this->getListenerGroups());
    }

    /**
     * Register listeners for the controller.
     * Get common listeners and listeners from the passed group names and initialize them.
     * @param array<string> $listenerGroupNames
     */
    protected function registerListeners(array $listenerGroupNames): void
    {
        $eventsManager = $this->dispatcher->getEventsManager();
        $eventsManager?->enablePriorities(true);

        $listeners = array_merge_recursive(Listeners::$commonListeners, $this->getListeners());

        foreach ($listenerGroupNames as $listenerGroupName) {
            $listeners = array_merge_recursive($listeners, Listeners::$listenerGroups[$listenerGroupName]);
        }

        foreach ($listeners as $event => $listenerClasses) {
            $priority = 100;

            /**
             * @var int|class-string<AbstractListener> $key
             * @var class-string<AbstractListener>|array<string, mixed> $value
             */
            foreach ($listenerClasses as $key => $value) {
                if (is_string($value)) {
                    $listenerClass = $value;
                    $config = [];
                } else {
                    $listenerClass = $key;
                    $config = $value;
                }

                $eventsManager?->attach(
                    $event,
                    function (...$args) use ($config, $listenerClass): void {
                        (new $listenerClass(...$args))
                            ->setRequestParams($this->params)
                            ->setConfig($config)
                            ->handle();
                    },
                    $priority,
                );

                $priority--;
            }
        }
    }

    protected function registerPaginator(): void
    {
        $this->paginator = Paginator::fromParams($this->params);
    }

    /**
     * Check permissions defined for the controller.
     * @throws HttpException
     */
    protected function checkControllerPrivs(): void
    {
        foreach ($this->_requiredPrivs as $priv) {
            if (!App::$user->hasPriv($priv)) {
                throw new HttpException(Lang::get('GENERAL_PERM_DENIED'), httpCode: 403);
            }
        }
    }

    /**
     * Check permissions for the action.
     * @throws AccessException
     */
    protected function checkPriv(): self
    {
        if (func_num_args() == 1) {
            // priv passed as array (module & priv pair)
            if (is_array(func_get_arg(0))) {
                [$module, $priv] = func_get_arg(0);

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

        if (!isset($module) || !isset($priv)) {
            throw new InvalidArgumentException('Module and priv must be set');
        }

        if (!$this->initialized) {
            $this->_requiredPrivs[] = [$module, $priv];
            return $this;
        }

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

        return $this;
    }

    /**
     * @param array<array{string, string}> $privs
     * @throws AccessException
     */
    protected function checkAnyPriv(array $privs): void
    {
        App::$user->checkAnyPriv($privs);
    }

    protected function getRequest(): Request
    {
        return $this->request;
    }

    protected function getResponse(): Response
    {
        return $this->response;
    }

    /**
     * Handle options request
     */
    public function optionsAction(): Response
    {
        return $this->getResponse();
    }

    /**
     * @throws InvalidArgumentException
     * @throws ReflectionException
     * @throws Exception
     */
    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);
    }
}
