// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * NERF SARIF Output — Static Analysis Results Interchange Format.
 *
 * Converts NERF scan findings into SARIF v2.1.0 for CI/CD integration
 * (GitHub Advanced Security, Azure DevOps, GitLab SAST, etc.).
 */

import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { randomUUID } from 'node:crypto';

const SARIF_VERSION = '2.1.0';
const SARIF_SCHEMA =
  'https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json';

/**
 * Map NERF severity to SARIF level.
 * @param {string} severity
 * @returns {string}
 */
function severityToLevel(severity) {
  const map = {
    critical: 'error',
    high: 'error',
    medium: 'warning',
    low: 'note',
    info: 'note',
  };
  return map[severity?.toLowerCase()] || 'warning';
}

/**
 * Map NERF severity to GitHub security-severity score string.
 * @param {string} severity
 * @returns {string}
 */
function severityScore(severity) {
  const map = {
    critical: '9.5',
    high: '8.0',
    medium: '5.5',
    low: '3.0',
    info: '1.0',
  };
  return map[severity?.toLowerCase()] || '5.0';
}

/**
 * A SARIF reporting descriptor (rule).
 */
class SarifRule {
  /**
   * @param {object} opts
   * @param {string} opts.id
   * @param {string} opts.name
   * @param {string} [opts.shortDescription]
   * @param {string} [opts.fullDescription]
   * @param {string} [opts.helpUri]
   * @param {string} [opts.level]
   * @param {string[]} [opts.tags]
   */
  constructor(opts = {}) {
    this.id = opts.id || '';
    this.name = opts.name || '';
    this.shortDescription = opts.shortDescription || '';
    this.fullDescription = opts.fullDescription || '';
    this.helpUri = opts.helpUri || '';
    this.level = opts.level || 'warning';
    this.tags = opts.tags || [];
  }

  toDict() {
    const rule = {
      id: this.id,
      name: this.name,
      shortDescription: { text: this.shortDescription || this.name },
    };
    if (this.fullDescription) {
      rule.fullDescription = { text: this.fullDescription };
    }
    if (this.helpUri) {
      rule.helpUri = this.helpUri;
    }
    rule.defaultConfiguration = { level: this.level };
    if (this.tags.length > 0) {
      rule.properties = { tags: this.tags };
    }
    return rule;
  }
}

/**
 * A single SARIF result (finding).
 */
class SarifResult {
  /**
   * @param {object} opts
   * @param {string} opts.ruleId
   * @param {string} opts.message
   * @param {string} [opts.level]
   * @param {string} [opts.uri]
   * @param {number} [opts.startLine]
   * @param {number} [opts.startColumn]
   * @param {number} [opts.endLine]
   * @param {string} [opts.snippet]
   * @param {string} [opts.fingerprint]
   * @param {object} [opts.properties]
   */
  constructor(opts = {}) {
    this.ruleId = opts.ruleId || '';
    this.message = opts.message || '';
    this.level = opts.level || 'warning';
    this.uri = opts.uri || '';
    this.startLine = opts.startLine || 0;
    this.startColumn = opts.startColumn || 0;
    this.endLine = opts.endLine || 0;
    this.snippet = opts.snippet || '';
    this.fingerprint = opts.fingerprint || '';
    this.properties = opts.properties || {};
  }

  toDict() {
    const result = {
      ruleId: this.ruleId,
      level: this.level,
      message: { text: this.message },
    };

    if (this.uri || this.startLine) {
      const location = {};
      if (this.uri) {
        location.physicalLocation = {
          artifactLocation: { uri: this.uri },
        };
        if (this.startLine) {
          const region = { startLine: this.startLine };
          if (this.startColumn) region.startColumn = this.startColumn;
          if (this.endLine) region.endLine = this.endLine;
          if (this.snippet) region.snippet = { text: this.snippet };
          location.physicalLocation.region = region;
        }
      }
      result.locations = [location];
    }

    if (this.fingerprint) {
      result.fingerprints = { 'nerf/v1': this.fingerprint };
    }

    if (Object.keys(this.properties).length > 0) {
      result.properties = this.properties;
    }

    return result;
  }
}

/**
 * Build a SARIF v2.1.0 report from NERF findings.
 */
class SarifReport {
  /**
   * @param {object} [opts]
   * @param {string} [opts.toolName]
   * @param {string} [opts.toolVersion]
   * @param {string} [opts.toolUri]
   */
  constructor(opts = {}) {
    this.toolName = opts.toolName || 'NERF';
    this.toolVersion = opts.toolVersion || '0.0.0';
    this.toolUri = opts.toolUri || 'https://github.com/defconxt/NERF';
    /** @type {Map<string, SarifRule>} */
    this._rules = new Map();
    /** @type {SarifResult[]} */
    this._results = [];
    this._invocationStart = new Date().toISOString();
  }

