/**
 * Lightweight HTTP proxy that intercepts Anthropic API calls, logs the full
 * request body (system prompt, tools, messages summary) to JSONL, and forwards
 * the request to the real Anthropic API.
 *
 * Dev-mode only — started lazily when `buildAgentEnv()` detects dev mode and
 * the Anthropic direct-API provider.  The SDK subprocess (`cli.js`) is pointed
 * to the proxy via `ANTHROPIC_BASE_URL=http://localhost:<port>`.
 *
 * Design: the proxy buffers the incoming request body, forwards it to the
 * upstream via Node.js `https.request()` with explicit HTTP/1.1 ALPN, then
 * pipes the response back to the SDK client.
 */
import http from "node:http";
import https from "node:https";
import fs from "node:fs";
import path from "node:path";
import zlib from "node:zlib";
import { URL } from "node:url";
import { logger } from "../logger.js";
let singleton = null;
// HTTPS agent that forces HTTP/1.1 via ALPN to avoid HTTP/2 negotiation
// issues with Cloudflare/Anthropic's edge.  Each request gets a fresh TLS
// connection (keepAlive off) to avoid stale-socket errors during the
// minutes-long idle periods between agent API calls.
const httpsAgent = new https.Agent({
    keepAlive: false,
    maxSockets: 10,
    ALPNProtocols: ["http/1.1"],
});
function extractSystemReminders(messages) {
    const reminders = [];
    for (const msg of messages) {
        const content = msg.content;
        if (!Array.isArray(content))
            continue;
        for (const block of content) {
            if (block.type === "text" &&
                typeof block.text === "string" &&
                block.text.includes("<system-reminder>")) {
                reminders.push(block.text);
            }
        }
    }
    return reminders;
}
function getHeaderValue(headers, name) {
    const value = headers[name];
    if (Array.isArray(value))
        return value[0];
    return value;
}
/**
 * Decompress a buffered upstream response body for logging only.
 *
 * The proxy forwards the original (possibly compressed) bytes to the SDK
 * client verbatim, so the SDK's transparent decompression keeps working.
 * For our JSONL log we want the readable text instead, so we honor
 * `content-encoding` here.
 */
function decodeResponseBodyForLogging(responseBody, headers) {
    if (responseBody.length === 0)
        return "";
    const encoding = getHeaderValue(headers, "content-encoding");
    const normalized = encoding?.toLowerCase().trim();
    try {
        if (normalized === "gzip" || normalized === "x-gzip") {
            return zlib.gunzipSync(responseBody).toString("utf-8");
        }
        if (normalized === "deflate") {
            return zlib.inflateSync(responseBody).toString("utf-8");
        }
        if (normalized === "br") {
            return zlib.brotliDecompressSync(responseBody).toString("utf-8");
        }
    }
    catch (err) {
        logger.warn(`[api-proxy] failed to decompress upstream body (encoding=${normalized ?? "none"}): ${err instanceof Error ? err.message : String(err)}`);
    }
    return responseBody.toString("utf-8");
}
function getUpstreamRequestId(headers) {
    return getHeaderValue(headers, "request-id")
        ?? getHeaderValue(headers, "x-request-id")
        ?? getHeaderValue(headers, "anthropic-request-id")
        ?? getHeaderValue(headers, "cf-request-id")
        ?? getHeaderValue(headers, "x-amzn-trace-id")
        ?? getHeaderValue(headers, "cf-ray")
        ?? null;
}
function parseAnthropicResponseBody(responseBodyRaw) {
    try {
        return JSON.parse(responseBodyRaw);
    }
    catch {
        return parseAnthropicSseFinalMessage(responseBodyRaw) ?? responseBodyRaw;
    }
}
function parseAnthropicSseFinalMessage(responseBodyRaw) {
    const events = responseBodyRaw.split(/\n\n+/u);
    let finalMessage;
    const contentBlocks = [];
    let stopReason;
    let stopSequence;
    let usage;
    for (const event of events) {
        const dataLines = event
            .split("\n")
            .filter((line) => line.startsWith("data:"))
            .map((line) => line.slice("data:".length).trim())
            .filter((line) => line.length > 0 && line !== "[DONE]");
        for (const dataLine of dataLines) {
            let payload;
            try {
                payload = JSON.parse(dataLine);
            }
            catch {
                continue;
            }
            if (payload.type === "message_start") {
                finalMessage = payload.message;
                continue;
            }
            if (payload.type === "content_block_start") {
                const index = payload.index;
                if (typeof index === "number") {
                    contentBlocks[index] = payload.content_block;
                }
                continue;
            }
            if (payload.type === "content_block_delta") {
                const index = payload.index;
                const delta = payload.delta;
                if (typeof index !== "number" || !delta)
                    continue;
                const block = contentBlocks[index] ?? {};
                contentBlocks[index] = block;
                if (delta.type === "text_delta" && typeof delta.text === "string") {
                    block.text = `${typeof block.text === "string" ? block.text : ""}${delta.text}`;
                }
                else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
                    block.partial_json = `${typeof block.partial_json === "string" ? block.partial_json : ""}${delta.partial_json}`;
                }
                continue;
            }
            if (payload.type === "message_delta") {
                const delta = payload.delta;
                stopReason = delta?.stop_reason;
                stopSequence = delta?.stop_sequence;
                usage = payload.usage;
            }
        }
    }
    if (!finalMessage)
        return undefined;
    return {
        ...finalMessage,
        content: contentBlocks.filter(Boolean).map(finalizeContentBlock),
        stop_reason: stopReason,
        stop_sequence: stopSequence,
        usage: usage ?? finalMessage.usage,
    };
}
function finalizeContentBlock(block) {
    if (typeof block.partial_json !== "string")
        return block;
    const { partial_json: partialJson, ...rest } = block;
    try {
        return {
            ...rest,
            input: JSON.parse(partialJson),
        };
    }
    catch {
        return {
            ...rest,
            input_json_raw: partialJson,
        };
    }
}
/**
 * Start the API logging proxy.  Returns a handle with the assigned port.
 * Subsequent calls return the same singleton.
 */
