<?php declare(strict_types = 1);

namespace PHPStan\ExtensionInstaller;

use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\Event;
use Composer\Script\ScriptEvents;
use Composer\Semver\Constraint\ConstraintInterface;
use Composer\Semver\Constraint\MultiConstraint;
use Composer\Semver\Intervals;
use Composer\Util\Filesystem;
use function array_key_exists;
use function array_keys;
use function class_exists;
use function count;
use function dirname;
use function file_exists;
use function file_put_contents;
use function getcwd;
use function in_array;
use function is_file;
use function ksort;
use function md5;
use function md5_file;
use function sort;
use function sprintf;
use function strpos;
use function var_export;
use const DIRECTORY_SEPARATOR;

final class Plugin implements PluginInterface, EventSubscriberInterface
{

	/** @var string */
	private static $generatedFileTemplate = <<<'PHP'
<?php declare(strict_types = 1);

namespace PHPStan\ExtensionInstaller;

/**
 * This class is generated by phpstan/extension-installer.
 * @internal
 */
final class GeneratedConfig
{

	public const EXTENSIONS = %s;

	public const NOT_INSTALLED = %s;

	/** @var string|null */
	public const PHPSTAN_VERSION_CONSTRAINT = %s;

	private function __construct()
	{
	}

}

PHP;

	public function activate(Composer $composer, IOInterface $io): void
	{
		// noop
	}

	public function deactivate(Composer $composer, IOInterface $io): void
	{
		// noop
	}

	public function uninstall(Composer $composer, IOInterface $io): void
	{
		// noop
	}

	/**
	 * @return array<string, string>
	 */
	public static function getSubscribedEvents(): array
	{
		return [
			ScriptEvents::POST_INSTALL_CMD => 'process',
			ScriptEvents::POST_UPDATE_CMD => 'process',
		];
	}

	public function process(Event $event): void
	{
		$io = $event->getIO();

		if (!file_exists(__DIR__)) {
			$io->write('<info>phpstan/extension-installer:</info> Package not found (probably scheduled for removal); extensions installation skipped.');
			return;
		}

		$composer = $event->getComposer();
		$installationManager = $composer->getInstallationManager();

		$generatedConfigFilePath = __DIR__ . '/GeneratedConfig.php';
		$oldGeneratedConfigFileHash = null;
		if (is_file($generatedConfigFilePath)) {
			$oldGeneratedConfigFileHash = md5_file($generatedConfigFilePath);
		}
		$notInstalledPackages = [];
		$installedPackages = [];
		$ignoredPackages = [];

		$data = [];
		$fs = new Filesystem();
		$ignore = [];

		$packageExtra = $composer->getPackage()->getExtra();

		if (isset($packageExtra['phpstan/extension-installer']['ignore'])) {
			$ignore = $packageExtra['phpstan/extension-installer']['ignore'];
		}

		$phpstanVersionConstraints = [];

		foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $package) {
			if (
				$package->getType() !== 'phpstan-extension'
				&& !isset($package->getExtra()['phpstan'])
			) {
				if (
					strpos($package->getName(), 'phpstan') !== false
					&& !in_array($package->getName(), [
						'phpstan/phpstan',
						'phpstan/phpstan-shim',
						'phpstan/phpdoc-parser',
						'phpstan/extension-installer',
					], true)
				) {
					$notInstalledPackages[$package->getName()] = $package->getFullPrettyVersion();
				}
				continue;
			}

			if (in_array($package->getName(), $ignore, true)) {
				$ignoredPackages[] = $package->getName();
				continue;
			}

			$installPath = $installationManager->getInstallPath($package);
			if ($installPath === null) {
				continue;
			}

			$absoluteInstallPath = $fs->isAbsolutePath($installPath)
				? $installPath
				: getcwd() . DIRECTORY_SEPARATOR . $installPath;

			$packageRequires = $package->getRequires();
			$phpstanConstraint = null;
			if (array_key_exists('phpstan/phpstan', $packageRequires)) {
				$phpstanConstraint = $packageRequires['phpstan/phpstan']->getConstraint();
				if ($phpstanConstraint->getLowerBound()->isZero()) {
					continue;
				}
				if ($phpstanConstraint->getUpperBound()->isPositiveInfinity()) {
					continue;
				}
				$phpstanVersionConstraints[] = $phpstanConstraint;
			}

			$data[$package->getName()] = [
				'install_path' => $absoluteInstallPath,
				'relative_install_path' => $fs->findShortestPath(dirname($generatedConfigFilePath), $absoluteInstallPath, true),
				'extra' => $package->getExtra()['phpstan'] ?? null,
				'version' => $package->getFullPrettyVersion(),
				'phpstanVersionConstraint' => $phpstanConstraint !== null ? $this->constraintIntoString($phpstanConstraint) : null,
			];

			$installedPackages[$package->getName()] = true;
		}

		$phpstanVersionConstraint = null;
		if (count($phpstanVersionConstraints) > 0 && class_exists(Intervals::class)) {
			if (count($phpstanVersionConstraints) === 1) {
				$multiConstraint = $phpstanVersionConstraints[0];
			} else {
				$multiConstraint = new MultiConstraint($phpstanVersionConstraints);
			}
			$phpstanVersionConstraint = $this->constraintIntoString(Intervals::compactConstraint($multiConstraint));
		}

		ksort($data);
		ksort($installedPackages);
		ksort($notInstalledPackages);
		sort($ignoredPackages);

		$generatedConfigFileContents = sprintf(self::$generatedFileTemplate, var_export($data, true), var_export($notInstalledPackages, true), var_export($phpstanVersionConstraint, true));
		file_put_contents($generatedConfigFilePath, $generatedConfigFileContents);
		$io->write('<info>phpstan/extension-installer:</info> Extensions installed');

		if ($oldGeneratedConfigFileHash === md5($generatedConfigFileContents)) {
			return;
		}

		foreach (array_keys($installedPackages) as $name) {
			$io->write(sprintf('> <info>%s:</info> installed', $name));
		}

		foreach (array_keys($notInstalledPackages) as $name) {
			$io->write(sprintf('> <comment>%s:</comment> not supported', $name));
		}

		foreach ($ignoredPackages as $name) {
			$io->write(sprintf('> <comment>%s:</comment> ignored', $name));
		}
	}

	private function constraintIntoString(ConstraintInterface $constraint): string
	{
		return sprintf(
			'%s%s, %s%s',
			$constraint->getLowerBound()->isInclusive() ? '>=' : '>',
			$constraint->getLowerBound()->getVersion(),
			$constraint->getUpperBound()->isInclusive() ? '<=' : '<',
			$constraint->getUpperBound()->getVersion()
		);
	}

}
