/**
 * LocalProxy — In-process proxy for local mode.
 *
 * When proxyMode is "local", there is no separate server, no port, no child
 * process, and no encryption. LocalProxy imports the core functions from
 * drawlatch and calls them directly in-process.
 *
 * Supports multiple caller aliases: routes are resolved per-caller and cached.
 * A single shared IngestorManager handles all event sources (ingestors are
 * config-level, not per-caller).
 *
 * Key design: httpRequest() calls executeProxyRequest() imported from
 * drawlatch — the exact same function the remote server uses.
 * No behavioral drift possible.
 */
import { loadRemoteConfig, saveRemoteConfig, resolveCallerRoutes, resolveRoutes, resolveSecrets, } from "@wolpertingerlabs/drawlatch/shared/config";
import { executeProxyRequest } from "@wolpertingerlabs/drawlatch/remote/server";
import { IngestorManager } from "@wolpertingerlabs/drawlatch/remote/ingestors";
import { resetCursorsForAlias } from "./event-watcher.js";
import { createLogger } from "../utils/logger.js";
import fs from "node:fs/promises";
import path from "node:path";
const log = createLogger("local-proxy");
export class LocalProxy {
    mcpConfigDir;
    /** Resolved routes per caller alias */
    routesByCallerAlias;
    _ingestorManager;
    /** The primary caller alias (used as fallback and for ingestor events) */
    primaryCallerAlias;
    constructor(mcpConfigDir, primaryCallerAlias) {
        this.mcpConfigDir = mcpConfigDir;
        this.primaryCallerAlias = primaryCallerAlias;
        this.routesByCallerAlias = new Map();
        // Load config and resolve routes for all callers
        const config = loadRemoteConfig();
        this.resolveAllCallers(config);
        // IngestorManager handles Discord bots, webhook receivers, poll loops, etc.
        this._ingestorManager = new IngestorManager(config);
        log.info(`LocalProxy initialized — primary="${primaryCallerAlias}", callers=${this.routesByCallerAlias.size}, configDir=${mcpConfigDir}`);
    }
    /** Resolve routes for all callers defined in config */
    resolveAllCallers(config) {
        this.routesByCallerAlias.clear();
        for (const [alias, caller] of Object.entries(config.callers)) {
            try {
                const callerRoutes = resolveCallerRoutes(config, alias);
                const callerEnv = resolveSecrets(caller?.env ?? {}, undefined, alias);
                this.routesByCallerAlias.set(alias, resolveRoutes(callerRoutes, callerEnv, alias));
            }
            catch (err) {
                log.warn(`Failed to resolve routes for caller "${alias}": ${err.message}`);
            }
        }
    }
    /** Get resolved routes for a specific caller (falls back to primary) */
    getRoutesForCaller(callerAlias) {
        return this.routesByCallerAlias.get(callerAlias) ?? this.routesByCallerAlias.get(this.primaryCallerAlias) ?? [];
    }
    /** Access the ingestor manager (for webhook route forwarding) */
    get ingestorManager() {
        return this._ingestorManager;
    }
    async start() {
        await this._ingestorManager.startAll();
        await this.stopUserStoppedListeners();
        log.info("LocalProxy ingestors started");
    }
    async stop() {
        await this._ingestorManager.stopAll();
        log.info("LocalProxy ingestors stopped");
    }
    /** Reinitialize after config/secret changes (re-reads config from disk) */
    async reinitialize() {
        await this.stop();
        const config = loadRemoteConfig();
        this.resolveAllCallers(config);
        this._ingestorManager = new IngestorManager(config);
        await this.start();
        // Reset event-watcher cursors for all callers — ingestor restarts reset
        // their event counters, so cursors pointing at old IDs would miss events.
        for (const alias of this.routesByCallerAlias.keys()) {
            resetCursorsForAlias(alias);
        }
        log.info(`LocalProxy reinitialized — callers=${this.routesByCallerAlias.size}`);
    }
    /**
     * Persist listener stopped/started state to remote.config.json.
     * For multi-instance listeners, sets listenerInstances[conn][instanceId].disabled.
     * For single-instance listeners, sets ingestorOverrides[conn].stopped.
     */
    persistListenerState(callerAlias, connection, stopped, instanceId) {
        try {
            const config = loadRemoteConfig();
            const caller = config.callers[callerAlias];
            if (!caller)
                return;
            if (instanceId) {
                // Multi-instance: use existing `disabled` field
                const instance = caller.listenerInstances?.[connection]?.[instanceId];
                if (instance) {
                    if (stopped) {
                        instance.disabled = true;
                    }
                    else {
                        delete instance.disabled;
                    }
                }
            }
            else {
                // Single-instance: use `stopped` field on ingestorOverrides
                caller.ingestorOverrides ??= {};
                caller.ingestorOverrides[connection] ??= {};
                if (stopped) {
                    caller.ingestorOverrides[connection].stopped = true;
                }
                else {
                    delete caller.ingestorOverrides[connection].stopped;
                }
                // Clean up empty override objects
                if (Object.keys(caller.ingestorOverrides[connection]).length === 0) {
                    delete caller.ingestorOverrides[connection];
                }
                if (Object.keys(caller.ingestorOverrides).length === 0) {
                    delete caller.ingestorOverrides;
                }
            }
            saveRemoteConfig(config);
            log.info(`Persisted listener state: ${callerAlias}:${connection}${instanceId ? `:${instanceId}` : ""} stopped=${stopped}`);
        }
        catch (err) {
            log.warn(`Failed to persist listener state: ${err.message}`);
        }
    }
    /**
     * After startAll(), stop any listeners the user had explicitly stopped.
     * Reads stopped state from remote.config.json.
     */
    async stopUserStoppedListeners() {
        const config = loadRemoteConfig();
        for (const [callerAlias, caller] of Object.entries(config.callers)) {
            // Single-instance: check ingestorOverrides[conn].stopped
            if (caller.ingestorOverrides) {
                for (const [conn, overrides] of Object.entries(caller.ingestorOverrides)) {
                    if (overrides?.stopped) {
                        try {
                            await this._ingestorManager.stopOne(callerAlias, conn);
                            log.info(`Auto-stopped listener ${callerAlias}:${conn} (user preference)`);
                        }
                        catch (err) {
                            log.warn(`Failed to auto-stop ${callerAlias}:${conn}: ${err.message}`);
                        }
                    }
                }
            }
            // Multi-instance: check listenerInstances[conn][id].disabled
            if (caller.listenerInstances) {
                for (const [conn, instances] of Object.entries(caller.listenerInstances)) {
                    for (const [instanceId, overrides] of Object.entries(instances)) {
                        if (overrides?.disabled) {
                            try {
                                await this._ingestorManager.stopOne(callerAlias, conn, instanceId);
                                log.info(`Auto-stopped listener instance ${callerAlias}:${conn}:${instanceId} (user preference)`);
                            }
                            catch (err) {
                                log.warn(`Failed to auto-stop ${callerAlias}:${conn}:${instanceId}: ${err.message}`);
                            }
                        }
                    }
                }
            }
        }
    }
    /**
     * Same interface as ProxyClient.callTool() — drop-in replacement.
     * Optional callerAlias parameter selects which caller's routes to use.
     */
    async callTool(toolName, toolInput, callerAlias) {
        const routes = this.getRoutesForCaller(callerAlias ?? this.primaryCallerAlias);
        const effectiveAlias = callerAlias ?? this.primaryCallerAlias;
        switch (toolName) {
            case "http_request": {
                // Delegates to the SAME function the remote server uses — no duplication.
                // In local mode, files arrive as { path } references — read & base64-encode
                // them here (mirrors what drawlatch's MCP server does before sending to remote).
                const input = toolInput;
                const proxyInput = {
                    method: input.method,
                    url: input.url,
                    headers: input.headers,
                    body: input.body,
                };
                if (input.files?.length) {
                    const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB per file
                    proxyInput.files = await Promise.all(input.files.map(async ({ field, path: filePath, filename, contentType }) => {
                        if (!path.isAbsolute(filePath)) {
                            throw new Error(`File path must be absolute: ${filePath}`);
                        }
                        let stat;
                        try {
                            stat = await fs.stat(filePath);
                        }
                        catch {
                            throw new Error(`File not found: ${filePath}`);
                        }
                        if (stat.size > MAX_FILE_SIZE) {
                            throw new Error(`File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB): ${filePath} — max ${MAX_FILE_SIZE / 1024 / 1024} MB`);
                        }
                        const buffer = await fs.readFile(filePath);
                        return { field, data: buffer.toString("base64"), filename, contentType };
                    }));
                    if (input.bodyFieldName) {
                        proxyInput.bodyFieldName = input.bodyFieldName;
                    }
                }
                return executeProxyRequest(proxyInput, routes);
            }
            case "list_routes":
                return routes.map((route, index) => {
                    // Cast to any for optional fields that may not exist in older drawlatch versions
                    const r = route;
                    const info = { index };
                    if (r.alias)
                        info.alias = r.alias;
                    if (route.name)
                        info.name = route.name;
                    if (route.description)
                        info.description = route.description;
                    if (route.docsUrl)
                        info.docsUrl = route.docsUrl;
                    if (route.openApiUrl)
                        info.openApiUrl = route.openApiUrl;
                    info.allowedEndpoints = route.allowedEndpoints;
                    info.secretNames = Object.keys(route.secrets);
                    info.autoHeaders = Object.keys(route.headers);
                    // Ingestor & testing metadata (matches remote server response)
                    // These fields may not exist on older ResolvedRoute versions
                    info.hasTestConnection = r.testConnection !== undefined;
                    info.hasIngestor = r.ingestorConfig !== undefined;
                    if (r.ingestorConfig) {
                        info.ingestorType = r.ingestorConfig.type;
                        info.hasTestIngestor = r.testIngestor !== undefined && r.testIngestor !== null;
                        info.hasListenerConfig = r.listenerConfig !== undefined;
                        if (r.listenerConfig) {
                            info.listenerParamKeys = r.listenerConfig.fields?.map((f) => f.key);
                            info.supportsMultiInstance = r.listenerConfig.supportsMultiInstance ?? false;
                        }
                    }
                    return info;
                });
            case "poll_events": {
                const { connection, after_id, instance_id } = (toolInput ?? {});
                if (connection) {
                    return this._ingestorManager.getEvents(effectiveAlias, connection, after_id ?? -1, instance_id);
                }
                return this._ingestorManager.getAllEvents(effectiveAlias, after_id ?? -1);
            }
            case "ingestor_status":
                return this._ingestorManager.getStatuses(effectiveAlias);
            case "test_connection": {
                const { connection } = (toolInput ?? {});
                const route = routes.find((r) => r.alias === connection);
                if (!route) {
                    return { success: false, connection, error: `Unknown connection: ${connection}` };
                }
                if (!route.testConnection) {
                    return { success: false, connection, supported: false, error: "This connection does not have a test configuration." };
                }
                const testConfig = route.testConnection;
                const method = testConfig.method ?? "GET";
                const expectedStatus = testConfig.expectedStatus ?? [200];
                try {
                    const result = await executeProxyRequest({ method, url: testConfig.url, headers: testConfig.headers, body: testConfig.body }, routes);
                    const isSuccess = expectedStatus.includes(result.status);
                    return {
                        success: isSuccess,
                        connection,
                        status: result.status,
                        statusText: result.statusText,
                        description: testConfig.description,
                        ...(isSuccess ? {} : { error: `Unexpected status ${result.status} (expected ${expectedStatus.join(" or ")})` }),
                    };
                }
                catch (err) {
                    return { success: false, connection, description: testConfig.description, error: err.message };
                }
            }
            case "test_ingestor": {
                const { connection: conn } = (toolInput ?? {});
                const route = routes.find((r) => r.alias === conn);
                if (!route) {
                    return { success: false, connection: conn, error: `Unknown connection: ${conn}` };
                }
                if (!route.ingestorConfig) {
                    return { success: false, connection: conn, supported: false, error: "This connection does not have an event listener." };
                }
                if (route.testIngestor === null) {
                    return { success: false, connection: conn, supported: false, error: "This event listener does not support testing." };
                }
                if (!route.testIngestor) {
                    return { success: false, connection: conn, supported: false, error: "This event listener does not have a test configuration." };
                }
                const ingestorTestConfig = route.testIngestor;
                try {
                    switch (ingestorTestConfig.strategy) {
                        case "webhook_verify": {
                            const missing = [];
                            for (const secretName of ingestorTestConfig.requireSecrets ?? []) {
                                if (!route.secrets[secretName])
                                    missing.push(secretName);
                            }
                            if (missing.length > 0) {
                                return {
                                    success: false,
                                    connection: conn,
                                    strategy: ingestorTestConfig.strategy,
                                    description: ingestorTestConfig.description,
                                    error: `Missing required secrets: ${missing.join(", ")}`,
                                };
                            }
                            return {
                                success: true,
                                connection: conn,
                                strategy: ingestorTestConfig.strategy,
                                description: ingestorTestConfig.description,
                                message: "All required webhook secrets are configured.",
                            };
                        }
                        case "websocket_auth":
                        case "http_request":
                        case "poll_once": {
                            if (!ingestorTestConfig.request) {
                                return {
                                    success: false,
                                    connection: conn,
                                    strategy: ingestorTestConfig.strategy,
                                    description: ingestorTestConfig.description,
                                    error: "Test configuration missing request details.",
                                };
                            }
                            const testMethod = ingestorTestConfig.request.method ?? "GET";
                            const expectedCodes = ingestorTestConfig.request.expectedStatus ?? [200];
                            const result = await executeProxyRequest({ method: testMethod, url: ingestorTestConfig.request.url, headers: ingestorTestConfig.request.headers, body: ingestorTestConfig.request.body }, routes);
                            const passed = expectedCodes.includes(result.status);
                            return {
                                success: passed,
                                connection: conn,
                                strategy: ingestorTestConfig.strategy,
                                status: result.status,
                                statusText: result.statusText,
                                description: ingestorTestConfig.description,
                                ...(passed ? { message: "Listener test passed." } : { error: `Unexpected status ${result.status}` }),
                            };
                        }
                        default:
                            return { success: false, connection: conn, error: `Unknown test strategy: ${String(ingestorTestConfig.strategy)}` };
                    }
                }
                catch (err) {
                    return { success: false, connection: conn, strategy: ingestorTestConfig.strategy, description: ingestorTestConfig.description, error: err.message };
                }
            }
            case "control_listener": {
                const { connection: conn, action, instance_id, } = (toolInput ?? {});
                try {
                    let result;
                    switch (action) {
                        case "start":
                            result = await this._ingestorManager.startOne(effectiveAlias, conn, instance_id);
                            break;
                        case "stop":
                            result = await this._ingestorManager.stopOne(effectiveAlias, conn, instance_id);
                            break;
                        case "restart":
                            result = await this._ingestorManager.restartOne(effectiveAlias, conn, instance_id);
                            break;
                        default:
                            return { success: false, connection: conn, error: `Unknown action: ${String(action)}` };
                    }
                    // Persist start/stop state so it survives restarts
                    if (action === "start" || action === "stop") {
                        this.persistListenerState(effectiveAlias, conn, action === "stop", instance_id);
                    }
                    return result;
                }
                catch (err) {
                    return { success: false, connection: conn, action, error: err.message };
                }
            }
            case "list_listener_configs": {
                return routes
                    .filter((r) => r.listenerConfig)
                    .map((r) => ({
                    connection: r.alias,
                    name: r.listenerConfig.name,
                    description: r.listenerConfig.description,
                    fields: r.listenerConfig.fields,
                    ingestorType: r.ingestorConfig?.type,
                    supportsMultiInstance: r.listenerConfig.supportsMultiInstance ?? false,
                    instanceKeyField: r.listenerConfig.fields?.find((f) => f.instanceKey)?.key,
                }));
            }
            case "resolve_listener_options": {
                const { connection: conn, paramKey } = (toolInput ?? {});
                const route = routes.find((r) => r.alias === conn);
                if (!route?.listenerConfig) {
                    return { success: false, error: `No listener config for connection: ${conn}` };
                }
                const field = route.listenerConfig.fields?.find((f) => f.key === paramKey);
                if (!field?.dynamicOptions) {
                    return { success: false, error: `No dynamic options for field: ${paramKey}` };
                }
                const { url, method = "GET", body, responsePath, labelField, valueField } = field.dynamicOptions;
                try {
                    const result = await executeProxyRequest({ method, url, headers: {}, body }, routes);
                    let items = result.body;
                    if (responsePath) {
                        for (const segment of responsePath.split(".")) {
                            items = items?.[segment];
                        }
                    }
                    if (!Array.isArray(items)) {
                        return { success: false, error: "Response did not contain an array at the expected path." };
                    }
                    const options = items.map((item) => ({
                        value: item[valueField],
                        label: item[labelField],
                    }));
                    return { success: true, connection: conn, paramKey, options };
                }
                catch (err) {
                    return { success: false, connection: conn, paramKey, error: err.message };
                }
            }
            case "get_listener_params": {
                const { connection, instance_id } = (toolInput ?? {});
                const route = routes.find((r) => r.alias === connection);
                if (!route) {
                    return { success: false, connection, error: `Unknown connection: ${connection}` };
                }
                if (!route.listenerConfig) {
                    return { success: false, connection, error: `No listener config for connection: ${connection}` };
                }
                // Build defaults from schema fields
                const defaults = {};
                for (const field of route.listenerConfig.fields ?? []) {
                    if (field.default !== undefined) {
                        defaults[field.key] = field.default;
                    }
                }
                // Read current overrides from config
                const paramConfig = loadRemoteConfig();
                const paramCaller = paramConfig.callers[effectiveAlias];
                let params = {};
                if (instance_id) {
                    // Multi-instance: read from listenerInstances
                    const instanceOverrides = paramCaller?.listenerInstances?.[connection]?.[instance_id];
                    if (!instanceOverrides) {
                        return { success: false, connection, instance_id, error: `Instance not found: ${instance_id}` };
                    }
                    params = instanceOverrides.params ?? {};
                }
                else {
                    // Single-instance: read from ingestorOverrides
                    const overrides = paramCaller?.ingestorOverrides?.[connection];
                    params = overrides?.params ?? {};
                }
                return {
                    success: true,
                    connection,
                    ...(instance_id && { instance_id }),
                    params,
                    defaults,
                };
            }
            case "set_listener_params": {
                const { connection, instance_id, params, create_instance } = (toolInput ?? {});
                const route = routes.find((r) => r.alias === connection);
                if (!route) {
                    return { success: false, connection, error: `Unknown connection: ${connection}` };
                }
                if (!route.listenerConfig) {
                    return { success: false, connection, error: `No listener config for connection: ${connection}` };
                }
                // Validate param keys against schema
                const validKeys = new Set((route.listenerConfig.fields ?? []).map((f) => f.key));
                const unknownKeys = Object.keys(params).filter((k) => !validKeys.has(k));
                if (unknownKeys.length > 0) {
                    return {
                        success: false,
                        connection,
                        error: `Unknown parameter keys: ${unknownKeys.join(", ")}. Valid keys: ${Array.from(validKeys).join(", ")}`,
                    };
                }
                // Load config, modify, save
                const setConfig = loadRemoteConfig();
                const setCaller = setConfig.callers[effectiveAlias];
                if (!setCaller) {
                    return { success: false, connection, error: `Caller not found: ${effectiveAlias}` };
                }
                let mergedParams;
                if (instance_id) {
                    // Multi-instance: write to listenerInstances
                    setCaller.listenerInstances ??= {};
                    setCaller.listenerInstances[connection] ??= {};
                    const existing = setCaller.listenerInstances[connection][instance_id];
                    if (!existing && !create_instance) {
                        return {
                            success: false,
                            connection,
                            instance_id,
                            error: `Instance "${instance_id}" does not exist. Set create_instance to true to create it.`,
                        };
                    }
                    if (existing) {
                        existing.params = { ...(existing.params ?? {}), ...params };
                        mergedParams = existing.params;
                    }
                    else {
                        setCaller.listenerInstances[connection][instance_id] = { params };
                        mergedParams = params;
                    }
                }
                else {
                    // Single-instance: write to ingestorOverrides
                    setCaller.ingestorOverrides ??= {};
                    setCaller.ingestorOverrides[connection] ??= {};
                    const overrides = setCaller.ingestorOverrides[connection];
                    overrides.params = { ...(overrides.params ?? {}), ...params };
                    mergedParams = overrides.params;
                }
                saveRemoteConfig(setConfig);
                // Re-resolve routes so the new config is picked up
                this.resolveAllCallers(loadRemoteConfig());
                // Restart just the affected ingestor so new params take effect immediately.
                // Matches drawlatch remote server behavior (restartOne, not full reinitialize).
                if (this._ingestorManager.has(effectiveAlias, connection, instance_id)) {
                    try {
                        await this._ingestorManager.restartOne(effectiveAlias, connection, instance_id);
                    }
                    catch (err) {
                        // Config was saved successfully — log the restart failure but don't fail the operation
                        log.warn(`Params saved but failed to restart ingestor ${effectiveAlias}:${connection}${instance_id ? `:${instance_id}` : ""}: ${err.message}`);
                        return {
                            success: true,
                            connection,
                            ...(instance_id && { instance_id }),
                            params: mergedParams,
                            warning: "Params saved but ingestor restart failed. Use control_listener to restart manually.",
                        };
                    }
                }
                return {
                    success: true,
                    connection,
                    ...(instance_id && { instance_id }),
                    params: mergedParams,
                };
            }
            case "list_listener_instances": {
                const { connection } = (toolInput ?? {});
                const route = routes.find((r) => r.alias === connection);
                if (!route) {
                    return { success: false, connection, error: `Unknown connection: ${connection}` };
                }
                if (!route.listenerConfig?.supportsMultiInstance) {
                    return { success: false, connection, error: "This connection does not support multi-instance listeners." };
                }
                // Read all instances from config (including stopped/disabled ones)
                const liConfig = loadRemoteConfig();
                const liCaller = liConfig.callers[effectiveAlias];
                if (!liCaller) {
                    return { success: false, connection, error: `Caller not found: ${effectiveAlias}` };
                }
                const instanceMap = liCaller.listenerInstances?.[connection] ?? {};
                const instanceList = Object.entries(instanceMap).map(([instanceId, overrides]) => ({
                    instanceId,
                    disabled: overrides?.disabled ?? false,
                    params: overrides?.params ?? {},
                }));
                return { success: true, connection, instances: instanceList };
            }
            case "delete_listener_instance": {
                const { connection, instance_id } = (toolInput ?? {});
                if (!instance_id) {
                    return { success: false, connection, error: "instance_id is required" };
                }
                const delConfig = loadRemoteConfig();
                const delCaller = delConfig.callers[effectiveAlias];
                if (!delCaller) {
                    return { success: false, connection, instance_id, error: `Caller not found: ${effectiveAlias}` };
                }
                const instances = delCaller.listenerInstances?.[connection];
                if (!instances || !(instance_id in instances)) {
                    return { success: false, connection, instance_id, error: `Instance "${instance_id}" not found for connection "${connection}".` };
                }
                // Stop the running ingestor if active
                if (this._ingestorManager.has(effectiveAlias, connection, instance_id)) {
                    try {
                        await this._ingestorManager.stopOne(effectiveAlias, connection, instance_id);
                    }
                    catch {
                        // Log but don't fail the delete
                        log.warn(`Failed to stop ingestor ${effectiveAlias}:${connection}:${instance_id} during delete`);
                    }
                }
                // Remove from config
                delete instances[instance_id];
                // Clean up empty maps
                if (Object.keys(instances).length === 0) {
                    delete delCaller.listenerInstances[connection];
                    if (Object.keys(delCaller.listenerInstances).length === 0) {
                        delete delCaller.listenerInstances;
                    }
                }
                saveRemoteConfig(delConfig);
                // Re-resolve routes so the deleted instance is no longer visible
                this.resolveAllCallers(loadRemoteConfig());
                return { success: true, connection, instance_id };
            }
            default:
                throw new Error(`Unknown tool: ${toolName}`);
        }
    }
}
