<?php

namespace Velis\Mvc\Controller;

use Exception;
use Phalcon\Events\Event;
use Phalcon\Http\Message\ResponseStatusCodeInterface;
use Phalcon\Http\Request;
use Phalcon\Http\Response;
use Phalcon\Http\ResponseInterface;
use Phalcon\Mvc\Controller;
use Phalcon\Mvc\Dispatcher;
use ReflectionException;
use Throwable;
use Velis\Api\Client as ApiClient;
use Velis\Api\HttpExceptionHandler\ExceptionHandler;
use Velis\Api\HttpExceptionHandler\HttpException;
use Velis\Api\Version;
use Velis\App;
use Velis\App\PhalconVersion;
use Velis\Dto\Exceptions\ValidationException;
use Velis\Filesystem\FilesystemFactory;
use Velis\Filter;
use Velis\Http\Response\CachedFileResponseBuilder;
use Velis\Lang;
use Velis\Mvc\Controller\RestRequestHandler\AuthenticationMiddleware;
use Velis\Mvc\Controller\Utils\DtoRequestTransformer;
use Velis\Timezone;

/**
 * Base for Rest Api controllers
 *
 * @author Michał Nosek <michal.nosek@velis.pl>
 * @author Olek Procki <olo@velis.pl>
 * @author Szymon Janaczek <szymon.janaczek@velistech.com>
 */
abstract class RestfulController extends Controller
{
    /**
     * Request input params
     * @var Filter
     */
    protected $_filter;


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


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


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


    /**
     * Privs for each controller action
     * @var array
     */
    private $_requiredPrivs = array();


    /**
     * @var bool
     */
    private $_initialized = false;


    /**
     * Override this if needed
     */
    public function init(): bool
    {
        $version = $this->dispatcher->getParam('version');
        if ($version) {
            if (Version::validate($version)) {
                $this->response->setHeader('X-Api-Version', $version);
            } else {
                $this->response->setStatusCode(404, 'Unsupported API version');
                return false;
            }
        }
        $this->dispatcher
            ->getEventsManager()
            ?->attach('dispatch:beforeException', [$this, 'beforeException']);

        return true;
    }


    /**
     * Prepare controller startup
     * @return bool
     */
    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'];

        $this->view->disable();

        $this->_initialized = true;

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

