/**
 * Evaluation Metrics
 *
 * Calculates precision, recall, F1 score, and other metrics
 * from evaluation results.
 *
 * @module eval/metrics
 */
/**
 * Calculate precision from true positives and false positives
 * Precision = TP / (TP + FP)
 */
export function calculatePrecision(truePositives, falsePositives) {
    const total = truePositives + falsePositives;
    if (total === 0)
        return 1; // No findings = perfect precision (no FPs)
    return truePositives / total;
}
/**
 * Calculate recall from true positives and false negatives
 * Recall = TP / (TP + FN)
 */
export function calculateRecall(truePositives, falseNegatives) {
    const total = truePositives + falseNegatives;
    if (total === 0)
        return 1; // No expected findings = perfect recall
    return truePositives / total;
}
/**
 * Calculate F1 score from precision and recall
 * F1 = 2 * (precision * recall) / (precision + recall)
 */
export function calculateF1Score(precision, recall) {
    if (precision + recall === 0)
        return 0;
    return (2 * precision * recall) / (precision + recall);
}
/**
 * Calculate aggregate metrics from fixture results
 */
export function calculateMetrics(results) {
    let totalExpected = 0;
    let totalActual = 0;
    let totalTruePositives = 0;
    let totalFalsePositives = 0;
    let totalFalseNegatives = 0;
    let totalDuration = 0;
    const byCategory = {};
    const byScanner = {};
    let successfulFixtures = 0;
    let failedFixtures = 0;
    for (const result of results) {
        if (result.success) {
            successfulFixtures++;
        }
        else {
            failedFixtures++;
        }
        totalExpected += result.expectedFindings.filter((e) => e.isVulnerable).length;
        totalActual += result.actualFindings.length;
        totalTruePositives += result.truePositives.length;
        totalFalsePositives += result.falsePositives.length;
        totalFalseNegatives += result.falseNegatives.length;
        totalDuration += result.duration;
        // Aggregate by category
        for (const expected of result.expectedFindings) {
            if (!expected.isVulnerable)
                continue;
            const cat = expected.category;
            if (!byCategory[cat]) {
                byCategory[cat] = {
                    expected: 0,
                    truePositives: 0,
                    falsePositives: 0,
                    falseNegatives: 0,
                };
            }
            byCategory[cat].expected++;
        }
        for (const tp of result.truePositives) {
            const cat = tp.expected.category;
            if (byCategory[cat]) {
                byCategory[cat].truePositives++;
            }
        }
        for (const fn of result.falseNegatives) {
            const cat = fn.category;
            if (byCategory[cat]) {
                byCategory[cat].falseNegatives++;
            }
        }
        // Aggregate by scanner
        for (const actual of result.actualFindings) {
            const scanner = actual.scanner;
            if (!byScanner[scanner]) {
                byScanner[scanner] = {
                    findings: 0,
                    truePositives: 0,
                    falsePositives: 0,
                };
            }
            byScanner[scanner].findings++;
        }
        for (const tp of result.truePositives) {
            const scanner = tp.actual.scanner;
            if (byScanner[scanner]) {
                byScanner[scanner].truePositives++;
            }
        }
        for (const fp of result.falsePositives) {
            const scanner = fp.scanner;
            if (byScanner[scanner]) {
                byScanner[scanner].falsePositives++;
            }
        }
    }
    // Calculate overall metrics
    const precision = calculatePrecision(totalTruePositives, totalFalsePositives);
    const recall = calculateRecall(totalTruePositives, totalFalseNegatives);
    const f1Score = calculateF1Score(precision, recall);
    // Calculate per-category metrics
    const categoryMetrics = {};
    for (const [cat, data] of Object.entries(byCategory)) {
        const catPrecision = calculatePrecision(data.truePositives, data.falsePositives);
        const catRecall = calculateRecall(data.truePositives, data.falseNegatives);
        categoryMetrics[cat] = {
            expected: data.expected,
            truePositives: data.truePositives,
            falsePositives: data.falsePositives,
            falseNegatives: data.falseNegatives,
            precision: catPrecision,
            recall: catRecall,
            f1Score: calculateF1Score(catPrecision, catRecall),
        };
    }
    // Calculate per-scanner metrics
    const scannerMetrics = {};
    for (const [scanner, data] of Object.entries(byScanner)) {
        scannerMetrics[scanner] = {
            findings: data.findings,
            truePositives: data.truePositives,
            falsePositives: data.falsePositives,
            precision: calculatePrecision(data.truePositives, data.falsePositives),
        };
    }
    return {
        totalFixtures: results.length,
        successfulFixtures,
        failedFixtures,
        totalExpected,
        totalActual,
        totalTruePositives,
        totalFalsePositives,
        totalFalseNegatives,
        precision,
        recall,
        f1Score,
        byCategory: categoryMetrics,
        byScanner: scannerMetrics,
        totalDuration,
        timestamp: new Date().toISOString(),
    };
}
/**
 * Calculate stability metrics from multiple runs
 */
