/**
 * ============================================================
 *  plugins/workflow-runner/src/stepExecutors/llm.ts —— llm step 执行
 * ------------------------------------------------------------
 *  单轮 LLM 生成。关键设计：
 *    - tools: [] —— 不开任何工具（工具调用应该写成 tool: step）
 *    - 自带独立 system + user 两条消息，不污染主 history
 *    - AsyncGenerator：非 schema 模式把 text_delta 流式 yield 给 UI（答案是主角，
 *      消除 TUI"无声"），最终仍 return StepResult；schema 模式静默累积 JSON 不回显。
 *      reasoning 字段在这里忽略；只关心最终文本
 *
 *  Phase 2 重构：
 *    - system prompt 现在追加 CapabilityHint（skill + plugin 轻量列表）
 *    - 即使 tools=[] 的 llm 步骤也知道有哪些 skill/plugin 可用，
 *      可以在输出文本中引用或建议用户后续使用（如 draft_plan 提到"建议用 /pptx 执行"）
 *
 *  capture：
 *    - `{ text: var_name }` 绑生成文本（无 output_schema 时）
 *    - `{ result: var_name }` 同义
 *    - `{ <field>: var_name }` 当 output_schema 命中时按 raw 上的字段名取
 *
 *  ADR-05: 当 step.output_schema 存在 → 走 provider-native structured output：
 *    - 通过 chat() 的 responseFormat 参数下发 JSON Schema
 *    - 收到的文本必须解析为 JSON 对象，整对象作为 raw 暴露给 capture
 *    - system prompt 切换为严格 JSON 模式，禁止 markdown 代码块包裹
 * ============================================================
 */
