/**
 * OpenCode SSE event subscription.
 *
 * Subscribes to the OpenCode SSE stream at GET /event and forwards
 * events through the gateway tunnel. Reconnects automatically on
 * disconnect with exponential backoff.
 *
 * SECURITY: Filters all SSE events by registered project paths.
 * Only events for projects the user has registered are forwarded.
 */
import { listProjects } from "../db/repositories/projects.js";
import { logger } from "../utils/logger.js";
const RECONNECT_BASE_MS = 1_000;
const RECONNECT_MAX_MS = 30_000;
const PROJECT_CACHE_TTL_MS = 30_000; // Refresh project paths every 30s
let sendFn = null;
// Project paths cache for filtering
let projectPathsCache = new Set();
let lastCacheRefresh = 0;
/**
 * Refresh the project paths cache from the database.
 * Called periodically to pick up newly added/removed projects.
 */
export function refreshProjectPathsCache() {
    try {
        const projects = listProjects();
        projectPathsCache = new Set(projects.map((p) => p.path));
        lastCacheRefresh = Date.now();
        logger.debug(`Project paths cache refreshed: ${projectPathsCache.size} projects`);
    }
    catch (err) {
        logger.warn(`Failed to refresh project paths cache: ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Check if a directory path is a registered project.
 * Refreshes the cache if it's stale.
 */
export function isRegisteredProjectPath(directory) {
    // Refresh cache if stale
    if (Date.now() - lastCacheRefresh > PROJECT_CACHE_TTL_MS) {
        refreshProjectPathsCache();
    }
    return projectPathsCache.has(directory);
}
/**
 * Clear the project paths cache (for testing).
 */
export function clearProjectPathsCache() {
    projectPathsCache.clear();
    lastCacheRefresh = 0;
}
let currentAuth = null;
let abortController = null;
let reconnectTimer = null;
let reconnectAttempt = 0;
let running = false;
/**
 * Set the send function used to forward SSE events through the gateway.
 */
export function setSseSendFn(fn) {
    sendFn = fn;
}
/**
 * Start subscribing to the OpenCode SSE event stream.
 * Connects to GET /event with Basic auth and forwards events.
 */
export function startSseSubscription(auth) {
    if (running) {
        return;
    }
    currentAuth = auth;
    running = true;
    reconnectAttempt = 0;
    // Initialize project paths cache before connecting
    refreshProjectPathsCache();
    connect();
}
/**
 * Stop the SSE subscription and clean up resources.
 */
export function stopSseSubscription() {
    running = false;
    currentAuth = null;
    if (abortController) {
        abortController.abort();
        abortController = null;
    }
    if (reconnectTimer) {
        clearTimeout(reconnectTimer);
        reconnectTimer = null;
    }
    reconnectAttempt = 0;
}
/**
 * Connect to the SSE stream and start processing events.
 */
function connect() {
    if (!running || !currentAuth) {
        return;
    }
    const url = `${currentAuth.baseUrl}/event`;
    const auth = currentAuth;
    abortController = new AbortController();
    logger.info(`Connecting to OpenCode SSE stream at ${url}`);
    fetch(url, {
        headers: {
            Authorization: auth.authHeader,
            Accept: "text/event-stream",
        },
        signal: abortController.signal,
    })
        .then((response) => {
        if (!response.ok) {
            throw new Error(`SSE connection failed: ${response.status} ${response.statusText}`);
        }
        if (!response.body) {
            throw new Error("SSE response has no body");
        }
        logger.info("Connected to OpenCode SSE stream");
        reconnectAttempt = 0;
        return processStream(response.body);
    })
        .then(() => {
        // Stream ended normally
        if (running) {
            logger.info("OpenCode SSE stream ended, reconnecting...");
            scheduleReconnect();
        }
    })
        .catch((err) => {
        if (!running) {
            return; // Intentional shutdown
        }
        const message = err instanceof Error ? err.message : String(err);
        // Don't log abort errors from intentional shutdown
        if (err instanceof DOMException && err.name === "AbortError") {
            return;
        }
        logger.warn(`OpenCode SSE error: ${message}`);
        scheduleReconnect();
    });
}
/**
 * Process the SSE stream body, parsing events and forwarding them.
 */
async function processStream(body) {
    const reader = body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let currentEvent = "";
    let currentData = "";
    try {
        while (running) {
            const { done, value } = await reader.read();
            if (done) {
                break;
            }
            buffer += decoder.decode(value, { stream: true });
            // Process complete lines
            const lines = buffer.split("\n");
            buffer = lines.pop() ?? ""; // Keep incomplete last line in buffer
            for (const line of lines) {
                if (line.startsWith("event:")) {
                    currentEvent = line.slice(6).trim();
                }
                else if (line.startsWith("data:")) {
                    currentData += (currentData ? "\n" : "") + line.slice(5).trim();
                }
                else if (line === "") {
                    // Empty line = end of event
                    if (currentEvent || currentData) {
                        forwardEvent(currentEvent || "message", currentData);
                        currentEvent = "";
                        currentData = "";
                    }
                }
            }
        }
    }
    finally {
        reader.releaseLock();
    }
}
/**
 * Forward a parsed SSE event through the gateway tunnel.
 *
 * SECURITY: Filters events by registered project paths. Only events for
 * projects the user has registered are forwarded. This prevents users
 * from seeing sessions/events for projects they haven't registered.
 */
function forwardEvent(event, data) {
    if (!sendFn) {
        return;
    }
    // Try to extract sessionId and directory from the data if it's JSON
    let sessionId;
    let directory;
    try {
        const parsed = JSON.parse(data);
        if (typeof parsed.properties === "object" && parsed.properties !== null) {
            const props = parsed.properties;
            if (typeof props.sessionID === "string") {
                sessionId = props.sessionID;
            }
            if (typeof props.directory === "string") {
                directory = props.directory;
            }
        }
    }
    catch {
        // Not JSON or no properties — forward without filtering
        // (some events like health/heartbeat may not have project info)
    }
    // SECURITY: Drop events for unregistered projects
    if (directory && !isRegisteredProjectPath(directory)) {
        logger.debug(`Dropping SSE event for unregistered project: ${directory}`);
        return;
    }
    const message = {
        type: "sse_event",
        payload: {
            event,
            data,
            sessionId,
        },
    };
    try {
        sendFn(message);
    }
    catch (err) {
        logger.debug(`Failed to forward SSE event: ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Schedule a reconnection with exponential backoff.
 */
function scheduleReconnect() {
    if (!running) {
        return;
    }
    const delay = Math.min(RECONNECT_BASE_MS * Math.pow(2, reconnectAttempt), RECONNECT_MAX_MS);
    reconnectAttempt++;
    logger.info(`Reconnecting to OpenCode SSE in ${delay}ms (attempt ${reconnectAttempt})`);
    reconnectTimer = setTimeout(() => {
        reconnectTimer = null;
        connect();
    }, delay);
}
//# sourceMappingURL=sse.js.map