<?php declare(strict_types=1);

namespace Newland\ToubizWidgetRendering\Renderer;

use Neos\Flow\Annotations as Flow;
use Newland\ToubizWidgetRendering\Utility\CacheUtility;
use Newland\ToubizWidgetRendering\Utility\CommandRunner;
use Newland\ToubizWidgetRendering\Utility\HtmlUtility;
use Newland\ToubizWidgetRendering\Utility\ValidationUtility;
use Newland\ToubizWidgetRendering\Utility\WebpackUtility;
use Newland\ToubizWidgetRendering\Utility\WorkPool;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;
use function Safe\sprintf;
use function Safe\unlink;

/**
 * Server side rendering implementation.
 * The server side rendering consists of 2 phases:
 *
 * ## 1. Precompilation
 * The precompilation phase happens on a development machine and generates files that
 * are needed in order to
 * - Render the vue app on the server side
 * - Hydrate the server-side rendered code on the client side
 *
 * ## 2. Rendering
 * The rendering part of the SSR setup happens on the server itself, when requests come in.
 * It uses the precompiled files from the precompilation step and thus does not require
 * node_modules to exist on the server itself.
 */
class ServerSideRenderer extends PrecompiledClientRenderer
{

    /**
     * @var CommandRunner
     * @Flow\Inject()
     */
    protected $commandRunner;

    /**
     * @var CacheUtility
     * @Flow\Inject()
     */
    protected $cache;

    /**
     * @var ValidationUtility
     * @Flow\Inject()
     */
    protected $validation;

    /**
     * @var WebpackUtility
     * @Flow\Inject()
     */
    protected $webpack;

    /**
     * @var WorkPool
     * @Flow\Inject()
     */
    protected $workPool;

    /** @var array */
    protected $configuration;

    public function setConfiguration(array $configuration): void
    {
        $this->validateConfiguration($configuration);
        $configuration = $this->replacePlaceholdersInBundlePaths($configuration);
        $this->configuration = $configuration;
    }

    public function render(string $tagName, array $arguments): string
    {
        $this->throwExceptionIfCompiledBundlesDontExist();
        $id = md5(random_bytes(32));
        return $this->serverSideRender($this->html->buildVue($tagName, $arguments), $id);
    }

    public function precompile(OutputInterface $output): void
    {
        if ($this->configuration['yarnInstall']) {
            $this->commandRunner->timeout(600)->run([ 'yarn', 'install', '--frozen-lockfile' ], $output);
        }

        if (is_dir($this->configuration['outputPath'])) {
            $finder = (new Finder())
                ->in($this->configuration['outputPath'])
                ->name([ '*.js', '*.json', '*.css', '*.png' ]);
            foreach ($finder as $file) {
                unlink($file->getRealPath());
            }
        }


        $this->workPool->inParallel($this->configuration['parallelPrecompilation'])->run(
            [
                function () use ($output) {
                    $this->webpack
                        ->run(array_replace_recursive($this->configuration, $this->configuration['cli']), $output);
                },
                function () use ($output) {
                    $this->webpack
                        ->run(array_replace_recursive($this->configuration, $this->configuration['server']), $output);
                },
                function () use ($output) {
                    $this->webpack
                        ->run(array_replace_recursive($this->configuration, $this->configuration['client']), $output);
                },
            ]
        );

        $this->cache->flush();
        $lines = [
            '',
            'TOUBIZ WIDGET RENDERING: Information',
            '',
            'Precompiled the manifest files required for server side rendering.',
            'These files should be part of your repository and should be deployed',
            'with the website.',
            '',
            'Please ensure, that the following directory is not excluded',
            'from your repository:',
            '',
            sprintf('- `%s`', $this->configuration['outputPath']),
            '',
        ];

        foreach ($lines as $line) {
            $output->writeln(
                sprintf(
                    '<success>  %s</success>',
                    str_pad($line, 80, ' ', STR_PAD_RIGHT)
                )
            );
        }
    }

    private function serverSideRender(string $template, string $id): string
    {
        return $this->cache->cacheOrGenerate(
            $id,
            function () use ($template, $id) {
                $env = [
                    'WRAPPER_ID' => $id,
                    'TAILWIND_CONFIG' => $this->configuration['tailwindConfig'],
                    'TOUBIZ_WIDGET_SSR_BUNDLE' => $this->configuration['server']['bundle'],
                    'TOUBIZ_WIDGET_SSR_CLIENT_MANIFEST' => $this->configuration['client']['bundle'],
                ];
                return $this->generateCredentialsScriptTag() . PHP_EOL .
                    $this->commandRunner->timeout(10)->run(
                        [ $this->configuration['nodeBinary'], $this->configuration['cli']['bundle'] ],
                        new NullOutput(),
                        $env,
                        $template
                    );
            }
        );
    }

    private function validateConfiguration(array $configuration): void
    {
        $this->validation->validateOrThrow(
            $configuration,
            'resource://Newland.ToubizWidgetRendering/Private/Schema/ServerSideRenderer.schema.yaml'
        );

        $this->validation->throwIfReferencedFilesDoNotExist(
            $configuration,
            [
                'cli.entry',
                'cli.webpackConfig',
                'server.entry',
                'server.webpackConfig',
                'client.entry',
                'client.webpackConfig',
            ]
        );
    }

    private function throwExceptionIfCompiledBundlesDontExist(): void
    {
        $this->validation->throwIfReferencedFilesDoNotExist(
            $this->configuration,
            [ 'cli.bundle', 'client.bundle', 'server.bundle' ],
            '
                 Manifest files for server side rendering could not be found.
                Please run `$ php flow toubizWidget:precompile` in your development environment and commit
                the generated files to your repository to ensure that they exist.
            '
        );
    }

    private function replacePlaceholdersInBundlePaths(array $configuration): array
    {
        $outputPath = $configuration['outputPath'];
        foreach ([ 'cli', 'client', 'server' ] as $path) {
            $configuration[$path]['bundle'] = str_replace(
                '[outputPath]',
                $outputPath,
                $configuration[$path]['bundle']
            );
        }
        return $configuration;
    }

    private function generateCredentialsScriptTag(): string
    {
        $apiToken = $this->configuration['toubizApiToken'] ?? '';

        if (!$apiToken) {
            throw new \Exception('API token is not configured.');
        }

        $credentials = [
            'credentials' => [
                'baseUri' => $this->configuration['toubizBaseUri'] ?? '',
                'pdfBaseUri' => $this->configuration['pdfBaseUri'] ?? '',
                'apiToken' => $apiToken,
            ],
        ];

        return (new HtmlUtility())->build(
            'script',
            [],
            [
                sprintf('window.ToubizWidget = %s;', \Safe\json_encode($credentials, JSON_UNESCAPED_SLASHES)),
            ]
        );
    }
}
