import type { PackageRule, RenovateConfig } from '../../../config/types';
import { NO_VULNERABILITY_ALERTS } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import { CrateDatasource } from '../../../modules/datasource/crate';
import { GoDatasource } from '../../../modules/datasource/go';
import { MavenDatasource } from '../../../modules/datasource/maven';
import { NpmDatasource } from '../../../modules/datasource/npm';
import { NugetDatasource } from '../../../modules/datasource/nuget';
import { PackagistDatasource } from '../../../modules/datasource/packagist';
import { PypiDatasource } from '../../../modules/datasource/pypi';
import { RubygemsDatasource } from '../../../modules/datasource/rubygems';
import { platform } from '../../../modules/platform';
import * as allVersioning from '../../../modules/versioning';
import * as composerVersioning from '../../../modules/versioning/composer';
import * as mavenVersioning from '../../../modules/versioning/maven';
import * as npmVersioning from '../../../modules/versioning/npm';
import * as pep440Versioning from '../../../modules/versioning/pep440';
import * as rubyVersioning from '../../../modules/versioning/ruby';
import * as semverVersioning from '../../../modules/versioning/semver';
import type { SecurityAdvisory } from '../../../types';
import { sanitizeMarkdown } from '../../../util/markdown';

type Datasource = string;
type DependencyName = string;
type FileName = string;

type CombinedAlert = Record<
  FileName,
  Record<
    Datasource,
    Record<
      DependencyName,
      {
        advisories: SecurityAdvisory[];
        fileType?: string;
        firstPatchedVersion?: string;
      }
    >
  >
>;

export function getFixedVersionByDatasource(
  fixedVersion: string,
  datasource: string,
): string {
  if (datasource === MavenDatasource.id || datasource === NugetDatasource.id) {
    return `[${fixedVersion},)`;
  }

  // crates.io, Go, Hackage, Hex, npm, RubyGems, PyPI
  return `>= ${fixedVersion}`;
}

// TODO can return `null` and `undefined` (#22198)
export async function detectVulnerabilityAlerts(
  input: RenovateConfig,
): Promise<RenovateConfig> {
  if (!input?.vulnerabilityAlerts) {
    return input;
  }
  if (input.vulnerabilityAlerts.enabled === false) {
    logger.debug('Vulnerability alerts are disabled');
    return input;
  }
  const alerts = await platform.getVulnerabilityAlerts?.();
  if (!alerts?.length) {
    logger.debug('No vulnerability alerts found');
    if (input.vulnerabilityAlertsOnly) {
      throw new Error(NO_VULNERABILITY_ALERTS);
    }
    return input;
  }
  const config = { ...input };
  const versionings: Record<string, string> = {
    'github-tags': semverVersioning.id,
    go: semverVersioning.id,
    packagist: composerVersioning.id,
    maven: mavenVersioning.id,
    npm: npmVersioning.id,
    nuget: semverVersioning.id,
    pypi: pep440Versioning.id,
    rubygems: rubyVersioning.id,
  };
  const combinedAlerts: CombinedAlert = {};
  for (const alert of alerts) {
    try {
      if (alert.dismissed_reason) {
        continue;
      }
      if (!alert.security_vulnerability?.first_patched_version) {
        logger.debug(
          { alert },
          'Vulnerability alert has no firstPatchedVersion - skipping',
        );
        continue;
      }
      const datasourceMapping: Record<string, string> = {
        composer: PackagistDatasource.id,
        go: GoDatasource.id,
        maven: MavenDatasource.id,
        npm: NpmDatasource.id,
        nuget: NugetDatasource.id,
        pip: PypiDatasource.id,
        rubygems: RubygemsDatasource.id,
        rust: CrateDatasource.id,
      };
      const datasource =
        datasourceMapping[alert.security_vulnerability.package.ecosystem];
      const depName = alert.security_vulnerability.package.name;
      const fileName = alert.dependency.manifest_path;
      const fileType = fileName.split('/').pop();
      const firstPatchedVersion =
        alert.security_vulnerability.first_patched_version.identifier;
      const advisory = alert.security_advisory;

      combinedAlerts[fileName] ??= {};
      combinedAlerts[fileName][datasource] ??= {};
      combinedAlerts[fileName][datasource][depName] ??= {
        advisories: [],
      };
      const alertDetails = combinedAlerts[fileName][datasource][depName];
      alertDetails.advisories.push(advisory);
      const versioningApi = allVersioning.get(versionings[datasource]);
      if (versioningApi.isVersion(firstPatchedVersion)) {
        if (
          !alertDetails.firstPatchedVersion ||
          versioningApi.isGreaterThan(
            firstPatchedVersion,
            alertDetails.firstPatchedVersion,
          )
        ) {
          alertDetails.firstPatchedVersion = firstPatchedVersion;
        }
      } else {
        logger.debug('Invalid firstPatchedVersion: ' + firstPatchedVersion);
      }
      alertDetails.fileType = fileType;
    } catch (err) {
      logger.warn({ err }, 'Error parsing vulnerability alert');
    }
  }
  const alertPackageRules: PackageRule[] = [];
  config.remediations = {} as never;
  for (const [fileName, files] of Object.entries(combinedAlerts)) {
    for (const [datasource, dependencies] of Object.entries(files)) {
      for (const [depName, val] of Object.entries(dependencies)) {
        let prBodyNotes: string[] = [];
        try {
          prBodyNotes = ['### GitHub Vulnerability Alerts'].concat(
            val.advisories.map((advisory) => {
              const identifiers = advisory.identifiers;
              const description = advisory.description;
              let content = '#### ';
              let heading: string;
              if (identifiers.some((id) => id.type === 'CVE')) {
                heading = identifiers
                  .filter((id) => id.type === 'CVE')
                  .map((id) => id.value)
                  .join(' / ');
              } else {
                heading = identifiers.map((id) => id.value).join(' / ');
              }
              if (advisory.references?.length) {
                heading = `[${heading}](${advisory.references[0].url})`;
              }
              content += heading;
              content += '\n\n';

              content += sanitizeMarkdown(description);
              return content;
            }),
          );
        } catch (err) /* istanbul ignore next */ {
          logger.warn({ err }, 'Error generating vulnerability PR notes');
        }
        // TODO: types (#22198)
        const matchFileNames =
          datasource === GoDatasource.id
            ? [fileName.replace('go.sum', 'go.mod')]
            : [fileName];
        let matchRule: PackageRule = {
          matchDatasources: [datasource],
          matchPackageNames: [depName],
          matchFileNames,
        };

        let matchCurrentVersion = `< ${val.firstPatchedVersion}`;
        if (
          datasource === MavenDatasource.id ||
          datasource === NugetDatasource.id
        ) {
          matchCurrentVersion = `(,${val.firstPatchedVersion})`;
        }

        // Remediate only direct dependencies
        matchRule = {
          ...matchRule,
          matchCurrentVersion,
          vulnerabilityFixVersion: val.firstPatchedVersion,
          prBodyNotes,
          isVulnerabilityAlert: true,
          force: {
            ...config.vulnerabilityAlerts,
          },
        };
        alertPackageRules.push(matchRule);
      }
    }
  }
  logger.debug({ alertPackageRules }, 'alert package rules');
  config.packageRules = (config.packageRules ?? []).concat(alertPackageRules);
  return config;
}
