<?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 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
    ): array {

        // @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);

        $lines = [];
        try {
            $process->mustRun(function ($type, $buffer) use ($output, &$lines, $id) {
                $lines[] = $buffer;
                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,
                'outputLines' => $lines
            ]);

            // The message of ProcessFailedException contains the full STDOUT and STDERR
            // In order to prevent the error messages from being to 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) {
                $shortMessage = $messageLines[0] . ' [...] ' . array_pop($messageLines);
            } else {
                $shortMessage = $message;
            }

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

        return $lines;
    }

    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;
    }
}
