<?php declare(strict_types=1);

namespace Newland\ToubizWidgetRendering\Utility;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Log\ThrowableStorageInterface;
use Newland\ToubizWidgetRendering\Renderer\ServerSideRenderingException;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Process;

class CommandRunner
{
    /**
     * @var ThrowableStorageInterface
     * @Flow\Inject()
     */
    protected $throwableLogger;

    /**
     * @var array
     * @Flow\InjectConfiguration()
     */
    protected $configuration;

    /** @var int */
    protected $timeout = 60;

    public function timeout(int $timeout): self
    {
        $runner = clone $this;
        $runner->timeout = $timeout;
        return $runner;
    }

    public function run(
        array $command,
        OutputInterface $output,
        array $env = [],
        string $stdin = null
    ): string {

        // @phpstan-ignore-next-line
        $packagePath = FLOW_PATH_PACKAGES . '/Plugins/Newland.ToubizWidgetRendering';
        $process = new Process($command, $packagePath, $env);
        $process->setTimeout($this->timeout);

        if ($stdin) {
            $process->setInput($stdin);
        }

        $id = substr(md5(random_bytes(32)), 0, 5);
        $commandInformation = $this->outputCommandInformation($command, $id, $process, $output);

        try {
            $process->mustRun(
                function ($type, $buffer) use ($output, $id) {
                    if ($type === Process::ERR) {
                        $output->write(sprintf('<warn>[%s|STDERR] %s</warn>', $id, $buffer));
                    } else {
                        $output->write(sprintf('[%s|STDOUT] %s', $id, $buffer));
                    }
                }
            );
        } catch (RuntimeException $exception) {
            $message = $this->throwableLogger->logThrowable(
                $exception,
                [
                    'commandInformation' => $commandInformation,
                    'stdout' => $process->getOutput(),
                    'stderr' => $process->getErrorOutput(),
                ]
            );

            $message = $this->trimMessage($message);

            $output->writeln(sprintf('<critical>%s</critical>', $message));
            throw new ServerSideRenderingException($message, $exception->getCode(), $exception);
        }

        return $process->getOutput();
    }

    private function trimMessage(string $message): string
    {
        if ($this->configuration['fullErrorMessage']) {
            return $message;
        }

        // The message of ProcessFailedException contains the full STDOUT and STDERR
        // In order to prevent the error messages from being too overwhelming, the first and last
        // lines are extracted (first line contains command call, last line contains reference to
        // file containing full message).
        $messageLines = explode("\n", $message);
        if (count($messageLines) > 2) {
            return $messageLines[0] . ' [...] ' . array_pop($messageLines);
        }

        return $message;
    }

    private function outputCommandInformation(
        array $command,
        string $id,
        Process $process,
        OutputInterface $output
    ): array {
        $lines = [
            '',
            'TOUBIZ WIDGET RENDERING: Shelling out',
            '',
            'CWD: ' . $process->getWorkingDirectory(),
        ];

        $env = $process->getEnv();
        if (!empty($env)) {
            $lines[] = '# ENV: ' . \Safe\json_encode($env);
        }

        $stdin = $process->getInput();
        if (!empty($stdin)) {
            $lines[] = '# STDIN: ' . $stdin;
        }

        $lines[] = sprintf('TIMEOUT: %ds', $process->getTimeout());
        $lines[] = 'CMD: $ ' . implode(' ', $command);
        $lines[] = '';

        foreach ($lines as $line) {
            $output->writeln(sprintf('<info>[%s] %s</info>', $id, $line));
        }

        return $lines;
    }
}
