/**
 * ╔══════════════════════════════════════════════════════════════════════╗
 * ║   OVERMIND BRIDGE — BridgeProxy (Base Client SDK Proxy)              ║
 * ║                                                                      ║
 * ║   [SCRIPT DE BASE - CLIENT SDK LOW-LEVEL TRANSPORT]                  ║
 * ║   Couche basse : transport JSON-RPC 2.0 vers Overmind MCP Server.   ║
 * ║   Gère : reconnexion, health checks, circuit breaker, SSE streaming.║
 * ║                                                                      ║
 * ║   ARCHITECTURE                                                       ║
 * ║   ─────────────                                                      ║
 * ║   OverBridgeService → BridgeProxy → HTTP localhost:3099/mcp          ║
 * ║                                              → Overmind MCP Server  ║
 * ╚══════════════════════════════════════════════════════════════════════╝
 */
import { DEFAULT_BRIDGE_CONFIG, DEFAULT_CIRCUIT_CONFIG } from './types.js';
import { createBridgeLogger, parseMcpResponseBody, withRetry, } from './utils.js';
// ─── Circuit Breaker ───────────────────────────────────────────────────────
class CircuitBreaker {
    config;
    state = 'closed';
    failureCount = 0;
    successCount = 0;
    lastFailureAt = 0;
    constructor(config) {
        this.config = config;
    }
    get currentState() {
        // Auto-transition open → half-open après resetTimeout
        if (this.state === 'open' && Date.now() - this.lastFailureAt > this.config.resetTimeoutMs) {
            this.state = 'half-open';
        }
        return this.state;
    }
    recordSuccess() {
        if (this.state === 'half-open') {
            this.successCount++;
            if (this.successCount >= this.config.successThreshold) {
                this.state = 'closed';
                this.failureCount = 0;
                this.successCount = 0;
            }
        }
        else {
            this.failureCount = 0;
        }
    }
    recordFailure() {
        this.failureCount++;
        this.lastFailureAt = Date.now();
        this.successCount = 0;
        if (this.failureCount >= this.config.failureThreshold) {
            this.state = 'open';
        }
    }
    canExecute() {
        return this.currentState !== 'open';
    }
}
// ─── BridgeProxy ───────────────────────────────────────────────────────────
export class BridgeProxy {
    rpcId = 1;
    config;
    circuit;
    log;
    constructor(config, circuitConfig, logger) {
        this.config = { ...DEFAULT_BRIDGE_CONFIG, ...config };
        this.circuit = new CircuitBreaker({ ...DEFAULT_CIRCUIT_CONFIG, ...circuitConfig });
        this.log = logger ?? createBridgeLogger('bridge-proxy');
    }
    get mcpUrl() {
        return this.config.mcpUrl;
    }
    get agentTimeout() {
        return this.config.agentTimeoutMs;
    }
    get circuitState() {
        return this.circuit.currentState;
    }
    // ─── Core RPC Call ───────────────────────────────────────────────────────
    /**
     * Appelle un tool MCP via JSON-RPC 2.0 sur HTTP.
     *
     * TIMEOUT STRATEGY (3 couches) :
     *   1. AbortController sur le fetch initial
     *   2. Body reader avec deadline absolue
     *   3. Promise.race per-chunk (sécurité si le serveur ne produit plus)
     *
     * @param toolName  Nom de l'outil MCP (ex: 'run_agent', 'memory_search')
     * @param args      Arguments de l'outil
     * @param timeoutMs Timeout en ms (default: config.defaultTimeoutMs)
     */
    async call(toolName, args, timeoutMs) {
        const timeout = timeoutMs ?? this.config.defaultTimeoutMs;
        // Circuit breaker gate
        if (!this.circuit.canExecute()) {
            throw Object.assign(new Error(`Circuit breaker OPEN — too many failures. Retry after ${this.config.retryDelayMs}ms`), { code: 'ECIRCUITOPEN' });
        }
        const request = {
            jsonrpc: '2.0',
            id: this.rpcId++,
            method: 'tools/call',
            params: { name: toolName, arguments: args },
        };
        this.log.info(`→ MCP ${toolName} (timeout: ${timeout / 1000}s)`);
        const startTime = Date.now();
        try {
            const response = await this._fetch(request, timeout);
            const elapsed = Date.now() - startTime;
            this.circuit.recordSuccess();
            this.log.info(`← MCP ${toolName} OK (${elapsed}ms)`);
            return response;
        }
        catch (err) {
            const error = err;
            this.circuit.recordFailure();
            // Erreurs transitoires → retry automatique
            const retryable = ['ETIMEDOUT', 'EBODYREAD', 'ECONNRESET', 'ECONNREFUSED'];
            if (retryable.includes(error?.code ?? '')) {
                this.log.warn(`🔁 ${error.code} on ${toolName}, retrying...`);
                return withRetry(() => this._fetch(request, timeout), {
                    maxAttempts: this.config.maxRetries,
                    delayMs: this.config.retryDelayMs,
                }, this.log);
            }
            throw err;
        }
    }
    // ─── JSON-RPC Protocol Ping ──────────────────────────────────────────────
    /**
     * Envoie une requête de ping JSON-RPC standard légère au serveur.
     */
    async ping(timeoutMs) {
        const timeout = timeoutMs ?? 5_000;
        const request = {
            jsonrpc: '2.0',
            id: this.rpcId++,
            method: 'ping',
            params: {},
        };
        try {
            const controller = new AbortController();
            const timer = setTimeout(() => controller.abort(), timeout);
            const response = await fetch(this.config.mcpUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                },
                body: JSON.stringify(request),
                signal: controller.signal,
            });
            clearTimeout(timer);
            if (!response.ok)
                return false;
            const bodyText = await response.text();
            const parsed = JSON.parse(bodyText.trim());
            return parsed && parsed.jsonrpc === '2.0' && !parsed.error;
        }
        catch {
            return false;
        }
    }
    // ─── Health Check ────────────────────────────────────────────────────────
    async healthCheck() {
        const startTime = Date.now();
        const isOnline = await this.ping(5_000);
        if (isOnline) {
            return {
                status: 'online',
                mcpUrl: this.config.mcpUrl,
                circuitState: this.circuit.currentState,
                latencyMs: Date.now() - startTime,
                checkedAt: Date.now(),
            };
        }
        else {
            return {
                status: this.circuit.currentState === 'open' ? 'offline' : 'degraded',
                mcpUrl: this.config.mcpUrl,
                circuitState: this.circuit.currentState,
                checkedAt: Date.now(),
            };
        }
    }
    // ─── Internal Fetch ──────────────────────────────────────────────────────
    async _fetch(request, timeoutMs) {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), timeoutMs);
        let response;
        try {
            response = await fetch(this.config.mcpUrl, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json, text/event-stream',
                },
                body: JSON.stringify(request),
                signal: controller.signal,
            });
        }
        catch (err) {
            clearTimeout(timer);
            const error = err;
            if (error.name === 'TimeoutError' || error.type === 'TimeoutError' || error.name === 'AbortError') {
                throw Object.assign(new Error(`TIMEOUT: ${request.params.name} no response after ${timeoutMs / 1000}s`), { code: 'ETIMEDOUT', toolName: request.params.name, timeoutMs });
            }
            throw err;
        }
        finally {
            clearTimeout(timer);
        }
        if (!response.ok) {
            throw Object.assign(new Error(`MCP HTTP ${response.status}: ${response.statusText}`), { code: 'EHTTPSTATUS', status: response.status });
        }
        // Lecture du body avec timeout effectif (3 couches)
        const text = await this._readBody(response, timeoutMs);
        return parseMcpResponseBody(text);
    }
    /**
     * Lecture du body response avec 3 couches de timeout :
     *   1. Deadline absolue (startRead + timeoutMs)
     *   2. Promise.race per-chunk
     *   3. reader.cancel() sur timeout
     */
    async _readBody(response, timeoutMs) {
        if (!response.body) {
            throw Object.assign(new Error('Response body is null'), { code: 'EBODYREAD' });
        }
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        const chunks = [];
        const startRead = Date.now();
        try {
            while (true) {
                // Couche 1 — deadline absolue
                if (Date.now() - startRead > timeoutMs) {
                    reader.cancel();
                    throw Object.assign(new Error(`Body read timeout after ${timeoutMs / 1000}s`), { code: 'EBODYREAD' });
                }
                // Couche 2+3 — Promise.race per-chunk (avec cleanup du timeout)
                let raceTimer;
                const { done, value } = await Promise.race([
                    reader.read(),
                    new Promise((_, reject) => {
                        raceTimer = setTimeout(() => reject(Object.assign(new Error(`Chunk read timeout after ${timeoutMs / 1000}s`), { code: 'EBODYREAD' })), timeoutMs);
                    }),
                ]);
                if (raceTimer) {
                    clearTimeout(raceTimer);
                }
                if (done)
                    break;
                chunks.push(decoder.decode(value, { stream: true }));
            }
            chunks.push(decoder.decode());
            return chunks.join('');
        }
        catch (err) {
            const error = err;
            if (error?.code === 'EBODYREAD')
                throw err;
            throw Object.assign(new Error(`Body read failed: ${error.message}`), { code: 'EBODYREAD' });
        }
    }
}
//# sourceMappingURL=BridgeProxy.js.map