/**
 * Multi-Server Manager
 *
 * Coordinates multiple MCP servers with load balancing, failover,
 * and unified tool discovery across all registered servers.
 *
 * Features:
 * - Load balancing strategies (round-robin, least-loaded, random)
 * - Health-aware routing
 * - Automatic failover
 * - Unified tool namespace management
 * - Cross-server tool discovery
 *
 * @module mcp/multiServerManager
 * @since 8.39.0
 */
import { EventEmitter } from "events";
import { logger } from "../utils/logger.js";
import { ErrorFactory } from "../utils/errorHandling.js";
/**
 * Multi-Server Manager
 *
 * Coordinates multiple MCP servers for unified tool access
 * with load balancing and failover capabilities.
 *
 * @example
 * ```typescript
 * const manager = new MultiServerManager({
 *   defaultStrategy: "round-robin",
 *   healthAwareRouting: true,
 *   autoNamespace: true,
 * });
 *
 * // Add servers
 * manager.addServer(server1Info);
 * manager.addServer(server2Info);
 *
 * // Create a group for redundant servers
 * manager.createGroup({
 *   id: "data-servers",
 *   name: "Data Processing Servers",
 *   servers: ["server1", "server2"],
 *   strategy: "least-loaded",
 * });
 *
 * // Get unified tool list
 * const tools = manager.getUnifiedTools();
 *
 * // Execute with automatic routing
 * const result = await manager.executeTool("readFile", { path: "/data" });
 * ```
 */
