<?php

namespace OCA\News\Vendor\Http\Discovery\Composer;

use OCA\News\Vendor\Composer\Composer;
use OCA\News\Vendor\Composer\DependencyResolver\Pool;
use OCA\News\Vendor\Composer\EventDispatcher\EventSubscriberInterface;
use OCA\News\Vendor\Composer\Factory;
use OCA\News\Vendor\Composer\Installer;
use OCA\News\Vendor\Composer\IO\IOInterface;
use OCA\News\Vendor\Composer\Json\JsonFile;
use OCA\News\Vendor\Composer\Json\JsonManipulator;
use OCA\News\Vendor\Composer\Package\Locker;
use OCA\News\Vendor\Composer\Package\Version\VersionParser;
use OCA\News\Vendor\Composer\Package\Version\VersionSelector;
use OCA\News\Vendor\Composer\Plugin\PluginInterface;
use OCA\News\Vendor\Composer\Repository\InstalledRepositoryInterface;
use OCA\News\Vendor\Composer\Repository\RepositorySet;
use OCA\News\Vendor\Composer\Script\Event;
use OCA\News\Vendor\Composer\Script\ScriptEvents;
use OCA\News\Vendor\Composer\Util\Filesystem;
use OCA\News\Vendor\Http\Discovery\ClassDiscovery;
/**
 * Auto-installs missing implementations.
 *
 * When a dependency requires both this package and one of the supported `*-implementation`
 * virtual packages, this plugin will auto-install a well-known implementation if none is
 * found. The plugin will first look at already installed packages and figure out the
 * preferred implementation to install based on the below stickyness rules (or on the first
 * listed implementation if no rules match.)
 *
 * Don't miss updating src/Strategy/Common*Strategy.php when adding a new supported package.
 *
 * @author Nicolas Grekas <p@tchwork.com>
 *
 * @internal
 */
