<?php

namespace Plainwork\Http;

use Plainwork\Core\App;
use Plainwork\Http\RequestFactory;
use Plainwork\Http\Request;
use Plainwork\Http\Redirect;
use Plainwork\Http\Response;
use Plainwork\Http\Router;
use Plainwork\Http\Uri;

class Server
{
    public $app = App::class;
    public $router = Router::class;
    public $requestFactory = RequestFactory::class;
    public $uri = Uri::class;
    private string $prefix = '';
    private array $prepareResponseFuncs = [];

    public function addPrepareResponseFunc(callable $func): self
    {
        $this->prepareResponseFuncs[] = $func;

        return $this;
    }

    public function setPrefix(string $prefix): self
    {
        $this->prefix = $prefix;

        return $this;
    }

    public function getPrefix(): string
    {
        return $this->prefix;
    }

    public function run(string $rootPath = '')
    {
        $this->uri->setParamPrefix($this->prefix);

        $request = $this->requestFactory->fromGlobals($this->prefix, $rootPath);
        $ret = $this->handle($request);
        $this->app->end();
        $response = $this->prepareResponse($ret, $request);

        return $this->emitResponse($response);
    }

// lightweight check, testing only protect middlewares
    public function can(Request $request): bool
    {
        $ret = true;

        $path = $request->getPath();

        list($route, $routeParams) = $this->router->parsePath($path);
        if (null === $route) {
            return false;
        }

        $request = $request->withRoutePattern($route);
        foreach ($routeParams as $k => $v) {
            $request = $request->withRouteParam($k, $v);
        }

        $chain = $this->router->find($route, 'perm');
        if (!$chain) {
            return $ret;
        }

// echo "FOR ROUTE '$route'<br>";
// _print_r($chain);

        foreach ($chain as $checker) {
        // just a bool provided
            if (is_bool($checker)) {
                $thisRet = $checker;
            } else {
                $checker = $this->factory($checker);
                $thisRet = $checker($request);
            }
            if (is_bool($thisRet)) {
                $ret = $thisRet;
                break;
            }
        }

        return $ret;
    }

    public function handle(Request $request)
    {
        $path = $request->getPath();
        $method = $request->getRequestMethod();

        list($route, $routeParams) = $this->router->parsePath($path);
        if (null === $route) {
            return null;
        }

        $request = $request->withRoutePattern($route);
        foreach ($routeParams as $k => $v) {
            $request = $request->withRouteParam($k, $v);
        }

        // $chain = $this->router->getHandlerChain($route, $method);
        $chain = $this->router->find($route, strtolower($method));
// _print_r($chain);
// exit;

        if (!$chain) {
            return null;
        }

        $handler = $this->buildHandler($chain, $request);
        $ret = $handler($request);

        return $ret;
    }

// build handler as a middleware queue
    private function buildHandler(array $chain, Request $request): callable
    {
        $checkKeys = [strtolower($request->getRequestMethod()), 'all'];

        $func = array_shift($chain);
    // array with details, not just controller
        if (is_array($func) && !isset($func[0])) {
            foreach ($checkKeys as $k) {
                if (isset($func[$k])) {
                    $func = $func[$k];
                    break;
                }
            }
        }
        $func = $this->factory($func);

        foreach ($chain as $decorator) {
        // array with details, not just controller
            if (is_array($decorator) && !isset($decorator[0])) {
                foreach ($checkKeys as $k) {
                    if (isset($decorator[$k])) {
                        $decorator = $decorator[$k];
                        break;
                    }
                }
            }

            $decorator = $this->factory($decorator);
            $func = function (Request $request) use ($decorator, $func) {
                return $decorator($request, $func);
            };
        }

        return $func;
    }

    public function prepareResponse($ret, Request $request): Response
    {
        $func = [$this, 'doPrepareResponse'];
        foreach ($this->prepareResponseFuncs as $decorator) {
            $func = fn($ret, Request $request) => $decorator($ret, $request, $func);
        }

        return $func($ret, $request);
    }

