import { maybeRepairCopilotAnthropicMessagesResponse } from "./copilot-anthropic-sse-repair.js";
import { isCopilotApiHost } from "./copilot-hosts.js";
import { isCopilotGeminiModel } from "./copilot-gemini-payload-sanitizer.js";
/** OpenRouter-style encrypted reasoning detail the pi-ai client round-trips. */
const REASONING_ENCRYPTED_TYPE = "reasoning.encrypted";
function isPlainObject(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
/**
 * Inject `reasoning_details` into a parsed CAPI Gemini streaming chunk so the
 * pi-ai OpenAI-completions parser captures the Gemini thought signature.
 *
 * For each `choices[].delta` that carries a non-empty `reasoning_opaque` string
 * and a `tool_calls[]` entry with an `id`, adds a single
 * `reasoning_details: [{ type: "reasoning.encrypted", id, data: <opaque> }]`
 * entry keyed by that tool-call id. Returns whether the chunk was mutated.
 *
 * No-op when the delta already has `reasoning_details`, has no id-bearing tool
 * call (e.g. argument-continuation deltas or pure-text thought chunks), or has
 * no `reasoning_opaque`.
 */
export function injectCopilotGeminiReasoningDetails(chunk) {
    if (!isPlainObject(chunk))
        return false;
    const choices = chunk.choices;
    if (!Array.isArray(choices))
        return false;
    let mutated = false;
    for (const choice of choices) {
        if (!isPlainObject(choice))
            continue;
        const delta = choice.delta;
        if (!isPlainObject(delta))
            continue;
        const opaque = delta.reasoning_opaque;
        if (typeof opaque !== "string" || opaque.length === 0)
            continue;
        // Already carries the encrypted detail (don't double-inject / clobber).
        if (Array.isArray(delta.reasoning_details) && delta.reasoning_details.length > 0) {
            continue;
        }
        const toolCalls = delta.tool_calls;
        if (!Array.isArray(toolCalls))
            continue;
        const idBearing = toolCalls.find((call) => isPlainObject(call) && typeof call.id === "string" && call.id.length > 0);
        if (!idBearing)
            continue;
        delta.reasoning_details = [
            { type: REASONING_ENCRYPTED_TYPE, id: idBearing.id, data: opaque },
        ];
        mutated = true;
    }
    return mutated;
}
/**
 * Rewrite the JSON payload of a single SSE `data:` line. Returns the original
 * string unchanged when it is not a Gemini chunk that needs a thought signature
 * bridged, or when parsing fails (fail-open: never corrupt the stream).
 */
export function rewriteCopilotGeminiSseData(dataPayload) {
    // Cheap gate: only chunks that actually carry a thought signature are touched.
    if (!dataPayload.includes("reasoning_opaque"))
        return dataPayload;
    let parsed;
    try {
        parsed = JSON.parse(dataPayload);
    }
    catch {
        return dataPayload;
    }
    if (!injectCopilotGeminiReasoningDetails(parsed))
        return dataPayload;
    return JSON.stringify(parsed);
}
/** Rewrite one SSE line, preserving a trailing carriage return when present. */
function rewriteSseLine(line) {
    const hasCr = line.endsWith("\r");
    const core = hasCr ? line.slice(0, -1) : line;
    if (!core.startsWith("data:"))
        return line;
    const payload = core.slice("data:".length).trimStart();
    if (payload.length === 0 || payload === "[DONE]")
        return line;
    const rewritten = rewriteCopilotGeminiSseData(payload);
    if (rewritten === payload)
        return line;
    const rebuilt = `data: ${rewritten}`;
    return hasCr ? `${rebuilt}\r` : rebuilt;
}
/**
 * Wrap a CAPI Gemini SSE byte stream so `reasoning_opaque` is bridged into
 * `reasoning_details`. Buffers across chunk boundaries and rewrites whole lines
 * only; bytes that are not affected pass through unchanged.
 *
 * Implemented as a `ReadableStream` over the source reader (rather than a
 * `TransformStream` piped via `pipeThrough`) so the transform pulls lazily and
 * propagates cancellation to the upstream body.
 */
export function createCopilotGeminiSseStream(source) {
    const reader = source.getReader();
    const decoder = new TextDecoder();
    const encoder = new TextEncoder();
    let buffer = "";
    return new ReadableStream({
        async pull(controller) {
            // Loop until we emit at least one chunk or close, so a read that yields no
            // complete line still makes progress without relying on the runtime to
            // re-invoke pull after a no-enqueue return.
            for (;;) {
                const { done, value } = await reader.read();
                if (done) {
                    buffer += decoder.decode();
                    if (buffer.length > 0) {
                        controller.enqueue(encoder.encode(rewriteSseLine(buffer)));
                        buffer = "";
                    }
                    controller.close();
                    return;
                }
                buffer += decoder.decode(value, { stream: true });
                let newlineIndex = buffer.indexOf("\n");
                let out = "";
                while (newlineIndex !== -1) {
                    const line = buffer.slice(0, newlineIndex);
                    buffer = buffer.slice(newlineIndex + 1);
                    out += `${rewriteSseLine(line)}\n`;
                    newlineIndex = buffer.indexOf("\n");
                }
                if (out.length > 0) {
                    controller.enqueue(encoder.encode(out));
                    return;
                }
            }
        },
        cancel(reason) {
            return reader.cancel(reason);
        },
    });
}
/**
 * Convert the `reasoning_details` the pi-ai client re-emits on replayed
 * assistant messages back into the single `reasoning_opaque` field CAPI reads.
 *
 * For GitHub Copilot Gemini payloads, each assistant message that carries a
 * `reasoning_details` entry of `type: "reasoning.encrypted"` has its `data`
 * (the original CAPI thought-signature blob) promoted to `reasoning_opaque`,
 * and the now-redundant `reasoning_details` removed. No-op for every other
 * provider/model and for payloads without such messages.
 */
export function restoreCopilotGeminiReasoningOpaque(payload, model) {
    if (!isCopilotGeminiModel(model))
        return payload;
    if (!isPlainObject(payload))
        return payload;
    const payloadObject = payload;
    const messages = payloadObject.messages;
    if (!Array.isArray(messages))
        return payload;
    let mutated = false;
    const nextMessages = messages.map((message) => {
        if (!isPlainObject(message) || message.role !== "assistant")
            return message;
        const details = message.reasoning_details;
        if (!Array.isArray(details) || details.length === 0)
            return message;
        const encrypted = details.find((detail) => isPlainObject(detail) &&
            detail.type === REASONING_ENCRYPTED_TYPE &&
            typeof detail.data === "string" &&
            detail.data.length > 0);
        if (!encrypted)
            return message;
        mutated = true;
        const { reasoning_details: _omitted, ...rest } = message;
        return { ...rest, reasoning_opaque: encrypted.data };
    });
    if (!mutated)
        return payload;
    return { ...payloadObject, messages: nextMessages };
}
/** Resolve the request URL string from a `fetch` input argument. */
function resolveRequestUrl(input) {
    if (typeof input === "string")
        return input;
    if (input instanceof URL)
        return input.href;
    if (typeof input === "object" && input !== null && "url" in input) {
        const url = input.url;
        if (typeof url === "string")
            return url;
    }
    return undefined;
}
/**
 * Rewrite a streaming CAPI Gemini response so its SSE body bridges
 * `reasoning_opaque` into `reasoning_details`. Returns the original response
 * untouched for non-Copilot hosts, non-event-stream responses, or bodyless
 * responses, keeping the blast radius to streaming CAPI Gemini turns only.
 */
export function maybeRewriteCopilotGeminiResponse(url, response) {
    if (!url || !isCopilotApiHost(url))
        return response;
    const contentType = response.headers.get("content-type") ?? "";
    if (!contentType.includes("text/event-stream"))
        return response;
    if (!response.body)
        return response;
    const transformed = createCopilotGeminiSseStream(response.body);
    return new Response(transformed, {
        status: response.status,
        statusText: response.statusText,
        headers: response.headers,
    });
}
/**
 * Apply Copilot response-body transforms before pi-ai parses streamed provider
 * output. Anthropic Messages repair gets first refusal for `/v1/messages`
 * streams; other Copilot event streams keep the existing Gemini reasoning bridge.
 */
export function maybeRewriteCopilotProviderResponse(url, response) {
    const anthropicRepaired = maybeRepairCopilotAnthropicMessagesResponse(url, response);
    if (anthropicRepaired !== response)
        return anthropicRepaired;
    return maybeRewriteCopilotGeminiResponse(url, response);
}
let originalFetch;
/**
 * Install a `globalThis.fetch` wrapper that applies Copilot-only stream
 * compatibility shims before pi-ai parses provider output. Currently this
 * includes the Gemini `reasoning_opaque` bridge and the Anthropic Messages
 * terminal `message_stop` repair. Idempotent.
 *
 * SDK clients resolve `globalThis.fetch` at client-construction time, and a new
 * client is built per request, so wrapping the global before the first request
 * is reliably picked up. Non-Copilot hosts and non-event-stream responses are
 * returned untouched by the underlying shims.
 */
export function installCopilotResponseInterceptor() {
    if (originalFetch)
        return;
    if (typeof globalThis.fetch !== "function")
        return;
    const base = globalThis.fetch;
    originalFetch = base;
    const boundFetch = base.bind(globalThis);
    const wrapped = (async (input, init) => {
        const response = await boundFetch(input, init);
        try {
            return maybeRewriteCopilotProviderResponse(resolveRequestUrl(input), response);
        }
        catch {
            return response;
        }
    });
    // Preserve `fetch.preconnect` so the wrapper remains a drop-in replacement.
    const preconnect = base.preconnect;
    if (typeof preconnect === "function") {
        wrapped.preconnect = preconnect.bind(globalThis);
    }
    globalThis.fetch = wrapped;
}
/** Backward-compatible name for the original Gemini-only interceptor export. */
export function installCopilotGeminiReasoningInterceptor() {
    installCopilotResponseInterceptor();
}
//# sourceMappingURL=copilot-gemini-reasoning.js.map