/**
 * Policy Propagation System
 *
 * Real-time policy distribution to agents within 30 seconds.
 * Implements FR148 for Epic 4.
 *
 * Features:
 * - Sub-30 second propagation
 * - WebSocket-based push notifications
 * - Acknowledgment tracking
 * - Fallback polling for offline agents
 *
 * @packageDocumentation
 */
import { createLogger } from '../../common/logger.js';
import { Counter, Histogram, Gauge } from 'prom-client';
import { intentRegistry } from '../../intent/metrics.js';
const logger = createLogger({ component: 'policy-propagation' });
// =============================================================================
// Metrics
// =============================================================================
const policyPropagations = new Counter({
    name: 'vorion_policy_propagations_total',
    help: 'Total policy propagation events',
    labelNames: ['status'],
    registers: [intentRegistry],
});
const propagationDuration = new Histogram({
    name: 'vorion_policy_propagation_duration_seconds',
    help: 'Time to propagate policy to all agents',
    buckets: [0.5, 1, 2, 5, 10, 15, 30, 60],
    registers: [intentRegistry],
});
const agentAcknowledgments = new Counter({
    name: 'vorion_policy_acks_total',
    help: 'Policy acknowledgments from agents',
    labelNames: ['status'],
    registers: [intentRegistry],
});
const pendingPropagations = new Gauge({
    name: 'vorion_pending_policy_propagations',
    help: 'Number of policies pending full propagation',
    registers: [intentRegistry],
});
// =============================================================================
// Policy Propagation Service
// =============================================================================
/**
 * Policy Propagation Service
 *
 * Manages real-time policy distribution to agents.
 */