    public function doPrepareResponse($ret, Request $request): Response
    {
        if (false === $ret) {
            $resp = new Response(403);
            $resp = $resp
                ->withBody('__This action is not allowed.__')
                ->withHeader('content-type', 'text/html')
                ;

            return $resp;
        }

        if ($ret instanceof Response) {
            $body = $ret->getBody();
            if (is_array($body)) {
                $body = json_encode($body);
                $ret = $resp
                    ->withBody($ret)
                    ->withHeader('content-type', 'application/json')
                    ;
            }

            return $ret;
        }

        if ($ret instanceof Dispatch) {
            $requestedPath = $request->getRequestedPath();
            $to = $ret->getPath();
            $p = $ret->getParams();

            $to = $request->resolvePath($to);
            $request = $this->requestFactory->fromOrParent($request, $to, $p);
            // $request = $request->withRequestedPath($to);
            $request = $request->withRequestedPath($requestedPath);
            $ret = $this->handle($request);

            return $this->prepareResponse($ret, $request);
        }

        if ($ret instanceof Redirect) {
            $path = $ret->getPath();
            $p = $ret->getParams();

            $request_ = $this->requestFactory->fromOrParent($request, $path, $p);
            $path = $request_->getPath();
            $p = $request_->getQueryParams();

            $to = $this->uri->to($path, $p);

            $resp = new Response(302);
            $resp = $resp
                ->withHeader('Location', $to)
                ;

            return $resp;
        }

        if (is_string($ret)) {
            $resp = new Response(200);
            $resp = $resp
                ->withBody($ret)
                ->withHeader('content-type', 'text/html')
                ;

            return $resp;
        }

        if (is_array($ret)) {
            $ret = json_encode($ret);
            $resp = new Response(200);
            $resp = $resp
                ->withBody($ret)
                ->withHeader('content-type', 'application/json')
                ;

            return $resp;
        }

        $resp = new Response(404);
        $resp = $resp
            ->withBody('__Not found__')
            ->withHeader('content-type', 'text/html')
            ;

        return $resp;
    }

    public function emitResponse(Response $response, int $bufferLength = 0)
    {
        $isRedirect = false;

    // headers
        if (!headers_sent()) {
            foreach ($response->getHeaders() as $name => $values) {
                if ('Location' == $name) {
                    $isRedirect = true;
                }
                $name = str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', (string) $name))));
                $firstReplace = !('Set-Cookie' === $name);
                foreach ($values as $value) {
                    header("$name: $value", $firstReplace);
// echo "$name: $value<br>";
                    $firstReplace = false;
                }
            }

        // status line
            $statusCode = (int) $response->getStatusCode();
            // $reasonPhrase = trim((string) $response->getReasonPhrase());
            $protocolVersion = trim((string) $response->getProtocolVersion());

            // $status = $statusCode . (!$reasonPhrase ? '' : " $reasonPhrase");
            $status = $statusCode;
            header("HTTP/$protocolVersion $status", true, $statusCode);
// echo "HTTP/$protocolVersion $status<br>";
// exit;
        }

        if ($isRedirect) {
            exit;
        }

        if (!$bufferLength) {
            $body = $response->getBody();
            // return ((null !== $body) && strlen($body)) ? $body : $statusCode;
            return $body;
        }

        flush();
        $body = $response->getBody();
        $range = $this->parseContentRange($response->getHeaderLine('Content-Range'));

        if ((null !== $range) && isset($range['unit']) && ('bytes' === $range['unit'])) {
            $this->emitBodyRange($body, $range['first'], $range['last']);
            return;
        }

        if ($body->isSeekable()) {
            $body->rewind();
        }

        while (!$body->eof()) {
            echo $body->read($this->bufferLength);
        }
    }

    private function factory($func)
    {
        return $this->app->makeFunc($func);
    }
}
