<commit_before><?php

declare(strict_types=1);

namespace Phpcq\Runner\Updater;

use Phpcq\Runner\Exception\RuntimeException;
use Phpcq\Runner\FileDownloader;
use Phpcq\GnuPG\Signature\SignatureVerifier;
use Phpcq\PluginApi\Version10\Output\OutputInterface;
use Phpcq\RepositoryDefinition\AbstractHash;
use Phpcq\RepositoryDefinition\Plugin\PhpFilePluginVersion;
use Phpcq\RepositoryDefinition\Plugin\PluginVersionInterface;
use Phpcq\RepositoryDefinition\Tool\ToolHash;
use Phpcq\RepositoryDefinition\Tool\ToolVersion;
use Phpcq\RepositoryDefinition\Tool\ToolVersionInterface;
use Phpcq\Runner\Repository\InstalledPlugin;
use Phpcq\Runner\Repository\InstalledRepository;
use Phpcq\Runner\Repository\InstalledRepositoryDumper;
use Phpcq\Runner\Repository\LockFileDumper;
use Symfony\Component\Filesystem\Filesystem;

use function assert;
use function file_get_contents;
use function file_put_contents;
use function getcwd;
use function sprintf;

/**
 * @psalm-import-type TInstallPluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TUpgradePluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TKeepPluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TRemovePluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TPluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TInstallToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TUpgradeToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TKeepToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TRemoveToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 */
final class UpdateExecutor
{
    /**
     * @var FileDownloader
     */
    private $downloader;

    /**
     * @var string
     */
    private $installedPluginPath;

    /**
     * @var OutputInterface
     */
    private $output;

    /**
     * @var SignatureVerifier
     */
    private $verifier;

    /**
     * @var Filesystem
     */
    private $filesystem;

    public function __construct(
        FileDownloader $downloader,
        SignatureVerifier $verifier,
        string $pluginPath,
        OutputInterface $output
    ) {
        $this->downloader          = $downloader;
        $this->verifier            = $verifier;
        $this->installedPluginPath = $pluginPath;
        $this->output              = $output;
        $this->filesystem          = new Filesystem();
    }

    /** @psalm-param list<TPluginTask> $tasks */
    public function execute(array $tasks): void
    {
        $installed      = new InstalledRepository();
        $lockRepository = new InstalledRepository();

        foreach ($tasks as $task) {
            /** @var PluginVersionInterface $desired */
            $desired = $task['version'];

            switch ($task['type']) {
                case 'keep':
                    /** @psalm-var TKeepPluginTask $task */
                    $plugin = new InstalledPlugin($task['plugin']->getPluginVersion());
                    $installed->addPlugin($plugin);
                    break;
                case 'install':
                    /** @psalm-var TInstallPluginTask $task */
                    $installed->addPlugin($this->installPlugin($desired, $task['signed']));
                    break;
                case 'upgrade':
                    /** @psalm-var TUpgradePluginTask $task */
                    $plugin = $this->upgradePlugin($desired, $task['old'], $task['signed']);
                    $installed->addPlugin($plugin);
                    break;
                case 'remove':
                    /** @psalm-var TRemovePluginTask $task */
                    $this->removePlugin($desired);
                    break;
            }

            if (isset($task['tasks'])) {
                $tools = $this->executePluginTasks($task['tasks'], $installed->getPlugin($desired->getName()));
                $lockRepository->addPlugin(
                    new InstalledPlugin($desired, $tools)
                );
            }
        }

        // Save installed repository.
        $filesystem = new Filesystem();
        $dumper = new InstalledRepositoryDumper($filesystem);
        $dumper->dump($installed, $this->installedPluginPath . '/installed.json');

        $dumper = new LockFileDumper($filesystem);
        $dumper->dump($lockRepository, getcwd() . '/.phpcq.lock');
    }

    private function installPlugin(PluginVersionInterface $plugin, bool $requireSigned): InstalledPlugin
    {
        $this->output->writeln(
            'Installing ' . $plugin->getName() . ' version ' . $plugin->getVersion(),
            OutputInterface::VERBOSITY_VERBOSE
        );

        return $this->installPluginVersion($plugin, $requireSigned);
    }

    private function upgradePlugin(
        PluginVersionInterface $plugin,
        PluginVersionInterface $old,
        bool $requireSigned
    ): InstalledPlugin {
        $this->output->writeln('Upgrading ' . $plugin->getName(), OutputInterface::VERBOSITY_VERBOSE);

        // Do not install new version before deleting old. Otherwise the reinstall of the same version will fail!
        $this->deletePluginFiles($old);

        return $this->installPluginVersion($plugin, $requireSigned);
    }

