import { execFile as execFileCallback, spawn } from "node:child_process";
import { promisify } from "node:util";
const execFile = promisify(execFileCallback);
class DapAdapterSession {
    sessionId;
    output = [];
    stoppedReason;
    exited = false;
    #process = null;
    #buffer = "";
    #nextSeq = 1;
    #pending = new Map();
    constructor(sessionId) {
        this.sessionId = sessionId;
    }
    start(command, args, cwd) {
        if (this.#process)
            return;
        this.#process = spawn(command, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
        this.#process.stdout?.on("data", (chunk) => {
            this.#buffer += chunk.toString("utf-8");
            this.#processMessages();
        });
        this.#process.stderr?.on("data", (chunk) => {
            this.output.push(chunk.toString("utf-8"));
        });
        this.#process.on("exit", () => {
            this.exited = true;
            this.#failPending(new Error(`DAP adapter ${this.sessionId} exited`));
        });
        this.#process.on("error", (error) => {
            this.exited = true;
            this.#failPending(error instanceof Error ? error : new Error(String(error)));
        });
    }
    async request(command, args, timeoutMs = 5000) {
        const seq = this.#nextSeq++;
        const message = { seq, type: "request", command };
        if (args !== undefined) {
            message.arguments = args;
        }
        return await new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                this.#pending.delete(seq);
                reject(new Error(`DAP request ${command} timed out`));
            }, timeoutMs);
            this.#pending.set(seq, { resolve, reject, timeout });
            try {
                this.#send(message);
            }
            catch (error) {
                this.#pending.delete(seq);
                clearTimeout(timeout);
                reject(error);
            }
        });
    }
    notification(command, args) {
        this.#send({
            seq: this.#nextSeq++,
            type: "event",
            command,
            ...(args === undefined ? {} : { arguments: args }),
        });
    }
    async disconnect() {
        if (!this.#process)
            return;
        try {
            await this.request("disconnect", { terminateDebuggee: true }, 1000);
        }
        catch { }
        this.#process.kill();
        this.#process = null;
        this.#failPending(new Error("DAP session disconnected"));
    }
    get running() {
        return this.#process !== null && !this.exited;
    }
    #send(message) {
        if (!this.#process?.stdin) {
            throw new Error("DAP adapter is not running");
        }
        const body = JSON.stringify(message);
        this.#process.stdin.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
    }
    #processMessages() {
        while (true) {
            const headerEnd = this.#buffer.indexOf("\r\n\r\n");
            if (headerEnd === -1)
                break;
            const header = this.#buffer.slice(0, headerEnd);
            const match = header.match(/Content-Length: (\d+)/i);
            if (!match) {
                this.#buffer = this.#buffer.slice(headerEnd + 4);
                continue;
            }
            const bodyStart = headerEnd + 4;
            const bodyEnd = bodyStart + Number(match[1]);
            if (this.#buffer.length < bodyEnd)
                break;
            const body = this.#buffer.slice(bodyStart, bodyEnd);
            this.#buffer = this.#buffer.slice(bodyEnd);
            try {
                this.#handleMessage(JSON.parse(body));
            }
            catch { }
        }
    }
    #handleMessage(message) {
        if (message.type === "response" && typeof message.request_seq === "number") {
            const pending = this.#pending.get(message.request_seq);
            if (!pending)
                return;
            this.#pending.delete(message.request_seq);
            clearTimeout(pending.timeout);
            if (message.success === false) {
                pending.reject(new Error(typeof message.message === "string" ? message.message : "DAP request failed"));
            }
            else {
                pending.resolve(message.body);
            }
            return;
        }
        if (message.type === "event" && message.event === "output") {
            const output = getObjectString(message.body, "output");
            if (output)
                this.output.push(output);
            return;
        }
        if (message.type === "event" && message.event === "stopped") {
            this.stoppedReason = getObjectString(message.body, "reason") ?? "stopped";
            return;
        }
        if (message.type === "event" && (message.event === "terminated" || message.event === "exited")) {
            this.exited = true;
        }
    }
    #failPending(error) {
        for (const [seq, pending] of this.#pending) {
            this.#pending.delete(seq);
            clearTimeout(pending.timeout);
            pending.reject(error);
        }
    }
}
export class DapClient {
    cwd;
    sessions = new Map();
    constructor(cwd) {
        this.cwd = cwd;
    }
    async launch(scriptPath, args) {
        try {
            const { stdout, stderr } = (await execFile("node", [scriptPath, ...(args ?? [])], {
                cwd: this.cwd,
                encoding: "utf-8",
                timeout: 30000,
                maxBuffer: 10 * 1024 * 1024,
            }));
            return {
                exitCode: 0,
                stdout,
                stderr,
            };
        }
        catch (error) {
            const failure = error;
            return {
                exitCode: typeof failure.code === "number" ? failure.code : 1,
                stdout: typeof failure.stdout === "string" ? failure.stdout : "",
                stderr: typeof failure.stderr === "string"
                    ? failure.stderr
                    : typeof failure.message === "string"
                        ? failure.message
                        : String(error),
            };
        }
    }
    async startSession(options) {
        if (this.sessions.has(options.sessionId)) {
            throw new Error(`DAP session already exists: ${options.sessionId}`);
        }
        const session = new DapAdapterSession(options.sessionId);
        this.sessions.set(options.sessionId, session);
        session.start(options.adapterCommand, options.adapterArgs ?? [], this.cwd);
        await session.request("initialize", {
            adapterID: "pie",
            linesStartAt1: true,
            columnsStartAt1: true,
            pathFormat: "path",
        });
        for (const [file, lines] of groupBreakpoints(options.breakpoints ?? [])) {
            await this.setBreakpoints(options.sessionId, file, lines);
        }
        await session.request("launch", {
            program: options.program,
            args: options.args ?? [],
            cwd: this.cwd,
            noDebug: false,
        });
        try {
            await session.request("configurationDone", {});
        }
        catch { }
        return {
            sessionId: options.sessionId,
            initialized: true,
            output: [...session.output],
            stoppedReason: session.stoppedReason,
            exited: session.exited,
        };
    }
    async setBreakpoints(sessionId, file, lines) {
        return await this.getSession(sessionId).request("setBreakpoints", {
            source: { path: file },
            breakpoints: lines.map((line) => ({ line })),
        });
    }
    async continue(sessionId, threadId = 1) {
        return await this.getSession(sessionId).request("continue", { threadId });
    }
    async stackTrace(sessionId, threadId = 1) {
        return await this.getSession(sessionId).request("stackTrace", { threadId });
    }
    async scopes(sessionId, frameId) {
        return await this.getSession(sessionId).request("scopes", { frameId });
    }
    async variables(sessionId, variablesReference) {
        return await this.getSession(sessionId).request("variables", { variablesReference });
    }
    async evaluate(sessionId, expression, frameId) {
        return await this.getSession(sessionId).request("evaluate", {
            expression,
            ...(frameId === undefined ? {} : { frameId }),
            context: "repl",
        });
    }
    async disconnect(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session)
            return;
        await session.disconnect();
        this.sessions.delete(sessionId);
    }
    status() {
        return [...this.sessions.values()].map((session) => ({
            sessionId: session.sessionId,
            running: session.running,
            outputEvents: session.output.length,
            stoppedReason: session.stoppedReason,
        }));
    }
    async disconnectAll() {
        await Promise.all([...this.sessions.keys()].map((sessionId) => this.disconnect(sessionId)));
    }
    getSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new Error(`DAP session not found: ${sessionId}`);
        }
        return session;
    }
}
function getObjectString(value, key) {
    if (!value || typeof value !== "object" || !(key in value))
        return undefined;
    const field = value[key];
    return typeof field === "string" ? field : undefined;
}
function groupBreakpoints(breakpoints) {
    const grouped = new Map();
    for (const breakpoint of breakpoints) {
        const lines = grouped.get(breakpoint.file) ?? [];
        lines.push(breakpoint.line);
        grouped.set(breakpoint.file, lines);
    }
    return grouped;
}
const clientsByCwd = new Map();
export function getOrCreateDapClient(cwd) {
    const existing = clientsByCwd.get(cwd);
    if (existing)
        return existing;
    const client = new DapClient(cwd);
    clientsByCwd.set(cwd, client);
    return client;
}
export async function closeDapClient() {
    await Promise.all([...clientsByCwd.values()].map((client) => client.disconnectAll()));
    clientsByCwd.clear();
}
//# sourceMappingURL=dap-client.js.map