/**
 * ============================================================
 *  src/cli/streamJson.ts —— `-p --output-format stream-json` 流式事件模式
 * ------------------------------------------------------------
 *  与 print.ts 的 text/json 一次性契约**并列、互不影响**（print.ts 一行未动）：
 *    - text  : 纯文本答案（向后兼容）
 *    - json  : 退出时一行结局契约
 *    - stream-json（本文件）: 把 runQuery 的**每个 LoopEvent 作为一行 JSON**
 *      实时写 stdout（jsonl）。给 webchat 本地服务 broker 用 —— 它 spawn 本模式、
 *      逐行转发成 WebSocket event 帧。也是给网关预留的"流式子进程"契约。
 *
 *  约定：
 *    - stdout 只允许 jsonl 事件流（一行一个 JSON.stringify(LoopEvent)），含「问答 over wire」
 *      的出站事件 `{type:'ask',requestId,questions}`（AskUserQuestion 在远程通道弹问题）。
 *    - stdin 是**逐行 JSON 控制通道**（见 attachStdinChannel）：首行 `{type:'prompt',text}` 为本轮输入，
 *      后续 `{type:'ask_answer',requestId,answers}` 回灌用户选择。兼容：首行非控制消息 → 整段当裸 prompt。
 *      argv 也可给 prompt（此时 stdin 全部行按 ask_answer 解析）。--compact 走压缩动作（无需 prompt）。
 *    - 跑完 / 中断 / 出错都先 saveContext 再退出（finalize 还会 unref stdin，让本轮结束后进程能退出）。
 * ============================================================
 */
import crypto from 'node:crypto';
import { getWorkingDir } from '../bootstrap/workingDir.js';
import { forceCompact } from '../context/compact.js';
import { recordRecentDir } from '../context/recentDirs.js';
import { saveContext } from '../context/persistContext.js';
import { setAskHandler } from '../askBridge.js';
import { runWithPlugins } from '../plugins/pluginRunner.js';
import { extractPromptArgs } from './print.js';
/** 一行一个 JSON 事件写 stdout。EPIPE 时静默。 */
function emitLine(ev) {
    try {
        process.stdout.write(`${JSON.stringify(ev)}\n`);
    }
    catch {
        /* ignore */
    }
}
function firstUserTitle(history) {
    const u = history.find((m) => m.role === 'user');
    return u && typeof u.content === 'string' ? u.content : undefined;
}
/**
 * stdin 控制通道：把 stdin 从「一次性裸 prompt」升级为**逐行 JSON 控制通道**，
 * 支撑「问答 over wire」的入站半边。两种控制消息：
 *   - `{ type:'prompt', text }`     —— 首行，本轮的用户输入（替代裸 prompt）
 *   - `{ type:'ask_answer', requestId, answers }` —— AskUserQuestion 的回答，按 requestId 关联
 *
 * 向后兼容：首个非空行**不是** `{type:'prompt'}` 控制消息时，判定为 legacy 模式 ——
 * 整段 stdin 当裸 prompt 读到 EOF（保 `echo "x" | minimal-agent -p --output-format stream-json` 仍可用，
 * legacy 下天然无交互、一次性）。argv 已给 prompt 时，stdin 全部行都按 ask_answer 解析。
 *
 * 返回 `{ prompt, control }`：`control=true` **仅**当 prompt 来自 `{type:'prompt'}` 控制行
 * （= broker 在管理本子进程、会回灌 ask_answer）；argv / 裸管道 / TTY / EOF 兜底均为 `false`
 * （无人回灌 → 调用方据此**不注册** wire ask handler，让 askBridge 内置「自动选推荐项」兜底，
 * 避免模型提问后永等挂死）。同时把 ask_answer 路由给 onAskAnswer。stdin 全程保持打开，
 * 直到子进程结束（broker 在 turn 末 end stdin）。
 */
