/**
 * Tunnel manager for local proxy mode.
 *
 * Manages a cloudflared quick tunnel that exposes the callboard server to the
 * internet so that webhook-based ingestors (Trello, GitHub, Stripe, etc.) can
 * receive events from external services.
 *
 * The tunnel URL is auto-populated into callback URL env vars (e.g.,
 * TRELLO_CALLBACK_URL) before LocalProxy is created, so that drawlatch's
 * secret resolution picks up the correct values.
 *
 * Lifecycle:
 *   startTunnelIfEnabled() → sets env vars → caller creates LocalProxy
 *   stopTunnel()           → tears down cloudflared child process
 */
import { startTunnel, isCloudflaredAvailable, waitForTunnelReady } from "@wolpertingerlabs/drawlatch/remote/tunnel";
import { loadRemoteConfig, resolveCallerRoutes } from "@wolpertingerlabs/drawlatch/shared/config";
import { getAgentSettings } from "./agent-settings.js";
import { createLogger } from "../utils/logger.js";
const log = createLogger("tunnel-manager");
// ── Module state ──────────────────────────────────────────────────────
let tunnelUrl = null;
let stopFn = null;
/**
 * Start a cloudflared tunnel if `tunnelEnabled` is true in agent settings.
 *
 * Must be called BEFORE `new LocalProxy()` so that callback URL env vars
 * (e.g., TRELLO_CALLBACK_URL) are available during drawlatch's
 * `resolveSecrets()` in the constructor.
 *
 * @param port — The local port callboard is listening on.
 * @param host — The local host (default "127.0.0.1").
 * @returns The tunnel URL if started, or null if skipped/failed.
 */
export async function startTunnelIfEnabled(port, host = "127.0.0.1") {
    const settings = getAgentSettings();
    if (!settings.tunnelEnabled) {
        log.debug("Tunnel not enabled in settings, skipping");
        return null;
    }
    // Pre-flight: is cloudflared installed?
    const available = await isCloudflaredAvailable();
    if (!available) {
        log.warn("cloudflared binary not found — tunnel cannot start. Install: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/");
        return null;
    }
    // If a tunnel is already running, stop it first
    if (stopFn) {
        log.info("Stopping existing tunnel before starting new one");
        await stopTunnel();
    }
    const numericPort = typeof port === "string" ? parseInt(port, 10) : port;
    try {
        log.info(`Starting cloudflared tunnel → http://${host}:${numericPort}`);
        const tunnel = await startTunnel({ port: numericPort, host });
        tunnelUrl = tunnel.url;
        stopFn = tunnel.stop;
        // Set the tunnel URL in process.env so it's available for secret resolution
        process.env.DRAWLATCH_TUNNEL_URL = tunnelUrl;
        // Auto-populate callback URL env vars for webhook ingestors
        autoPopulateCallbackUrls(tunnelUrl);
        log.info(`Tunnel active: ${tunnelUrl}`);
        log.info(`Webhook URL:   ${tunnelUrl}/webhooks/<path>`);
        // Wait for the tunnel to be fully connected before returning.
        // cloudflared reports the URL before the QUIC connection is established;
        // services like Trello validate the callback URL at registration time.
        await waitForTunnelReady(tunnelUrl);
        return tunnelUrl;
    }
    catch (err) {
        log.error(`Failed to start tunnel: ${err.message}`);
        log.warn("Continuing without tunnel. Webhook ingestors will only work on localhost.");
        tunnelUrl = null;
        stopFn = null;
        return null;
    }
}
/**
 * Stop the active cloudflared tunnel (if any).
 * Safe to call multiple times / when no tunnel is active.
 */
export async function stopTunnel() {
    if (!stopFn)
        return;
    try {
        await stopFn();
        log.info("Tunnel stopped");
    }
    catch (err) {
        log.error(`Error stopping tunnel: ${err.message}`);
    }
    finally {
        tunnelUrl = null;
        stopFn = null;
        delete process.env.DRAWLATCH_TUNNEL_URL;
        clearAutoPopulatedCallbackUrls();
    }
}
/**
 * Get current tunnel status.
 */
export function getTunnelStatus() {
    return {
        active: tunnelUrl !== null,
        ...(tunnelUrl && { url: tunnelUrl }),
    };
}
/**
 * Get current tunnel status including cloudflared availability check.
 * The availability check spawns a process, so this is async.
 */
export async function getTunnelStatusFull() {
    const available = await isCloudflaredAvailable();
    return {
        active: tunnelUrl !== null,
        ...(tunnelUrl && { url: tunnelUrl }),
        cloudflaredAvailable: available,
    };
}
// ── Internal helpers ──────────────────────────────────────────────────
/** Env var names that were auto-populated by autoPopulateCallbackUrls().
 *  Tracked so stopTunnel() can clean them up to prevent stale URLs. */
const autoPopulatedEnvVars = new Set();
/**
 * Auto-populate callback URL env vars for webhook ingestors.
 *
 * Scans all callers' connection routes for webhook ingestors that reference
 * an env var in their callbackUrl (e.g., "${TRELLO_CALLBACK_URL}"). Sets
 * (or overwrites) the env var with the tunnel URL + the webhook path.
 *
 * Always overwrites existing values — after a tunnel restart the URL changes,
 * so any previously set callback URL is stale and must be replaced.
 *
 * Ported from drawlatch server.ts tunnel integration (lines 1403-1421).
 */
function autoPopulateCallbackUrls(url) {
    try {
        const config = loadRemoteConfig();
        for (const [callerAlias] of Object.entries(config.callers)) {
            const rawRoutes = resolveCallerRoutes(config, callerAlias);
            for (const route of rawRoutes) {
                const callbackTpl = route.ingestor?.webhook?.callbackUrl;
                const webhookPath = route.ingestor?.webhook?.path;
                if (!callbackTpl || !webhookPath)
                    continue;
                // Extract env var name from "${VAR}" pattern
                const match = /^\$\{(\w+)\}$/.exec(callbackTpl);
                if (match) {
                    const envVar = match[1];
                    const fullUrl = `${url}/webhooks/${webhookPath}`;
                    // Set bare env var
                    process.env[envVar] = fullUrl;
                    autoPopulatedEnvVars.add(envVar);
                    log.info(`Auto-set ${envVar}=${fullUrl}`);
                    // Also set prefixed env var so caller-scoped secret resolution
                    // (which checks PREFIX_VAR, not bare VAR) can find it.
                    const prefix = callerAlias.toUpperCase().replace(/-/g, "_");
                    const prefixedEnvVar = `${prefix}_${envVar}`;
                    process.env[prefixedEnvVar] = fullUrl;
                    autoPopulatedEnvVars.add(prefixedEnvVar);
                    log.info(`Auto-set ${prefixedEnvVar}=${fullUrl}`);
                }
            }
        }
    }
    catch (err) {
        log.warn(`Failed to auto-populate callback URLs: ${err.message}`);
    }
}
/**
 * Clear all auto-populated callback URL env vars.
 * Called during stopTunnel() so that stale URLs from a previous tunnel
 * don't persist and get used by a subsequent tunnel with a different URL.
 */
function clearAutoPopulatedCallbackUrls() {
    for (const envVar of autoPopulatedEnvVars) {
        delete process.env[envVar];
        log.debug(`Cleared auto-populated ${envVar}`);
    }
    autoPopulatedEnvVars.clear();
}
