/**
 * BiasMonitor -- EU AI Act Art. 9 bias assessment and OOD detection.
 *
 * Monitors AI agent outputs for statistical bias, demographic
 * imbalance, and out-of-distribution inputs. Required for
 * high-risk AI systems under EU AI Act Art. 9 (risk management)
 * and Art. 10 (data governance).
 *
 * T-072: Bias assessment for EU AI Act Art. 9.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from '../util/log.js';
const log = createLogger('bias-monitor');
export class BiasMonitor {
    config;
    samplesFile;
    oodFile;
    assessmentFile;
    constructor(config) {
        this.config = {
            minSamples: 20,
            biasThreshold: 0.15,
            oodThreshold: 3.0,
            ...config,
        };
        if (!fs.existsSync(config.dataDir)) {
            fs.mkdirSync(config.dataDir, { recursive: true });
        }
        this.samplesFile = path.join(config.dataDir, `bias-samples-${config.agentName}.json`);
        this.oodFile = path.join(config.dataDir, `ood-${config.agentName}.json`);
        this.assessmentFile = path.join(config.dataDir, `bias-assessment-${config.agentName}.json`);
    }
    /**
     * Record a sample for bias tracking.
     *
     * Categories map demographic/protected attributes to group values.
     * Example: { gender: 'female', age_group: '25-34', region: 'EU' }
     */
    recordSample(categories, numericFeatures) {
        const samples = this.loadSamples();
        samples.push({
            timestamp: new Date().toISOString(),
            categories,
            numericFeatures,
        });
        // Keep last 10000 samples
        const trimmed = samples.slice(-10000);
        this.saveSamples(trimmed);
    }
    /**
     * Run bias assessment across all recorded samples.
     */
    assess() {
        const samples = this.loadSamples();
        const assessment = {
            agentName: this.config.agentName,
            assessedAt: new Date().toISOString(),
            totalSamples: samples.length,
            categories: [],
            overallBiased: false,
            biasScore: 0,
            oodDetections: this.loadOOD().length,
            recommendations: [],
        };
        if (samples.length < this.config.minSamples) {
            assessment.recommendations.push(`Insufficient samples (${samples.length}/${this.config.minSamples}). Collect more data.`);
            this.saveAssessment(assessment);
            return assessment;
        }
        // Analyze each category for bias
        const categoryNames = new Set();
        for (const s of samples) {
            for (const cat of Object.keys(s.categories)) {
                categoryNames.add(cat);
            }
        }
        let totalBias = 0;
        let categoryCount = 0;
        for (const catName of categoryNames) {
            const category = this.analyzeCategory(catName, samples);
            assessment.categories.push(category);
            if (category.biased) {
                assessment.overallBiased = true;
                totalBias += Math.min(1, category.chiSquare / 20); // Normalize
            }
            categoryCount++;
        }
        assessment.biasScore = categoryCount > 0 ? totalBias / categoryCount : 0;
        // Generate recommendations
        if (assessment.overallBiased) {
            const biasedCats = assessment.categories.filter(c => c.biased);
            for (const cat of biasedCats) {
                const overrep = cat.groups.filter(g => g.deviation > this.config.biasThreshold);
                const underrep = cat.groups.filter(g => g.deviation < -this.config.biasThreshold);
                if (overrep.length > 0) {
                    assessment.recommendations.push(`[Art. 9] Category "${cat.name}": Groups ${overrep.map(g => g.group).join(', ')} are over-represented.`);
                }
                if (underrep.length > 0) {
                    assessment.recommendations.push(`[Art. 9] Category "${cat.name}": Groups ${underrep.map(g => g.group).join(', ')} are under-represented.`);
                }
            }
            assessment.recommendations.push('[Art. 10] Review training data for representation bias.', '[Art. 9] Document bias findings in risk management system.');
        }
        if (assessment.oodDetections > 0) {
            assessment.recommendations.push(`[Art. 9] ${assessment.oodDetections} out-of-distribution detections. Review input data quality.`);
        }
        this.saveAssessment(assessment);
        if (assessment.overallBiased) {
            log.warn('Bias detected in agent outputs', {
                agent: this.config.agentName,
                score: assessment.biasScore.toFixed(3),
                categories: assessment.categories.filter(c => c.biased).map(c => c.name),
            });
        }
        return assessment;
    }
    /**
     * Check a numeric input for out-of-distribution anomalies.
     */
    checkOOD(metric, value) {
        const samples = this.loadSamples();
        const values = samples
            .filter(s => s.numericFeatures && s.numericFeatures[metric] !== undefined)
            .map(s => s.numericFeatures[metric]);
        if (values.length < this.config.minSamples)
            return null;
        const mean = values.reduce((a, b) => a + b, 0) / values.length;
        const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length;
        const stdDev = Math.sqrt(variance);
        if (stdDev === 0)
            return null;
        const zScore = Math.abs((value - mean) / stdDev);
        if (zScore >= this.config.oodThreshold) {
            const detection = {
                inputHash: `ood_${Date.now()}`,
                metric,
                value,
                mean,
                stdDev,
                zScore,
                detectedAt: new Date().toISOString(),
                agentName: this.config.agentName,
            };
            this.persistOOD(detection);
            log.warn('Out-of-distribution input detected', {
                metric,
                zScore: zScore.toFixed(2),
                agent: this.config.agentName,
            });
            return detection;
        }
        return null;
    }
    /**
     * Get the most recent bias assessment.
     */
    getLatestAssessment() {
        if (!fs.existsSync(this.assessmentFile))
            return null;
        try {
            return JSON.parse(fs.readFileSync(this.assessmentFile, 'utf-8'));
        }
        catch {
            return null;
        }
    }
    /**
     * Get OOD detection history.
     */
    getOODHistory() {
        return this.loadOOD();
    }
    /**
     * Get sample count.
     */
    getSampleCount() {
        return this.loadSamples().length;
    }
    // --- Private ---
    analyzeCategory(catName, samples) {
        // Count occurrences of each group
        const groupCounts = new Map();
        let total = 0;
        for (const s of samples) {
            const group = s.categories[catName];
            if (group) {
                groupCounts.set(group, (groupCounts.get(group) || 0) + 1);
                total++;
            }
        }
        if (total === 0) {
            return { name: catName, groups: [], biased: false, chiSquare: 0 };
        }
        const numGroups = groupCounts.size;
        const expectedRate = 1 / numGroups; // Uniform distribution assumption
        const expectedCount = total / numGroups;
        let chiSquare = 0;
        const groups = [];
        for (const [group, count] of groupCounts) {
            const rate = count / total;
            const deviation = rate - expectedRate;
            const biased = Math.abs(deviation) > this.config.biasThreshold;
            // Chi-square contribution
            chiSquare += Math.pow(count - expectedCount, 2) / expectedCount;
            groups.push({
                category: catName,
                group,
                count,
                rate,
                expectedRate,
                deviation,
                biased,
            });
        }
        // Chi-square critical value at p=0.05 for df=numGroups-1
        // Simplified: use 3.841 for df=1, 5.991 for df=2, etc.
        const criticalValues = { 1: 3.841, 2: 5.991, 3: 7.815, 4: 9.488, 5: 11.07 };
        const df = numGroups - 1;
        const critical = criticalValues[df] ?? (df * 2.5); // Rough approximation for higher df
        return {
            name: catName,
            groups,
            biased: chiSquare > critical,
            chiSquare,
        };
    }
    loadSamples() {
        if (!fs.existsSync(this.samplesFile))
            return [];
        try {
            return JSON.parse(fs.readFileSync(this.samplesFile, 'utf-8'));
        }
        catch {
            return [];
        }
    }
    saveSamples(samples) {
        fs.writeFileSync(this.samplesFile, JSON.stringify(samples));
    }
    loadOOD() {
        if (!fs.existsSync(this.oodFile))
            return [];
        try {
            return JSON.parse(fs.readFileSync(this.oodFile, 'utf-8'));
        }
        catch {
            return [];
        }
    }
    persistOOD(detection) {
        try {
            const existing = this.loadOOD();
            existing.push(detection);
            const trimmed = existing.slice(-500);
            fs.writeFileSync(this.oodFile, JSON.stringify(trimmed));
        }
        catch {
            log.error('Failed to persist OOD detection');
        }
    }
    saveAssessment(assessment) {
        try {
            fs.writeFileSync(this.assessmentFile, JSON.stringify(assessment, null, 2));
        }
        catch {
            log.error('Failed to save bias assessment');
        }
    }
}
//# sourceMappingURL=bias-monitor.js.map