<?php

namespace engine\http;

use engine\exceptions\InvalidDomainException;
use engine\exceptions\NotImplementedException;
use engine\http\errors\BaseRedirect;
use engine\http\errors\HTTPError;
use engine\http\errors\InvalidRequest;
use engine\http\errors\NotFound;
use engine\http\errors\NotImplemented;
use engine\Router;
use engine\skin\BaseSkin;
use engine\skin\ErrorSkin;

abstract class RequestHandler
{
    protected array $routerInput = [];
    public readonly BaseSkin $skin;

    public static final function resolveAndDispatch(bool $only_set_project = false): void {
        global $config, $globalContext;
        try {
            if ($_SERVER['HTTP_HOST'] !== $config['domain'] && !str_ends_with($_SERVER['HTTP_HOST'], '.'.$config['domain']))
                throw new InvalidDomainException('invalid domain '.$_SERVER['HTTP_HOST']);

            if (strlen($_SERVER['HTTP_HOST']) > ($orig_domain_len = strlen($config['domain']))) {
                $sub = substr($_SERVER['HTTP_HOST'], 0, -$orig_domain_len - 1);
                if (!array_key_exists($sub, $config['subdomains']))
                    throw new InvalidDomainException('invalid subdomain '.$sub);
                $globalContext->setProject($config['subdomains'][$sub]);
            } else {
                $globalContext->setProject($config['default_project']);
            }

            if ($only_set_project)
                return;

            if (!in_array($_SERVER['REQUEST_METHOD'], ['POST', 'GET']))
                throw new NotImplemented('Method '.$_SERVER['REQUEST_METHOD'].' not implemented');

            $uri = $_SERVER['REQUEST_URI'];
            if (($pos = strpos($uri, '?')) !== false)
                $uri = substr($uri, 0, $pos);

            $router = router::getInstance();
            $route = $router->find($uri);
            if ($route === null)
                throw new NotFound('Route not found');

            $route = preg_split('/ +/', $route);
            $handler_class = 'app\\'.$globalContext->project.'\\'.$route[0].'Handler';
            if (!class_exists($handler_class))
                throw new NotFound(isDev() ? 'Handler class "'.$handler_class.'" not found' : '');

            $action = $route[1];
            $input = [];
            if (count($route) > 2) {
                for ($i = 2; $i < count($route); $i++) {
                    $var = $route[$i];
                    list($k, $v) = explode('=', $var);
                    $input[trim($k)] = trim($v);
                }
            }

            $rh = new $handler_class();
            $globalContext->setRequestHandler($rh);
            $response = $rh->callAct($_SERVER['REQUEST_METHOD'], $action, $input);
        }

        catch (InvalidDomainException|NotImplementedException $e) {
            $stacktrace = \engine\logging\Util::getErrorFullStackTrace($e);
            logError($e, stacktrace: $stacktrace);
            self::renderError($e->getMessage(), HTTPCode::InternalServerError, $stacktrace);
        }

        catch (BaseRedirect $e) {
            if (isXHRRequest()) {
                $response = new AjaxOk(['redirect' => $e->getLocation()]);
            } else {
                header('Location: '.$e->getLocation(), $e->shouldReplace(), $e->getHTTPCode()->value);
                exit;
            }
        }

        catch (HTTPError $e) {
            if (isXHRRequest()) {
                $data = [];
                $message = $e->getDescription();
                if ($message != '')
                    $data['message'] = $message;
                $response = new AjaxError((object)$data, $e->getHTTPCode());
            } else {
                self::renderError($e->getMessage(), $e->getHTTPCode());
            }
        }

        catch (\Throwable $e) {
            $stacktrace = \engine\logging\Util::getErrorFullStackTrace($e);
            logError(get_class($e).': '.$e->getMessage(), stacktrace: $stacktrace);
            self::renderError(get_class($e).': '.$e->getMessage(), HTTPCode::InternalServerError, $stacktrace);
        }

        finally {
            if (!empty($response)) {
                if ($response instanceof Response) {
                    $response->send();
                } else {
                    logError(__METHOD__.': $response is not Response');
                }
            }
        }
    }

    protected static function renderError(string $message, HTTPCode $code, ?string $stacktrace = null): never {
        http_response_code($code->value);
        $skin = new ErrorSkin();
        switch ($code) {
            case HTTPCode::NotFound:
                $skin->renderNotFound();
            default:
                $show_debug_info = isDev() || isAdmin();
                $skin->renderError($code->getTitle(),
                    $show_debug_info ? $message : null,
                    $show_debug_info ? $stacktrace : null);
        }
    }

    protected function beforeDispatch(string $http_method, string $action) {}

    public function callAct(string $http_method, string $action, array $input = []) {
        $handler_method = $_SERVER['REQUEST_METHOD'].'_'.$action;
        if (!method_exists($this, $handler_method))
            throw new NotFound(static::class.'::'.$handler_method.' is not defined');

        if (!(new \ReflectionMethod($this, $handler_method)->isPublic()))
            throw new NotFound(static::class.'::'.$handler_method.' is not public');

        if (!empty($input))
            $this->routerInput += $input;

        $args = $this->beforeDispatch($http_method, $action);
        return call_user_func_array([$this, $handler_method], is_array($args) ? [$args] : []);
    }

    protected function getPage(int $per_page, ?int $count = null): array {
        list($page) = $this->input('i:page');
        $pages = $count !== null ? ceil($count / $per_page) : null;
        if ($pages !== null && $page > $pages)
            $page = $pages;
        if ($page < 1)
            $page = 1;
        $offset = $per_page * ($page - 1);
        return [$page, $pages, $offset];
    }

    public function input(string $input,
                          bool $trim = false): array
    {
        $input = preg_split('/,\s+?/', $input, -1, PREG_SPLIT_NO_EMPTY);
        $result = [];
        foreach ($input as $var) {
            $pos = strpos($var, ':');
            if ($pos === 1) {
                $type = InputVarType::from(substr($var, 0, $pos));
                $name = trim(substr($var, $pos + 1));
            } else {
                $type = InputVarType::STRING;
                $name = trim($var);
            }
            $val = null;
            if (isset($this->routerInput[$name]))
                $val = $this->routerInput[$name];
            else if (isset($_POST[$name]))
                $val = $_POST[$name];
            else if (isset($_GET[$name]))
                $val = $_GET[$name];
            if (is_array($val))
                $val = implode($val);
            $result[] = match ($type) {
                InputVarType::INTEGER => (int)$val,
                InputVarType::FLOAT => (float)$val,
                InputVarType::BOOLEAN => (bool)$val,
                default => $trim ? trim((string)$val) : (string)$val
            };
        }
        return $result;
    }

    public function getCSRF(string $key): string {
        global $config;
        $user_key = isAdmin() ? \app\Admin::getCSRFSalt() : $_SERVER['REMOTE_ADDR'];
        return substr(hash('sha256', $config['csrf_token'].$user_key.$key), 0, 20);
    }

    /**
     * @throws errors\Forbidden
     */
    protected function checkCSRF(string $key): void {
        if ($this->getCSRF($key) != ($_REQUEST['token'] ?? ''))
            throw new errors\Forbidden('invalid token');
    }

    /**
     * @throws InvalidRequest
     */
    protected function ensureIsXHR(): void {
        if (!isXHRRequest())
            throw new InvalidRequest();
    }
}
