/**
 * ============================================================
 *  src/cli/print.ts —— CLI 非交互模式（-p / --print）
 * ------------------------------------------------------------
 *  不启动 Ink TUI，直接执行单次问答，最后落盘 history。
 *  适合脚本集成、CI/CD、宿主程序（hermes/openclaw）spawn 调用。
 *
 *  两种输出格式（--output-format，默认 text）：
 *    text —— 把最终回答原样打到 stdout（向后兼容，逐字节不变）
 *    json —— stdout 只输出**一行** JSON 结局契约，其它诊断全走 stderr
 *
 *  v2 改进（对齐 kakadeai CLI 工程实践）：
 *    1. SIGINT 先保存 context 再退出（防丢失）
 *    2. handleEvent 返回退出码，不直接 process.exit（单一职责）
 *    3. EPIPE 保护：管道断开时静默退出不崩溃
 *    4. stdin peek 超时：防止非 TTY 无管道时永久挂起
 *
 *  使用方式：
 *    minimal-agent -p "提示"
 *    echo "提示" | minimal-agent -p
 *    minimal-agent -p --verbose "提示"
 *    minimal-agent -p --output-format json "提示"        # 结构化结局
 * ============================================================
 */
import { getContextPath, saveContext } from '../context/persistContext.js';
import { runWithPlugins } from '../plugins/pluginRunner.js';
import { BOOLEAN_FLAGS, VALUE_FLAGS } from './args.js';
/** stderr 显示工具结果时的截断阈值；超过即末尾加 "..." */
const TOOL_OUTPUT_PREVIEW_MAX = 200;
/** stdin 读取超时（ms），防止非 TTY + 无管道时永久挂起 */
const STDIN_TIMEOUT_MS = 3000;
/** 已告警过的未知事件类型（去重，避免高频插件私有事件刷屏 stderr）。 */
const warnedUnknownEventTypes = new Set();
function warnUnknownEventType(type) {
    const t = typeof type === 'string' ? type : String(type);
    if (warnedUnknownEventTypes.has(t))
        return;
    warnedUnknownEventTypes.add(t);
    process.stderr.write(`[minimal-agent] 忽略未识别事件类型 "${t}"（插件私有事件；若是 yield 时拼错的 LoopEvent type，请检查插件）\n`);
}
function handleEPIPE(stream) {
    return (err) => {
        if (err.code === 'EPIPE')
            stream.destroy();
    };
}
export function truncateForDisplay(content, max = TOOL_OUTPUT_PREVIEW_MAX) {
    if (content.length <= max)
        return content;
    return content.slice(0, max) + '...';
}
export function extractPromptArgs(args) {
    // flag 定义集中在 cli/args.ts（BOOLEAN_FLAGS / VALUE_FLAGS），这里只消费、不再各维护一份。
    const result = [];
    for (let i = 0; i < args.length; i++) {
        const a = args[i];
        if (BOOLEAN_FLAGS.has(a))
            continue;
        if (VALUE_FLAGS.has(a)) {
            i++; // 跳过带值标志紧跟的值（否则 --output-format 的值 json 会被当成 prompt 文本污染）
            continue;
        }
        result.push(a);
    }
    return result;
}
/**
 * 输出 `-p --output-format json` 的结局契约（**一行** JSON 到 stdout）。
 *
 * ⚠️ 诚实声明：`ok=true` 只代表「Agent 正常跑完（stop_reason==='end_turn'）——
 * 未报错、未被中断、未撞最大轮数」，**不代表任务目标真的达成**。工具失败会被
 * 吞回 LLM 让它自行重试，LLM 完全可能在没产出有效结果的情况下正常收尾、给出
 * end_turn。调用方（宿主程序）若要判断「任务是否真做成」，必须额外校验 `result`
 * 文本内容 / 产物文件，不能只看 ok。
 *
 * 字段语义：
 *   ok          = (stop_reason === 'end_turn')
 *   result      = 累积的 assistant 文本
 *   is_error    = !ok
 *   stop_reason = end_turn | max_turns | error | interrupted | config_error
 *   num_turns   = assistant_message 事件计数
 *   error       = 失败文案（成功时 null）
 *   session_file（可选）= 当前对话落盘路径；拿得到才填，拿不到省略该键
 *
 * 导出供单测断言。
 */
export function emitJsonResult(result, sessionFile) {
    const stopReason = result.stopReason ?? 'error';
    const ok = stopReason === 'end_turn';
    console.log(JSON.stringify({
        ok,
        result: result.buffer,
        is_error: !ok,
        stop_reason: stopReason,
        num_turns: result.numTurns,
        error: result.error,
        ...(sessionFile ? { session_file: sessionFile } : {}),
    }));
}
/**
 * 尝试拿当前会话落盘路径（给 json 契约的 session_file 字段）。
 * 取不到（异常等）返回 undefined，emitJsonResult 据此省略该键（契约允许）。
 */