import { ALL_TOOLS, chat, } from '../../../../src/plugin-sdk.js';
import { interpolate } from '../expressions.js';
import { extractJsonPayload } from '../jsonPayload.js';
import { runMiniReAct } from '../miniReAct.js';
import { assembleCapabilityHint } from '../../../../src/prompts/promptAssembly.js';
const LLM_STEP_SYSTEM_PLAIN = '你正在被一个 workflow 调用。只输出本步骤要求的内容本身，不要寒暄、不要列要求复述、不要工具调用语法。';
const LLM_STEP_SYSTEM_SCHEMA = `你正在被一个 workflow 调用。
要求：
1. 严格按指定 JSON Schema 输出有效 JSON，**不要 markdown 代码块包裹**（不要 \`\`\`json 也不要 \`\`\`）
2. 不要寒暄、不要解释、不要额外字段
3. 字段缺失或类型不符会导致 workflow 失败
4. 字符串内特殊字符必须正确转义（双引号 \\", 反斜杠 \\\\, 换行 \\n）`;
const LLM_STEP_SYSTEM_WITH_TOOLS = `你正在被一个 workflow 调用。你被授予了有限的工具（通常是 Read）以查询提供的上下文文件。

工作流程：
1. 先用 Read 工具读取你需要的文件（按需读取，**不要全部读完再思考**——大文件分段读、按 hint 选读）
2. 基于读到的内容生成最终回答
3. 不要寒暄、不要复述要求、不要解释你的推理过程
4. 完成后**不要**再调用工具，直接输出最终回答

如果 workflow 要求结构化输出，请在最终回答里严格遵循 JSON Schema（无 markdown 代码块包裹）。`;
export async function* execLlmStep(step, vars, ctx) {
    if (typeof step.llm !== 'string')
        throw new Error(`step ${step.id}: 缺少 llm 字段`);
    const prompt = interpolate(step.llm, vars);
    // ===== ADR-05 阶段 4: context_files → miniReAct 子循环 =====
    if (step.context_files && step.context_files.length > 0) {
        const fileList = step.context_files
            .map((f) => `- ${interpolate(f.path, vars)}${f.hint ? ` — ${f.hint}` : ''}`)
            .join('\n');
        const augmented = `${prompt}\n\n**以下文件可用，请用 Read 工具按需读取**：\n${fileList}`;
        const allowedNames = step.allowed_tools ?? ['Read'];
        const allowedTools = ALL_TOOLS.filter((t) => allowedNames.includes(t.name));
        if (allowedTools.length === 0) {
            throw new Error(`step ${step.id}: context_files 启用但 allowed_tools=${JSON.stringify(allowedNames)} 没有匹配的可用工具`);
        }
        return yield* runMiniReAct({
            messages: [
                { role: 'system', content: LLM_STEP_SYSTEM_WITH_TOOLS },
                { role: 'user', content: augmented },
            ],
            tools: allowedTools,
            output_schema: step.output_schema,
            provider: ctx.provider,
            signal: ctx.signal,
            maxTurns: step.maxTurns ?? 10,
        });
    }
    // ===== END NEW =====
    const useSchema = step.output_schema !== undefined;
    // schema 模式：把 schema 直接写进 system prompt。这样无论 provider 是否支持
    // response_format.json_schema（火山 GLM 等不支持、会被 client.ts 退化掉 response_format），
    // 模型都知道确切字段名与结构 —— 退化路径也能产出正确形状的 JSON，而非瞎猜字段。
    const schemaHint = useSchema
        ? `\n\n最终只输出匹配以下 JSON Schema 的 JSON 对象本身（不要解释、不要 markdown 代码块）：\n${JSON.stringify(step.output_schema)}`
        : '';
    // ✅ Phase 2: 追加 CapabilityHint —— 让 llm 步骤知道有哪些 skill/plugin 可用
    // 即使 tools=[] 不能执行工具，模型也能在输出中引用或建议用户后续使用
    const capabilityHint = await assembleCapabilityHint(ctx.cwd);
    const baseSystem = (useSchema ? LLM_STEP_SYSTEM_SCHEMA : LLM_STEP_SYSTEM_PLAIN) + schemaHint;
    const messages = [
        {
            role: 'system',
            content: baseSystem + capabilityHint,
        },
        { role: 'user', content: prompt },
    ];
    const responseFormat = useSchema
        ? {
            type: 'json_schema',
            json_schema: {
                name: `${step.id}_output`,
                schema: step.output_schema,
                strict: true,
            },
        }
        : undefined;
    let text = '';
    for await (const ev of chat({
        provider: ctx.provider,
        messages,
        tools: [],
        responseFormat,
        signal: ctx.signal,
    })) {
        if (ev.type === 'text_delta') {
            text += ev.delta;
            // 非 schema 模式：把答案流式 yield 给 UI（答案是主角，消除"无声"）。
            // schema 模式输出是原始 JSON，不当"答案文本"回显，避免屏幕闪 JSON。
            if (!useSchema)
                yield { type: 'text', delta: ev.delta };
        }
        if (ev.type === 'done' && ev.stopReason === 'aborted') {
            throw new Error('用户中断');
        }
    }
    const trimmed = text.trim();
    if (useSchema) {
        // strict structured output：解析 JSON 后整个对象作为 raw。
        // 思考型模型即便在 strict 下仍会把 think…</think 写进 content（见 jsonPayload.ts
        // 头注与 src/llm/client.ts:200），故先剥思维链/前后散文再 parse；报错仍带原文便于排查。
        const payload = extractJsonPayload(text);
        let parsed;
        try {
            parsed = JSON.parse(payload);
        }
        catch (e) {
            throw new Error(`step ${step.id}: output_schema 要求 JSON 输出但解析失败: ${e.message}\n收到: ${trimmed.slice(0, 200)}`);
        }
        if (parsed === null || typeof parsed !== 'object') {
            throw new Error(`step ${step.id}: output_schema 要求对象输出，得到 ${typeof parsed}: ${trimmed.slice(0, 100)}`);
        }
        return {
            raw: parsed,
            preview: `[json] ${trimmed.slice(0, 180)}${trimmed.length > 180 ? '...' : ''}`,
        };
    }
    // 老路径：裸文本
    return {
        raw: { text: trimmed, result: trimmed },
        preview: trimmed.length > 200 ? `${trimmed.slice(0, 200)}...` : trimmed,
    };
}
