import { createInterface } from "node:readline";
import chalk from "chalk";
export function parseAttachClientArgs(args) {
    let url;
    let token;
    let clientToken;
    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        if ((arg === "--token" || arg === "--attach-token") && i + 1 < args.length) {
            token = args[++i];
        }
        else if (arg === "--client-token" && i + 1 < args.length) {
            clientToken = args[++i];
        }
        else if ((arg === "--url" || arg === "--attach-url") && i + 1 < args.length) {
            url = args[++i];
        }
        else if (arg === "--help" || arg === "-h") {
            throw new AttachClientUsageError();
        }
        else if (!arg.startsWith("-") && url === undefined) {
            url = arg;
        }
        else {
            throw new Error(`Unknown attach client argument: ${arg}`);
        }
    }
    if (!url) {
        throw new AttachClientUsageError();
    }
    return { url, token, clientToken };
}
export class AttachClientUsageError extends Error {
    constructor() {
        super("Attach client usage requested");
        this.name = "AttachClientUsageError";
    }
}
export function formatAttachClientUsage(appName = "thread") {
    return `${appName} attach <url> [--token <token>] [--client-token <token>]\n\nConnect to a running Threadwell attach server. The URL can be the tokenized /state URL printed by --attach.`;
}
export function resolveAttachClientTarget(options) {
    const url = new URL(options.url);
    const token = options.token ?? url.searchParams.get("token") ?? undefined;
    if (!token) {
        throw new Error("Attach client requires a token in the URL or --token");
    }
    return {
        baseUrl: url.origin,
        token,
    };
}
export async function runAttachClient(options) {
    const target = resolveAttachClientTarget(options);
    const stdin = options.stdin ?? process.stdin;
    const stdout = options.stdout ?? process.stdout;
    const stderr = options.stderr ?? process.stderr;
    const state = { running: true };
    try {
        const initialState = await getAttachJson(target, "/state");
        stdout.write(`${chalk.green("Connected to Threadwell attach server")}\n`);
        stdout.write(`${chalk.dim(JSON.stringify(initialState))}\n`);
        stdout.write(chalk.dim("Type a prompt, /abort, /steer <text>, /follow-up <text>, or /quit.\n"));
        await Promise.race([
            streamEvents(target, stdout, state),
            readInputLoop(target, stdin, stdout, stderr, state, options.clientToken),
        ]);
        return 0;
    }
    catch (error) {
        stderr.write(`${chalk.red("Attach client error:")} ${error instanceof Error ? error.message : String(error)}\n`);
        return 1;
    }
    finally {
        state.running = false;
        state.eventReader?.releaseLock();
        state.readline?.close();
    }
}
async function getAttachJson(target, path) {
    const response = await fetch(`${target.baseUrl}${path}`, {
        headers: { authorization: `Bearer ${target.token}` },
    });
    if (!response.ok) {
        throw new Error(`${path} failed with HTTP ${response.status}`);
    }
    return response.json();
}
async function streamEvents(target, stdout, state) {
    const response = await fetch(`${target.baseUrl}/events`, {
        headers: { authorization: `Bearer ${target.token}` },
    });
    if (!response.ok) {
        throw new Error(`/events failed with HTTP ${response.status}`);
    }
    if (!response.body) {
        throw new Error("Attach event stream did not return a body");
    }
    const reader = response.body.getReader();
    state.eventReader = reader;
    const decoder = new TextDecoder();
    let buffer = "";
    while (state.running) {
        const result = await reader.read();
        if (result.done)
            break;
        buffer += decoder.decode(result.value, { stream: true });
        const chunks = buffer.split("\n\n");
        buffer = chunks.pop() ?? "";
        for (const chunk of chunks) {
            const rendered = renderSseChunk(chunk);
            if (rendered)
                stdout.write(`${rendered}\n`);
        }
    }
}
async function readInputLoop(target, stdin, stdout, stderr, state, clientToken) {
    const readline = createInterface({ input: stdin, output: stdout, terminal: isTtyReadable(stdin) });
    state.readline = readline;
    for await (const rawLine of readline) {
        const line = String(rawLine).trim();
        if (line.length === 0)
            continue;
        if (line === "/quit" || line === "/exit") {
            state.running = false;
            return;
        }
        try {
            await sendAttachCommand(target, parseInputCommand(line), clientToken);
        }
        catch (error) {
            stderr.write(`${chalk.red("Command failed:")} ${error instanceof Error ? error.message : String(error)}\n`);
        }
    }
    state.running = false;
}
export function parseInputCommand(line) {
    if (line === "/abort")
        return { type: "abort" };
    if (line.startsWith("/steer "))
        return { type: "steer", message: line.slice("/steer ".length).trim() };
    if (line.startsWith("/follow-up "))
        return { type: "follow_up", message: line.slice("/follow-up ".length).trim() };
    return { type: "prompt", message: line };
}
async function sendAttachCommand(target, command, clientToken) {
    const headers = {
        authorization: `Bearer ${target.token}`,
        "content-type": "application/json",
    };
    if (clientToken)
        headers["x-threadwell-client-token"] = clientToken;
    const response = await fetch(`${target.baseUrl}/command`, {
        method: "POST",
        headers,
        body: JSON.stringify(command),
    });
    if (!response.ok) {
        throw new Error(`/command failed with HTTP ${response.status}`);
    }
}
function isTtyReadable(stream) {
    return "isTTY" in stream && stream.isTTY === true;
}
function renderSseChunk(chunk) {
    const lines = chunk.split("\n");
    const event = lines.find((line) => line.startsWith("event: "))?.slice("event: ".length) ?? "message";
    const data = lines
        .filter((line) => line.startsWith("data: "))
        .map((line) => line.slice("data: ".length))
        .join("\n");
    if (!data)
        return undefined;
    if (event === "state")
        return `${chalk.cyan("state")} ${data}`;
    return `${chalk.cyan(event)} ${data}`;
}
//# sourceMappingURL=attach-client.js.map