function trySessionFile() {
    try {
        return getContextPath();
    }
    catch {
        return undefined;
    }
}
/**
 * CLI 非交互模式：读取 prompt，执行 runQuery，最后落盘 history。
 *
 *  - text 模式：流式 / 收尾把答案打到 stdout（逐字节兼容旧行为）
 *  - json 模式：stdout 只在退出时输出一行 JSON 结局契约（emitJsonResult）
 */
export async function runPrintMode(provider, args, initialHistory, options) {
    // EPIPE 保护：管道断开时不崩溃（如 `bun -p "..." | head -1`）
    process.stdout.on('error', handleEPIPE(process.stdout));
    process.stderr.on('error', handleEPIPE(process.stderr));
    const outputFormat = options.outputFormat ?? 'text';
    const isJson = outputFormat === 'json';
    const promptArgs = extractPromptArgs(args);
    let prompt;
    if (promptArgs.length > 0) {
        prompt = promptArgs.join(' ');
    }
    else if (!process.stdin.isTTY) {
        prompt = await readFromStdin();
    }
    else {
        console.error('错误: 请提供提示文本或使用管道输入');
        process.exit(1);
    }
    if (!prompt.trim()) {
        console.error('错误: 提示不能为空');
        process.exit(1);
    }
    const abortController = new AbortController();
    let interrupted = false;
    const history = initialHistory;
    // text 模式只关心 buffer；json 模式还要 numTurns / stopReason / error。
    // 用同一个对象承载，handleEvent 按 outputFormat 决定写哪些字段。
    const result = {
        buffer: '',
        numTurns: 0,
        stopReason: undefined,
        error: null,
    };
    // 收尾守卫：saveContext + (json) emit 只执行一次，所有退出路径 await 同一个
    // promise 再 process.exit。
    // 没有它的话：SIGINT 触发 abort 后，SIGINT handler 与 for-await 的 AbortError
    // catch 会同时奔向收尾 → 双写 session 文件 + 往 stdout 打两行 JSON（破坏「一行契约」）。
    // 用「缓存 promise」而非「布尔 once」是关键：后到的路径必须 await 到 emit 真正
    // 完成再退出，否则可能抢先把进程杀掉、导致那一行 JSON 根本没发出。
    let finalizePromise = null;
    function finalizeOnce() {
        if (!finalizePromise) {
            finalizePromise = (async () => {
                await saveContext(history);
                if (isJson)
                    emitJsonResult(result, trySessionFile());
            })();
        }
        return finalizePromise;
    }
    process.on('SIGINT', () => {
        if (interrupted)
            process.exit(130);
        interrupted = true;
        abortController.abort();
        console.error('\n已中断');
        // json 模式：SIGINT 也要给出结构化结局（stop_reason='interrupted'）
        if (isJson)
            result.stopReason = 'interrupted';
        void finalizeOnce().finally(() => process.exit(130));
    });
    try {
        for await (const event of runWithPlugins(prompt, {
            provider,
            history,
            signal: abortController.signal,
            maxTurns: options.maxTurns,
        })) {
            // event 是 LoopEvent | PluginEvent；handleEvent 只认 LoopEvent，未识别的插件私有事件走 default 静默忽略
            const handled = handleEvent(event, result, options.verbose, outputFormat);
            if (handled.exitCode !== undefined) {
                await finalizeOnce();
                process.exit(handled.exitCode);
            }
        }
    }
    catch (e) {
        if (e.name === 'AbortError') {
            if (isJson)
                result.stopReason = 'interrupted';
            await finalizeOnce();
            process.exit(130);
        }
        console.error(`\n未捕获异常: ${e.message}`);
        if (isJson) {
            // 未分类异常按 error 收尾（stopReason 仍为空 → emitJsonResult 兜底 'error'）
            result.error = result.error ?? e.message;
        }
        await finalizeOnce();
        process.exit(1);
    }
    // 正常跑完（没触发任何 exitCode 退出点）也走同一个收尾（json 模式 emit 一行结局）。
    await finalizeOnce();
}
/**
 * 处理 runQuery yield 出来的 LoopEvent。
 * 纯事件分发函数，返回 { exitCode? } 由调用方决定是否退出。
 *
 * @param result      累积态（buffer 始终累积；json 模式还会写 numTurns/stopReason/error）
 * @param verbose     是否把工具/压缩诊断打到 stderr
 * @param outputFormat 'text'（默认）逐字节兼容旧行为；'json' 时 stdout 静默（只在退出时由 emitJsonResult 输出）
 *
 * ⚠️ 向后兼容：旧调用方 `handleEvent(ev, output, verbose)` 三参不传 outputFormat，
 *    默认 'text'，行为与改前逐字节一致。
 */
