<?php
declare(strict_types=1);

namespace PhpList\Core\Core;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Debug\Debug;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * This class bootstraps the phpList core system.
 *
 * Include it from the entry point and call Bootstrap::getInstance() to get an instance,
 * and $bootstrap->setEnvironment($environment) if you would like to run the application in
 * the development or testing environment. (For the production environment,
 * the setEnvironment call is not needed).
 *
 * After that, call $bootstrap->configure() and $bootstrap->dispatch().
 *
 * This class is the only "real" singleton in the system. All other singletons will use
 * Symfony dependency injection.
 *
 * @author Oliver Klee <oliver@phplist.com>
 */
class Bootstrap
{
    /**
     * @var Bootstrap|null
     */
    private static $instance = null;

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

    /**
     * @var string
     */
    private $environment = Environment::DEFAULT_ENVIRONMENT;

    /**
     * @var ApplicationKernel
     */
    private $applicationKernel = null;

    /**
     * @var ApplicationStructure
     */
    private $applicationStructure = null;

    /**
     * Protected constructor to avoid direct instantiation of this class.
     *
     * Please use getInstance instead.
     */
    protected function __construct()
    {
        $this->applicationStructure = new ApplicationStructure();
    }

    /**
     * Disable direct cloning of this object.
     */
    protected function __clone()
    {
    }

    /**
     * Returns 'this' as singleton.
     *
     * @return Bootstrap
     */
    public static function getInstance(): Bootstrap
    {
        if (self::$instance === null) {
            self::$instance = new static();
        }

        return self::$instance;
    }

    /**
     * Purges the singleton instance.
     *
     * Note: This method is intended to be used for tests only.
     *
     * @return void
     */
    public static function purgeInstance()
    {
        self::$instance = null;
    }

    /**
     * @param string $environment must be one of the Environment::* constants
     *
     * @return Bootstrap fluent interface
     *
     * @throws \UnexpectedValueException
     */
    public function setEnvironment(string $environment): Bootstrap
    {
        Environment::validateEnvironment($environment);
        $this->environment = $environment;

        return $this;
    }

    /**
     * @return string
     */
    public function getEnvironment(): string
    {
        return $this->environment;
    }

    /**
     * @return bool
     */
    private function isSymfonyDebugModeEnabled(): bool
    {
        return $this->environment !== Environment::PRODUCTION;
    }

    /**
     * @return bool
     */
    private function isDebugEnabled(): bool
    {
        return $this->environment !== Environment::PRODUCTION;
    }

    /**
     * Checks that the application is running on a local testing machine or via CLI, not on a production server.
     *
     * If a production server is detected, a 403 header is sent and execution is stopped.
     *
     * @SuppressWarnings("PHPMD.ExitExpression")
     * @SuppressWarnings("PHPMD.Superglobals")
     *
     * @return Bootstrap|null fluent interface
     */
    public function ensureDevelopmentOrTestingEnvironment()
    {
        $usesProxy = isset($_SERVER['HTTP_CLIENT_IP']) || isset($_SERVER['HTTP_X_FORWARDED_FOR']);
        $isOnCli = PHP_SAPI === 'cli' || PHP_SAPI === 'cli-server';
        $isLocalRequest = isset($_SERVER['REMOTE_ADDR'])
            && in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'], true);
        if ($usesProxy || (!$isOnCli && !$isLocalRequest)) {
            header('HTTP/1.0 403 Forbidden');
            exit('You are not allowed to access this file.');
        }

        return $this;
    }

    /**
     * Main entry point called at every request usually from global scope. Checks if everything is correct
     * and loads the configuration.
     *
     * @return Bootstrap fluent interface
     */
    public function configure(): Bootstrap
    {
        $this->isConfigured = true;

        return $this->configureDebugging()
            ->configureApplicationKernel();
    }

    /**
     * Makes sure that configure has been called before.
     *
     * @return void
     *
     * @throws \RuntimeException if configure has not been called before
     */
    private function assertConfigureHasBeenCalled()
    {
        if (!$this->isConfigured) {
            throw new \RuntimeException('Please call configure() first.', 1501170550);
        }
    }

    /**
     * Dispatches the current HTTP request (if there is any).
     *
     * @SuppressWarnings("PHPMD.StaticAccess")
     *
     * @return null
     *
     * @throws \RuntimeException if configure has not been called before
     * @throws \Exception
     */
    public function dispatch()
    {
        $this->assertConfigureHasBeenCalled();

        if(strpos($_SERVER['REQUEST_URI'], '/api.php') === false) {
            $splittedUrl = explode('/', $_SERVER['REQUEST_URI']);

            foreach($splittedUrl as $key => $urlFragment) {
                if($urlFragment === 'api' && $splittedUrl[$key + 1] === 'v2') {
                    unset($splittedUrl[$key + 1]);
                    $splittedUrl[$key] = 'api.php';
                    break;
                }
            }

            $_SERVER['REQUEST_URI'] = implode('/', $splittedUrl);
        }

        $request = Request::createFromGlobals();
        $response = $this->getApplicationKernel()->handle($request);
        $response->send();
        $this->getApplicationKernel()->terminate($request, $response);

        return null;
    }

    /**
     * @return Bootstrap fluent interface
     */
    private function configureDebugging(): Bootstrap
    {
        if ($this->isDebugEnabled()) {
            Debug::enable();
        }

        return $this;
    }

    /**
     * @return Bootstrap fluent interface
     */
    private function configureApplicationKernel(): Bootstrap
    {
        $this->applicationKernel = new ApplicationKernel(
            $this->getEnvironment(),
            $this->isSymfonyDebugModeEnabled()
        );

        return $this;
    }

    /**
     * @return ApplicationKernel
     *
     * @throws \RuntimeException if configure has not been called before
     */
    public function getApplicationKernel(): ApplicationKernel
    {
        $this->assertConfigureHasBeenCalled();

        return $this->applicationKernel;
    }

    /**
     * Returns the Symfony DI container.
     *
     * @return ContainerInterface
     */
    public function getContainer(): ContainerInterface
    {
        $this->applicationKernel->boot();

        return $this->getApplicationKernel()->getContainer();
    }

    /**
     * @return EntityManagerInterface
     *
     * @throws \RuntimeException if configure has not been called before
     */
    public function getEntityManager(): EntityManagerInterface
    {
        $this->assertConfigureHasBeenCalled();

        return $this->getContainer()->get('doctrine.orm.entity_manager');
    }

    /**
     * Returns the absolute path to the application root.
     *
     * When core is installed as a dependency (library) of an application, this method will return
     * the application's package path.
     *
     * When phpList4-core is installed stand-alone (i.e., as an application - usually only for testing),
     * this method will be the phpList4-core package path.
     *
     * @return string the absolute path without the trailing slash.
     *
     * @throws \RuntimeException if there is no composer.json in the application root
     */
    public function getApplicationRoot(): string
    {
        return $this->applicationStructure->getApplicationRoot();
    }
}
