<?php declare(strict_types=1);

namespace Bref\SymfonyBridge;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel;

abstract class BrefKernel extends Kernel
{
    public function isLambda(): bool
    {
        return getenv('LAMBDA_TASK_ROOT') !== false;
    }

    public function getCacheDir(): string
    {
        if ($this->isLambda()) {
            return '/tmp/cache/' . $this->environment;
        }

        return parent::getCacheDir();
    }

    public function getLogDir(): string
    {
        if ($this->isLambda()) {
            return '/tmp/log/';
        }

        return parent::getLogDir();
    }

    /**
     * {@inheritDoc}
     *
     * When on the lambda, copy the cache dir over to /tmp where it is writable
     * We have to do this before Symfony does anything else with the Kernel
     * otherwise it might prematurely warm the cache before we can copy it
     *
     * @see https://github.com/brefphp/symfony-bridge/pull/37
     */
    public function handle(Request $request, int $type = HttpKernelInterface::MAIN_REQUEST, bool $catch = true): Response
    {
        $this->prepareCacheDir(parent::getCacheDir(), $this->getCacheDir());

        return parent::handle($request, $type, $catch);
    }

    /**
     * {@inheritdoc}
     *
     * We also need to prepare the Cache dir in Kernel::boot in case we are in a Console or worker context
     * in which Kernel::handle is not called.
     */
    public function boot(): void
    {
        $this->prepareCacheDir(parent::getCacheDir(), $this->getCacheDir());
        $this->ensureLogDir($this->getLogDir());

        parent::boot();
    }

    /**
     * Return the pre-warmed directories in var/cache/[env] that should be copied to
     * a writable directory in the Lambda environment.
     *
     * For optimal performance one should prewarm the cache folder and override this
     * function to return an empty array.
     */
    protected function getWritableCacheDirectories(): array
    {
        return ['pools'];
    }

    protected function prepareCacheDir(string $readOnlyDir, string $writeDir): void
    {
        if (! $this->isLambda() || is_dir($writeDir) || ! is_dir($readOnlyDir)) {
            return;
        }

        $startTime = microtime(true);
        $cacheDirectoriesToCopy = $this->getWritableCacheDirectories();
        $filesystem = new Filesystem;
        $filesystem->mkdir($writeDir);

        $scandir = scandir($readOnlyDir, SCANDIR_SORT_NONE);
        if ($scandir === false) {
            return;
        }

        foreach ($scandir as $item) {
            if (in_array($item, ['.', '..'])) {
                continue;
            }

            // Copy directories to a writable space on Lambda.
            if (in_array($item, $cacheDirectoriesToCopy)) {
                $filesystem->mirror("$readOnlyDir/$item", "$writeDir/$item");
                continue;
            }

            // Symlink all other directories
            // This is especially important with the Container* directories since it uses require_once statements
            if (is_dir("$readOnlyDir/$item")) {
                $filesystem->symlink("$readOnlyDir/$item", "$writeDir/$item");
                continue;
            }

            // Copy all other files.
            $filesystem->copy("$readOnlyDir/$item", "$writeDir/$item");
        }

        $this->logToStderr(sprintf(
            'Symfony cache directory prepared in %s ms.',
            number_format((microtime(true) - $startTime) * 1000, 2)
        ));
    }

    /**
     * Even though applications should never write into it on Lambda, there are parts of Symfony
     * (like "about" CLI command) that expect the log dir exists, so we have to make sure of it.
     *
     * @see https://github.com/brefphp/symfony-bridge/issues/42
     */
    private function ensureLogDir(string $writeLogDir): void
    {
        if (! $this->isLambda() || is_dir($writeLogDir)) {
            return;
        }

        $filesystem = new Filesystem;
        $filesystem->mkdir($writeLogDir);
    }

    /**
     * This method logs to stderr.
     *
     * It must only be used in a lambda environment since all error output will be logged.
     *
     * @param string $message The message to log
     */
    protected function logToStderr(string $message): void
    {
        file_put_contents('php://stderr', date('[c] ') . $message . PHP_EOL, FILE_APPEND);
    }
}
