// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * Decision Aggregator
 *
 * Aggregates decisions from multiple policy sources with:
 * - Conflict resolution strategies
 * - Decision audit trail
 * - Source weighting
 *
 * @packageDocumentation
 */
import { createLogger } from '../common/logger.js';
import { withSpan } from '../common/trace.js';
import { secureRandomString } from '../common/random.js';
const logger = createLogger({ component: 'decision-aggregator' });
/**
 * Action restrictiveness order (higher index = more restrictive)
 */
const ACTION_RESTRICTIVENESS = [
    'allow',
    'monitor',
    'limit',
    'escalate',
    'terminate',
    'deny',
];
// =============================================================================
// DECISION AGGREGATOR
// =============================================================================
/**
 * DecisionAggregator class for combining decisions from multiple sources
 */
export class DecisionAggregator {
    options;
    auditTrail = [];
    sourcePriorities;
    sourceWeights;
    constructor(options = {}) {
        this.options = {
            defaultStrategy: options.defaultStrategy ?? 'deny-overrides',
            enableTracing: options.enableTracing ?? true,
            enableAudit: options.enableAudit ?? true,
            maxAuditEntries: options.maxAuditEntries ?? 10000,
            sourcePriorities: options.sourcePriorities ?? new Map(),
            defaultWeights: options.defaultWeights ?? new Map(),
            minConfidenceThreshold: options.minConfidenceThreshold ?? 0,
        };
        // Initialize default priorities (lower = higher priority)
        this.sourcePriorities = new Map([
            ['override', 0],
            ['escalation-engine', 10],
            ['policy-engine', 20],
            ['constraint-evaluator', 30],
            ['governance-engine', 40],
            ['external', 50],
            ...this.options.sourcePriorities,
        ]);
        // Initialize default weights
        this.sourceWeights = new Map([
            ['override', 1.0],
            ['escalation-engine', 0.9],
            ['policy-engine', 0.8],
            ['constraint-evaluator', 0.7],
            ['governance-engine', 0.7],
            ['external', 0.5],
            ...this.options.defaultWeights,
        ]);
        logger.info({
            defaultStrategy: this.options.defaultStrategy,
            enableAudit: this.options.enableAudit,
        }, 'Decision aggregator initialized');
    }
    /**
     * Aggregate decisions from multiple sources
     */
    aggregate(decisions, context, strategy) {
        const startTime = performance.now();
        const effectiveStrategy = strategy ?? this.options.defaultStrategy;
        // Filter by confidence threshold
        const validDecisions = decisions.filter(d => d.confidence >= this.options.minConfidenceThreshold);
        if (validDecisions.length === 0) {
            return this.createEmptyDecision(startTime);
        }
        // Apply default weights if not specified
        const weightedDecisions = validDecisions.map(d => ({
            ...d,
            weight: d.weight > 0 ? d.weight : (this.sourceWeights.get(d.sourceType) ?? 1.0),
        }));
        // Check for conflicts
        const uniqueActions = [...new Set(weightedDecisions.map(d => d.action))];
        const hasConflict = uniqueActions.length > 1;
        // Resolve to final action
        const { action, confidence, resolutionReason } = this.resolveConflict(weightedDecisions, effectiveStrategy);
        // Categorize sources
        const agreingSources = weightedDecisions
            .filter(d => d.action === action)
            .map(d => d.sourceId);
        const disagreingSources = weightedDecisions
            .filter(d => d.action !== action)
            .map(d => d.sourceId);
        // Collect constraints
        const appliedConstraints = this.collectConstraints(weightedDecisions, action);
        // Collect reasons
        const reasons = weightedDecisions.map(d => `[${d.sourceName}] ${d.reason}`);
        const durationMs = performance.now() - startTime;
        const result = {
            action,
            confidence,
            sourceDecisions: weightedDecisions,
            agreingSources,
            disagreingSources,
            conflictResolution: hasConflict ? {
                strategy: effectiveStrategy,
                hadConflict: true,
                conflictingActions: uniqueActions,
                resolutionReason,
            } : undefined,
            appliedConstraints,
            reasons,
            aggregatedAt: new Date().toISOString(),
            durationMs,
        };
        // Record audit
        if (this.options.enableAudit) {
            this.recordAudit(context, result);
        }
        logger.info({
            intentId: context.intent.id,
            action,
            confidence,
            sourcesCount: validDecisions.length,
            hadConflict: hasConflict,
            strategy: effectiveStrategy,
            durationMs,
        }, 'Decision aggregated');
        return result;
    }
    /**
     * Aggregate with OpenTelemetry tracing
     */
    async aggregateWithTracing(decisions, context, strategy) {
        if (!this.options.enableTracing) {
            return this.aggregate(decisions, context, strategy);
        }
        return withSpan('enforce.aggregateDecisions', async (span) => {
            span.attributes['intent.id'] = context.intent.id;
            span.attributes['sources.count'] = decisions.length;
            span.attributes['strategy'] = strategy ?? this.options.defaultStrategy;
            const result = this.aggregate(decisions, context, strategy);
            span.attributes['result.action'] = result.action;
            span.attributes['result.confidence'] = result.confidence;
            span.attributes['result.hadConflict'] = result.conflictResolution?.hadConflict ?? false;
            return result;
        }, { 'tenant.id': context.intent.tenantId });
    }
    /**
     * Convert aggregated decision to standard Decision type
     */
    toDecision(aggregated, context) {
        return {
            intentId: context.intent.id,
            action: aggregated.action,
            constraintsEvaluated: aggregated.sourceDecisions.map(d => ({
                constraintId: d.sourceId,
                passed: d.action === aggregated.action,
                action: d.action,
                reason: d.reason,
                details: d.details ?? {},
                durationMs: d.durationMs ?? 0,
                evaluatedAt: d.decidedAt,
            })),
            trustScore: context.trustScore,
            trustLevel: context.trustLevel,
            decidedAt: aggregated.aggregatedAt,
        };
    }
    /**
     * Resolve conflict between decisions
     */
    resolveConflict(decisions, strategy) {
        switch (strategy) {
            case 'deny-overrides':
                return this.resolveDenyOverrides(decisions);
            case 'allow-overrides':
                return this.resolveAllowOverrides(decisions);
            case 'most-restrictive':
                return this.resolveMostRestrictive(decisions);
            case 'least-restrictive':
                return this.resolveLeastRestrictive(decisions);
            case 'weighted-average':
                return this.resolveWeightedAverage(decisions);
            case 'unanimous-allow':
                return this.resolveUnanimousAllow(decisions);
            case 'majority-rules':
                return this.resolveMajorityRules(decisions);
            case 'priority-based':
                return this.resolvePriorityBased(decisions);
            default:
                return this.resolveDenyOverrides(decisions);
        }
    }
    resolveDenyOverrides(decisions) {
        const denyDecision = decisions.find(d => d.action === 'deny');
        if (denyDecision) {
            return {
                action: 'deny',
                confidence: denyDecision.confidence,
                resolutionReason: `Deny from ${denyDecision.sourceName} overrides all`,
            };
        }
        // Find most common non-deny action
        const actions = decisions.map(d => d.action);
        const actionCounts = new Map();
        for (const action of actions) {
            actionCounts.set(action, (actionCounts.get(action) ?? 0) + 1);
        }
        let maxAction = 'allow';
        let maxCount = 0;
        for (const [action, count] of actionCounts) {
            if (count > maxCount) {
                maxCount = count;
                maxAction = action;
            }
        }
        const avgConfidence = decisions.reduce((sum, d) => sum + d.confidence, 0) / decisions.length;
        return {
            action: maxAction,
            confidence: avgConfidence,
            resolutionReason: 'No deny decisions; using most common action',
        };
    }
    resolveAllowOverrides(decisions) {
        const allowDecision = decisions.find(d => d.action === 'allow');
        if (allowDecision) {
            return {
                action: 'allow',
                confidence: allowDecision.confidence,
                resolutionReason: `Allow from ${allowDecision.sourceName} overrides all`,
            };
        }
        // Find least restrictive
        return this.resolveLeastRestrictive(decisions);
    }
    resolveMostRestrictive(decisions) {
        let mostRestrictive = decisions[0];
        let maxIndex = ACTION_RESTRICTIVENESS.indexOf(mostRestrictive.action);
        for (const decision of decisions.slice(1)) {
            const index = ACTION_RESTRICTIVENESS.indexOf(decision.action);
            if (index > maxIndex) {
                maxIndex = index;
                mostRestrictive = decision;
            }
        }
        return {
            action: mostRestrictive.action,
            confidence: mostRestrictive.confidence,
            resolutionReason: `Most restrictive action from ${mostRestrictive.sourceName}`,
        };
    }
    resolveLeastRestrictive(decisions) {
        let leastRestrictive = decisions[0];
        let minIndex = ACTION_RESTRICTIVENESS.indexOf(leastRestrictive.action);
        for (const decision of decisions.slice(1)) {
            const index = ACTION_RESTRICTIVENESS.indexOf(decision.action);
            if (index < minIndex) {
                minIndex = index;
                leastRestrictive = decision;
            }
        }
        return {
            action: leastRestrictive.action,
            confidence: leastRestrictive.confidence,
            resolutionReason: `Least restrictive action from ${leastRestrictive.sourceName}`,
        };
    }
    resolveWeightedAverage(decisions) {
        // Calculate weighted score for each action
        const actionScores = new Map();
        let totalWeight = 0;
        for (const decision of decisions) {
            const score = decision.confidence * decision.weight;
            const current = actionScores.get(decision.action) ?? 0;
            actionScores.set(decision.action, current + score);
            totalWeight += decision.weight;
        }
        // Find action with highest weighted score
        let bestAction = 'deny';
        let bestScore = -1;
        for (const [action, score] of actionScores) {
            if (score > bestScore) {
                bestScore = score;
                bestAction = action;
            }
        }
        const confidence = totalWeight > 0 ? bestScore / totalWeight : 0;
        return {
            action: bestAction,
            confidence: Math.min(1, confidence),
            resolutionReason: `Weighted average favors ${bestAction}`,
        };
    }
    resolveUnanimousAllow(decisions) {
        const allAllow = decisions.every(d => d.action === 'allow');
        if (allAllow) {
            const avgConfidence = decisions.reduce((sum, d) => sum + d.confidence, 0) / decisions.length;
            return {
                action: 'allow',
                confidence: avgConfidence,
                resolutionReason: 'All sources unanimously allow',
            };
        }
        // Find most restrictive non-allow
        const nonAllow = decisions.filter(d => d.action !== 'allow');
        return this.resolveMostRestrictive(nonAllow.length > 0 ? nonAllow : decisions);
    }
    resolveMajorityRules(decisions) {
        // Count votes for each action
        const votes = new Map();
        for (const decision of decisions) {
            votes.set(decision.action, (votes.get(decision.action) ?? 0) + 1);
        }
        // Find majority
        const threshold = decisions.length / 2;
        let majorityAction = null;
        let maxVotes = 0;
        for (const [action, count] of votes) {
            if (count > threshold && count > maxVotes) {
                maxVotes = count;
                majorityAction = action;
            }
        }
        if (majorityAction) {
            const majorityDecisions = decisions.filter(d => d.action === majorityAction);
            const avgConfidence = majorityDecisions.reduce((sum, d) => sum + d.confidence, 0) / majorityDecisions.length;
            return {
                action: majorityAction,
                confidence: avgConfidence,
                resolutionReason: `Majority (${maxVotes}/${decisions.length}) voted ${majorityAction}`,
            };
        }
        // No majority - fall back to most restrictive
        return {
            ...this.resolveMostRestrictive(decisions),
            resolutionReason: 'No majority; using most restrictive action',
        };
    }
    resolvePriorityBased(decisions) {
        // Sort by source priority (lower = higher priority)
        const sorted = [...decisions].sort((a, b) => {
            const priorityA = this.sourcePriorities.get(a.sourceType) ?? 100;
            const priorityB = this.sourcePriorities.get(b.sourceType) ?? 100;
            return priorityA - priorityB;
        });
        const highest = sorted[0];
        return {
            action: highest.action,
            confidence: highest.confidence,
            resolutionReason: `Highest priority source ${highest.sourceName} decides`,
        };
    }
    /**
     * Collect constraints from decisions based on final action
     */
    collectConstraints(decisions, finalAction) {
        const constraints = new Set();
        for (const decision of decisions) {
            // Include constraints if decision agrees with final action or is 'limit'
            if (decision.action === finalAction || decision.action === 'limit') {
                if (decision.constraints) {
                    for (const constraint of decision.constraints) {
                        constraints.add(constraint);
                    }
                }
            }
        }
        return Array.from(constraints);
    }
    /**
     * Create empty decision when no valid sources
     */
    createEmptyDecision(startTime) {
        return {
            action: 'deny',
            confidence: 0,
            sourceDecisions: [],
            agreingSources: [],
            disagreingSources: [],
            appliedConstraints: [],
            reasons: ['No valid decision sources'],
            aggregatedAt: new Date().toISOString(),
            durationMs: performance.now() - startTime,
        };
    }
    // =============================================================================
    // AUDIT TRAIL
    // =============================================================================
    /**
     * Record audit entry
     */
    recordAudit(context, decision) {
        const entry = {
            id: `audit-${Date.now()}-${secureRandomString(9)}`,
            intentId: context.intent.id,
            tenantId: context.intent.tenantId,
            entityId: context.intent.entityId,
            decision,
            trustScore: context.trustScore,
            trustLevel: context.trustLevel,
            contextSnapshot: context.context ?? {},
            timestamp: new Date().toISOString(),
        };
        this.auditTrail.push(entry);
        // Trim if exceeds max
        if (this.auditTrail.length > this.options.maxAuditEntries) {
            this.auditTrail.splice(0, this.auditTrail.length - this.options.maxAuditEntries);
        }
        logger.debug({ auditId: entry.id, intentId: entry.intentId }, 'Audit entry recorded');
    }
    /**
     * Query audit trail
     */
    queryAudit(options = {}) {
        let results = [...this.auditTrail];
        // Apply filters
        if (options.intentId) {
            results = results.filter(e => e.intentId === options.intentId);
        }
        if (options.entityId) {
            results = results.filter(e => e.entityId === options.entityId);
        }
        if (options.tenantId) {
            results = results.filter(e => e.tenantId === options.tenantId);
        }
        if (options.action) {
            results = results.filter(e => e.decision.action === options.action);
        }
        if (options.sourceType) {
            results = results.filter(e => e.decision.sourceDecisions.some(s => s.sourceType === options.sourceType));
        }
        if (options.startTime) {
            const start = new Date(options.startTime).getTime();
            results = results.filter(e => new Date(e.timestamp).getTime() >= start);
        }
        if (options.endTime) {
            const end = new Date(options.endTime).getTime();
            results = results.filter(e => new Date(e.timestamp).getTime() <= end);
        }
        // Sort by timestamp descending
        results.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
        // Apply pagination
        const offset = options.offset ?? 0;
        const limit = options.limit ?? 100;
        return results.slice(offset, offset + limit);
    }
    /**
     * Get audit entry by ID
     */
    getAuditEntry(auditId) {
        return this.auditTrail.find(e => e.id === auditId);
    }
    /**
     * Get audit statistics
     */
    getAuditStats() {
        const byAction = {};
        const bySourceType = {};
        let conflictCount = 0;
        for (const entry of this.auditTrail) {
            // Count by action
            const action = entry.decision.action;
            byAction[action] = (byAction[action] ?? 0) + 1;
            // Count by source type
            for (const source of entry.decision.sourceDecisions) {
                bySourceType[source.sourceType] = (bySourceType[source.sourceType] ?? 0) + 1;
            }
            // Count conflicts
            if (entry.decision.conflictResolution?.hadConflict) {
                conflictCount++;
            }
        }
        return {
            totalEntries: this.auditTrail.length,
            byAction: byAction,
            bySourceType,
            conflictRate: this.auditTrail.length > 0
                ? conflictCount / this.auditTrail.length
                : 0,
        };
    }
    /**
     * Clear audit trail
     */
    clearAudit() {
        this.auditTrail = [];
        logger.info('Audit trail cleared');
    }
    /**
     * Export audit trail
     */
    exportAudit() {
        return [...this.auditTrail];
    }
    // =============================================================================
    // CONFIGURATION
    // =============================================================================
    /**
     * Set source priority
     */
    setSourcePriority(sourceType, priority) {
        this.sourcePriorities.set(sourceType, priority);
        logger.debug({ sourceType, priority }, 'Source priority updated');
    }
    /**
     * Set source weight
     */
    setSourceWeight(sourceType, weight) {
        this.sourceWeights.set(sourceType, weight);
        logger.debug({ sourceType, weight }, 'Source weight updated');
    }
    /**
     * Get source priorities
     */
    getSourcePriorities() {
        return new Map(this.sourcePriorities);
    }
    /**
     * Get source weights
     */
    getSourceWeights() {
        return new Map(this.sourceWeights);
    }
    /**
     * Get aggregator statistics
     */
    getStats() {
        return {
            auditEntries: this.auditTrail.length,
            sourcePriorities: Array.from(this.sourcePriorities.entries()),
            sourceWeights: Array.from(this.sourceWeights.entries()),
            defaultStrategy: this.options.defaultStrategy,
        };
    }
}
/**
 * Create a new decision aggregator instance
 */
export function createDecisionAggregator(options) {
    return new DecisionAggregator(options);
}
/**
 * Create a source decision
 */
export function createSourceDecision(partial) {
    return {
        confidence: 1.0,
        weight: 1.0,
        decidedAt: new Date().toISOString(),
        ...partial,
    };
}
//# sourceMappingURL=decision-aggregator.js.map