<?php

namespace App\MessageHandler;

use App\Exception\CommandExecuteException;
use App\Message\UpdateKeylighterMessage;
use FilesystemIterator;
use Psr\Log\LoggerInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Messenger\Handler\MessageSubscriberInterface;

class UpdateKeylighterHandler implements MessageHandlerInterface, MessageSubscriberInterface
{
    private $logger;
    private $filesystem;

    private $temp = '/tmp/keylighter/';
    private $installed;

    private $composer;
    private $box;

    public function __construct(LoggerInterface $logger, Filesystem $filesystem, $composer, $box, $installed)
    {
        $this->logger     = $logger;
        $this->composer   = $composer;
        $this->box        = $box;
        $this->filesystem = $filesystem;
        $this->installed  = $installed;
    }

    /**
     * @return mixed
     */
    public function getTempFile()
    {
        return $this->filesystem->tempnam($this->temp, 'keylighter');
    }

    public function getTempDir()
    {
        $path = $this->temp.DIRECTORY_SEPARATOR.uniqid('keylighter');

        $this->filesystem->mkdir($path, 0755);

        return realpath($path);
    }

    public function __invoke(UpdateKeylighterMessage $message)
    {
        $this->logger->info(sprintf('Downloading KeyLighter revision %s to %s', $message->getRevision()['version'], $message->getDestination()));

        return $this->download($message->getRevision(), $message->getDestination());
    }

    private function download($revision, $destination)
    {
        $temp = $this->getTempDir();

        $archive = new \ZipArchive();
        $archive->open($this->downloadArchive($revision));
        $archive->extractTo($temp);
        $archive->close();

        $this->move($temp, $destination);

        $cwd = getcwd();
        chdir($destination);
        try {
            $this->runCommands([ $this->composer.' install -n --no-ansi --quiet --no-dev' ]);

            if (file_exists('./box.json.dist')) {
                $this->runCommands([ $this->box.' build -n --no-ansi']);
                $this->logger->info(sprintf('keylighter.phar build OK'));
            }
        } catch (CommandExecuteException $exception) {
            $this->logger->error($exception->getMessage(), [
                'code'   => $exception->getCode(),
                'output' => $exception->getOutput()
            ]);

            return false;
        } finally {
            chdir($cwd);
        }

        $this->logger->info(sprintf('Download OK'));
        $this->addInstalledEntry($revision['version'], $revision['dist']['reference']);

        return true;
    }

    private function runCommands(array $commands)
    {
        $commands = array_map(function($command) {
            return str_replace('@php', PHP_BINARY, $command);
        }, $commands);
        $commands = implode('; ', $commands);

        $this->logger->debug('Executing: '.$commands);

        exec($commands, $output, $code);
        if ($code !== 0) {
            throw new CommandExecuteException("Command `$commands` returned non-zero exit code.", implode("\n", $output), $code);
        }

        return true;
    }

    private function move($temp, $final)
    {
        try {
            /** @var \SplFileInfo[] $files */
            $files = array_values(iterator_to_array(new FilesystemIterator($temp, FilesystemIterator::SKIP_DOTS)));

            // really nasty hack
            if (count($files) === 1 && $files[0]->isDir()) {
                $dir = $files[0]->getPathName();
            } else {
                $dir = $temp;
            }

            if ($this->filesystem->exists($final)) {
                $this->filesystem->remove($final);
            }

            $this->filesystem->rename($dir, $final, true);

            return true;
        } catch (IOException $exception) {
            return false;
        }
    }

    private function downloadArchive($revision)
    {
        try {
            $temp   = $this->getTempFile();
            $result = fopen($temp, 'w');
            $curl   = curl_init();

            curl_setopt($curl, CURLOPT_URL, $revision['dist']['url']);
            curl_setopt($curl, CURLOPT_FILE, $result);
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($curl, CURLOPT_USERAGENT, "keylighter.kadet.net/1.0");

            if (!curl_exec($curl)) {
                throw new \RuntimeException(curl_error($curl), curl_errno($curl));
            }

            if (($code = curl_getinfo($curl, CURLINFO_HTTP_CODE)) !== 200) {
                throw new \RuntimeException("Response code was $code.");
            }

            return $temp;
        } finally {
            curl_close($curl);
            fclose($result);
        }
    }

    private function addInstalledEntry($version, $checksum)
    {
        $entries = file_exists($this->installed) ? json_decode(file_get_contents($this->installed), true) : [];
        $entries[$version] = $checksum;

        file_put_contents($this->installed, json_encode($entries, true), LOCK_EX);
    }

    /**
     * @inheritDoc
     */
    public static function getHandledMessages(): iterable
    {
        yield UpdateKeylighterMessage::class;
    }
}
