/**
 * Web mode: Run agent with browser-based UI.
 *
 * The default UI is a fork of the llama.cpp server Web UI, served with a small
 * OpenAI/llama.cpp-compatible API shim backed by Avis sessions.
 */
import * as fs from "node:fs";
import * as http from "node:http";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { WebSocket, WebSocketServer } from "ws";
import { createAgentSession } from "../../core/sdk.js";
import { runRpcOverTransport } from "../rpc/rpc-transport.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function sendJson(res, status, body) {
    res.writeHead(status, {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    });
    res.end(JSON.stringify(body));
}
function readJsonBody(req) {
    return new Promise((resolve, reject) => {
        let body = "";
        req.on("data", (chunk) => {
            body += chunk;
        });
        req.on("end", () => {
            try {
                resolve(body ? JSON.parse(body) : {});
            }
            catch (error) {
                reject(error);
            }
        });
        req.on("error", reject);
    });
}
function extractTextContent(content) {
    if (typeof content === "string")
        return content;
    if (Array.isArray(content)) {
        return content
            .map((part) => {
            if (typeof part === "string")
                return part;
            if (part && typeof part === "object" && "text" in part)
                return String(part.text ?? "");
            return "";
        })
            .join("\n");
    }
    return "";
}
function getLastUserMessage(messages) {
    const userMessages = (messages ?? []).filter((message) => message.role === "user");
    const last = userMessages.at(-1);
    return extractTextContent(last?.content).trim();
}
function getMessageText(message) {
    if (message.role !== "assistant")
        return "";
    const content = message.content;
    if (typeof content === "string")
        return content;
    if (!Array.isArray(content))
        return "";
    return content
        .filter((part) => part && typeof part === "object" && part.type === "text")
        .map((part) => String(part.text ?? ""))
        .join("");
}
function writeSse(res, data) {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
}
async function handleChatCompletion(req, res, options) {
    const body = await readJsonBody(req);
    const prompt = getLastUserMessage(body.messages);
    if (!prompt) {
        sendJson(res, 400, { error: { message: "No user message provided" } });
        return;
    }
    const { session } = await createAgentSession(await options.sessionOptionsFactory());
    const modelName = body.model ?? session.model?.id ?? "avis";
    if (body.stream !== false) {
        res.writeHead(200, {
            "Content-Type": "text/event-stream; charset=utf-8",
            "Cache-Control": "no-cache, no-transform",
            Connection: "keep-alive",
            "Access-Control-Allow-Origin": "*",
        });
        let lastText = "";
        const unsubscribe = session.subscribe((event) => {
            if (event.type !== "message_update" || event.message.role !== "assistant")
                return;
            const text = getMessageText(event.message);
            if (!text.startsWith(lastText))
                return;
            const delta = text.slice(lastText.length);
            lastText = text;
            if (!delta)
                return;
            writeSse(res, {
                id: `chatcmpl-${session.sessionId}`,
                object: "chat.completion.chunk",
                created: Math.floor(Date.now() / 1000),
                model: modelName,
                choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],
            });
        });
        try {
            await session.prompt(prompt, { source: "rpc" });
            writeSse(res, {
                id: `chatcmpl-${session.sessionId}`,
                object: "chat.completion.chunk",
                created: Math.floor(Date.now() / 1000),
                model: modelName,
                choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
            });
            res.write("data: [DONE]\n\n");
        }
        finally {
            unsubscribe();
            session.dispose();
            res.end();
        }
        return;
    }
    try {
        await session.prompt(prompt, { source: "rpc" });
        const content = getMessageText(session.messages.filter((message) => message.role === "assistant").at(-1));
        sendJson(res, 200, {
            id: `chatcmpl-${session.sessionId}`,
            object: "chat.completion",
            created: Math.floor(Date.now() / 1000),
            model: modelName,
            choices: [{ index: 0, message: { role: "assistant", content }, finish_reason: "stop" }],
        });
    }
    finally {
        session.dispose();
    }
}
function serveStatic(req, res, staticDir) {
    const rawUrl = new URL(req.url ?? "/", "http://127.0.0.1");
    const pathname = rawUrl.pathname === "/" ? "/index.html" : rawUrl.pathname;
    const filePath = path.normalize(path.join(staticDir, pathname));
    if (!filePath.startsWith(staticDir)) {
        res.writeHead(403);
        res.end("Forbidden");
        return;
    }
    if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
        res.writeHead(404);
        res.end("Not found");
        return;
    }
    const ext = path.extname(filePath);
    const contentType = ext === ".html"
        ? "text/html; charset=utf-8"
        : ext === ".js"
            ? "text/javascript; charset=utf-8"
            : ext === ".css"
                ? "text/css; charset=utf-8"
                : "application/octet-stream";
    res.writeHead(200, { "Content-Type": contentType });
    fs.createReadStream(filePath).pipe(res);
}
export async function runWebMode(options) {
    const port = options.port ?? 8080;
    const staticDir = path.join(__dirname, "llama");
    const server = http.createServer(async (req, res) => {
        try {
            if (req.method === "OPTIONS") {
                sendJson(res, 204, null);
                return;
            }
            const url = new URL(req.url ?? "/", "http://127.0.0.1");
            if (req.method === "GET" && url.pathname === "/props") {
                sendJson(res, 200, { default_generation_settings: {}, total_slots: 1, chat_template: true });
                return;
            }
            if (req.method === "GET" && url.pathname === "/slots") {
                sendJson(res, 200, [{ id: 0, is_processing: false }]);
                return;
            }
            if (req.method === "GET" && url.pathname === "/v1/models") {
                const { session } = await createAgentSession(await options.sessionOptionsFactory());
                try {
                    const models = await session.modelRegistry.getAvailable();
                    sendJson(res, 200, {
                        object: "list",
                        data: models.map((model) => ({
                            id: model.id,
                            object: "model",
                            owned_by: model.provider,
                        })),
                    });
                }
                finally {
                    session.dispose();
                }
                return;
            }
            if (req.method === "POST" && url.pathname === "/v1/chat/completions") {
                await handleChatCompletion(req, res, options);
                return;
            }
            if (req.method === "POST" && (url.pathname === "/models/load" || url.pathname === "/models/unload")) {
                sendJson(res, 200, { ok: true });
                return;
            }
            serveStatic(req, res, staticDir);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            sendJson(res, 500, { error: { message } });
        }
    });
    const wss = new WebSocketServer({ server });
    let connectionCount = 0;
    wss.on("connection", async (ws) => {
        const connId = ++connectionCount;
        console.log(`[web] rpc connection #${connId} opened`);
        try {
            const sessionOptions = await options.sessionOptionsFactory();
            const { session } = await createAgentSession(sessionOptions);
            const transport = {
                send(data) {
                    if (ws.readyState === WebSocket.OPEN)
                        ws.send(data);
                },
                onMessage(callback) {
                    ws.on("message", (data) => callback(data.toString()));
                },
                onClose(callback) {
                    ws.on("close", callback);
                },
            };
            await runRpcOverTransport(session, transport);
        }
        catch (error) {
            console.error(`[web] error in rpc connection #${connId}:`, error);
            ws.close();
        }
        ws.on("close", () => console.log(`[web] rpc connection #${connId} closed`));
    });
    await new Promise((resolve) => {
        server.listen(port, "127.0.0.1", () => {
            const url = `http://127.0.0.1:${port}`;
            console.log(`[web] Avis running at ${url}`);
            const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
            import("node:child_process").then(({ exec }) => exec(`${opener} ${url}`));
            resolve();
        });
    });
    return new Promise(() => { });
}
//# sourceMappingURL=web-mode.js.map