import { readFile } from 'node:fs/promises';
import { KnowledgeSafetyService, SECRET_PATTERN_NAMES, } from '../security/knowledge-safety.js';
export async function loadSafetyFixture(filePath) {
    const raw = await readFile(filePath, 'utf8');
    const parsed = JSON.parse(raw);
    return parseSafetyFixture(parsed, filePath);
}
export function parseSafetyFixture(value, source = 'safety fixture') {
    if (!value || typeof value !== 'object') {
        throw new Error(`${source}: expected an object.`);
    }
    const record = value;
    const name = typeof record.name === 'string' && record.name.length > 0 ? record.name : 'safety-fixture';
    const description = typeof record.description === 'string' ? record.description : undefined;
    const rawCases = record.cases;
    if (!Array.isArray(rawCases) || rawCases.length === 0) {
        throw new Error(`${source}: cases array is required and non-empty.`);
    }
    const cases = rawCases.map((entry, index) => parseSafetyCase(entry, `${source}.cases[${index}]`));
    ensureUniqueIds(cases.map((entry) => entry.id), `${source}.cases[].id`);
    return { name, description, cases };
}
function parseSafetyCase(value, source) {
    if (!value || typeof value !== 'object') {
        throw new Error(`${source}: expected an object.`);
    }
    const record = value;
    const id = expectString(record.id, `${source}.id`);
    const kind = expectString(record.kind, `${source}.kind`);
    if (kind !== 'true_positive' && kind !== 'true_negative') {
        throw new Error(`${source}.kind must be 'true_positive' or 'true_negative' (was ${kind}).`);
    }
    const category = expectString(record.category, `${source}.category`);
    const text = expectString(record.text, `${source}.text`);
    let expectedPattern = null;
    if (kind === 'true_positive') {
        expectedPattern = expectString(record.expectedPattern, `${source}.expectedPattern`);
        if (!SECRET_PATTERN_NAMES.includes(expectedPattern)) {
            throw new Error(`${source}.expectedPattern must be one of [${SECRET_PATTERN_NAMES.join(', ')}] (was ${expectedPattern}).`);
        }
    }
    else if (record.expectedPattern !== null && record.expectedPattern !== undefined) {
        throw new Error(`${source}.expectedPattern must be null for true_negative cases.`);
    }
    return { id, kind, category, expectedPattern, text };
}
function expectString(value, label) {
    if (typeof value !== 'string' || value.length === 0) {
        throw new Error(`${label} is required.`);
    }
    return value;
}
function ensureUniqueIds(ids, label) {
    const seen = new Set();
    for (const id of ids) {
        if (seen.has(id)) {
            throw new Error(`${label}: duplicate id ${id}.`);
        }
        seen.add(id);
    }
}
export class SafetyEvaluator {
    safety;
    constructor(options = {}) {
        this.safety = options.safety ?? new KnowledgeSafetyService();
    }
    run(fixture) {
        const cases = fixture.cases.map((entry) => this.evaluateCase(entry));
        const overall = computeOverallMetrics(cases);
        const perPattern = computePerPatternMetrics(cases);
        return {
            fixtureName: fixture.name,
            evaluatedAt: new Date().toISOString(),
            totalCases: cases.length,
            overall,
            perPattern,
            cases,
        };
    }
    evaluateCase(entry) {
        const scan = this.safety.scanForSecrets(entry.text);
        const redacted = scan.redactionCount > 0;
        const passed = entry.kind === 'true_positive' ? redacted : !redacted;
        return {
            id: entry.id,
            kind: entry.kind,
            category: entry.category,
            expectedPattern: entry.expectedPattern,
            text: entry.text,
            redacted,
            firedPatterns: scan.firedPatterns,
            redactionCount: scan.redactionCount,
            passed,
        };
    }
}
function computeOverallMetrics(cases) {
    let truePositives = 0;
    let falseNegatives = 0;
    let falsePositives = 0;
    let trueNegatives = 0;
    for (const entry of cases) {
        if (entry.kind === 'true_positive') {
            if (entry.redacted)
                truePositives += 1;
            else
                falseNegatives += 1;
        }
        else if (entry.redacted) {
            falsePositives += 1;
        }
        else {
            trueNegatives += 1;
        }
    }
    const precision = truePositives + falsePositives === 0 ? null : truePositives / (truePositives + falsePositives);
    const recall = truePositives + falseNegatives === 0 ? null : truePositives / (truePositives + falseNegatives);
    const f1 = precision === null || recall === null || precision + recall === 0
        ? null
        : (2 * precision * recall) / (precision + recall);
    return { truePositives, falsePositives, trueNegatives, falseNegatives, precision, recall, f1 };
}
function computePerPatternMetrics(cases) {
    return SECRET_PATTERN_NAMES.map((name) => {
        let truePositives = 0;
        let falsePositives = 0;
        let falseNegatives = 0;
        for (const entry of cases) {
            const fired = entry.firedPatterns.includes(name);
            if (entry.kind === 'true_positive') {
                const expected = entry.expectedPattern === name;
                if (fired && expected)
                    truePositives += 1;
                else if (!fired && expected)
                    falseNegatives += 1;
                // fired && !expected on a TP case is treated as a benign overlap — it
                // does not count against precision because the redaction was warranted.
            }
            else if (fired) {
                falsePositives += 1;
            }
        }
        const precision = truePositives + falsePositives === 0 ? null : truePositives / (truePositives + falsePositives);
        const recall = truePositives + falseNegatives === 0 ? null : truePositives / (truePositives + falseNegatives);
        const f1 = precision === null || recall === null || precision + recall === 0
            ? null
            : (2 * precision * recall) / (precision + recall);
        return { name, truePositives, falsePositives, falseNegatives, precision, recall, f1 };
    });
}
