<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\Component\AssetMapper\Command;

use Symfony\Component\AssetMapper\ImportMap\ImportMapAuditor;
use Symfony\Component\AssetMapper\ImportMap\ImportMapPackageAuditVulnerability;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'importmap:audit', description: 'Check for security vulnerability advisories for dependencies')]
class ImportMapAuditCommand extends Command
{
    private const SEVERITY_COLORS = [
        'critical' => 'red',
        'high' => 'red',
        'medium' => 'yellow',
        'low' => 'default',
        'unknown' => 'default',
    ];

    private SymfonyStyle $io;

    public function __construct(
        private readonly ImportMapAuditor $importMapAuditor,
    ) {
        parent::__construct();
    }

    protected function configure(): void
    {
        $this
            ->addOption(
                name: 'format',
                mode: InputOption::VALUE_REQUIRED,
                description: \sprintf('The output format ("%s")', implode(', ', $this->getAvailableFormatOptions())),
                default: 'txt',
            )
            ->setHelp(<<<'EOT'
The <info>--format</info> option specifies the format of the command output:

  <info>php %command.full_name% --format=json</info>
EOT
            );
    }

    protected function initialize(InputInterface $input, OutputInterface $output): void
    {
        $this->io = new SymfonyStyle($input, $output);
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $format = $input->getOption('format');

        $audit = $this->importMapAuditor->audit();

        return match ($format) {
            'txt' => $this->displayTxt($audit),
            'json' => $this->displayJson($audit),
            default => throw new \InvalidArgumentException(\sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))),
        };
    }

    private function displayTxt(array $audit): int
    {
        $rows = [];

        $packagesWithoutVersion = [];
        $vulnerabilitiesCount = array_map(fn () => 0, self::SEVERITY_COLORS);
        foreach ($audit as $packageAudit) {
            if (!$packageAudit->version) {
                $packagesWithoutVersion[] = $packageAudit->package;
            }
            foreach ($packageAudit->vulnerabilities as $vulnerability) {
                $rows[] = [
                    \sprintf('<fg=%s>%s</>', self::SEVERITY_COLORS[$vulnerability->severity] ?? 'default', ucfirst($vulnerability->severity)),
                    $vulnerability->summary,
                    $packageAudit->package,
                    $packageAudit->version ?? 'n/a',
                    $vulnerability->firstPatchedVersion ?? 'n/a',
                    $vulnerability->url,
                ];
                ++$vulnerabilitiesCount[$vulnerability->severity];
            }
        }
        $packagesCount = \count($audit);
        $packagesWithoutVersionCount = \count($packagesWithoutVersion);

        if (!$rows && !$packagesWithoutVersionCount) {
            $this->io->info('No vulnerabilities found.');

            return self::SUCCESS;
        }

        if ($rows) {
            $table = $this->io->createTable();
            $table->setHeaders([
                'Severity',
                'Title',
                'Package',
                'Version',
                'Patched in',
                'More info',
            ]);
            $table->addRows($rows);
            $table->render();
            $this->io->newLine();
        }

        $this->io->text(\sprintf('%d package%s found: %d audited / %d skipped',
            $packagesCount,
            1 === $packagesCount ? '' : 's',
            $packagesCount - $packagesWithoutVersionCount,
            $packagesWithoutVersionCount,
        ));

        if (0 < $packagesWithoutVersionCount) {
            $this->io->warning(\sprintf('Unable to retrieve versions for package%s: %s',
                1 === $packagesWithoutVersionCount ? '' : 's',
                implode(', ', $packagesWithoutVersion)
            ));
        }

        if ([] !== $rows) {
            $vulnerabilityCount = 0;
            $vulnerabilitySummary = [];
            foreach ($vulnerabilitiesCount as $severity => $count) {
                if (!$count) {
                    continue;
                }
                $vulnerabilitySummary[] = \sprintf('%d %s', $count, ucfirst($severity));
                $vulnerabilityCount += $count;
            }
            $this->io->text(\sprintf('%d vulnerabilit%s found: %s',
                $vulnerabilityCount,
                1 === $vulnerabilityCount ? 'y' : 'ies',
                implode(' / ', $vulnerabilitySummary),
            ));
        }

        return self::FAILURE;
    }

    private function displayJson(array $audit): int
    {
        $vulnerabilitiesCount = array_map(fn () => 0, self::SEVERITY_COLORS);

        $json = [
            'packages' => [],
            'summary' => $vulnerabilitiesCount,
        ];

        foreach ($audit as $packageAudit) {
            $json['packages'][] = [
                'package' => $packageAudit->package,
                'version' => $packageAudit->version,
                'vulnerabilities' => array_map(fn (ImportMapPackageAuditVulnerability $v) => [
                    'ghsa_id' => $v->ghsaId,
                    'cve_id' => $v->cveId,
                    'url' => $v->url,
                    'summary' => $v->summary,
                    'severity' => $v->severity,
                    'vulnerable_version_range' => $v->vulnerableVersionRange,
                    'first_patched_version' => $v->firstPatchedVersion,
                ], $packageAudit->vulnerabilities),
            ];
            foreach ($packageAudit->vulnerabilities as $vulnerability) {
                ++$json['summary'][$vulnerability->severity];
            }
        }

        $this->io->write(json_encode($json));

        return 0 < array_sum($json['summary']) ? self::FAILURE : self::SUCCESS;
    }

    public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
    {
        if ($input->mustSuggestOptionValuesFor('format')) {
            $suggestions->suggestValues($this->getAvailableFormatOptions());
        }
    }

    /** @return string[] */
    private function getAvailableFormatOptions(): array
    {
        return ['txt', 'json'];
    }
}