export function calculateStabilityMetrics(allRuns) {
    if (allRuns.length === 0) {
        return {
            runs: 0,
            consistentFindings: 0,
            inconsistentFindings: 0,
            stabilityPercent: 100,
            byFixture: {},
        };
    }
    const runs = allRuns.length;
    const byFixture = {};
    // Group results by fixture
    const fixtureResults = new Map();
    for (const run of allRuns) {
        for (const result of run) {
            if (!fixtureResults.has(result.fixtureId)) {
                fixtureResults.set(result.fixtureId, []);
            }
            fixtureResults.get(result.fixtureId).push(result);
        }
    }
    let totalConsistent = 0;
    let totalInconsistent = 0;
    for (const [fixtureId, results] of fixtureResults) {
        // Count findings by signature across all runs
        const findingCounts = new Map();
        for (const result of results) {
            for (const finding of result.actualFindings) {
                const signature = `${finding.file}:${finding.line}:${finding.ruleId}`;
                findingCounts.set(signature, (findingCounts.get(signature) || 0) + 1);
            }
        }
        // Consistent = appeared in all runs, Inconsistent = appeared in some
        let consistent = 0;
        let inconsistent = 0;
        for (const count of findingCounts.values()) {
            if (count === runs) {
                consistent++;
            }
            else {
                inconsistent++;
            }
        }
        const totalFindings = consistent + inconsistent;
        byFixture[fixtureId] = totalFindings > 0 ? consistent / totalFindings : 1;
        totalConsistent += consistent;
        totalInconsistent += inconsistent;
    }
    const totalFindings = totalConsistent + totalInconsistent;
    const stabilityPercent = totalFindings > 0
        ? (totalConsistent / totalFindings) * 100
        : 100;
    return {
        runs,
        consistentFindings: totalConsistent,
        inconsistentFindings: totalInconsistent,
        stabilityPercent,
        byFixture,
    };
}
/**
 * Check if metrics meet target thresholds
 */
export function meetsTargets(metrics, targets = {}) {
    const defaultTargets = {
        precision: 0.9,
        recall: 0.85,
        f1Score: 0.87,
    };
    const finalTargets = { ...defaultTargets, ...targets };
    const results = {};
    let meetsAll = true;
    if (finalTargets.precision !== undefined) {
        const met = metrics.precision >= finalTargets.precision;
        results.precision = {
            target: finalTargets.precision,
            actual: metrics.precision,
            met,
        };
        if (!met)
            meetsAll = false;
    }
    if (finalTargets.recall !== undefined) {
        const met = metrics.recall >= finalTargets.recall;
        results.recall = {
            target: finalTargets.recall,
            actual: metrics.recall,
            met,
        };
        if (!met)
            meetsAll = false;
    }
    if (finalTargets.f1Score !== undefined) {
        const met = metrics.f1Score >= finalTargets.f1Score;
        results.f1Score = {
            target: finalTargets.f1Score,
            actual: metrics.f1Score,
            met,
        };
        if (!met)
            meetsAll = false;
    }
    return { meetsAll, results };
}
/**
 * Format percentage for display
 */
export function formatPercent(value, decimals = 1) {
    return (value * 100).toFixed(decimals) + "%";
}
/**
 * Get grade based on score
 */
export function getGrade(score) {
    if (score >= 0.95)
        return "A+";
    if (score >= 0.9)
        return "A";
    if (score >= 0.85)
        return "B+";
    if (score >= 0.8)
        return "B";
    if (score >= 0.75)
        return "C+";
    if (score >= 0.7)
        return "C";
    if (score >= 0.6)
        return "D";
    return "F";
}
//# sourceMappingURL=metrics.js.map