export function handleEvent(event, result, verbose, outputFormat = 'text') {
    const isJson = outputFormat === 'json';
    switch (event.type) {
        case 'text':
            // text 模式：verbose 才流式写 stdout（与旧行为逐字节一致）。
            // json 模式：绝不往 stdout 写任何东西（stdout 只允许最后那一行 JSON），仅累积 buffer。
            if (verbose && !isJson)
                process.stdout.write(event.delta);
            result.buffer += event.delta;
            return {};
        case 'assistant_message':
            // 一轮 assistant 消息完整组装 → 回合计数 +1（json 契约的 num_turns）
            result.numTurns++;
            return {};
        case 'tool_start':
            if (verbose) {
                process.stderr.write(`\n🔧 工具: ${event.toolName}(${event.argsPreview})\n`);
            }
            return {};
        case 'tool_end':
            if (verbose) {
                process.stderr.write(`${event.ok ? '✅' : '❌'} ${event.toolName}: ${truncateForDisplay(event.content)}\n`);
            }
            return {};
        case 'compact_start':
            if (verbose)
                process.stderr.write('\n📝 正在压缩上下文...\n');
            return {};
        case 'compact_done':
            if (verbose) {
                process.stderr.write(`📝 压缩完成: ${event.before} → ${event.after} tokens\n`);
            }
            return {};
        case 'warning':
            // 通用非致命警告（如 workflow 多余参数、pause 被自动跳过等）始终走 stderr，
            // 不受 verbose 控制 —— 这是用户必须看到的信号，避免静默丢弃语义被忽略。
            process.stderr.write(`${event.message}\n`);
            return {};
        case 'turn_done':
            // text 模式：非 verbose 时这里一次性把累积答案打到 stdout（旧行为，逐字节不变）。
            // json 模式：只记 stopReason='end_turn'，stdout 保持静默。
            result.stopReason = 'end_turn';
            if (!isJson && !verbose)
                console.log(result.buffer);
            return {};
        case 'error':
            // compact_failed 是「非致命」信号：loop 压缩失败后会继续硬上（不 return），
            // 这里**不退出**，只 stderr 警告 —— 对齐 loop 语义。
            // （这同时修了一个旧瑕疵：之前 -p 收到该 error 会被误当致命退出。）
            if (event.code === 'compact_failed') {
                process.stderr.write(`${event.error}\n`);
                return {};
            }
            // 其余 error 都是终结性的。
            if (isJson) {
                result.stopReason = event.code === 'max_turns' ? 'max_turns' : 'error';
                result.error = event.error;
                // json 模式不在这里打 stdout/stderr；由外层 emitJsonResult 统一输出
                return { exitCode: 1 };
            }
            // text 模式：保持旧行为（红字 stderr + exitCode 1）
            console.error(`\n错误: ${event.error}`);
            return { exitCode: 1 };
        case 'interrupted':
            // 用户中断：json 模式记 stop_reason；外层退出点（catch / for-await 收尾 / SIGINT）emit。
            // text 模式无需特殊处理（与旧行为一致：静默忽略，靠 SIGINT handler 退出）。
            if (isJson)
                result.stopReason = 'interrupted';
            return {};
        // 已知但 -p 模式无需呈现的框架事件：显式忽略，好让 default 只剩"真正未知"的插件私有事件。
        case 'user_message_committed':
        case 'tool_messages_committed':
        case 'plugin_progress':
        case 'reasoning':
        case 'stage_change':
            return {};
        default:
            // 落到这里 = 插件私有事件（PluginEvent，type 任意字符串）。正常静默忽略；
            // verbose 下打一行 stderr 帮插件作者发现 yield 了拼错的 type（如 tool_strat）。
            if (verbose)
                warnUnknownEventType(event.type);
            return {};
    }
}
function readFromStdin() {
    return new Promise((resolve) => {
        let data = '';
        let settled = false;
        const timer = setTimeout(() => {
            if (!settled) {
                settled = true;
                resolve('');
            }
        }, STDIN_TIMEOUT_MS);
        process.stdin.setEncoding('utf8');
        function onData(chunk) {
            data += chunk;
        }
        function onEnd() {
            if (!settled) {
                clearTimeout(timer);
                settled = true;
                resolve(data.trim());
            }
        }
        process.stdin.on('data', onData);
        process.stdin.on('end', onEnd);
    });
}
export { readFromStdin };
