/**
 * A2A Message Router
 *
 * Routes messages between agents, handling discovery, load balancing,
 * and message delivery with retry logic.
 *
 * @packageDocumentation
 */
import { randomUUID } from 'crypto';
import { createLogger } from '../common/logger.js';
import { DEFAULT_A2A_TIMEOUT_MS, } from './types.js';
import { getTrustNegotiationService, } from './trust-negotiation.js';
const logger = createLogger({ component: 'a2a-router' });
// ============================================================================
// A2A Router
// ============================================================================
export class A2ARouter {
    config;
    endpoints = new Map();
    pendingRequests = new Map();
    circuitBreakers = new Map();
    handlers = new Map();
    trustService = null;
    constructor(config = {}) {
        this.config = {
            defaultTimeoutMs: config.defaultTimeoutMs ?? DEFAULT_A2A_TIMEOUT_MS,
            maxRetries: config.maxRetries ?? 3,
            retryDelayMs: config.retryDelayMs ?? 1000,
            circuitBreakerEnabled: config.circuitBreakerEnabled ?? true,
            circuitBreakerThreshold: config.circuitBreakerThreshold ?? 5,
            circuitBreakerResetMs: config.circuitBreakerResetMs ?? 30000,
        };
        logger.info({ config: this.config }, 'A2A router initialized');
    }
    /**
     * Set trust service (lazy initialization)
     */
    setTrustService(service) {
        this.trustService = service;
    }
    // ==========================================================================
    // Endpoint Management
    // ==========================================================================
    /**
     * Register an agent endpoint
     */
    registerEndpoint(endpoint) {
        this.endpoints.set(endpoint.carId, endpoint);
        this.initCircuitBreaker(endpoint.carId);
        logger.info({ carId: endpoint.carId, url: endpoint.url }, 'Endpoint registered');
    }
    /**
     * Unregister an agent endpoint
     */
    unregisterEndpoint(carId) {
        this.endpoints.delete(carId);
        this.circuitBreakers.delete(carId);
        logger.info({ carId }, 'Endpoint unregistered');
    }
    /**
     * Get endpoint for a CAR ID
     */
    getEndpoint(carId) {
        return this.endpoints.get(carId);
    }
    /**
     * Discover endpoints matching criteria
     */
    discoverEndpoints(criteria) {
        const results = [];
        for (const endpoint of this.endpoints.values()) {
            // Check capabilities
            if (criteria.capabilities) {
                const hasAll = criteria.capabilities.every((cap) => endpoint.capabilities.includes(cap));
                if (!hasAll)
                    continue;
            }
            // Check tier
            if (criteria.minTier !== undefined) {
                if (endpoint.trustRequirements.minTier < criteria.minTier)
                    continue;
            }
            // Check action
            if (criteria.action) {
                const hasAction = endpoint.actions.some((a) => a.name === criteria.action);
                if (!hasAction)
                    continue;
            }
            results.push(endpoint);
        }
        return results;
    }
    // ==========================================================================
    // Message Routing
    // ==========================================================================
    /**
     * Send an A2A message and wait for response
     */
    async send(from, to, payload, trustContext, options = {}) {
        const messageId = randomUUID();
        const timeoutMs = options.timeoutMs ?? this.config.defaultTimeoutMs;
        // Check circuit breaker
        if (!this.canSend(to)) {
            throw this.createError('AGENT_UNAVAILABLE', `Circuit breaker open for ${to}`);
        }
        // Build message
        const message = {
            id: messageId,
            version: '1.0',
            type: this.getMessageType(payload),
            from,
            to,
            timestamp: new Date().toISOString(),
            correlationId: options.correlationId,
            trustContext,
            payload,
        };
        // Get endpoint
        const endpoint = this.endpoints.get(to);
        if (!endpoint) {
            throw this.createError('AGENT_UNAVAILABLE', `No endpoint for ${to}`);
        }
        // Create pending request
        return new Promise((resolve, reject) => {
            const timeoutHandle = setTimeout(() => {
                this.handleTimeout(messageId);
            }, timeoutMs);
            const pendingRequest = {
                id: messageId,
                message,
                sentAt: new Date(),
                timeoutMs,
                retryCount: 0,
                resolve,
                reject,
                timeoutHandle,
            };
            this.pendingRequests.set(messageId, pendingRequest);
            // Send message
            this.deliverMessage(message, endpoint)
                .catch((error) => {
                this.handleDeliveryError(messageId, error);
            });
        });
    }
    /**
     * Send fire-and-forget message (no response expected)
     */
    async sendAsync(from, to, payload, trustContext) {
        const messageId = randomUUID();
        if (!this.canSend(to)) {
            throw this.createError('AGENT_UNAVAILABLE', `Circuit breaker open for ${to}`);
        }
        const message = {
            id: messageId,
            version: '1.0',
            type: this.getMessageType(payload),
            from,
            to,
            timestamp: new Date().toISOString(),
            trustContext,
            payload,
        };
        const endpoint = this.endpoints.get(to);
        if (!endpoint) {
            throw this.createError('AGENT_UNAVAILABLE', `No endpoint for ${to}`);
        }
        await this.deliverMessage(message, endpoint);
        return messageId;
    }
    /**
     * Deliver message to endpoint
     */
    async deliverMessage(message, endpoint) {
        try {
            // In production, this would make an HTTP/gRPC call
            // For now, we simulate local delivery via handlers
            logger.debug({ messageId: message.id, from: message.from, to: message.to }, 'Delivering message');
            // Check if we have a local handler
            const handler = this.handlers.get(message.to);
            if (handler) {
                await this.handleLocally(message, handler);
            }
            else {
                // Would make HTTP call here
                logger.warn({ to: message.to }, 'No handler for agent, remote delivery not implemented');
                throw new Error('Remote delivery not implemented');
            }
            this.recordSuccess(message.to);
        }
        catch (error) {
            this.recordFailure(message.to);
            throw error;
        }
    }
    /**
     * Handle message locally
     */
    async handleLocally(message, handler) {
        // Verify trust
        const trustService = this.trustService ?? getTrustNegotiationService();
        const endpoint = this.endpoints.get(message.to);
        if (!endpoint) {
            throw this.createError('AGENT_UNAVAILABLE', 'Endpoint not found');
        }
        const verification = await trustService.verifyCallerTrust(message.from, message.to, message.trustContext, endpoint.trustRequirements);
        if (!verification.verified) {
            // Send error response
            await this.sendErrorResponse(message, 'TRUST_INSUFFICIENT', verification.reason || 'Trust verification failed');
            return;
        }
        // Build context
        const context = {
            trustContext: message.trustContext,
            effectiveTier: verification.effectiveTier,
            effectiveScore: verification.effectiveScore,
            chainContext: message.payload.chainContext,
            metadata: {},
        };
        // Call handler
        const responsePayload = await handler(message, context);
        // Send response if expected
        if (responsePayload && message.type === 'invoke') {
            await this.sendResponse(message, responsePayload);
        }
    }
    /**
     * Send response to a message
     */
    async sendResponse(originalMessage, payload) {
        const response = {
            id: randomUUID(),
            version: '1.0',
            type: 'response',
            from: originalMessage.to,
            to: originalMessage.from,
            timestamp: new Date().toISOString(),
            correlationId: originalMessage.id,
            trustContext: originalMessage.trustContext, // Echo back
            payload,
        };
        // Resolve pending request
        const pending = this.pendingRequests.get(originalMessage.id);
        if (pending) {
            clearTimeout(pending.timeoutHandle);
            this.pendingRequests.delete(originalMessage.id);
            pending.resolve(response);
        }
    }
    /**
     * Send error response
     */
    async sendErrorResponse(originalMessage, code, message) {
        const errorPayload = {
            type: 'response',
            success: false,
            error: { code, message },
            metrics: {
                durationMs: 0,
                subCallCount: 0,
            },
        };
        await this.sendResponse(originalMessage, errorPayload);
    }
    // ==========================================================================
    // Handler Registration
    // ==========================================================================
    /**
     * Register a message handler for a CAR ID
     */
    registerHandler(carId, handler) {
        this.handlers.set(carId, handler);
        logger.info({ carId }, 'Handler registered');
    }
    /**
     * Unregister a message handler
     */
    unregisterHandler(carId) {
        this.handlers.delete(carId);
        logger.info({ carId }, 'Handler unregistered');
    }
    // ==========================================================================
    // Circuit Breaker
    // ==========================================================================
    /**
     * Initialize circuit breaker for endpoint
     */
    initCircuitBreaker(carId) {
        this.circuitBreakers.set(carId, {
            failures: 0,
            lastFailure: null,
            state: 'closed',
            nextAttempt: null,
        });
    }
    /**
     * Check if we can send to endpoint
     */
    canSend(carId) {
        if (!this.config.circuitBreakerEnabled)
            return true;
        const breaker = this.circuitBreakers.get(carId);
        if (!breaker)
            return true;
        switch (breaker.state) {
            case 'closed':
                return true;
            case 'open':
                // Check if reset time has passed
                if (breaker.nextAttempt && new Date() >= breaker.nextAttempt) {
                    breaker.state = 'half-open';
                    return true;
                }
                return false;
            case 'half-open':
                return true;
        }
    }
    /**
     * Record successful call
     */
    recordSuccess(carId) {
        const breaker = this.circuitBreakers.get(carId);
        if (!breaker)
            return;
        if (breaker.state === 'half-open') {
            // Reset on success in half-open
            breaker.failures = 0;
            breaker.state = 'closed';
            breaker.lastFailure = null;
            breaker.nextAttempt = null;
            logger.info({ carId }, 'Circuit breaker closed');
        }
    }
    /**
     * Record failed call
     */
    recordFailure(carId) {
        const breaker = this.circuitBreakers.get(carId);
        if (!breaker)
            return;
        breaker.failures++;
        breaker.lastFailure = new Date();
        if (breaker.state === 'half-open') {
            // Failure in half-open reopens
            breaker.state = 'open';
            breaker.nextAttempt = new Date(Date.now() + this.config.circuitBreakerResetMs);
            logger.warn({ carId }, 'Circuit breaker reopened');
        }
        else if (breaker.failures >= this.config.circuitBreakerThreshold) {
            // Threshold reached
            breaker.state = 'open';
            breaker.nextAttempt = new Date(Date.now() + this.config.circuitBreakerResetMs);
            logger.warn({ carId, failures: breaker.failures }, 'Circuit breaker opened');
        }
    }
    // ==========================================================================
    // Error Handling
    // ==========================================================================
    /**
     * Handle timeout
     */
    handleTimeout(messageId) {
        const pending = this.pendingRequests.get(messageId);
        if (!pending)
            return;
        // Attempt retry
        if (pending.retryCount < this.config.maxRetries) {
            pending.retryCount++;
            const delay = this.config.retryDelayMs * Math.pow(2, pending.retryCount - 1);
            logger.debug({ messageId, retry: pending.retryCount, delay }, 'Retrying message');
            setTimeout(() => {
                const endpoint = this.endpoints.get(pending.message.to);
                if (endpoint) {
                    this.deliverMessage(pending.message, endpoint).catch((error) => {
                        this.handleDeliveryError(messageId, error);
                    });
                }
            }, delay);
            // Reset timeout
            pending.timeoutHandle = setTimeout(() => {
                this.handleTimeout(messageId);
            }, pending.timeoutMs);
        }
        else {
            // Max retries exceeded
            this.pendingRequests.delete(messageId);
            pending.reject(new Error(`A2A timeout after ${pending.retryCount} retries`));
            this.recordFailure(pending.message.to);
        }
    }
    /**
     * Handle delivery error
     */
    handleDeliveryError(messageId, error) {
        const pending = this.pendingRequests.get(messageId);
        if (!pending)
            return;
        // For some errors, don't retry
        if (error.message.includes('TRUST_INSUFFICIENT') ||
            error.message.includes('CAPABILITY_DENIED')) {
            clearTimeout(pending.timeoutHandle);
            this.pendingRequests.delete(messageId);
            pending.reject(error);
            return;
        }
        // Let timeout handle retry
        logger.debug({ messageId, error: error.message }, 'Delivery error, awaiting retry');
    }
    // ==========================================================================
    // Helpers
    // ==========================================================================
    /**
     * Get message type from payload
     */
    getMessageType(payload) {
        return payload.type;
    }
    /**
     * Create A2A error
     */
    createError(code, message) {
        const error = new Error(message);
        error.code = code;
        return error;
    }
    /**
     * Get router statistics
     */
    getStats() {
        return {
            endpoints: this.endpoints.size,
            pendingRequests: this.pendingRequests.size,
            circuitBreakers: Object.fromEntries(this.circuitBreakers),
        };
    }
}
// ============================================================================
// Factory Functions
// ============================================================================
let instance = null;
export function createA2ARouter(config) {
    if (!instance) {
        instance = new A2ARouter(config);
    }
    return instance;
}
export function getA2ARouter() {
    if (!instance) {
        throw new Error('A2ARouter not initialized');
    }
    return instance;
}
//# sourceMappingURL=router.js.map