<?php
namespace Newland\NeosCommon\Service;

use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\Node;
use Neos\Flow\Http\Request;
use Neos\Flow\Http\Response;
use Neos\Flow\Http\Uri;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Controller\Arguments;
use Neos\Flow\Mvc\Controller\ControllerContext;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Neos\Neos\Domain\Model\Domain;
use Neos\Neos\Domain\Model\Site;
use Neos\Neos\Domain\Service\ContentContext;

/**
 * @Flow\Scope("singleton")
 */
class ControllerContextFactory
{

    /**
     * Initializes a new fake & independent controller context.
     * The controller context that is being created mimics a controller context that is available
     * during a HTTP Request but can also be built during CLI commands.
     *
     * NOTE: During the initialization the global environment variables are changed since neos internals
     *       assume that these are set. If you create multiple contexts be sure that you create them immediately
     *       before using them instead of creating them in bulk.
     *
     * @example
     * // Correct: Initialize as you need
     * foreach ($nodes as $node) {
     *      $this->articleCache->generateAndStoreInCache(
     *          $this->createQuery($node),
     *          $this->contextFactory->initializeFakeControllerContext($node)
     *      );
     * }
     *
     * @example
     * // Incorrect: Initialize before you need
     * $contexts = array_map(
     *      function($node) { return $this->contextFactory->initializeFakeControllerContext($node) },
     *      $nodes
     * );
     *
     * // ...
     *
     * @param NodeInterface $node
     * @param array $environment
     * @return ControllerContext
     */
    public function initializeFakeControllerContext(NodeInterface $node, array $environment = []): ControllerContext
    {
        $environment = $this->applyDefaultEnvironment($environment);
        $this->applyEnvironmentVariables($environment);

        $uriBuilder = $this->initializeFakeUriBuilder($node, $environment);
        return new ControllerContext(
            $uriBuilder->getRequest(),
            new Response(),
            new Arguments([]),
            $uriBuilder
        );
    }

    public function initializeFakeUriBuilder(NodeInterface $node, array $environment = []): UriBuilder
    {
        $uriBuilder = new UriBuilder();
        $uriBuilder->setRequest($this->initializeFakeActionRequest($node, $environment));
        return $uriBuilder;
    }

    public function initializeFakeActionRequest(NodeInterface $node, array $env = []): ActionRequest
    {
        $httpRequest = new Request([], [], [], $env);
        $httpRequest->setContent('');

        $baseUri = $this->primaryDomainUri($node);
        if ($baseUri !== null) {
            $httpRequest->setBaseUri($baseUri);
        }

        $request = new ActionRequest($httpRequest);
        $request->setArgument('__node', $node);

        return $request;
    }

    private function applyDefaultEnvironment(array $environment): array
    {
        if (!array_key_exists('FLOW_REWRITEURLS', $environment)) {
            $environment['FLOW_REWRITEURLS'] = 1;
        }
        if (!array_key_exists('SCRIPT_NAME', $environment)) {
            $environment['SCRIPT_NAME'] = '/';
        }
        return $environment;
    }


    private function primaryDomainUri(NodeInterface $node): ?Uri
    {
        $context = $node->getContext();
        if (!($context instanceof ContentContext)) {
            return null;
        }

        /** @var Site|null $site */
        $site = $context->getCurrentSite();
        if ($site === null) {
            return null;
        }

        /** @var Domain|null $domain */
        $domain = $site->getPrimaryDomain();
        if ($domain === null) {
            return null;
        }

        return $this->domainToUri($domain);
    }


    private function domainToUri(Domain $domain): Uri
    {
        $uri = new Uri('/');
        $uri = $uri->withHost($domain->getHostname());

        $scheme = $domain->getScheme();
        if (!$scheme) {
            $scheme = $this->getCurrentProtocol();
        }
        $uri = $uri->withScheme($scheme);

        $port = $domain->getPort();
        if ($port) {
            $uri = $uri->withPort($port);
        }

        return $uri;
    }

    private function applyEnvironmentVariables(array $environment): void
    {
        foreach ($environment as $name => $value) {
            putenv($name . '=' . $value);
        }
    }

    /**
     * Extract the protocol from the current request
     *
     * @return string
     */
    private function getCurrentProtocol(): string
    {
        return ($this->requestIsOverHttps()) ? 'https' : 'http';
    }

    private function requestIsOverHttps(): bool
    {
        if (!empty($_SERVER['HTTPS'])) {
            return $_SERVER['HTTPS'] !== 'off';
        }
        if (!empty($_SERVER['SERVER_PORT'])) {
            return ((int) $_SERVER['SERVER_PORT']) === 443;
        }
        return false;
    }
}
