<?php namespace SokoWeb\Response;

use ErrorException;
use Exception;
use SokoWeb\Interfaces\Response\IRedirect;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
use SokoWeb\Interfaces\Authorization\ISecured;
use SokoWeb\Interfaces\Routing\IRouteCollection;
use SokoWeb\Interfaces\Database\IConnection;
use SokoWeb\Interfaces\Request\IRequest;
use SokoWeb\Response\Redirect;
use SokoWeb\Response\HtmlContent;
use SokoWeb\Response\JsonContent;

class HttpResponse
{
    private IRequest $request;

    private IConnection $dbConnection;

    private IRouteCollection $routeCollection;

    private array $appConfig;

    private string $method;

    private string $rawUrl;

    private array $parsedUrl;

    public function __construct(
        IRequest $request,
        IConnection $dbConnection,
        IRouteCollection $routeCollection,
        array $appConfig,
        string $requestMethod,
        string $requestUrl
    ) {
        set_error_handler([$this, 'exceptionsErrorHandler']);

        $this->request = $request;
        $this->dbConnection = $dbConnection;
        $this->routeCollection = $routeCollection;
        $this->appConfig = $appConfig;
        $this->method = strtolower($requestMethod);
        $this->parsedUrl = parse_url($requestUrl);
        $this->rawUrl = $requestUrl;
    }

    public function exceptionsErrorHandler($severity, $message, $filename, $lineno)
    {
        throw new ErrorException($message, 0, $severity, $filename, $lineno);
    }

    public function render(): void
    {
        $this->handleCors();
        if ($this->method === 'options') {
            return;
        }

        $match = $this->routeCollection->match($this->method, $this->parsedUrl['path']);
        if ($match === null) {
            $this->render404();
            return;
        }

        list($route, $params) = $match;
        $this->request->setParsedRouteParams($params);
        $handler = $route->getHandler();
        $controller = new $handler[0]();

        if (
            $controller instanceof IAuthenticationRequired &&
            $controller->isAuthenticationRequired() &&
            $this->request->user() === null
        ) {
            $this->redirectToLogin();
            return;
        }

        if (
            $this->method === 'post' &&
            !in_array($this->parsedUrl['path'], $this->appConfig['antiCsrfTokenExceptions']) &&
            $this->request->post($this->appConfig['antiCsrfTokenName']) !== $this->request->session()->get($this->appConfig['antiCsrfTokenName'])
        ) {
            $this->renderAntiCsrfError();
            return;
        }

        if ($controller instanceof ISecured && !$controller->authorize()) {
            $this->render404();
            return;
        }

        $this->dbConnection->startTransaction();
        try {
            $response = call_user_func([$controller, $handler[1]]);
        } catch (Exception $exception) {
            $this->dbConnection->rollback();
            error_log($exception);
            $this->render500($exception);
            return;
        }
        $this->dbConnection->commit();

        if ($response instanceof IContent) {
            header('Content-Type: ' . $response->getContentType() . '; charset=UTF-8');
            $response->render();
        } elseif ($response instanceof IRedirect) {
            header('Location: ' . $this->getRedirectUrl($response), true, $response->getHttpCode());
        } else {
            $this->render404();
        }
    }

    private function handleCors(): void
    {
        $origin = $this->request->header('Origin');
        if (!$origin) {
            return;
        }

        if (isset($this->appConfig['cors']['allow_origins'])) {
            if (in_array($origin, $this->appConfig['cors']['allow_origins']) || in_array('*', $this->appConfig['cors']['allow_origins'])) {
                header("Access-Control-Allow-Origin: {$origin}");
            }
        }

        if (!empty($this->appConfig['cors']['allow_credentials'])) {
            header('Access-Control-Allow-Credentials: true');
        }

        if ($this->method !== 'options') {
            return;
        }

        if (isset($this->appConfig['cors']['allow_headers'])) {
            $headers = explode(',', $this->request->header('Access-Control-Request-Headers'));
            if (in_array('*', $this->appConfig['cors']['allow_headers'])) {
                $allow_headers = $headers;
            } else {
                $allow_headers = array_intersect($this->appConfig['cors']['allow_headers'], $headers);
            }

            if (count($allow_headers) > 0) {
                header('Access-Control-Allow-Headers: ' . join(', ', $allow_headers));
            }
        }

        if (isset($this->appConfig['cors']['allow_methods'])) {
            if (in_array('*', $this->appConfig['cors']['allow_methods'])) {
                $allow_methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PATCH', 'POST', 'PUT'];
            } else {
                $allow_methods = $this->appConfig['cors']['allow_methods'];
            }

            if (count($allow_methods) > 0) {
                header('Access-Control-Allow-Methods: ' . join(', ', $allow_methods));
            }
        }

        $max_age = $this->appConfig['cors']['max_age'] ?? 600;
        header("Access-Control-Max-Age: {$max_age}");
    }

    private function redirectToLogin(): void
    {
        $this->request->session()->set('redirect_after_login', $this->rawUrl);
        $response = new Redirect(
            $this->routeCollection->getRoute($this->appConfig['loginRouteId'])
                ->generateLink(['redirect_after_login' => urlencode($this->rawUrl)]),
            IRedirect::TEMPORARY);
        header('Location: ' . $this->getRedirectUrl($response), true, $response->getHttpCode());
    }

    private function renderAntiCsrfError(): void
    {
        $content = new JsonContent($this->appConfig['antiCsrfTokenErrorResponse']);
        header('Content-Type: text/html; charset=UTF-8', true, 403);
        $content->render();
    }

    private function render404(): void
    {
        $content = new HtmlContent($this->appConfig['error404View']);
        header('Content-Type: text/html; charset=UTF-8', true, 404);
        $content->render();
    }

    private function render500(Exception $exception): void
    {
        if (empty($_ENV['DEV'])) {
            $exceptionToPrint = null;
        } else {
            $exceptionToPrint = (string)$exception;
        }

        $content = new HtmlContent($this->appConfig['error500View'], ['exceptionToPrint' => $exceptionToPrint]);
        header('Content-Type: text/html; charset=UTF-8', true, 500);
        $content->render();
    }

    private function getRedirectUrl(IRedirect $redirect): string
    {
        $url = $redirect->getTarget();
        if (preg_match('/^http(s)?/', $url) !== 1) {
            $url = $this->request->getBase() . $url;
        }
        return $url;
    }
}