export class PolicyPropagationService {
    connections = new Map();
    propagations = new Map();
    acknowledgments = new Map();
    /**
     * Register an agent connection
     */
    registerConnection(connection) {
        this.connections.set(connection.agentId, connection);
        logger.debug({ agentId: connection.agentId }, 'Agent connection registered');
    }
    /**
     * Remove an agent connection
     */
    removeConnection(agentId) {
        this.connections.delete(agentId);
        logger.debug({ agentId }, 'Agent connection removed');
    }
    /**
     * Get connected agent count
     */
    getConnectedAgentCount() {
        return Array.from(this.connections.values()).filter(c => c.connected).length;
    }
    /**
     * Propagate a policy update to agents
     */
    async propagate(event, options = {}) {
        const startTime = Date.now();
        const eventId = event.eventId;
        logger.info({
            eventId,
            policyId: event.policyId,
            action: event.action,
        }, 'Starting policy propagation');
        pendingPropagations.inc();
        policyPropagations.inc({ status: 'started' });
        // Determine target agents
        const targetAgents = this.getTargetAgents(options);
        const targetAgentIds = targetAgents.map(a => a.agentId);
        // Initialize propagation status
        const status = {
            eventId,
            policyId: event.policyId,
            startedAt: new Date().toISOString(),
            targetAgents: targetAgents.length,
            acknowledgedAgents: 0,
            pendingAgents: [...targetAgentIds],
            failedAgents: [],
            status: 'in_progress',
        };
        this.propagations.set(eventId, status);
        this.acknowledgments.set(eventId, new Map());
        // Send to all connected agents in parallel
        const sendPromises = targetAgents.map(async (connection) => {
            try {
                await connection.send(event);
                logger.debug({ agentId: connection.agentId, eventId }, 'Policy update sent');
            }
            catch (error) {
                status.failedAgents.push({
                    agentId: connection.agentId,
                    error: error instanceof Error ? error.message : 'Unknown error',
                });
                status.pendingAgents = status.pendingAgents.filter(id => id !== connection.agentId);
                logger.warn({ agentId: connection.agentId, error }, 'Failed to send policy update');
            }
        });
        // Wait for all sends to complete
        await Promise.allSettled(sendPromises);
        // Wait for acknowledgments (with timeout)
        const ackTimeout = options.ackTimeoutMs ?? 30000;
        const minAckPercentage = options.minAckPercentage ?? 80;
        const ackResult = await this.waitForAcknowledgments(eventId, targetAgentIds, ackTimeout, minAckPercentage);
        // Update final status
        status.completedAt = new Date().toISOString();
        status.acknowledgedAgents = ackResult.acknowledged;
        status.pendingAgents = ackResult.pending;
        // Calculate latencies
        const acks = this.acknowledgments.get(eventId);
        if (acks && acks.size > 0) {
            const latencies = Array.from(acks.values())
                .map(a => a.latencyMs)
                .sort((a, b) => a - b);
            status.latencyP50Ms = latencies[Math.floor(latencies.length * 0.5)];
            status.latencyP95Ms = latencies[Math.floor(latencies.length * 0.95)];
        }
        // Determine final status
        const ackPercentage = (status.acknowledgedAgents / status.targetAgents) * 100;
        if (ackPercentage >= 100) {
            status.status = 'completed';
            policyPropagations.inc({ status: 'completed' });
        }
        else if (ackPercentage >= minAckPercentage) {
            status.status = 'partial';
            policyPropagations.inc({ status: 'partial' });
        }
        else {
            status.status = 'failed';
            policyPropagations.inc({ status: 'failed' });
        }
        const duration = Date.now() - startTime;
        propagationDuration.observe(duration / 1000);
        pendingPropagations.dec();
        logger.info({
            eventId,
            policyId: event.policyId,
            status: status.status,
            acknowledged: status.acknowledgedAgents,
            total: status.targetAgents,
            durationMs: duration,
        }, 'Policy propagation completed');
        return status;
    }
    /**
     * Record an acknowledgment from an agent
     */
    recordAcknowledgment(ack) {
        // Find the propagation event
        for (const [eventId, status] of this.propagations) {
            if (status.policyId === ack.policyId) {
                const acks = this.acknowledgments.get(eventId);
                if (acks) {
                    acks.set(ack.agentId, ack);
                    // Update status
                    status.pendingAgents = status.pendingAgents.filter(id => id !== ack.agentId);
                    status.acknowledgedAgents++;
                    if (ack.status === 'error') {
                        status.failedAgents.push({
                            agentId: ack.agentId,
                            error: ack.errorMessage ?? 'Unknown error',
                        });
                    }
                }
                break;
            }
        }
        agentAcknowledgments.inc({ status: ack.status });
        logger.debug({
            agentId: ack.agentId,
            policyId: ack.policyId,
            status: ack.status,
            latencyMs: ack.latencyMs,
        }, 'Policy acknowledgment recorded');
    }
    /**
     * Get propagation status
     */
    getStatus(eventId) {
        return this.propagations.get(eventId);
    }
    /**
     * Get recent propagations
     */
    getRecentPropagations(limit = 10) {
        return Array.from(this.propagations.values())
            .sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
            .slice(0, limit);
    }
    /**
     * Create a policy update event
     */
    createEvent(policyId, policyName, version, action, checksum, definition) {
        return {
            eventId: `prop_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
            policyId,
            policyName,
            version,
            action,
            checksum,
            timestamp: new Date().toISOString(),
            definition,
        };
    }
    /**
     * Get target agents based on options
     */
    getTargetAgents(options) {
        let agents = Array.from(this.connections.values()).filter(c => c.connected);
        if (options.targetAgentIds && options.targetAgentIds.length > 0) {
            const targetSet = new Set(options.targetAgentIds);
            agents = agents.filter(a => targetSet.has(a.agentId));
        }
        if (options.targetNamespaces && options.targetNamespaces.length > 0) {
            const nsSet = new Set(options.targetNamespaces);
            agents = agents.filter(a => a.namespace && nsSet.has(a.namespace));
        }
        return agents;
    }
    /**
     * Wait for acknowledgments with timeout
     */
    async waitForAcknowledgments(eventId, targetAgentIds, timeoutMs, minPercentage) {
        const startTime = Date.now();
        const targetCount = targetAgentIds.length;
        const minAcks = Math.ceil((targetCount * minPercentage) / 100);
        return new Promise((resolve) => {
            const checkInterval = setInterval(() => {
                const acks = this.acknowledgments.get(eventId);
                const acknowledged = acks?.size ?? 0;
                const elapsed = Date.now() - startTime;
                // Check if we have enough acknowledgments
                if (acknowledged >= minAcks || acknowledged >= targetCount) {
                    clearInterval(checkInterval);
                    const pending = targetAgentIds.filter(id => !acks?.has(id));
                    resolve({ acknowledged, pending });
                    return;
                }
                // Check timeout
                if (elapsed >= timeoutMs) {
                    clearInterval(checkInterval);
                    const pending = targetAgentIds.filter(id => !acks?.has(id));
                    resolve({ acknowledged, pending });
                    return;
                }
            }, 100);
        });
    }
    /**
     * Clean up old propagation records
     */
    cleanup(maxAgeMs = 24 * 60 * 60 * 1000) {
        const cutoff = Date.now() - maxAgeMs;
        let cleaned = 0;
        for (const [eventId, status] of this.propagations) {
            if (new Date(status.startedAt).getTime() < cutoff) {
                this.propagations.delete(eventId);
                this.acknowledgments.delete(eventId);
                cleaned++;
            }
        }
        if (cleaned > 0) {
            logger.info({ cleaned }, 'Cleaned up old propagation records');
        }
        return cleaned;
    }
}
// =============================================================================
// Factory Functions
// =============================================================================
/**
 * Create a policy propagation service instance
 */
export function createPolicyPropagationService() {
    return new PolicyPropagationService();
}
//# sourceMappingURL=propagation.js.map