export class OpenCodeHttpError extends Error {
    status;
    method;
    path;
    responseBody;
    constructor(status, method, path, responseBody) {
        super(`OpenCode HTTP ${method} ${path} failed with status ${status}.`);
        this.status = status;
        this.method = method;
        this.path = path;
        this.responseBody = responseBody;
        this.name = "OpenCodeHttpError";
    }
}
export class OpenCodeHttpClient {
    #baseUrl;
    #serverPassword;
    #fetch;
    constructor(options) {
        this.#baseUrl = new URL(options.baseUrl);
        this.#serverPassword = options.serverPassword;
        this.#fetch = options.fetchImpl ?? fetch;
    }
    async health() {
        return this.#requestJson("GET", "/global/health");
    }
    async listSessions() {
        return this.#requestJson("GET", "/session");
    }
    async createSession(input = {}) {
        return this.#requestJson("POST", "/session", compactObject(input));
    }
    async sendPromptAsync(sessionID, input) {
        await this.#requestNoContent("POST", `/session/${encodeURIComponent(sessionID)}/prompt_async`, {
            ...compactObject({
                messageID: input.messageID,
                model: input.model,
                agent: input.agent,
                noReply: input.noReply,
                tools: input.tools,
                system: input.system,
                variant: input.variant,
            }),
            parts: [{ type: "text", text: input.text }],
        });
    }
    async respondToSessionPermission(sessionID, permissionID, response) {
        return this.#requestJson("POST", `/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(permissionID)}`, { response });
    }
    async replyToPermission(requestID, reply, message) {
        return this.#requestJson("POST", `/permission/${encodeURIComponent(requestID)}/reply`, compactObject({ reply, message }));
    }
    async *subscribeEvents(signal) {
        const init = {
            method: "GET",
            headers: this.#headers({ accept: "text/event-stream" }),
        };
        if (signal)
            init.signal = signal;
        const response = await this.#fetch(this.#url("/event"), init);
        if (!response.ok) {
            throw await this.#errorFromResponse(response, "GET", "/event");
        }
        if (!response.body)
            return;
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = "";
        while (true) {
            const { value, done } = await reader.read();
            if (done)
                break;
            buffer += decoder.decode(value, { stream: true });
            const events = drainSseEvents(buffer);
            buffer = events.remainder;
            for (const data of events.data) {
                if (!data || data === "[DONE]")
                    continue;
                yield JSON.parse(data);
            }
        }
        buffer += decoder.decode();
        const finalEvents = drainSseEvents(`${buffer}\n\n`);
        for (const data of finalEvents.data) {
            if (!data || data === "[DONE]")
                continue;
            yield JSON.parse(data);
        }
    }
    async #requestJson(method, path, body) {
        const response = await this.#request(method, path, body);
        if (!response.ok)
            throw await this.#errorFromResponse(response, method, path);
        return response.json();
    }
    async #requestNoContent(method, path, body) {
        const response = await this.#request(method, path, body);
        if (!response.ok)
            throw await this.#errorFromResponse(response, method, path);
    }
    async #request(method, path, body) {
        const init = {
            method,
            headers: this.#headers(body === undefined ? undefined : { "content-type": "application/json" }),
        };
        if (body !== undefined)
            init.body = JSON.stringify(body);
        return this.#fetch(this.#url(path), init);
    }
    #headers(extra) {
        const headers = { ...(extra ?? {}) };
        if (this.#serverPassword) {
            headers.authorization = `Basic ${Buffer.from(`opencode:${this.#serverPassword}`, "utf8").toString("base64")}`;
        }
        return headers;
    }
    #url(path) {
        return new URL(path, this.#baseUrl);
    }
    async #errorFromResponse(response, method, path) {
        const responseBody = await response.text();
        return new OpenCodeHttpError(response.status, method, path, responseBody.slice(0, 2_000));
    }
}
function compactObject(value) {
    return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
}
function drainSseEvents(buffer) {
    const data = [];
    let cursor = 0;
    while (true) {
        const boundary = buffer.indexOf("\n\n", cursor);
        if (boundary === -1)
            break;
        const rawEvent = buffer.slice(cursor, boundary);
        cursor = boundary + 2;
        const lines = rawEvent.split(/\r?\n/);
        const dataLines = lines
            .filter((line) => line.startsWith("data:"))
            .map((line) => line.slice("data:".length).trimStart());
        if (dataLines.length > 0)
            data.push(dataLines.join("\n"));
    }
    return { data, remainder: buffer.slice(cursor) };
}