class Plugin implements PluginInterface, EventSubscriberInterface
{
    /**
     * Describes, for every supported virtual implementation, which packages
     * provide said implementation and which extra dependencies each package
     * requires to provide the implementation.
     */
    private const PROVIDE_RULES = ['php-http/async-client-implementation' => ['symfony/http-client:>=6.3' => ['guzzlehttp/promises', 'psr/http-factory-implementation', 'php-http/httplug'], 'symfony/http-client' => ['guzzlehttp/promises', 'php-http/message-factory', 'psr/http-factory-implementation', 'php-http/httplug'], 'php-http/guzzle7-adapter' => [], 'php-http/guzzle6-adapter' => [], 'php-http/curl-client' => [], 'php-http/react-adapter' => []], 'php-http/client-implementation' => ['symfony/http-client:>=6.3' => ['psr/http-factory-implementation', 'php-http/httplug'], 'symfony/http-client' => ['php-http/message-factory', 'psr/http-factory-implementation', 'php-http/httplug'], 'php-http/guzzle7-adapter' => [], 'php-http/guzzle6-adapter' => [], 'php-http/cakephp-adapter' => [], 'php-http/curl-client' => [], 'php-http/react-adapter' => [], 'php-http/buzz-adapter' => [], 'php-http/artax-adapter' => [], 'kriswallsmith/buzz:^1' => []], 'psr/http-client-implementation' => ['symfony/http-client' => ['psr/http-factory-implementation', 'psr/http-client'], 'guzzlehttp/guzzle' => [], 'kriswallsmith/buzz:^1' => []], 'psr/http-message-implementation' => ['php-http/discovery' => ['psr/http-factory-implementation']], 'psr/http-factory-implementation' => ['nyholm/psr7' => [], 'guzzlehttp/psr7:>=2' => [], 'slim/psr7' => [], 'laminas/laminas-diactoros' => [], 'phalcon/cphalcon:^4' => [], 'http-interop/http-factory-guzzle' => [], 'http-interop/http-factory-diactoros' => [], 'http-interop/http-factory-slim' => [], 'httpsoft/http-message' => []]];
    /**
     * Describes which package should be preferred on the left side
     * depending on which one is already installed on the right side.
     */
    private const STICKYNESS_RULES = ['symfony/http-client' => 'symfony/framework-bundle', 'php-http/guzzle7-adapter' => 'guzzlehttp/guzzle:^7', 'php-http/guzzle6-adapter' => 'guzzlehttp/guzzle:^6', 'php-http/guzzle5-adapter' => 'guzzlehttp/guzzle:^5', 'php-http/cakephp-adapter' => 'cakephp/cakephp', 'php-http/react-adapter' => 'react/event-loop', 'php-http/buzz-adapter' => 'kriswallsmith/buzz:^0.15.1', 'php-http/artax-adapter' => 'amphp/artax:^3', 'http-interop/http-factory-guzzle' => 'guzzlehttp/psr7:^1', 'http-interop/http-factory-slim' => 'slim/slim:^3'];
    private const INTERFACE_MAP = ['php-http/async-client-implementation' => ['OCA\News\Vendor\Http\Client\HttpAsyncClient'], 'php-http/client-implementation' => ['OCA\News\Vendor\Http\Client\HttpClient'], 'psr/http-client-implementation' => ['OCA\News\Vendor\Psr\Http\Client\ClientInterface'], 'psr/http-factory-implementation' => ['OCA\News\Vendor\Psr\Http\Message\RequestFactoryInterface', 'OCA\News\Vendor\Psr\Http\Message\ResponseFactoryInterface', 'OCA\News\Vendor\Psr\Http\Message\ServerRequestFactoryInterface', 'OCA\News\Vendor\Psr\Http\Message\StreamFactoryInterface', 'OCA\News\Vendor\Psr\Http\Message\UploadedFileFactoryInterface', 'OCA\News\Vendor\Psr\Http\Message\UriFactoryInterface']];
    public static function getSubscribedEvents(): array
    {
        return [ScriptEvents::PRE_AUTOLOAD_DUMP => 'preAutoloadDump', ScriptEvents::POST_UPDATE_CMD => 'postUpdate'];
    }
    public function activate(Composer $composer, IOInterface $io): void
    {
    }
    public function deactivate(Composer $composer, IOInterface $io)
    {
    }
    public function uninstall(Composer $composer, IOInterface $io)
    {
    }
    public function postUpdate(Event $event)
    {
        $composer = $event->getComposer();
        $repo = $composer->getRepositoryManager()->getLocalRepository();
        $requires = [$composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires()];
        $pinnedAbstractions = [];
        $pinned = $composer->getPackage()->getExtra()['discovery'] ?? [];
        foreach (self::INTERFACE_MAP as $abstraction => $interfaces) {
            foreach (isset($pinned[$abstraction]) ? [] : $interfaces as $interface) {
                if (!isset($pinned[$interface])) {
                    continue 2;
                }
            }
            $pinnedAbstractions[$abstraction] = \true;
        }
        $missingRequires = $this->getMissingRequires($repo, $requires, 'project' === $composer->getPackage()->getType(), $pinnedAbstractions);
        $missingRequires = ['require' => array_fill_keys(array_merge([], ...array_values($missingRequires[0])), '*'), 'require-dev' => array_fill_keys(array_merge([], ...array_values($missingRequires[1])), '*'), 'remove' => array_fill_keys(array_merge([], ...array_values($missingRequires[2])), '*')];
        if (!$missingRequires = array_filter($missingRequires)) {
            return;
        }
        $composerJsonContents = file_get_contents(Factory::getComposerFile());
        $this->updateComposerJson($missingRequires, $composer->getConfig()->get('sort-packages'));
        $installer = null;
        // Find the composer installer, hack borrowed from symfony/flex
        foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT) as $trace) {
            if (isset($trace['object']) && $trace['object'] instanceof Installer) {
                $installer = $trace['object'];
                break;
            }
        }
        if (!$installer) {
            return;
        }
        $event->stopPropagation();
        $dispatcher = $composer->getEventDispatcher();
        $disableScripts = !method_exists($dispatcher, 'setRunScripts') || !((array) $dispatcher)["\x00*\x00runScripts"];
        $composer = Factory::create($event->getIO(), null, \false, $disableScripts);
        /** @var Installer $installer */
        $installer = clone $installer;
        if (method_exists($installer, 'setAudit')) {
            $trace['object']->setAudit(\false);
        }
        // we need a clone of the installer to preserve its configuration state but with our own service objects
        $installer->__construct($event->getIO(), $composer->getConfig(), $composer->getPackage(), $composer->getDownloadManager(), $composer->getRepositoryManager(), $composer->getLocker(), $composer->getInstallationManager(), $composer->getEventDispatcher(), $composer->getAutoloadGenerator());
        if (method_exists($installer, 'setPlatformRequirementFilter')) {
            $installer->setPlatformRequirementFilter(((array) $trace['object'])["\x00*\x00platformRequirementFilter"]);
        }
        if (0 !== $installer->run()) {
            file_put_contents(Factory::getComposerFile(), $composerJsonContents);
            return;
        }
        $versionSelector = new VersionSelector(ClassDiscovery::safeClassExists(RepositorySet::class) ? new RepositorySet() : new Pool());
        $updateComposerJson = \false;
        foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $package) {
            foreach (['require', 'require-dev'] as $key) {
                if (!isset($missingRequires[$key][$package->getName()])) {
                    continue;
                }
                $updateComposerJson = \true;
                $missingRequires[$key][$package->getName()] = $versionSelector->findRecommendedRequireVersion($package);
            }
        }
        if ($updateComposerJson) {
            $this->updateComposerJson($missingRequires, $composer->getConfig()->get('sort-packages'));
            $this->updateComposerLock($composer, $event->getIO());
        }
    }
    public function getMissingRequires(InstalledRepositoryInterface $repo, array $requires, bool $isProject, array $pinnedAbstractions): array
    {
        $allPackages = [];
        $devPackages = method_exists($repo, 'getDevPackageNames') ? array_fill_keys($repo->getDevPackageNames(), \true) : [];
        // One must require "php-http/discovery"
        // to opt-in for auto-installation of virtual package implementations
        if (!isset($requires[0]['php-http/discovery'])) {
            $requires = [[], []];
        }
        foreach ($repo->getPackages() as $package) {
            $allPackages[$package->getName()] = \true;
            if (1 < \count($names = $package->getNames(\false))) {
                $allPackages += array_fill_keys($names, \false);
                if (isset($devPackages[$package->getName()])) {
                    $devPackages += $names;
                }
            }
            if (isset($package->getRequires()['php-http/discovery'])) {
                $requires[(int) isset($devPackages[$package->getName()])] += $package->getRequires();
            }
        }
        $missingRequires = [[], [], []];
        $versionParser = new VersionParser();
        if (ClassDiscovery::safeClassExists(\OCA\News\Vendor\Phalcon\Http\Message\RequestFactory::class, \false)) {
            $missingRequires[0]['psr/http-factory-implementation'] = [];
            $missingRequires[1]['psr/http-factory-implementation'] = [];
        }
        foreach ($requires as $dev => $rules) {
            $abstractions = [];
            $rules = array_intersect_key(self::PROVIDE_RULES, $rules);
            while ($rules) {
                $abstraction = key($rules);
                if (isset($pinnedAbstractions[$abstraction])) {
                    unset($rules[$abstraction]);
                    continue;
                }
                $abstractions[] = $abstraction;
                foreach (array_shift($rules) as $candidate => $deps) {
                    [$candidate, $version] = explode(':', $candidate, 2) + [1 => null];
                    if (!isset($allPackages[$candidate])) {
                        continue;
                    }
                    if (null !== $version && !$repo->findPackage($candidate, $versionParser->parseConstraints($version))) {
                        continue;
                    }
                    if ($isProject && !$dev && isset($devPackages[$candidate])) {
                        $missingRequires[0][$abstraction] = [$candidate];
                        $missingRequires[2][$abstraction] = [$candidate];
                    } else {
                        $missingRequires[$dev][$abstraction] = [];
                    }
                    foreach ($deps as $dep) {
                        if (isset(self::PROVIDE_RULES[$dep])) {
                            $rules[$dep] = self::PROVIDE_RULES[$dep];
                        } elseif (!isset($allPackages[$dep])) {
                            $missingRequires[$dev][$abstraction][] = $dep;
                        } elseif ($isProject && !$dev && isset($devPackages[$dep])) {
                            $missingRequires[0][$abstraction][] = $dep;
                            $missingRequires[2][$abstraction][] = $dep;
                        }
                    }
                    break;
                }
            }
            while ($abstractions) {
                $abstraction = array_shift($abstractions);
                if (isset($missingRequires[$dev][$abstraction])) {
                    continue;
                }
                $candidates = self::PROVIDE_RULES[$abstraction];
                foreach ($candidates as $candidate => $deps) {
                    [$candidate, $version] = explode(':', $candidate, 2) + [1 => null];
                    if (null !== $version && !$repo->findPackage($candidate, $versionParser->parseConstraints($version))) {
                        continue;
                    }
                    if (isset($allPackages[$candidate]) && (!$isProject || $dev || !isset($devPackages[$candidate]))) {
                        continue 2;
                    }
                }
                foreach (array_intersect_key(self::STICKYNESS_RULES, $candidates) as $candidate => $stickyRule) {
                    [$stickyName, $stickyVersion] = explode(':', $stickyRule, 2) + [1 => null];
                    if (!isset($allPackages[$stickyName]) || $isProject && !$dev && isset($devPackages[$stickyName])) {
                        continue;
                    }
                    if (null !== $stickyVersion && !$repo->findPackage($stickyName, $versionParser->parseConstraints($stickyVersion))) {
                        continue;
                    }
                    $candidates = [$candidate => $candidates[$candidate]];
                    break;
                }
                $dep = key($candidates);
                [$dep] = explode(':', $dep, 2);
                $missingRequires[$dev][$abstraction] = [$dep];
                if ($isProject && !$dev && isset($devPackages[$dep])) {
                    $missingRequires[2][$abstraction][] = $dep;
                }
            }
        }
        $missingRequires[1] = array_diff_key($missingRequires[1], $missingRequires[0]);
        return $missingRequires;
    }
    public function preAutoloadDump(Event $event)
    {
        $filesystem = new Filesystem();
        // Double realpath() on purpose, see https://bugs.php.net/72738
        $vendorDir = $filesystem->normalizePath(realpath(realpath($event->getComposer()->getConfig()->get('vendor-dir'))));
        $filesystem->ensureDirectoryExists($vendorDir . '/composer');
        $pinned = $event->getComposer()->getPackage()->getExtra()['discovery'] ?? [];
        $candidates = [];
        $allInterfaces = array_merge(...array_values(self::INTERFACE_MAP));
        foreach ($pinned as $abstraction => $class) {
            if (isset(self::INTERFACE_MAP[$abstraction])) {
                $interfaces = self::INTERFACE_MAP[$abstraction];
            } elseif (\false !== $k = array_search($abstraction, $allInterfaces, \true)) {
                $interfaces = [$allInterfaces[$k]];
            } else {
                throw new \UnexpectedValueException(sprintf('Invalid "extra.discovery" pinned in composer.json: "%s" is not one of ["%s"].', $abstraction, implode('", "', array_keys(self::INTERFACE_MAP))));
            }
            foreach ($interfaces as $interface) {
                $candidates[] = sprintf("case %s: return [['class' => %s]];\n", var_export($interface, \true), var_export($class, \true));
            }
        }
        $file = $vendorDir . '/composer/GeneratedDiscoveryStrategy.php';
        if (!$candidates) {
            if (file_exists($file)) {
                unlink($file);
            }
            return;
        }
        $candidates = implode('            ', $candidates);
        $code = <<<EOPHP
<?php

namespace Http\\Discovery\\Strategy;

class GeneratedDiscoveryStrategy implements DiscoveryStrategy
{
    public static function getCandidates(\$type)
    {
        switch (\$type) {
            {$candidates}
            default: return [];
        }
    }
}

EOPHP;
        if (!file_exists($file) || $code !== file_get_contents($file)) {
            file_put_contents($file, $code);
        }
        $rootPackage = $event->getComposer()->getPackage();
        $autoload = $rootPackage->getAutoload();
        $autoload['classmap'][] = $vendorDir . '/composer/GeneratedDiscoveryStrategy.php';
        $rootPackage->setAutoload($autoload);
    }
    private function updateComposerJson(array $missingRequires, bool $sortPackages)
    {
        $file = Factory::getComposerFile();
        $contents = file_get_contents($file);
        $manipulator = new JsonManipulator($contents);
        foreach ($missingRequires as $key => $packages) {
            foreach ($packages as $package => $constraint) {
                if ('remove' === $key) {
                    $manipulator->removeSubNode('require-dev', $package);
                } else {
                    $manipulator->addLink($key, $package, $constraint, $sortPackages);
                }
            }
        }
        file_put_contents($file, $manipulator->getContents());
    }
    private function updateComposerLock(Composer $composer, IOInterface $io)
    {
        if (\false === $composer->getConfig()->get('lock')) {
            return;
        }
        $lock = substr(Factory::getComposerFile(), 0, -4) . 'lock';
        $composerJson = file_get_contents(Factory::getComposerFile());
        $lockFile = new JsonFile($lock, null, $io);
        $locker = ClassDiscovery::safeClassExists(RepositorySet::class) ? new Locker($io, $lockFile, $composer->getInstallationManager(), $composerJson) : new Locker($io, $lockFile, $composer->getRepositoryManager(), $composer->getInstallationManager(), $composerJson);
        if (!$locker->isLocked()) {
            return;
        }
        $lockData = $locker->getLockData();
        $lockData['content-hash'] = Locker::getContentHash($composerJson);
        $lockFile->write($lockData);
    }
}
