/**
 * PolicyTuningEngine: False positive reduction system for governance decisions.
 *
 * Tracks override patterns, computes confidence scores, generates tuning
 * reports, and suggests policy relaxations. Critical for channel partner
 * adoption: partners need to streamline what passes through to reduce
 * false positives for their clients.
 *
 * @module governance/policy-tuning
 */
import * as crypto from 'node:crypto';
// ---- Implementation ----
const DEFAULT_CONFIG = {
    suggestionThreshold: 5,
    windowMs: 7 * 24 * 60 * 60 * 1000, // 7 days
    maxRecords: 10_000,
    shadowMode: false,
};
export class PolicyTuningEngine {
    config;
    decisions = [];
    overrides = [];
    shadowLog = [];
    constructor(config = {}) {
        this.config = { ...DEFAULT_CONFIG, ...config };
    }
    /**
     * Record a governance decision for tracking.
     */
    recordDecision(record) {
        this.decisions.push({ ...record, overridden: false });
        this.trimRecords();
    }
    /**
     * Record a shadow mode decision (what WOULD have been blocked).
     */
    recordShadowDecision(shadow) {
        this.shadowLog.push(shadow);
        if (this.shadowLog.length > this.config.maxRecords) {
            this.shadowLog = this.shadowLog.slice(-this.config.maxRecords);
        }
    }
    /**
     * Submit override feedback (false positive report).
     */
    submitOverride(feedback) {
        const override = {
            ...feedback,
            id: crypto.randomUUID(),
            timestamp: new Date().toISOString(),
        };
        this.overrides.push(override);
        // Mark the original decision as overridden
        const decision = this.decisions.find(d => d.auditId === feedback.auditId);
        if (decision) {
            decision.overridden = true;
        }
        this.trimRecords();
        return override;
    }
    /**
     * Compute confidence score for a governance decision.
     *
     * Lower confidence = more likely to be a false positive based on
     * historical override patterns.
     */
    computeConfidence(tool, agentName, policyRule, orgId) {
        const factors = [];
        const cutoff = Date.now() - this.config.windowMs;
        // Factor 1: Tool override rate
        const toolDecisions = this.decisions.filter(d => d.tool === tool && d.orgId === orgId && new Date(d.timestamp).getTime() > cutoff);
        const toolOverrides = toolDecisions.filter(d => d.overridden).length;
        const toolRate = toolDecisions.length > 0 ? toolOverrides / toolDecisions.length : 0;
        factors.push({
            name: 'tool_override_rate',
            weight: 0.3,
            value: 1 - toolRate,
            description: `${tool}: ${toolOverrides}/${toolDecisions.length} overridden`,
        });
        // Factor 2: Agent override rate
        const agentDecisions = this.decisions.filter(d => d.agentName === agentName && d.orgId === orgId && new Date(d.timestamp).getTime() > cutoff);
        const agentOverrides = agentDecisions.filter(d => d.overridden).length;
        const agentRate = agentDecisions.length > 0 ? agentOverrides / agentDecisions.length : 0;
        factors.push({
            name: 'agent_override_rate',
            weight: 0.2,
            value: 1 - agentRate,
            description: `${agentName}: ${agentOverrides}/${agentDecisions.length} overridden`,
        });
        // Factor 3: Policy rule override rate
        const policyDecisions = this.decisions.filter(d => d.policyRule === policyRule && d.orgId === orgId && new Date(d.timestamp).getTime() > cutoff);
        const policyOverrides = policyDecisions.filter(d => d.overridden).length;
        const policyRate = policyDecisions.length > 0 ? policyOverrides / policyDecisions.length : 0;
        factors.push({
            name: 'policy_override_rate',
            weight: 0.3,
            value: 1 - policyRate,
            description: `${policyRule}: ${policyOverrides}/${policyDecisions.length} overridden`,
        });
        // Factor 4: Combination novelty (have we seen this exact combo before?)
        const comboDecisions = this.decisions.filter(d => d.tool === tool &&
            d.agentName === agentName &&
            d.policyRule === policyRule &&
            d.orgId === orgId &&
            new Date(d.timestamp).getTime() > cutoff);
        const comboOverrides = comboDecisions.filter(d => d.overridden).length;
        const comboRate = comboDecisions.length > 0 ? comboOverrides / comboDecisions.length : 0;
        factors.push({
            name: 'combination_override_rate',
            weight: 0.2,
            value: 1 - comboRate,
            description: `${tool}+${agentName}+${policyRule}: ${comboOverrides}/${comboDecisions.length} overridden`,
        });
        // Weighted sum
        const score = factors.reduce((sum, f) => sum + f.weight * f.value, 0);
        return { score: Math.round(score * 1000) / 1000, factors };
    }
    /**
     * Generate a tuning report for an organization.
     */
    generateReport(orgId, periodMs) {
        const window = periodMs ?? this.config.windowMs;
        const cutoff = Date.now() - window;
        const periodStart = new Date(cutoff).toISOString();
        const periodEnd = new Date().toISOString();
        const orgDecisions = this.decisions.filter(d => d.orgId === orgId && new Date(d.timestamp).getTime() > cutoff);
        const orgOverrides = orgDecisions.filter(d => d.overridden);
        const totalDecisions = orgDecisions.length;
        const totalOverrides = orgOverrides.length;
        const falsePositiveRate = totalDecisions > 0 ? totalOverrides / totalDecisions : 0;
        // Aggregate by tool
        const topByTool = this.aggregateBy(orgDecisions, d => d.tool);
        // Aggregate by agent
        const topByAgent = this.aggregateBy(orgDecisions, d => d.agentName);
        // Aggregate by policy
        const topByPolicy = this.aggregateBy(orgDecisions, d => d.policyRule);
        // Generate suggestions
        const suggestions = this.generateSuggestions(orgDecisions);
        return {
            periodStart,
            periodEnd,
            orgId,
            totalDecisions,
            totalOverrides,
            falsePositiveRate: Math.round(falsePositiveRate * 10000) / 10000,
            topByTool,
            topByAgent,
            topByPolicy,
            suggestions,
        };
    }
    /**
     * Generate partner-level aggregate report across all client orgs.
     */
    generatePartnerReport(partnerOrgId, clientOrgIds, periodMs) {
        const clientReports = clientOrgIds.map(orgId => this.generateReport(orgId, periodMs));
        const totalDecisions = clientReports.reduce((sum, r) => sum + r.totalDecisions, 0);
        const totalOverrides = clientReports.reduce((sum, r) => sum + r.totalOverrides, 0);
        const aggregateFalsePositiveRate = totalDecisions > 0 ? totalOverrides / totalDecisions : 0;
        // Collect all suggestions, deduplicate by policy rule, sort by override count
        const allSuggestions = clientReports.flatMap(r => r.suggestions);
        const uniqueSuggestions = new Map();
        for (const s of allSuggestions) {
            const key = `${s.policyRule}:${s.tool}`;
            const existing = uniqueSuggestions.get(key);
            if (!existing || s.overrideCount > existing.overrideCount) {
                uniqueSuggestions.set(key, s);
            }
        }
        const topSuggestionsAcrossClients = Array.from(uniqueSuggestions.values())
            .sort((a, b) => b.overrideCount - a.overrideCount)
            .slice(0, 10);
        return {
            partnerOrgId,
            clientReports,
            aggregateFalsePositiveRate: Math.round(aggregateFalsePositiveRate * 10000) / 10000,
            topSuggestionsAcrossClients,
        };
    }
    /**
     * Get shadow mode log entries.
     */
    getShadowLog(orgId) {
        return [...this.shadowLog];
    }
    /**
     * Check if shadow mode is enabled.
     */
    isShadowMode() {
        return this.config.shadowMode;
    }
    /**
     * Get all overrides for an org.
     */
    getOverrides(orgId) {
        return this.overrides.filter(o => o.orgId === orgId);
    }
    /**
     * Get decision count for an org.
     */
    getDecisionCount(orgId) {
        return this.decisions.filter(d => d.orgId === orgId).length;
    }
    /**
     * P1C-03: per-policy-rule confidence summary for the UI.
     *
     * Returns one row per distinct policyRule observed in the window:
     *   - decisions fired under this rule
     *   - how many were overridden by the operator
     *   - confidence score (higher = fewer overrides, policy trusted)
     *   - most-recent timestamp so the UI can sort "freshest first"
     */
    getRuleConfidences(orgId, windowMs) {
        const window = windowMs ?? this.config.windowMs;
        const cutoff = Date.now() - window;
        const orgDecisions = this.decisions.filter((d) => d.orgId === orgId && new Date(d.timestamp).getTime() > cutoff);
        const byRule = new Map();
        for (const d of orgDecisions) {
            const entry = byRule.get(d.policyRule) ?? { total: 0, overrides: 0, lastSeenAt: d.timestamp };
            entry.total += 1;
            if (d.overridden)
                entry.overrides += 1;
            if (d.timestamp > entry.lastSeenAt)
                entry.lastSeenAt = d.timestamp;
            byRule.set(d.policyRule, entry);
        }
        return Array.from(byRule.entries())
            .map(([policyRule, entry]) => {
            const overrideRate = entry.total > 0 ? entry.overrides / entry.total : 0;
            // Confidence bias: policies with no data default to neutral 0.5
            // so the UI can distinguish them from "proven good" (rate=0, many samples).
            const confidence = entry.total === 0
                ? 0.5
                : Math.round((1 - overrideRate) * 1000) / 1000;
            return {
                policyRule,
                totalDecisions: entry.total,
                overrides: entry.overrides,
                overrideRate: Math.round(overrideRate * 1000) / 1000,
                confidence,
                lastSeenAt: entry.lastSeenAt,
            };
        })
            .sort((a, b) => b.totalDecisions - a.totalDecisions);
    }
    // ---- Private helpers ----
    aggregateBy(decisions, keyFn) {
        const groups = new Map();
        for (const d of decisions) {
            const key = keyFn(d);
            const group = groups.get(key) ?? { total: 0, overrides: 0 };
            group.total++;
            if (d.overridden)
                group.overrides++;
            groups.set(key, group);
        }
        return Array.from(groups.entries())
            .map(([key, { total, overrides }]) => ({
            key,
            overrideCount: overrides,
            totalDecisions: total,
            rate: Math.round((overrides / total) * 10000) / 10000,
        }))
            .filter(e => e.overrideCount > 0)
            .sort((a, b) => b.overrideCount - a.overrideCount)
            .slice(0, 10);
    }
    generateSuggestions(decisions) {
        const suggestions = [];
        // Group by policy rule + tool
        const groups = new Map();
        for (const d of decisions) {
            if (d.decision === 'ALLOW')
                continue; // Only suggest for blocks
            const key = `${d.policyRule}:${d.tool}`;
            const group = groups.get(key) ?? [];
            group.push(d);
            groups.set(key, group);
        }
        for (const [key, group] of groups) {
            const overrides = group.filter(d => d.overridden).length;
            if (overrides < this.config.suggestionThreshold)
                continue;
            const rate = overrides / group.length;
            const [policyRule, tool] = key.split(':');
            let suggestedAction;
            let rationale;
            if (rate > 0.8) {
                suggestedAction = 'ALLOW';
                rationale = `${Math.round(rate * 100)}% override rate. This rule is blocking legitimate work.`;
            }
            else if (rate > 0.5) {
                suggestedAction = 'REQUIRE_APPROVAL';
                rationale = `${Math.round(rate * 100)}% override rate. Consider approval-based flow instead of hard block.`;
            }
            else {
                suggestedAction = 'SHADOW';
                rationale = `${Math.round(rate * 100)}% override rate. Run in shadow mode to gather more data.`;
            }
            suggestions.push({
                id: crypto.randomUUID(),
                policyRule,
                tool,
                currentAction: group[0].decision,
                suggestedAction,
                overrideCount: overrides,
                confidence: Math.round((1 - rate) * 1000) / 1000,
                rationale,
            });
        }
        return suggestions.sort((a, b) => b.overrideCount - a.overrideCount);
    }
    trimRecords() {
        if (this.decisions.length > this.config.maxRecords) {
            this.decisions = this.decisions.slice(-this.config.maxRecords);
        }
        if (this.overrides.length > this.config.maxRecords) {
            this.overrides = this.overrides.slice(-this.config.maxRecords);
        }
    }
}
//# sourceMappingURL=policy-tuning.js.map