export async function startApiProxy(opts) {
    if (singleton)
        return singleton;
    const targetBase = opts.targetBaseUrl ?? "https://api.anthropic.com";
    const logDir = opts.logDir;
    if (!fs.existsSync(logDir)) {
        fs.mkdirSync(logDir, { recursive: true });
    }
    // Headers to strip when forwarding
    const STRIP_HEADERS = new Set([
        "host", "connection", "transfer-encoding",
    ]);
    const server = http.createServer((req, res) => {
        const isMessagesEndpoint = req.method === "POST" && req.url?.startsWith("/v1/messages");
        const targetUrl = new URL(req.url ?? "/", targetBase);
        // Build outgoing headers
        const fwdHeaders = {};
        for (const [key, value] of Object.entries(req.headers)) {
            if (STRIP_HEADERS.has(key))
                continue;
            fwdHeaders[key] = value;
        }
        fwdHeaders.host = targetUrl.host;
        // Buffer the incoming request body, then forward
        const chunks = [];
        req.on("data", (chunk) => {
            chunks.push(chunk);
        });
        req.on("error", (err) => {
            logger.warn(`[api-proxy] client request error: ${err.message}`);
        });
        req.on("end", () => {
            const body = Buffer.concat(chunks);
            const requestBodyRaw = body.toString("utf-8");
            let parsedRequestBody;
            if (isMessagesEndpoint && body.length > 0) {
                try {
                    parsedRequestBody = JSON.parse(requestBodyRaw);
                }
                catch {
                    // Best-effort logging — don't break the proxy
                }
            }
            // Forward the complete body to the upstream API via HTTPS.
            // We set content-length explicitly since we have the full body.
            fwdHeaders["content-length"] = String(body.length);
            const proxyReq = https.request({
                hostname: targetUrl.hostname,
                port: 443,
                path: targetUrl.pathname + targetUrl.search,
                method: req.method,
                headers: fwdHeaders,
                agent: httpsAgent,
            }, (proxyRes) => {
                const responseChunks = [];
                res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
                proxyRes.on("data", (chunk) => {
                    responseChunks.push(chunk);
                    res.write(chunk);
                });
                proxyRes.on("end", () => {
                    if (!res.writableEnded) {
                        res.end();
                    }
                    if (isMessagesEndpoint && body.length > 0) {
                        try {
                            const responseBody = Buffer.concat(responseChunks);
                            const responseBodyRaw = decodeResponseBodyForLogging(responseBody, proxyRes.headers);
                            const parsedResponseBody = parseAnthropicResponseBody(responseBodyRaw);
                            const messages = parsedRequestBody?.messages;
                            const entry = {
                                timestamp: new Date().toISOString(),
                                method: req.method,
                                url: req.url,
                                model: parsedRequestBody?.model,
                                request_body: parsedRequestBody ?? requestBodyRaw,
                                request_body_raw: requestBodyRaw,
                                system_reminders: messages ? extractSystemReminders(messages) : [],
                                messages_count: messages?.length ?? 0,
                                request_size_bytes: body.length,
                                response_status_code: proxyRes.statusCode,
                                upstream_request_id: getUpstreamRequestId(proxyRes.headers),
                                response_headers: proxyRes.headers,
                                response_body: parsedResponseBody,
                                response_body_raw: responseBodyRaw,
                                response_size_bytes: responseBody.length,
                            };
                            const date = new Date().toISOString().slice(0, 10);
                            const logPath = path.join(logDir, `api-calls-${date}.jsonl`);
                            fs.appendFileSync(logPath, `${JSON.stringify(entry)}\n`, "utf-8");
                        }
                        catch {
                            // Best-effort logging — don't break the proxy
                        }
                    }
                });
                proxyRes.on("error", (err) => {
                    logger.warn(`[api-proxy] upstream response error: ${err.message}`);
                    if (!res.writableEnded) {
                        res.destroy(err);
                    }
                });
            });
            proxyReq.on("error", (err) => {
                if (!req.destroyed) {
                    logger.warn(`[api-proxy] upstream error: ${err.message}`);
                }
                if (!res.headersSent) {
                    res.writeHead(502, { "Content-Type": "application/json" });
                }
                if (!res.writableEnded) {
                    res.end(JSON.stringify({ error: "proxy_error", message: err.message }));
                }
            });
            // Send the buffered body and end the request
            proxyReq.end(body);
        });
    });
    return new Promise((resolve, reject) => {
        server.on("error", reject);
        server.listen(0, "127.0.0.1", () => {
            const addr = server.address();
            if (!addr || typeof addr === "string") {
                reject(new Error("Failed to get proxy server address"));
                return;
            }
            const handle = {
                port: addr.port,
                close: () => {
                    server.close();
                    httpsAgent.destroy();
                    singleton = null;
                },
            };
            singleton = handle;
            logger.system(`[api-proxy] listening on 127.0.0.1:${addr.port}`);
            resolve(handle);
        });
    });
}
//# sourceMappingURL=api-proxy.js.map