  /**
   * Register a rule (deduplicates by ID).
   * @param {SarifRule} rule
   */
  addRule(rule) {
    this._rules.set(rule.id, rule);
  }

  /**
   * Add a finding result.
   * @param {SarifResult} result
   */
  addResult(result) {
    this._results.push(result);
  }

  /**
   * Convert a NERF finding dict to SARIF rule + result.
   *
   * Expected keys: template_id, name, description, severity, host,
   *                matched_at, tags, cve_ids, reference
   * @param {object} finding
   */
  addFinding(finding) {
    const ruleId =
      finding.template_id || finding.templateId || finding.id || randomUUID().slice(0, 8);
    const severity = (finding.severity || 'medium').toLowerCase();
    const level = severityToLevel(severity);

    // Auto-register rule
    if (!this._rules.has(ruleId)) {
      this.addRule(
        new SarifRule({
          id: ruleId,
          name: finding.name || ruleId,
          shortDescription: finding.description || '',
          level,
          tags: finding.tags || [],
          helpUri: finding.reference || '',
        }),
      );
    }

    // Build result
    const props = {};
    if (finding.cve_ids || finding.cveIds) {
      props.cve = finding.cve_ids || finding.cveIds;
    }
    if (finding.severity) {
      props['security-severity'] = severityScore(severity);
    }

    this.addResult(
      new SarifResult({
        ruleId,
        message: `${finding.name || 'Finding'} at ${finding.host || finding.matched_at || finding.matchedAt || 'unknown'}`,
        level,
        uri: finding.matched_at || finding.matchedAt || finding.host || '',
        properties: Object.keys(props).length > 0 ? props : {},
      }),
    );
  }

  /**
   * Convert a NERF diff analysis finding to SARIF.
   *
   * Expected keys: type, severity, file, line, description, snippet
   * @param {object} finding
   */
  addDiffFinding(finding) {
    const ruleId = `nerf-diff-${finding.type || 'generic'}`;
    const severity = (finding.severity || 'medium').toLowerCase();
    const level = severityToLevel(severity);

    if (!this._rules.has(ruleId)) {
      this.addRule(
        new SarifRule({
          id: ruleId,
          name: `Security Diff: ${finding.type || 'issue'}`,
          shortDescription:
            finding.description || 'Security-relevant code change detected',
          level,
          tags: ['security', 'diff-analysis'],
        }),
      );
    }

    this.addResult(
      new SarifResult({
        ruleId,
        message: finding.description || 'Security-relevant change',
        level,
        uri: finding.file || '',
        startLine: finding.line || 0,
        snippet: finding.snippet || '',
        properties: { 'security-severity': severityScore(severity) },
      }),
    );
  }

  /**
   * Build the complete SARIF JSON structure.
   * @returns {object}
   */
  toDict() {
    const run = {
      tool: {
        driver: {
          name: this.toolName,
          version: this.toolVersion,
          informationUri: this.toolUri,
          rules: Array.from(this._rules.values()).map((r) => r.toDict()),
        },
      },
      invocations: [
        {
          executionSuccessful: true,
          startTimeUtc: this._invocationStart,
          endTimeUtc: new Date().toISOString(),
        },
      ],
      results: this._results.map((r) => r.toDict()),
    };

    return {
      $schema: SARIF_SCHEMA,
      version: SARIF_VERSION,
      runs: [run],
    };
  }

  /**
   * Serialize to JSON string.
   * @param {number} [indent=2]
   * @returns {string}
   */
  toJson(indent = 2) {
    return JSON.stringify(this.toDict(), null, indent);
  }

  /**
   * Write SARIF to file and return the path.
   * @param {string} path
   * @returns {string}
   */
  write(path) {
    mkdirSync(dirname(path), { recursive: true });
    writeFileSync(path, this.toJson());
    return path;
  }

  /** @returns {number} */
  get resultCount() {
    return this._results.length;
  }

  /** @returns {number} */
  get ruleCount() {
    return this._rules.size;
  }

  /**
   * Return a summary of the report.
   * @returns {object}
   */
  summary() {
    const byLevel = {};
    for (const r of this._results) {
      byLevel[r.level] = (byLevel[r.level] || 0) + 1;
    }
    return {
      rules: this.ruleCount,
      results: this.resultCount,
      by_level: byLevel,
    };
  }
}

export {
  // Constants
  SARIF_VERSION,
  SARIF_SCHEMA,
  // Helpers
  severityToLevel,
  severityScore,
  // Data classes
  SarifRule,
  SarifResult,
  // Report
  SarifReport,
};