export class MultiServerManager extends EventEmitter {
    config;
    servers = new Map();
    groups = new Map();
    metrics = new Map();
    roundRobinCounters = new Map();
    toolPreferences = new Map(); // toolName -> preferred serverId
    constructor(config = {}) {
        super();
        this.config = {
            defaultStrategy: config.defaultStrategy ?? "round-robin",
            healthAwareRouting: config.healthAwareRouting ?? true,
            healthCheckInterval: config.healthCheckInterval ?? 30000,
            maxFailoverRetries: config.maxFailoverRetries ?? 3,
            namespaceSeparator: config.namespaceSeparator ?? ".",
            autoNamespace: config.autoNamespace ?? false,
            conflictResolution: config.conflictResolution ?? "first-wins",
        };
    }
    /**
     * Add a server to the manager
     */
    addServer(server) {
        this.servers.set(server.id, server);
        // Initialize metrics
        this.metrics.set(server.id, {
            activeRequests: 0,
            totalRequests: 0,
            completedRequests: 0,
            averageResponseTime: 0,
            errorRate: 0,
            isHealthy: server.status === "connected",
        });
        this.emit("serverAdded", { serverId: server.id, server });
        logger.debug(`[MultiServerManager] Added server: ${server.id} (${server.name})`);
    }
    /**
     * Remove a server from the manager
     */
    removeServer(serverId) {
        const server = this.servers.get(serverId);
        if (!server) {
            return false;
        }
        // Remove from all groups
        for (const [groupId, group] of this.groups) {
            const index = group.servers.indexOf(serverId);
            if (index !== -1) {
                group.servers.splice(index, 1);
                // Remove empty groups
                if (group.servers.length === 0) {
                    this.groups.delete(groupId);
                    this.roundRobinCounters.delete(groupId);
                }
            }
        }
        this.servers.delete(serverId);
        this.metrics.delete(serverId);
        // Clear tool preferences for this server
        for (const [toolName, preferredServer] of this.toolPreferences) {
            if (preferredServer === serverId) {
                this.toolPreferences.delete(toolName);
            }
        }
        this.emit("serverRemoved", { serverId });
        logger.debug(`[MultiServerManager] Removed server: ${serverId}`);
        return true;
    }
    /**
     * Update server info
     */
    updateServer(serverId, updates) {
        const server = this.servers.get(serverId);
        if (!server) {
            throw ErrorFactory.invalidConfiguration("serverId", `Server '${serverId}' not found`, { serverId });
        }
        const updatedServer = { ...server, ...updates, id: serverId };
        this.servers.set(serverId, updatedServer);
        // Update health status in metrics
        const metrics = this.metrics.get(serverId);
        if (metrics && updates.status !== undefined) {
            metrics.isHealthy = updates.status === "connected";
        }
        this.emit("serverUpdated", { serverId, server: updatedServer });
    }
    /**
     * Create a server group
     */
    createGroup(group) {
        // Validate servers exist
        for (const serverId of group.servers) {
            if (!this.servers.has(serverId)) {
                throw ErrorFactory.invalidConfiguration("serverGroup.servers", `Server '${serverId}' not found when creating group '${group.id}'`, { serverId, groupId: group.id });
            }
        }
        this.groups.set(group.id, group);
        this.roundRobinCounters.set(group.id, 0);
        this.emit("groupCreated", { group });
        logger.debug(`[MultiServerManager] Created group: ${group.id} with ${group.servers.length} servers`);
    }
    /**
     * Remove a server group
     */
    removeGroup(groupId) {
        const removed = this.groups.delete(groupId);
        if (removed) {
            this.roundRobinCounters.delete(groupId);
            this.emit("groupRemoved", { groupId });
        }
        return removed;
    }
    /**
     * Add a server to a group
     */
    addServerToGroup(serverId, groupId) {
        const group = this.groups.get(groupId);
        if (!group) {
            throw ErrorFactory.invalidConfiguration("groupId", `Group '${groupId}' not found`, { groupId });
        }
        if (!this.servers.has(serverId)) {
            throw ErrorFactory.invalidConfiguration("serverId", `Server '${serverId}' not found`, { serverId, groupId });
        }
        if (!group.servers.includes(serverId)) {
            group.servers.push(serverId);
            this.emit("serverAddedToGroup", { serverId, groupId });
        }
    }
    /**
     * Remove a server from a group
     */
    removeServerFromGroup(serverId, groupId) {
        const group = this.groups.get(groupId);
        if (!group) {
            return false;
        }
        const index = group.servers.indexOf(serverId);
        if (index !== -1) {
            group.servers.splice(index, 1);
            this.emit("serverRemovedFromGroup", { serverId, groupId });
            return true;
        }
        return false;
    }
    /**
     * Get unified tool list from all servers
     */
    getUnifiedTools() {
        const toolMap = new Map();
        for (const [serverId, server] of this.servers) {
            const metrics = this.metrics.get(serverId);
            const isHealthy = metrics?.isHealthy ?? true;
            // Skip unhealthy servers in health-aware mode
            if (this.config.healthAwareRouting && !isHealthy) {
                continue;
            }
            for (const tool of server.tools || []) {
                const existingTool = toolMap.get(tool.name);
                if (existingTool) {
                    // Tool exists from another server - mark as conflict
                    existingTool.hasConflict = true;
                    existingTool.servers.push({
                        serverId,
                        serverName: server.name,
                        inputSchema: tool.inputSchema,
                        priority: this.getServerPriority(serverId),
                    });
                }
                else {
                    // New tool
                    toolMap.set(tool.name, {
                        name: tool.name,
                        description: tool.description,
                        servers: [
                            {
                                serverId,
                                serverName: server.name,
                                inputSchema: tool.inputSchema,
                                priority: this.getServerPriority(serverId),
                            },
                        ],
                        hasConflict: false,
                        preferredServerId: this.toolPreferences.get(tool.name),
                    });
                }
            }
        }
        // Sort servers by priority within each tool
        for (const tool of toolMap.values()) {
            tool.servers.sort((a, b) => a.priority - b.priority);
            // Set preferred server if not already set
            if (!tool.preferredServerId && tool.servers.length > 0) {
                tool.preferredServerId = tool.servers[0].serverId;
            }
        }
        return Array.from(toolMap.values());
    }
    /**
     * Get namespaced tools (server.toolName format)
     */
    getNamespacedTools() {
        const tools = [];
        for (const [serverId, server] of this.servers) {
            // Skip unhealthy servers in health-aware mode
            if (this.config.healthAwareRouting) {
                const metrics = this.metrics.get(serverId);
                const isHealthy = metrics?.isHealthy ?? true;
                if (!isHealthy) {
                    continue;
                }
            }
            for (const tool of server.tools || []) {
                tools.push({
                    fullName: `${serverId}${this.config.namespaceSeparator}${tool.name}`,
                    toolName: tool.name,
                    serverId,
                    serverName: server.name,
                    description: tool.description,
                    inputSchema: tool.inputSchema,
                });
            }
        }
        return tools;
    }
    /**
     * Set tool preference for routing
     */
    setToolPreference(toolName, serverId) {
        if (!this.servers.has(serverId)) {
            throw ErrorFactory.invalidConfiguration("serverId", `Server '${serverId}' not found`, { serverId, toolName });
        }
        this.toolPreferences.set(toolName, serverId);
        this.emit("toolPreferenceSet", { toolName, serverId });
    }
    /**
     * Clear tool preference
     */
    clearToolPreference(toolName) {
        this.toolPreferences.delete(toolName);
    }
    /**
     * Select a server for a tool using load balancing
     */
    selectServer(toolName, groupId) {
        // Check for tool preference first
        const preferredServerId = this.toolPreferences.get(toolName);
        if (preferredServerId) {
            const server = this.servers.get(preferredServerId);
            const metrics = this.metrics.get(preferredServerId);
            if (server && (!this.config.healthAwareRouting || metrics?.isHealthy)) {
                // Check if server has the tool
                if (server.tools?.some((t) => t.name === toolName)) {
                    return { serverId: preferredServerId, server };
                }
            }
        }
        // Get candidate servers (from group or all servers)
        let candidates;
        if (groupId) {
            const group = this.groups.get(groupId);
            if (!group) {
                logger.warn(`[MultiServerManager] Group '${groupId}' not found`);
                return null;
            }
            // Filter group servers to only those that have the requested tool
            candidates = group.servers.filter((serverId) => {
                const server = this.servers.get(serverId);
                return server?.tools?.some((t) => t.name === toolName);
            });
        }
        else {
            // Find all servers that have this tool
            candidates = [];
            for (const [serverId, server] of this.servers) {
                if (server.tools?.some((t) => t.name === toolName)) {
                    candidates.push(serverId);
                }
            }
        }
        if (candidates.length === 0) {
            return null;
        }
        // Filter by health if enabled (prefer group-level flag, fall back to global)
        const healthAware = groupId
            ? (this.groups.get(groupId)?.healthAware ??
                this.config.healthAwareRouting)
            : this.config.healthAwareRouting;
        if (healthAware) {
            candidates = candidates.filter((id) => {
                const metrics = this.metrics.get(id);
                return metrics?.isHealthy ?? true;
            });
            if (candidates.length === 0) {
                logger.warn(`[MultiServerManager] No healthy servers available for tool '${toolName}'`);
                return null;
            }
        }
        // Apply load balancing strategy
        const strategy = groupId
            ? (this.groups.get(groupId)?.strategy ?? this.config.defaultStrategy)
            : this.config.defaultStrategy;
        const selectedId = this.applyStrategy(strategy, candidates, groupId);
        if (!selectedId) {
            return null;
        }
        const server = this.servers.get(selectedId);
        return server ? { serverId: selectedId, server } : null;
    }
    /**
     * Apply load balancing strategy
     */
    applyStrategy(strategy, candidates, groupId) {
        if (candidates.length === 0) {
            return null;
        }
        if (candidates.length === 1) {
            return candidates[0];
        }
        switch (strategy) {
            case "round-robin": {
                const counterKey = groupId ?? "default";
                const counter = this.roundRobinCounters.get(counterKey) ?? 0;
                const selected = candidates[counter % candidates.length];
                this.roundRobinCounters.set(counterKey, counter + 1);
                return selected;
            }
            case "least-loaded": {
                let minLoad = Infinity;
                let selected = candidates[0];
                for (const serverId of candidates) {
                    const metrics = this.metrics.get(serverId);
                    const load = metrics?.activeRequests ?? 0;
                    if (load < minLoad) {
                        minLoad = load;
                        selected = serverId;
                    }
                }
                return selected;
            }
            case "random": {
                const index = Math.floor(Math.random() * candidates.length);
                return candidates[index];
            }
            case "weighted": {
                if (!groupId) {
                    // Fall back to random for non-group selection
                    const index = Math.floor(Math.random() * candidates.length);
                    return candidates[index];
                }
                const group = this.groups.get(groupId);
                if (!group?.weights) {
                    const index = Math.floor(Math.random() * candidates.length);
                    return candidates[index];
                }
                // Build effective weights: use configured weight or default of 1 for unlisted candidates
                const DEFAULT_WEIGHT = 1;
                const effectiveWeights = candidates.map((serverId) => {
                    const weights = group.weights ?? [];
                    const configured = weights.find((w) => w.serverId === serverId);
                    return {
                        serverId,
                        weight: configured?.weight ?? DEFAULT_WEIGHT,
                    };
                });
                const totalWeight = effectiveWeights.reduce((sum, w) => sum + w.weight, 0);
                if (totalWeight === 0) {
                    const index = Math.floor(Math.random() * candidates.length);
                    return candidates[index];
                }
                let random = Math.random() * totalWeight;
                for (const ew of effectiveWeights) {
                    random -= ew.weight;
                    if (random <= 0) {
                        return ew.serverId;
                    }
                }
                return candidates[0];
            }
            case "failover-only": {
                // Return first healthy server by priority
                const serverPriorities = candidates
                    .map((id) => ({
                    id,
                    priority: this.getServerPriority(id, groupId),
                }))
                    .sort((a, b) => a.priority - b.priority);
                return serverPriorities[0]?.id ?? null;
            }
            default:
                return candidates[0];
        }
    }
    /**
     * Get server priority (lower = higher priority)
     *
     * @param serverId - The server to look up
     * @param groupId - Optional group to scope the lookup to, avoiding
     *   nondeterministic iteration across all groups.
     */
    getServerPriority(serverId, groupId) {
        // Scoped lookup: check only the specified group
        if (groupId) {
            const group = this.groups.get(groupId);
            if (group?.weights) {
                const weight = group.weights.find((w) => w.serverId === serverId);
                if (weight) {
                    return weight.priority;
                }
            }
        }
        // Fallback: check all groups for weight/priority settings
        for (const group of this.groups.values()) {
            if (group.weights) {
                const weight = group.weights.find((w) => w.serverId === serverId);
                if (weight) {
                    return weight.priority;
                }
            }
        }
        // Default priority based on order added
        const serverIds = Array.from(this.servers.keys());
        return serverIds.indexOf(serverId);
    }
    /**
     * Update server metrics
     */
    updateMetrics(serverId, updates) {
        const metrics = this.metrics.get(serverId);
        if (metrics) {
            Object.assign(metrics, updates);
            this.emit("metricsUpdated", { serverId, metrics: { ...metrics } });
        }
    }
    /**
     * Mark request started
     */
    requestStarted(serverId) {
        const metrics = this.metrics.get(serverId);
        if (metrics) {
            metrics.activeRequests++;
            metrics.totalRequests++;
        }
    }
    /**
     * Mark request completed
     */
    requestCompleted(serverId, duration, success) {
        const metrics = this.metrics.get(serverId);
        if (metrics) {
            metrics.activeRequests = Math.max(0, metrics.activeRequests - 1);
            metrics.completedRequests++;
            // Update average response time using only completed requests
            const totalTime = metrics.averageResponseTime * (metrics.completedRequests - 1) +
                duration;
            metrics.averageResponseTime = totalTime / metrics.completedRequests;
            // Update error rate (simple moving average)
            const alpha = 0.1; // Smoothing factor
            metrics.errorRate =
                metrics.errorRate * (1 - alpha) + (success ? 0 : 1) * alpha;
        }
    }
    /**
     * Get all servers
     */
    getServers() {
        return Array.from(this.servers.values());
    }
    /**
     * Get server by ID
     */
    getServer(serverId) {
        return this.servers.get(serverId);
    }
    /**
     * Get all groups
     */
    getGroups() {
        return Array.from(this.groups.values());
    }
    /**
     * Get group by ID
     */
    getGroup(groupId) {
        return this.groups.get(groupId);
    }
    /**
     * Get server metrics
     */
    getServerMetrics(serverId) {
        return this.metrics.get(serverId);
    }
    /**
     * Get all metrics
     */
    getAllMetrics() {
        return new Map(this.metrics);
    }
    /**
     * Get statistics
     */
    getStatistics() {
        let healthyServers = 0;
        let totalRequests = 0;
        let activeRequests = 0;
        for (const metrics of this.metrics.values()) {
            if (metrics.isHealthy) {
                healthyServers++;
            }
            totalRequests += metrics.totalRequests;
            activeRequests += metrics.activeRequests;
        }
        const unifiedTools = this.getUnifiedTools();
        const conflictingTools = unifiedTools.filter((t) => t.hasConflict).length;
        return {
            totalServers: this.servers.size,
            healthyServers,
            totalGroups: this.groups.size,
            totalTools: unifiedTools.length,
            conflictingTools,
            totalRequests,
            activeRequests,
        };
    }
}
/**
 * Global multi-server manager instance
 */
export const globalMultiServerManager = new MultiServerManager();
//# sourceMappingURL=multiServerManager.js.map