    private function removePlugin(PluginVersionInterface $pluginVersion): void
    {
        $this->output->writeln(
            'Removing ' . $pluginVersion->getName() . ' version ' . $pluginVersion->getVersion(),
            OutputInterface::VERBOSITY_VERBOSE
        );

        $this->filesystem->remove($this->installedPluginPath . '/' . $pluginVersion->getName());
    }

    private function installPluginVersion(PluginVersionInterface $plugin, bool $requireSigned): InstalledPlugin
    {
        assert($plugin instanceof PhpFilePluginVersion);
        $bootstrapFile = sprintf('%1$s/plugin.php', $plugin->getName());
        $bootstrapPath = $this->installedPluginPath . '/' . $bootstrapFile;
        $this->output->writeln('Downloading ' . $plugin->getFilePath(), OutputInterface::VERBOSITY_VERY_VERBOSE);

        $this->downloader->downloadFileTo($plugin->getFilePath(), $bootstrapPath);
        $this->validateHash($bootstrapPath, $plugin->getHash());
        $signatureName = $this->verifyPluginSignature($bootstrapPath, $plugin, $requireSigned);

        return new InstalledPlugin(
            new PhpFilePluginVersion(
                $plugin->getName(),
                $plugin->getVersion(),
                $plugin->getApiVersion(),
                $plugin->getRequirements(),
                $bootstrapFile,
                $signatureName,
                $plugin->getHash()
            )
        );
    }

    private function deletePluginFiles(PluginVersionInterface $pluginVersion): void
    {
        $this->filesystem->remove($this->installedPluginPath . '/' . $pluginVersion->getName());
        if ($pluginVersion instanceof PhpFilePluginVersion) {
            $this->deleteFile($this->installedPluginPath . '/' . $pluginVersion->getFilePath());
        }

        if ($signatureUrl = $pluginVersion->getSignaturePath()) {
            $this->deleteFile($this->installedPluginPath . '/' . $signatureUrl);
        }
    }

    private function deleteFile(string $path): void
    {
        $this->output->writeln('Removing file ' . $path, OutputInterface::VERBOSITY_VERBOSE);
        $this->filesystem->remove($path);
    }