export function attachStdinChannel(opts) {
    const input = opts.input ?? process.stdin;
    return new Promise((resolvePrompt) => {
        let promptResolved = false;
        const resolveOnce = (text, control) => {
            if (promptResolved)
                return;
            promptResolved = true;
            resolvePrompt({ prompt: text, control });
        };
        // argv 给的 prompt：直连 CLI、无控制通道（broker 走 stdin 的 {type:'prompt'}，不用 argv）。
        if (opts.argvPrompt !== undefined)
            resolveOnce(opts.argvPrompt, false);
        // 无 stdin 管道（TTY）：argv 没给 prompt 也只能返回空（调用方据空报错）。
        if (input.isTTY) {
            resolveOnce('', false);
            return;
        }
        let buf = '';
        let legacy = false;
        input.setEncoding('utf8');
        input.on('data', (chunk) => {
            buf += chunk;
            if (legacy)
                return; // legacy：整段累积到 EOF
            let nl;
            while ((nl = buf.indexOf('\n')) >= 0) {
                const line = buf.slice(0, nl);
                buf = buf.slice(nl + 1);
                if (!line.trim())
                    continue;
                let msg = null;
                try {
                    msg = JSON.parse(line);
                }
                catch {
                    msg = null;
                }
                if (!promptResolved) {
                    if (msg && msg.type === 'prompt' && typeof msg.text === 'string') {
                        resolveOnce(msg.text, true); // 控制行 → 存在控制通道（broker 会回灌 ask_answer）
                    }
                    else {
                        // 首行不是 prompt 控制消息 → legacy：把它放回去，整段当裸 prompt 读到 EOF
                        legacy = true;
                        buf = buf.length ? `${line}\n${buf}` : line;
                        return;
                    }
                }
                else if (msg && msg.type === 'ask_answer' && typeof msg.requestId === 'string') {
                    opts.onAskAnswer(msg.requestId, Array.isArray(msg.answers) ? msg.answers : []);
                }
            }
        });
        input.on('end', () => {
            // legacy 一次性：EOF 时整段 stdin = 裸 prompt，无控制通道
            if (!promptResolved)
                resolveOnce(buf.trim(), false);
        });
    });
}
export async function runStreamJsonMode(provider, args, initialHistory, options) {
    process.stdout.on('error', (err) => {
        if (err.code === 'EPIPE')
            process.stdout.destroy();
    });
    const history = initialHistory;
    let finalized = false;
    const finalize = async () => {
        if (finalized)
            return;
        finalized = true;
        // 释放 stdin：attachStdinChannel 的 'data' 监听会让进程在本轮结束后**仍存活**
        // （等下一行输入）→ broker 永远收不到子进程 close、web 端 conn.running 卡死。
        // turn 已结束（无挂起提问），unref 让 stdout 排空后进程自然退出；中途挂起期间
        // 不会走到这里，故等答案时进程仍被 stdin 监听保活。
        try {
            process.stdin.unref();
        }
        catch {
            /* 某些 stdin 类型无 unref，忽略 */
        }
        setAskHandler(null); // 注销 handler，避免全局污染（测试复用 / 同进程多次调用）
        await saveContext(history);
    };
    // --- 压缩动作（/compact）：不需要 prompt ---
    if (options.compact) {
        try {
            emitLine({ type: 'compact_start' });
            const r = await forceCompact(history, provider);
            history.length = 0;
            history.push(...r.messages);
            emitLine({ type: 'compact_done', before: r.before, after: r.after });
            emitLine({ type: 'turn_done' });
        }
        catch (e) {
            emitLine({ type: 'error', error: `压缩失败：${e.message}`, code: 'compact_failed' });
        }
        await finalize();
        return;
    }
    // --- 普通一轮 ---
    // 「问答 over wire」入站半边：pendingAsks 持本轮未决提问（requestId → resolver）。
    const pendingAsks = new Map();
    const rejectAllAsks = (reason) => {
        for (const p of pendingAsks.values())
            p.reject(new Error(reason));
        pendingAsks.clear();
    };
    const promptArgs = extractPromptArgs(args);
    const { prompt, control } = await attachStdinChannel({
        argvPrompt: promptArgs.length > 0 ? promptArgs.join(' ') : undefined,
        onAskAnswer: (requestId, answers) => {
            const p = pendingAsks.get(requestId);
            if (p) {
                pendingAsks.delete(requestId);
                p.resolve(answers);
            }
        },
    });
    // 仅当存在控制通道（broker 经 `{type:'prompt'}` 启动、会回灌 ask_answer）才注册 wire handler：
    // 工具调 requestUserChoice → emit `ask` 到 stdout、挂起等 stdin 回灌。无控制通道（argv / 裸管道 /
    // TTY）时**不注册** → 落 askBridge 内置「自动选推荐项」兜底，避免模型提问后永等挂死。
    // loop 在 prompt resolve 后才启动 → 此处注册时机安全（注册前不会有 ask 触发）。
    if (control) {
        setAskHandler((questions) => new Promise((resolve, reject) => {
            const requestId = crypto.randomUUID();
            pendingAsks.set(requestId, { resolve, reject });
            emitLine({ type: 'ask', requestId, questions });
        }));
    }
    if (!prompt.trim()) {
        emitLine({ type: 'error', error: '未提供 prompt（argv / stdin 都为空）', code: 'llm_error' });
        process.exit(1);
    }
    // 记录最近目录（侧栏用）；title 优先用历史里第一条用户消息（稳定），否则用当前 prompt
    await recordRecentDir(getWorkingDir(), firstUserTitle(history) ?? prompt);
    const abortController = new AbortController();
    let interrupted = false;
    process.on('SIGINT', () => {
        if (interrupted)
            process.exit(130);
        interrupted = true;
        abortController.abort();
        rejectAllAsks('已中断'); // 挂起的提问立即落败，工具回 ok:false → loop 收口 interrupted
        emitLine({ type: 'interrupted' });
        void finalize().finally(() => process.exit(130));
    });
    try {
        for await (const event of runWithPlugins(prompt, {
            provider,
            history,
            signal: abortController.signal,
            maxTurns: options.maxTurns,
        })) {
            // event 是 LoopEvent | PluginEvent，原样逐行 emit；前端不识别的插件私有事件会静默忽略
            emitLine(event);
        }
    }
    catch (e) {
        if (e.name === 'AbortError') {
            emitLine({ type: 'interrupted' });
            await finalize();
            process.exit(130);
        }
        emitLine({ type: 'error', error: e.message, code: 'llm_error' });
        await finalize();
        process.exit(1);
    }
    await finalize();
}
