/**
 * Proxy client manager.
 *
 * Creates and caches ProxyClient instances per key alias. Each alias
 * corresponds to a subdirectory under {mcpConfigDir}/keys/callers/{alias}/
 * containing the Ed25519/X25519 keypair for that identity. The remote
 * server's public keys are always at {mcpConfigDir}/keys/server/.
 *
 * In local mode, a shared LocalProxy instance is used instead of per-alias
 * ProxyClient instances. getProxy() returns the appropriate implementation
 * based on the configured proxyMode.
 *
 * Reads mcpConfigDir from agent-settings.json (set via the Agent Settings UI).
 * Falls back to env vars EVENT_WATCHER_KEYS_DIR / EVENT_WATCHER_REMOTE_KEYS_DIR
 * for backwards compatibility.
 *
 * Configuration:
 *   EVENT_WATCHER_REMOTE_URL — remote proxy server URL (default: http://127.0.0.1:9999)
 */
import { join } from "path";
import { existsSync } from "fs";
import { ProxyClient } from "./proxy-client.js";
import { LocalProxy } from "./local-proxy.js";
import { getAgentSettings, discoverKeyAliases, getActiveMcpConfigDir, ensureLocalProxyConfigDir, ensureRemoteProxyConfigDir } from "./agent-settings.js";
import { startTunnelIfEnabled, stopTunnel } from "./tunnel-manager.js";
import { createLogger } from "../utils/logger.js";
const log = createLogger("proxy-manager");
const REMOTE_URL = process.env.EVENT_WATCHER_REMOTE_URL || "http://127.0.0.1:9999";
// ── Singleton LocalProxy instance (shared across all sessions) ──────
let localProxyInstance = null;
export function getLocalProxyInstance() {
    return localProxyInstance;
}
export function setLocalProxyInstance(proxy) {
    localProxyInstance = proxy;
}
// ── Per-alias client cache (remote mode) ────────────────────────────
const clientCache = new Map();
const failedAliases = new Set();
/**
 * Resolve key paths for a given alias.
 * Returns null if mcpConfigDir is not set or key files don't exist.
 */
function resolveKeyPaths(alias) {
    const configDir = getActiveMcpConfigDir();
    if (!configDir)
        return null;
    const keysDir = join(configDir, "keys", "callers", alias);
    const remoteKeysDir = join(configDir, "keys", "server");
    // Verify both directories exist with required key files
    if (!existsSync(keysDir) ||
        !existsSync(remoteKeysDir) ||
        !existsSync(join(keysDir, "signing.key.pem")) ||
        !existsSync(join(remoteKeysDir, "signing.pub.pem"))) {
        return null;
    }
    return { keysDir, remoteKeysDir };
}
// ── Public API ──────────────────────────────────────────────────────
/**
 * Get the appropriate proxy for a given alias.
 * In local mode: returns a wrapper around the shared LocalProxy that
 *   routes calls through the specific caller alias's routes.
 * In remote mode: returns a cached ProxyClient for the alias.
 */
export function getProxy(alias) {
    const settings = getAgentSettings();
    if (settings.proxyMode === "local") {
        if (!localProxyInstance)
            return null;
        // Return a wrapper that passes the caller alias to callTool
        return {
            callTool: (toolName, toolInput) => localProxyInstance.callTool(toolName, toolInput, alias),
        };
    }
    else {
        return getProxyClient(alias); // existing behavior
    }
}
/**
 * Get a ProxyClient for a specific key alias (remote mode).
 * Creates and caches the client on first call. Returns null if keys
 * are missing or client creation fails.
 */
export function getProxyClient(alias) {
    if (failedAliases.has(alias))
        return null;
    const cached = clientCache.get(alias);
    if (cached)
        return cached;
    const paths = resolveKeyPaths(alias);
    if (!paths) {
        log.debug(`No valid keys for alias "${alias}"`);
        return null;
    }
    const settings = getAgentSettings();
    const remoteUrl = settings.remoteServerUrl || REMOTE_URL;
    try {
        const client = new ProxyClient(remoteUrl, paths.keysDir, paths.remoteKeysDir);
        clientCache.set(alias, client);
        log.info(`Proxy client created for alias "${alias}" — remote=${remoteUrl}`);
        return client;
    }
    catch (err) {
        log.error(`Failed to create proxy client for alias "${alias}": ${err.message}`);
        failedAliases.add(alias);
        return null;
    }
}
/**
 * Check whether the proxy is configured (mcpConfigDir set with appropriate mode config).
 */
export function isProxyConfigured() {
    const settings = getAgentSettings();
    const configDir = getActiveMcpConfigDir();
    if (!configDir)
        return false;
    // In local mode, proxy is configured if config dir is set and mode is "local"
    if (settings.proxyMode === "local")
        return true;
    // In remote mode, check for usable key aliases
    const aliases = discoverKeyAliases();
    return aliases.some((a) => a.hasSigningPub && a.hasExchangePub);
}
/**
 * Get all configured aliases that have valid key files.
 */
export function getConfiguredAliases() {
    const aliases = discoverKeyAliases();
    return aliases.filter((a) => a.hasSigningPub && a.hasExchangePub).map((a) => a.alias);
}
/**
 * Remove a cached client, forcing a fresh ProxyClient on next getProxyClient() call.
 */
