// eslint-disable-next-line @typescript-eslint/no-var-requires
const snykPolicy = require('snyk-policy');

export type KnownSeverity = 'low' | 'medium' | 'high' | 'critical';
export type Severity = KnownSeverity | 'unknown';

export type SnykPolicyRules = any;

export type VulnerabilityInfo = {
  id: string;
  score?: number;
  fixedIn: string[];
  origins: string[];
  severity: Severity;
  title: string;
  description: string;
  packageName: string;
  packageVersion: string;
  vulnerableSemver: string;
  cves: string[];
  policy?: {
    type: 'ignore';
    reason: string;
    expires: any;
  };
  urls: { title: string; url: string }[];
};

export type SnykVulnerability = {
  id: string;
  type?: 'license';
  title: string;
  CVSSv3: string;
  credit: string[];
  semver: {
    vulnerable: string;
  };
  exploit: string;
  patched: string[];
  patches: never[];
  fixedIn: string[];
  insights: {
    triageAdvice: null;
  };
  language: string;
  severity: Severity;
  cvssScore: number | undefined;
  functions: never[];
  moduleName: string;
  references: {
    url: string;
    title: string;
  }[];
  cvssDetails: never[];
  description: string;
  epssDetails: null;
  identifiers: {
    CVE: string[];
  };
  packageName: string;
  proprietary: boolean;
  creationTime: string;
  functions_new: never[];
  alternativeIds: never[];
  disclosureTime: string;
  packageManager: string;
  publicationTime: string;
  modificationTime: string;
  socialTrendAlert: boolean;
  severityWithCritical: Severity;
  from: string[];
  upgradePath: never[];
  isUpgradable: boolean;
  isPatchable: boolean;
  name: string;
  version: string;
};

export type SnykTestProjectResult = {
  vulnerabilities: SnykVulnerability[];
};

type Score = number | undefined;

const SEVERITY_TO_SCORE: Record<Severity, Score> = {
  low: 0,
  medium: 4,
  high: 7,
  critical: 9,
  unknown: undefined,
};

export function severityToScore(severity: Severity): Score {
  return SEVERITY_TO_SCORE[severity];
}

export function scoreToSeverity(score: number | undefined): Severity {
  if (score === undefined) {
    return 'unknown';
  }

  if (score >= 9) {
    return 'critical';
  }
  if (score >= 7) {
    return 'high';
  }
  if (score >= 4) {
    return 'medium';
  }
  return 'low';
}

export function vulnerabilityToSnyk(
  vulnerability: VulnerabilityInfo
): SnykVulnerability | PromiseLike<SnykVulnerability> {
  const {
    id,
    packageName,
    packageVersion,
    score,
    origins,
    cves,
    vulnerableSemver,
    fixedIn,
    description,
    urls,
  } = vulnerability;

  const severity = scoreToSeverity(score);
  return {
    id,
    title: id,
    CVSSv3: '-',
    credit: ['-'],
    semver: {
      vulnerable: vulnerableSemver,
    },
    exploit: '-',
    patched: fixedIn,
    patches: [],
    fixedIn: fixedIn,
    insights: {
      triageAdvice: null,
    },
    language: 'js',
    severity: severity,
    cvssScore: score,
    functions: [],
    moduleName: packageName,
    references: urls,
    cvssDetails: [],
    description: description ?? '',
    epssDetails: null,
    identifiers: {
      CVE: cves,
    },
    packageName: packageName,
    proprietary: true,
    creationTime: '-',
    functions_new: [],
    alternativeIds: [],
    disclosureTime: '-',
    packageManager: 'npm',
    publicationTime: '-',
    modificationTime: '-',
    socialTrendAlert: false,
    severityWithCritical: severity,
    from:
      origins && origins.length
        ? [...origins]
        : [`${packageName}@${packageVersion}`],
    upgradePath: [],
    isUpgradable: true,
    isPatchable: false,
    name: packageName,
    version: packageVersion,
  };
}

export function vulnerabilityFromSnyk(
  snykVulnerability: SnykVulnerability,
  rules: SnykPolicyRules
): VulnerabilityInfo {
  const urls = [];

  if (snykVulnerability.id?.startsWith('NSWG-COR-')) {
    const id = snykVulnerability.id.split('-').reverse()[0];
    urls.push({
      title: snykVulnerability.id,
      url: `https://github.com/nodejs/security-wg/blob/main/vuln/core/${id}.json`,
    });
  } else {
    urls.push({
      title: snykVulnerability.id,
      url: `https://security.snyk.io/vuln/${snykVulnerability.id}`,
    });
    urls.push({
      title: `${snykVulnerability.name}@${snykVulnerability.version} vulnerabilities`,
      url: `https://security.snyk.io/package/npm/${snykVulnerability.name}/${snykVulnerability.version}`,
    });
  }

  for (const cve of snykVulnerability.identifiers?.CVE ?? []) {
    urls.push({ title: cve, url: `https://nvd.nist.gov/vuln/detail/${cve}` });
  }

  return {
    packageName: snykVulnerability.name,
    packageVersion: snykVulnerability.version,
    id: snykVulnerability.id,
    score: snykVulnerability.cvssScore,
    severity: snykVulnerability.severity,
    title: snykVulnerability.title,
    description: snykVulnerability.description,
    fixedIn: snykVulnerability.fixedIn,
    cves: snykVulnerability.identifiers?.CVE ?? [],
    origins: snykVulnerability.from ? [snykVulnerability.from.join(' > ')] : [],
    vulnerableSemver:
      snykVulnerability.semver?.vulnerable ?? snykVulnerability.version,
    policy: snykPolicy.getByVuln(rules, snykVulnerability),
    urls: urls,
  };
}

export const loadSnykPolicyRules = async (
  snykPolicyPath: string | undefined
): Promise<SnykPolicyRules> =>
  await snykPolicy.load(snykPolicyPath ?? process.cwd(), {
    loose: true,
  });

export function isIgnored(vulnerability: VulnerabilityInfo): boolean {
  return hasIgnorePolicy(vulnerability) && !hasExpiredPolicy(vulnerability);
}

export function hasIgnorePolicy(vulnerability: VulnerabilityInfo): boolean {
  return vulnerability.policy?.type === 'ignore';
}

export function hasExpiredPolicy(vulnerability: VulnerabilityInfo): boolean {
  return new Date() >= vulnerability.policy?.expires;
}

export function hasKnownRemediation(vulnerability: VulnerabilityInfo): boolean {
  return !!vulnerability.fixedIn.length;
}
