/**
 * SARIF 2.1.0 output for ShieldCortex findings.
 *
 * Phase 15b — turns X-Ray / audit / mcp-scan findings into a SARIF 2.1.0
 * document so they surface in GitHub Code Scanning (the Security tab).
 *
 * Lives in the xray layer (its `index.ts` already re-exports the other
 * formatters) but is shape-agnostic: it accepts BOTH the xray `XRayFinding`
 * and the audit `AuditFinding` via a single internal normaliser, so the SARIF
 * builder never branches on the source shape.
 *
 * Spec: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html
 */
import path from 'path';
import { createRequire } from 'module';
const INFORMATION_URI = 'https://shieldcortex.ai';
const SARIF_SCHEMA = 'https://json.schemastore.org/sarif-2.1.0.json';
// ── Helpers ────────────────────────────────────────────────────
/**
 * Map a ShieldCortex severity onto a SARIF result level.
 *   critical / high → error
 *   medium          → warning
 *   low / info      → note
 */
function severityToLevel(severity) {
    switch (severity) {
        case 'critical':
        case 'high':
            return 'error';
        case 'medium':
            return 'warning';
        case 'low':
        case 'info':
        default:
            return 'note';
    }
}
/**
 * Stable rule id from a category/scanner key + title. SARIF rule ids should be
 * opaque, stable, and free of whitespace so they read well in the GitHub UI and
 * dedupe deterministically across runs.
 */
function makeRuleId(prefix, title) {
    const slug = `${prefix}/${title}`
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, '');
    return `shieldcortex/${slug}`;
}
function isXRayFinding(f) {
    // XRayFinding has `category`; AuditFinding has `scanner`. They are disjoint.
    return f.category !== undefined;
}
/** Make a file path relative to baseDir + posix-separated for SARIF URIs. */
function toUri(file, baseDir) {
    let rel = file;
    if (path.isAbsolute(file)) {
        rel = path.relative(baseDir, file);
        // If the file is outside baseDir, relative() yields ../ — fall back to the
        // absolute path rather than emitting a confusing climb-out URI.
        if (rel.startsWith('..'))
            rel = file;
    }
    return rel.split(path.sep).join('/');
}
function normalise(f) {
    if (isXRayFinding(f)) {
        const category = f.category;
        return {
            ruleId: makeRuleId(category, f.title),
            ruleName: f.title,
            level: severityToLevel(f.severity),
            message: f.description,
            file: f.file,
            line: f.line,
        };
    }
    // AuditFinding. May or may not have a filePath.
    const audit = f;
    const norm = {
        ruleId: makeRuleId(audit.scanner, audit.title),
        ruleName: audit.title,
        level: severityToLevel(audit.severity),
        message: audit.description,
        helpUri: audit.learnMoreUrl,
    };
    if (audit.filePath) {
        norm.file = audit.filePath;
    }
    else {
        // Fileless: synthesise a logical location from scanner + title so the
        // result still carries a location and stays schema-valid without a
        // physicalLocation.
        norm.logicalName = `${audit.scanner}/${audit.title}`;
    }
    return norm;
}
// ── Builder ────────────────────────────────────────────────────
function resolveVersion() {
    try {
        const require = createRequire(import.meta.url);
        const pkg = require('../../package.json');
        return pkg.version ?? '0.0.0';
    }
    catch {
        return '0.0.0';
    }
}
/**
 * Build a SARIF 2.1.0 log from a list of findings (either shape).
 * Rules are deduped into `tool.driver.rules` by ruleId.
 */
export function toSarif(findings, options = {}) {
    const version = options.version ?? resolveVersion();
    const baseDir = options.baseDir ?? process.cwd();
    const rulesById = new Map();
    const results = [];
    for (const finding of findings) {
        const n = normalise(finding);
        if (!rulesById.has(n.ruleId)) {
            const rule = {
                id: n.ruleId,
                name: n.ruleName,
                shortDescription: { text: n.ruleName },
            };
            if (n.helpUri)
                rule.helpUri = n.helpUri;
            rulesById.set(n.ruleId, rule);
        }
        const result = {
            ruleId: n.ruleId,
            level: n.level,
            message: { text: n.message },
        };
        if (n.file) {
            const physicalLocation = {
                artifactLocation: { uri: toUri(n.file, baseDir) },
            };
            if (typeof n.line === 'number') {
                physicalLocation.region = { startLine: n.line };
            }
            result.locations = [{ physicalLocation }];
        }
        else if (n.logicalName) {
            result.locations = [{ logicalLocations: [{ fullyQualifiedName: n.logicalName }] }];
        }
        results.push(result);
    }
    return {
        $schema: SARIF_SCHEMA,
        version: '2.1.0',
        runs: [
            {
                tool: {
                    driver: {
                        name: 'ShieldCortex',
                        version,
                        informationUri: INFORMATION_URI,
                        rules: Array.from(rulesById.values()),
                    },
                },
                results,
            },
        ],
    };
}