        return true;
    }


    /**
     * 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();
        }

        if (!$this->_initialized) {
            $this->_requiredPrivs[] = array($module, $priv);
        } elseif (!App::$user->hasPriv($module, $priv)) {
            throw new AccessException(Lang::get('GENERAL_PERM_DENIED'));
        }

        return $this;
    }


    /**
     * This is executed before every found action
     * @return bool
     * @throws Exception
     */
    final public function beforeExecuteRoute()
    {
        if (!App::hasModule('Api')) {
            $this->deny();
            return false;
        }

        if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
            return true;
        }

        $acceptable = array_map(fn ($format) => $format['accept'], $this->request->getAcceptableContent());
        if (!in_array('application/json', $acceptable) && !in_array('*/*', $acceptable)) {
            $this->unsupportedMediaType();
            return false;
        }

        if ($this->request->getHeader("lang")) {
            Lang::switchLanguage($this->request->getHeader("lang"));
        }

        Timezone::setTimezone();

        // check access

        if (
            !$this->request->isAuthorization()
            && !App::$user->isLogged()
            && !App::$user->tokenLogin($this->getRequest())
            && $_SERVER['REQUEST_METHOD'] != 'OPTIONS'
        ) {
            $authenticationMiddleware = new AuthenticationMiddleware();
            $authenticationMiddleware->handle($this->getRequest());
        }

        $apiClient = new ApiClient($this->request);
        if (!$apiClient->checkMobileVersion()) {
            $this->error('Upgrade Required', 426);
            return false;
        }

        foreach ($this->_requiredPrivs as $priv) {
            if (!App::$user->hasPriv($priv)) {
                $this->unauthorized();
                return false;
            }
        }

        if (!$this->init()) {
            $this->deny();
            return false;
        }

        if (App::getPhalconMajorVersion() >= PhalconVersion::PHALCON5) {
            $requestTransformer = new DtoRequestTransformer($this->getRequest());
            try {
                $requestTransformer->transformRequest($this->dispatcher);
            } catch (ValidationException $e) {
                $this->error($e->getMessage(), 400, $e->getErrors());
                return false;
            };
        } else {
            $actionName = $this->dispatcher->getActionName();
            if (in_array($actionName, ['get', 'delete', 'update', 'patch', 'put'])) {
                $params = $this->dispatcher->getParams();
                $this->dispatcher->setParams(array_filter($params));

                $id = $params['id'];
                call_user_func([$this, $actionName . 'Action'], $id);
                return false;
            }
        }
    }

    public function beforeException(Event $event, Dispatcher $dispatcher, Throwable $exception): bool
    {
        $this->handleException($exception);

        return false;
    }


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


    /**
     * Returns authorization error message
     * @param string|null $message
     * @return Response|ResponseInterface
     */
    public function unauthorized($message = null)
    {
        return $this->error(Lang::get('USER_AUTHORIZATION_FAILED') . ($message ? ' (' . $message . ')' : ''), 401);
    }


    /**
     * Returns authorization error message
     * @return Response|ResponseInterface
     */
    public function tokenExpired()
    {
        return $this->error('Authorization token expired', 406);
    }


    /**
     * Returns access denied error message
     * @param string|null $msg
     * @return Response|ResponseInterface
     */
    public function deny($msg = null)
    {
        return $this->error($msg ?: 'Access denied', 403);
    }


    /**
     * Returns method not implemented error message
     * @return Response|ResponseInterface
     */
    public function methodNotAllowed()
    {
        return $this->error('Method not allowed', 405);
    }


    /**
     * Returns resource "not found" error message
     */
    public function notFoundAction(?string $message = null)
    {
        $message ??= 'Resource not found';

        $this->error($message, ResponseStatusCodeInterface::STATUS_NOT_FOUND);
    }


    /**
     * Returns unsupported expected response format
     * @return Response|ResponseInterface
     */
    public function unsupportedMediaType()
    {
        return $this->error('Only json mode available', 415);
    }

    /**
     * Returns json formatted error message
     */
    public function error(
        ?string $msg = null,
        $code = ResponseStatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR,
        ?array $details = null
    ): void {
        /**
         * We do not want this exception to be caught in the controller - we want to handle it in the ExceptionHandler.
         * So we do not raise it to the higher logic.
         * @noinspection PhpUnhandledExceptionInspection
         */
        throw new HttpException(message: $msg, details: $details, httpCode: $code);
    }


    /**
     * Exceptions handling function
     */
    public function handleException(Throwable $exception): void
    {
        $response = $this->getResponse();
        $handler = ExceptionHandler::parse($exception);

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


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


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


    /**
     * Returns objects list - empty by default
     */
    public function listAction()
    {
        return $this->methodNotAllowed();
    }


    /**
     * Retrieves object by id - empty result by default
     * @param string $id
     */
    public function getAction($id)
    {
        return $this->methodNotAllowed();
    }


    /**
     * Deleting disabled by default - can be overridden
     * @param int $id
     * @return Response|ResponseInterface
     */
    public function deleteAction($id)
    {
        return $this->methodNotAllowed();
    }


    /**
     * Updating disabled by default - can be overridden
     * @param string $id
     * @return Response|ResponseInterface
     */
    public function putAction($id)
    {
        return $this->methodNotAllowed();
    }

    public function patchAction($id)
    {
        return $this->methodNotAllowed();
    }

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

    /**
     * @param string $path
     * @param bool $useCache
     * @param string $filesystemType
     * @param string $expireAfter
     * @return void
     * @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);
    }
}
