/**
 * False Positive Feedback
 *
 * Handles user feedback on findings and maintains a feedback database
 * for improving FP detection over time.
 *
 * @module scanners/fp-feedback
 */
import { readFile, writeFile, mkdir } from "fs/promises";
import { join, dirname } from "path";
import { markFalsePositive, markTruePositive } from "./fp-tracker.js";
import { logger } from "../logger.js";
/**
 * FP reason descriptions for UI
 */
export const FP_REASON_DESCRIPTIONS = {
    "test-code": "Finding is in test code and doesn't affect production",
    "false-pattern-match": "Pattern matched but context shows it's not a real issue",
    "sanitized-elsewhere": "Input is sanitized/validated elsewhere in the codebase",
    "intentional": "This is intentional behavior (documented risk acceptance)",
    "vendor-code": "Third-party/vendored code that cannot be modified",
    "generated-code": "Auto-generated code that will be regenerated",
    "example-code": "Example/demo code not used in production",
    "configuration": "Scanner configuration issue (rule too broad)",
    "other": "Other reason (specify in details)",
};
/**
 * Get feedback file path
 */
function getFeedbackFilePath(projectPath) {
    return join(projectPath, ".vaspera", "fp-feedback.json");
}
/**
 * Generate unique feedback ID
 */
function generateFeedbackId() {
    return `fb-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
/**
 * Load feedback database
 */
export async function loadFeedbackDatabase(projectPath) {
    const filePath = getFeedbackFilePath(projectPath);
    try {
        const content = await readFile(filePath, "utf-8");
        return JSON.parse(content);
    }
    catch {
        return {
            version: "1.0.0",
            projectPath,
            entries: [],
            stats: {
                totalFeedback: 0,
                tpCount: 0,
                fpCount: 0,
                byScanner: {},
                byReason: {},
            },
        };
    }
}
/**
 * Save feedback database
 */
export async function saveFeedbackDatabase(projectPath, db) {
    const filePath = getFeedbackFilePath(projectPath);
    await mkdir(dirname(filePath), { recursive: true });
    await writeFile(filePath, JSON.stringify(db, null, 2), "utf-8");
    logger.debug("fp_feedback.saved", { filePath, entries: db.entries.length });
}
/**
 * Submit feedback for a finding
 */
export async function submitFeedback(projectPath, finding, verdict, options) {
    const db = await loadFeedbackDatabase(projectPath);
    // Create feedback entry
    const entry = {
        id: generateFeedbackId(),
        findingId: `${finding.scanner}-${finding.ruleId}-${finding.file}:${finding.line}`,
        scanner: finding.scanner,
        ruleId: finding.ruleId,
        file: finding.file,
        line: finding.line,
        severity: finding.severity,
        verdict,
        reason: verdict === "fp" ? options?.reason : undefined,
        details: options?.details,
        submittedBy: options?.submittedBy,
        submittedAt: new Date().toISOString(),
        codeHash: options?.codeHash,
    };
    // Add to database
    db.entries.push(entry);
    // Update stats
    db.stats.totalFeedback++;
    if (verdict === "tp") {
        db.stats.tpCount++;
    }
    else {
        db.stats.fpCount++;
        if (options?.reason) {
            db.stats.byReason[options.reason] = (db.stats.byReason[options.reason] || 0) + 1;
        }
    }
    // Update scanner stats
    if (!db.stats.byScanner[finding.scanner]) {
        db.stats.byScanner[finding.scanner] = { tp: 0, fp: 0 };
    }
    db.stats.byScanner[finding.scanner][verdict]++;
    await saveFeedbackDatabase(projectPath, db);
    // Also update the FP tracker
    if (verdict === "fp") {
        await markFalsePositive(projectPath, finding.scanner, finding.ruleId, options?.reason);
    }
    else {
        await markTruePositive(projectPath, finding.scanner, finding.ruleId);
    }
    logger.info("fp_feedback.submitted", {
        id: entry.id,
        scanner: finding.scanner,
        ruleId: finding.ruleId,
        verdict,
        reason: options?.reason,
    });
    return entry;
}
/**
 * Get feedback for a specific finding
 */
export async function getFeedbackForFinding(projectPath, scanner, ruleId, file, line) {
    const db = await loadFeedbackDatabase(projectPath);
    return db.entries.filter((entry) => {
        const matchesScanner = entry.scanner === scanner;
        const matchesRule = entry.ruleId === ruleId;
        const matchesFile = entry.file === file;
        const matchesLine = line === undefined || entry.line === line;
        return matchesScanner && matchesRule && matchesFile && matchesLine;
    });
}
/**
 * Get all feedback for a rule
 */
export async function getFeedbackForRule(projectPath, scanner, ruleId) {
    const db = await loadFeedbackDatabase(projectPath);
    return db.entries.filter((entry) => entry.scanner === scanner && entry.ruleId === ruleId);
}
/**
 * Check if a finding has existing feedback
 */
export async function hasFeedback(projectPath, scanner, ruleId, file, line) {
    const feedback = await getFeedbackForFinding(projectPath, scanner, ruleId, file, line);
    return feedback.length > 0;
}
/**
 * Get suppression suggestions based on feedback
 */
export async function getSuppressionSuggestions(projectPath, options) {
    const minFPRate = options?.minFPRate ?? 0.5;
    const minSampleSize = options?.minSampleSize ?? 5;
    const db = await loadFeedbackDatabase(projectPath);
    // Group feedback by scanner/rule
    const ruleGroups = new Map();
    for (const entry of db.entries) {
        const key = `${entry.scanner}:${entry.ruleId}`;
        const existing = ruleGroups.get(key) || [];
        existing.push(entry);
        ruleGroups.set(key, existing);
    }
    const suggestions = [];
    for (const [key, entries] of ruleGroups) {
        const [scanner, ruleId] = key.split(":");
        if (entries.length < minSampleSize) {
            continue;
        }
        const fpCount = entries.filter((e) => e.verdict === "fp").length;
        const fpRate = fpCount / entries.length;
        if (fpRate < minFPRate) {
            continue;
        }
        // Get common FP reasons
        const reasonCounts = new Map();
        for (const entry of entries) {
            if (entry.verdict === "fp" && entry.reason) {
                reasonCounts.set(entry.reason, (reasonCounts.get(entry.reason) || 0) + 1);
            }
        }
        const commonReasons = Array.from(reasonCounts.entries())
            .sort((a, b) => b[1] - a[1])
            .slice(0, 3)
            .map(([reason]) => reason);
        // Determine suggestion
        let suggestion;
        if (fpRate >= 0.8) {
            suggestion = "disable";
        }
        else if (fpRate >= 0.5) {
            suggestion = "review";
        }
        else {
            suggestion = "keep";
        }
        suggestions.push({
            scanner,
            ruleId,
            fpRate,
            sampleSize: entries.length,
            suggestion,
            commonReasons,
        });
    }
    // Sort by FP rate descending
    return suggestions.sort((a, b) => b.fpRate - a.fpRate);
}
/**
 * Generate feedback summary report
 */
export async function generateFeedbackReport(projectPath) {
    const db = await loadFeedbackDatabase(projectPath);
    const suppressionSuggestions = await getSuppressionSuggestions(projectPath);
    const totalFeedback = db.stats.totalFeedback;
    const overallFPRate = totalFeedback > 0 ? db.stats.fpCount / totalFeedback : 0;
    // By scanner
    const byScanner = Object.entries(db.stats.byScanner).map(([scanner, stats]) => ({
        scanner: scanner,
        total: stats.tp + stats.fp,
        tp: stats.tp,
        fp: stats.fp,
        fpRate: (stats.tp + stats.fp) > 0 ? stats.fp / (stats.tp + stats.fp) : 0,
    }));
    // Top FP reasons
    const topFPReasons = Object.entries(db.stats.byReason)
        .sort(([, a], [, b]) => b - a)
        .slice(0, 5)
        .map(([reason, count]) => ({
        reason: reason,
        count,
        percentage: db.stats.fpCount > 0 ? (count / db.stats.fpCount) * 100 : 0,
    }));
    // Recent feedback (last 10)
    const recentFeedback = [...db.entries]
        .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime())
        .slice(0, 10);
    return {
        overview: {
            totalFeedback,
            tpCount: db.stats.tpCount,
            fpCount: db.stats.fpCount,
            overallFPRate,
        },
        byScanner,
        topFPReasons,
        recentFeedback,
        suppressionSuggestions,
    };
}
/**
 * Clear old feedback entries
 */
export async function pruneOldFeedback(projectPath, maxAgeDays = 90) {
    const db = await loadFeedbackDatabase(projectPath);
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
    const originalCount = db.entries.length;
    db.entries = db.entries.filter((entry) => {
        return new Date(entry.submittedAt) >= cutoffDate;
    });
    const removedCount = originalCount - db.entries.length;
    if (removedCount > 0) {
        // Recalculate stats
        db.stats = {
            totalFeedback: db.entries.length,
            tpCount: db.entries.filter((e) => e.verdict === "tp").length,
            fpCount: db.entries.filter((e) => e.verdict === "fp").length,
            byScanner: {},
            byReason: {},
        };
        for (const entry of db.entries) {
            if (!db.stats.byScanner[entry.scanner]) {
                db.stats.byScanner[entry.scanner] = { tp: 0, fp: 0 };
            }
            db.stats.byScanner[entry.scanner][entry.verdict]++;
            if (entry.verdict === "fp" && entry.reason) {
                db.stats.byReason[entry.reason] = (db.stats.byReason[entry.reason] || 0) + 1;
            }
        }
        await saveFeedbackDatabase(projectPath, db);
        logger.info("fp_feedback.pruned", { removed: removedCount, remaining: db.entries.length });
    }
    return removedCount;
}
//# sourceMappingURL=fp-feedback.js.map