export function resetClient(alias) {
    clientCache.delete(alias);
    failedAliases.delete(alias);
    log.info(`Reset proxy client cache for alias "${alias}"`);
}
/**
 * Clear all cached clients and failed aliases.
 */
export function resetAllClients() {
    clientCache.clear();
    failedAliases.clear();
    log.info("Reset all proxy client caches");
}
/**
 * Handle proxy mode switching at runtime.
 *
 * When switching to "local": creates and starts a LocalProxy instance.
 * When switching away from "local": stops and clears the LocalProxy.
 * Always resets cached remote ProxyClient instances.
 */
export async function switchProxyMode(newMode) {
    resetAllClients();
    // Tear down existing tunnel and LocalProxy when any local-mode state changes
    // (switching away from local, OR re-creating local with changed settings like tunnelEnabled)
    if (localProxyInstance) {
        try {
            await stopTunnel();
        }
        catch (err) {
            log.warn(`Failed to stop tunnel during mode switch: ${err.message}`);
        }
        try {
            await localProxyInstance.stop();
            log.info("LocalProxy stopped (mode switch / settings change)");
        }
        catch (err) {
            log.warn(`Failed to stop LocalProxy during mode switch: ${err.message}`);
        }
        localProxyInstance = null;
    }
    // Create LocalProxy if switching to (or staying in) local mode
    if (newMode === "local") {
        ensureLocalProxyConfigDir();
        const configDir = getActiveMcpConfigDir();
        if (configDir) {
            // Sync MCP_CONFIG_DIR so drawlatch's loadRemoteConfig reads the right dir
            process.env.MCP_CONFIG_DIR = configDir;
            // Load .env secrets into process.env
            const { loadMcpEnvIntoProcess } = await import("./connection-manager.js");
            loadMcpEnvIntoProcess();
            // Start tunnel if enabled — must happen before LocalProxy constructor
            // so callback URL env vars are available during resolveSecrets()
            const PORT = process.env.PORT || 8000;
            try {
                await startTunnelIfEnabled(PORT);
            }
            catch (err) {
                log.error(`Tunnel startup failed during mode switch: ${err.message}`);
            }
            try {
                const proxy = new LocalProxy(configDir, "default");
                await proxy.start();
                localProxyInstance = proxy;
                log.info(`LocalProxy created and started (configDir=${configDir})`);
            }
            catch (err) {
                log.error(`Failed to create LocalProxy during mode switch: ${err.message}`);
            }
        }
        else {
            log.warn("Cannot create LocalProxy: no MCP config directory configured");
        }
    }
    else if (newMode === "remote") {
        ensureRemoteProxyConfigDir();
    }
}
// ── Backwards-compatible shims ──────────────────────────────────────
// These are kept temporarily so any code not yet migrated doesn't break.
// They try the first configured alias.
/**
 * @deprecated Use getProxyClient(alias) instead
 */
export function getSharedProxyClient() {
    const aliases = getConfiguredAliases();
    if (aliases.length === 0)
        return null;
    return getProxyClient(aliases[0]);
}
/**
 * Test connectivity to a remote proxy server.
 *
 * 1. Health check — is the server reachable?
 * 2. Full handshake — are keys valid and authorized?
 * 3. List routes — can we make authenticated requests?
 */
export async function testRemoteConnection(url, alias) {
    // ── Step 1: Health check ──────────────────────────────────────────
    try {
        const healthRes = await fetch(`${url}/health`, {
            signal: AbortSignal.timeout(5000),
        });
        if (!healthRes.ok) {
            return {
                status: "unreachable",
                message: `Server responded with HTTP ${healthRes.status}`,
            };
        }
    }
    catch (err) {
        const code = err?.cause?.code || err?.code || "";
        if (code === "ECONNREFUSED") {
            return { status: "unreachable", message: "Connection refused — server may not be running" };
        }
        if (code === "ENOTFOUND") {
            return { status: "unreachable", message: "Host not found — check the URL" };
        }
        if (err.name === "TimeoutError" || err.name === "AbortError") {
            return { status: "unreachable", message: "Connection timed out — server may not be running" };
        }
        return { status: "unreachable", message: `Cannot reach server: ${err.message}` };
    }
    // ── Step 2: Handshake ─────────────────────────────────────────────
    const paths = resolveKeyPaths(alias);
    if (!paths) {
        return {
            status: "handshake_failed",
            message: `No valid keys found for alias "${alias}". Check that keys exist in the MCP config directory.`,
        };
    }
    let client;
    try {
        client = new ProxyClient(url, paths.keysDir, paths.remoteKeysDir);
    }
    catch (err) {
        return {
            status: "handshake_failed",
            message: `Failed to load keys: ${err.message}`,
        };
    }
    try {
        await client.handshake();
    }
    catch (err) {
        return {
            status: "handshake_failed",
            message: `Handshake failed: ${err.message}`,
        };
    }
    // ── Step 3: List routes (proves the encrypted channel works) ──────
    try {
        const routes = (await client.callTool("list_routes"));
        return {
            status: "connected",
            message: `Connected successfully — ${routes?.length ?? 0} route(s) available`,
            routeCount: routes?.length ?? 0,
        };
    }
    catch (err) {
        // Handshake succeeded but request failed — still partially connected
        return {
            status: "connected",
            message: `Handshake succeeded but route listing failed: ${err.message}`,
            routeCount: 0,
        };
    }
}