    private function validateHash(string $pathToPhar, ?AbstractHash $hash): void
    {
        if (null === $hash) {
            return;<commit_msg>Do not remove plugin directory in `removePluginFiles()`<commit_after><?php

declare(strict_types=1);

namespace Phpcq\Runner\Updater;

use Phpcq\RepositoryDefinition\Plugin\PhpFilePluginVersionInterface;
use Phpcq\Runner\Exception\RuntimeException;
use Phpcq\Runner\FileDownloader;
use Phpcq\GnuPG\Signature\SignatureVerifier;
use Phpcq\PluginApi\Version10\Output\OutputInterface;
use Phpcq\RepositoryDefinition\AbstractHash;
use Phpcq\RepositoryDefinition\Plugin\PhpFilePluginVersion;
use Phpcq\RepositoryDefinition\Plugin\PluginVersionInterface;
use Phpcq\RepositoryDefinition\Tool\ToolHash;
use Phpcq\RepositoryDefinition\Tool\ToolVersion;
use Phpcq\RepositoryDefinition\Tool\ToolVersionInterface;
use Phpcq\Runner\Repository\InstalledPlugin;
use Phpcq\Runner\Repository\InstalledRepository;
use Phpcq\Runner\Repository\InstalledRepositoryDumper;
use Phpcq\Runner\Repository\LockFileDumper;
use Symfony\Component\Filesystem\Filesystem;

use function assert;
use function file_get_contents;
use function file_put_contents;
use function getcwd;
use function sprintf;

/**
 * @psalm-import-type TInstallPluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TUpgradePluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TKeepPluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TRemovePluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TPluginTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TInstallToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TUpgradeToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TKeepToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 * @psalm-import-type TRemoveToolTask from \Phpcq\Runner\Updater\UpdateCalculator
 */
final class UpdateExecutor
{
    /**
     * @var FileDownloader
     */
    private $downloader;

    /**
     * @var string
     */
    private $installedPluginPath;

    /**
     * @var OutputInterface
     */
    private $output;

    /**
     * @var SignatureVerifier
     */
    private $verifier;

    /**
     * @var Filesystem
     */
    private $filesystem;

    public function __construct(
        FileDownloader $downloader,
        SignatureVerifier $verifier,
        string $pluginPath,
        OutputInterface $output
    ) {
        $this->downloader          = $downloader;
        $this->verifier            = $verifier;
        $this->installedPluginPath = $pluginPath;
        $this->output              = $output;
        $this->filesystem          = new Filesystem();
    }

    /** @psalm-param list<TPluginTask> $tasks */
    public function execute(array $tasks): void
    {
        $installed      = new InstalledRepository();
        $lockRepository = new InstalledRepository();

        foreach ($tasks as $task) {
            /** @var PluginVersionInterface $desired */
            $desired = $task['version'];

            switch ($task['type']) {
                case 'keep':
                    /** @psalm-var TKeepPluginTask $task */
                    $plugin = new InstalledPlugin($task['plugin']->getPluginVersion());
                    $installed->addPlugin($plugin);
                    break;
                case 'install':
                    /** @psalm-var TInstallPluginTask $task */
                    $installed->addPlugin($this->installPlugin($desired, $task['signed']));
                    break;
                case 'upgrade':
                    /** @psalm-var TUpgradePluginTask $task */
                    $plugin = $this->upgradePlugin($desired, $task['old'], $task['signed']);
                    $installed->addPlugin($plugin);
                    break;
                case 'remove':
                    /** @psalm-var TRemovePluginTask $task */
                    $this->removePlugin($desired);
                    break;
            }

            if (isset($task['tasks'])) {
                $tools = $this->executePluginTasks($task['tasks'], $installed->getPlugin($desired->getName()));
                $lockRepository->addPlugin(
                    new InstalledPlugin($desired, $tools)
                );
            }
        }

        // Save installed repository.
        $filesystem = new Filesystem();
        $dumper = new InstalledRepositoryDumper($filesystem);
        $dumper->dump($installed, $this->installedPluginPath . '/installed.json');

        $dumper = new LockFileDumper($filesystem);
        $dumper->dump($lockRepository, getcwd() . '/.phpcq.lock');
    }

    private function installPlugin(PluginVersionInterface $plugin, bool $requireSigned): InstalledPlugin
    {
        $this->output->writeln(
            'Installing ' . $plugin->getName() . ' version ' . $plugin->getVersion(),
            OutputInterface::VERBOSITY_VERBOSE
        );

        return $this->installPluginVersion($plugin, $requireSigned);
    }

    private function upgradePlugin(
        PluginVersionInterface $plugin,
        PluginVersionInterface $old,
        bool $requireSigned
    ): InstalledPlugin {
        $this->output->writeln('Upgrading ' . $plugin->getName(), OutputInterface::VERBOSITY_VERBOSE);

        // Do not install new version before deleting old. Otherwise the reinstall of the same version will fail!
        $this->deletePluginFiles($old);

        return $this->installPluginVersion($plugin, $requireSigned);
    }

    private function removePlugin(PluginVersionInterface $pluginVersion): void
    {
        $this->output->writeln(
            'Removing ' . $pluginVersion->getName() . ' version ' . $pluginVersion->getVersion(),
            OutputInterface::VERBOSITY_VERBOSE
        );

        $this->filesystem->remove($this->installedPluginPath . '/' . $pluginVersion->getName());
    }

    private function installPluginVersion(PluginVersionInterface $plugin, bool $requireSigned): InstalledPlugin
    {
        assert($plugin instanceof PhpFilePluginVersion);
        $bootstrapFile = sprintf('%1$s/plugin.php', $plugin->getName());
        $bootstrapPath = $this->installedPluginPath . '/' . $bootstrapFile;
        $this->output->writeln('Downloading ' . $plugin->getFilePath(), OutputInterface::VERBOSITY_VERY_VERBOSE);

        $this->downloader->downloadFileTo($plugin->getFilePath(), $bootstrapPath);
        $this->validateHash($bootstrapPath, $plugin->getHash());
        $signatureName = $this->verifyPluginSignature($bootstrapPath, $plugin, $requireSigned);

        return new InstalledPlugin(
            new PhpFilePluginVersion(
                $plugin->getName(),
                $plugin->getVersion(),
                $plugin->getApiVersion(),
                $plugin->getRequirements(),
                $bootstrapFile,
                $signatureName,
                $plugin->getHash()
            )
        );
    }

    private function deletePluginFiles(PluginVersionInterface $pluginVersion): void
    {
        if ($pluginVersion instanceof PhpFilePluginVersionInterface) {
            $this->deleteFile($this->installedPluginPath . '/' . $pluginVersion->getFilePath());
        }

        if ($signatureUrl = $pluginVersion->getSignaturePath()) {
            $this->deleteFile($this->installedPluginPath . '/' . $signatureUrl);
        }
    }

    private function deleteFile(string $path): void
    {
        $this->output->writeln('Removing file ' . $path, OutputInterface::VERBOSITY_VERBOSE);
        $this->filesystem->remove($path);
    }

    private function validateHash(string $pathToPhar, ?AbstractHash $hash): void
    {
        if (null === $hash) {
            return;
{'fraction_non_alphanumeric': 0.12075349139330951, 'fraction_numerical': 0.0005196492367651835, 'mean_word_length': 2.6448863636363638, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 135, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7533310', 'n_tokens_mistral': 4275, 'n_tokens_neox': 4103, 'n_words': 731}