/**
 * ============================================================
 *  src/context/compact.ts —— 自动压缩逻辑（v0.2 修复版）
 * ============================================================
 *  做的事：
 *    1. 估算当前历史的 token 数
 *    2. 若超过阈值（contextWindow × compactRatio，默认 0.85），触发压缩
 *    3. 压缩流程：
 *         a. 把整段历史 + 9 段式压缩 prompt 发给 LLM（非流式）
 *         b. 提取 <summary> 块
 *         c. 用"会话延续"包装把摘要做成 user 消息，紧跟最后 N 条原消息
 *
 *  v0.2 修复说明（2026-05-08）：
 *    - 🚨 修复致命 bug：/compact 后 tool_call / tool_result 配对关系破坏
 *      导致 MiniMax API 返回 400 "tool id not found" 错误
 *    - ✅ 新增 findTailWithCompleteToolChains() 函数
 *      智能截取尾部消息，保证每条 tool 消息都有对应的 tool_call 父消息
 *    - ✅ 不再使用固定的 KEEP_RECENT_MESSAGES = 4 简单切片
 *    - 改为动态扫描算法：从末尾向前检查完整性，必要时自动扩展
 *
 *  为什么保留最后 N 条？
 *    模型刚执行完工具的 tool_result 不能丢，否则后续推理失去依据。
 *    但现在不是固定 N 条，而是动态保证 tool 消息完整性。
 *
 *  为什么用"延续"包装而不是直接塞摘要？
 *    借鉴 kakadeai：在摘要前后加上"this session is continued from..."
 *    + "Recent messages are preserved verbatim" + "Continue from where it
 *    left off without asking" 三句话，可以让模型直接接着干活而不是把摘要
 *    当成新任务"重新介绍一遍"。
 *
 *  ---- prompt 模板内联自 src/prompts/compact.ts ----
 */
import { chat } from '../llm/client.js';
import { countMessagesTokens } from './tokenCounter.js';
/** 压缩这一轮严禁调工具——前置警告 */
const NO_TOOLS_PREAMBLE = `CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.

- Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
- You already have all the context you need in the conversation above.
- Tool calls will be REJECTED and will waste your only turn — you will fail the task.
- Your entire response must be plain text: an <analysis> block followed by a <summary> block.

`;
/** 同样的警告再放在 prompt 末尾——adaptive-thinking 模型在长 prompt 末尾容易"飘" */
const NO_TOOLS_TRAILER = '\n\nREMINDER: Do NOT call any tools. Respond with plain text only — ' +
    'an <analysis> block followed by a <summary> block. ' +
    'Tool calls will be rejected and you will fail the task.';
/**
 * 9 段式压缩模板主体——UP_TO 变体。
 */
const COMPACT_PROMPT_BODY = `Your task is to create a detailed summary of this conversation. This summary will be placed at the start of a continuing session; newer messages that build on this context will follow after your summary. Summarize thoroughly so that someone reading only your summary and then the newer messages can fully understand what happened and continue the work.

Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:

1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
   - The user's explicit requests and intents
   - Your approach to addressing the user's requests
   - Key decisions, technical concepts and code patterns
   - Specific details like file names, full code snippets, function signatures, file edits
   - Errors that you ran into and how you fixed them
   - Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.

Your summary should include the following sections:

1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Include FULL code snippets where applicable and include a summary of why this file read or edit is important. (Code snippets are NOT compressed — preserve them verbatim.)
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent. (User messages are NOT compressed — preserve them verbatim.)
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
8. Work Completed: Describe what was accomplished by the end of this portion.
9. Context for Continuing Work: Summarize any context, decisions, or state that would be needed to understand and continue the work in subsequent messages. Include direct quotes from the most recent conversation showing exactly where work was left off — this should be verbatim to ensure there's no drift.

Here's an example of how your output should be structured:

<example>
<analysis>
[Your thought process, ensuring all points are covered thoroughly and accurately]
</analysis>

<summary>
1. Primary Request and Intent:
   [Detailed description]

2. Key Technical Concepts:
   - [Concept 1]
   - [...]

3. Files and Code Sections:
   - [File Name 1]
      - [Summary of why this file is important]
      - [Important Code Snippet]
   - [...]

4. Errors and fixes:
    - [Detailed description of error 1]:
      - [How you fixed the error]
      - [User feedback on the error if any]
    - [...]

5. Problem Solving:
  [Description of solved problems and ongoing troubleshooting]

6. All user messages:
    - [Detailed non tool use user message]
    - [...]

7. Pending Tasks:
   - [Task 1]
   - [...]

8. Work Completed:
  [What was accomplished]

9. Context for Continuing Work:
  [Key context, decisions, or state needed to continue]

</summary>
</example>

Please provide your summary following this structure, ensuring precision and thoroughness in your response.`;
/**
 * 完整的压缩 prompt：preamble + 9 段模板 + 可选的 customInstructions + trailer.
 */
export function buildCompactPrompt(customInstructions) {
    let prompt = NO_TOOLS_PREAMBLE + COMPACT_PROMPT_BODY;
    if (customInstructions && customInstructions.trim() !== '') {
        prompt += `\n\nAdditional Instructions:\n${customInstructions.trim()}`;
    }
    prompt += NO_TOOLS_TRAILER;
    return prompt;
}
/**
 * 兼容旧名：现在等价于 buildCompactPrompt() 的默认调用。
 */
export const BASE_COMPACT_PROMPT = buildCompactPrompt();
/**
 * 把摘要包装成"会话延续"指令——这是让 LLM 接着干活而不是"重新介绍一遍"的关键。
 */
export function buildContinuationUserMessage(args) {
    const { summary, recentMessagesPreserved } = args;
    let msg = `This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

${summary}`;
    if (recentMessagesPreserved) {
        msg += `\n\nRecent messages are preserved verbatim.`;
    }
    msg += `\n\nContinue the conversation from where it left off without asking the user any further questions. Resume directly — do not acknowledge the summary, do not recap what was happening, do not preface with "I'll continue" or similar. Pick up the last task as if the break never happened.`;
    return msg;
}
/**
 * 从 LLM 的压缩响应里提取 <summary>...</summary> 内的内容。
 */
export function formatCompactSummary(rawResponse) {
    const match = rawResponse.match(/<summary>([\s\S]*?)<\/summary>/);
    if (match)
        return match[1].trim();
    // 退化路径：去掉 <analysis> 块（如果有）
    const stripped = rawResponse.replace(/<analysis>[\s\S]*?<\/analysis>/g, '').trim();
    return stripped || rawResponse.trim();
}
/** 自动压缩触发阈值默认占 contextWindow 的比例（可被 provider.compactRatio 覆盖）。 */
export const DEFAULT_COMPACT_RATIO = 0.85;
/**
 * 阈值的绝对安全下限：至少给下一轮 LLM 输出留这么多 token。
 *
 * 设计意图：无论 ratio 设多大，contextWindow - 8000 都必须空出来给
 * 下一轮 LLM 输出（assistant 回答 + tool_calls），防止撑爆。
 */
export const COMPACT_OUTPUT_BUFFER_TOKENS = 8_000;
/**
 * 最少保留的消息数（即使没有 tool 调用也要保留这么多最近对话）
 */
export const MIN_KEEP_RECENT_MESSAGES = 4;
/**
 * 给定 provider 的压缩触发阈值（按 contextWindow 比例缩放，provider 无关）。
 *
 * 公式：min(contextWindow × compactRatio, contextWindow − 8000)，且至少 1000。
 *
 * 为何改成比例而不是固定 buffer：
 *   - 128K 窗口的 25K buffer = 81%，4K 窗口的 25K buffer = 负数（不合法）
 *   - 比例自动适配所有模型，测试时把 ratio 调到 0.2 即可轻松触发压缩
 *   - COMPACT_OUTPUT_BUFFER_TOKENS = 8000 是绝对安全下限，保证 LLM 有足够的输出空间
 */
export function getCompactThreshold(provider) {
    const ratio = provider.compactRatio ?? DEFAULT_COMPACT_RATIO;
    return Math.max(1000, Math.min(Math.floor(provider.contextWindow * ratio), provider.contextWindow - COMPACT_OUTPUT_BUFFER_TOKENS));
}
/**
 * ✅ 核心修复函数：智能截取尾部消息，保证 tool_call / tool_result 完整性。
 *
 * 问题背景：
 *   OpenAI 协议要求 tool 消息的 tool_call_id 必须对应前面某条 assistant 消息
 *   的 tool_calls[].id。如果压缩时只保留 tool 消息但丢掉了对应的 tool_call，
 *   API 会返回 400 错误 "tool id not found"。
 *
 * 算法：
 *   1. 从消息数组末尾开始，至少保留 minKeep 条消息
 *   2. 维护一个 Set<tool_call_id> 记录已见过的 tool_call ID
 *   3. 遇到 tool 消息时，检查其 tool_call_id 是否在 Set 中
 *      - 如果不在 → 说明对应的 tool_call 消息被截断了 → 继续向前扩展
 *      - 如果在 → 正常保留
 *   4. 遇到 assistant 消息且有 tool_calls → 把所有 ID 加入 Set
 *   5. 直到所有 tool 消息都有父级或到达数组开头
 *
 * @param messages 非系统消息数组
 * @param minKeep 最少保留条数（默认 4）
 * @returns 截取后的尾部子数组（保证 tool 消息完整性）
 *
 * 示例：
 *   输入: [user, assistant(tool:A), tool(id:A), user, assistant]
 *   minKeep: 3
 *   输出: [assistant(tool:A), tool(id:A), user, assistant] (4条，自动扩展)
 *
 * 时间复杂度: O(N × M), N=消息数, M=平均扩展次数(通常<5)
 * 空间复杂度: O(T), T=唯一 tool_call_id 数量(通常<20)
 */
export function findTailWithCompleteToolChains(messages, minKeep = MIN_KEEP_RECENT_MESSAGES) {
    if (messages.length <= minKeep)
        return [...messages];
    // 至少保留 minKeep 条
    const tailEnd = messages.length;
    let tailStart = tailEnd - minKeep;
    // 收集已经保留的 assistant 消息中的 tool_call IDs
    const knownToolCallIds = new Set();
    // 初始化已知 ID（从 minKeep 范围内）
    for (let i = tailStart; i < tailEnd; i++) {
        const msg = messages[i];
        if (msg.role === 'assistant' && msg.tool_calls) {
            for (const tc of msg.tool_calls) {
                knownToolCallIds.add(tc.id);
            }
        }
    }
    // 检查是否有孤儿 tool 消息，如果有则向前扩展
    let needsExpansion = true;
    while (needsExpansion && tailStart > 0) {
        needsExpansion = false;
        for (let i = tailStart; i < tailEnd; i++) {
            const msg = messages[i];
            // 检查 tool 消息是否孤儿
            if (msg.role === 'tool' && msg.tool_call_id) {
                if (!knownToolCallIds.has(msg.tool_call_id)) {
                    // ❌ 找到孤儿 tool 消息！需要向前扩展
                    needsExpansion = true;
                    tailStart--;
                    // 把新加入的消息中的 tool_call ID 也加入集合
                    const newMsg = messages[tailStart];
                    if (newMsg.role === 'assistant' && newMsg.tool_calls) {
                        for (const tc of newMsg.tool_calls) {
                            knownToolCallIds.add(tc.id);
                        }
                    }
                    // 重新开始检查（因为新加入的消息可能也是 tool 孤儿）
                    break;
                }
            }
        }
    }
    // 不变量（Y3）：尾部不能以「未配对的 assistant.tool_calls」结尾 —— 它的 tool 响应不在
    // 本切片内，直接发 OpenAI 会 400（assistant 的每个 tool_calls 必须有后续对应的 tool 消息）。
    // 当前 autoCompact 在每轮轮首调用、输入末尾天然完整；这里仍主动裁剪，解除对「调用时机」的
    // 隐式依赖，让算法自身健壮（被裁的是裸 assistant，其 tool 响应本就不存在，不会留下孤儿 tool）。
    let tail = messages.slice(tailStart);
    while (tail.length > 0 &&
        tail[tail.length - 1].role === 'assistant' &&
        (tail[tail.length - 1].tool_calls
            ?.length ?? 0) > 0) {
        tail = tail.slice(0, -1);
    }
    return tail;
}
/**
 * 检查并按需压缩历史。
 *
 * @param messages 当前完整历史（必含一条 system）
 * @param provider 当前 provider（取 contextWindow / compactRatio）
 * @param opts     可选的辅助数据，用于校正系统性低估：
 *   - actualPromptTokens：本轮 LLM done 事件返回的真实 prompt token 数（如有）
 *   - toolsTokens：工具 schema 占的估算 token 数（由 estimateToolsTokens 算出）
 * @returns 可能更短的新 messages 数组（不修改原数组）
 *
 * 判据（取"真实 usage"与"估算+工具token"两者之大者）：
 *   - countMessagesTokens 只数历史消息，漏了工具 schema token → 系统性低估
 *   - LLM 返回的真实 usage 是最精确的，但不是每次都有（老 provider 或关了 include_usage）
 *   - 取 max 确保：有真实值时用真实值；没有时用估算+工具补偿；二者都有时取最保守的
 *
 * 当 tokens 不超阈值时**直接返回原数组**（同引用），调用方可据此判断"是否压缩了"。
 */
export async function autoCompactIfNeeded(messages, provider, opts) {
    const before = countMessagesTokens(messages); // 纯历史 token，保持语义不变（UI/snip/对比均依赖此值）
    // 判据：真实 usage（若有）与「估算 + 工具 schema」取大者，校正系统性低估
    const estimated = before + (opts?.toolsTokens ?? 0);
    const judge = Math.max(opts?.actualPromptTokens ?? 0, estimated);
    const threshold = getCompactThreshold(provider);
    if (judge < threshold) {
        return { messages, compacted: false, before, after: before };
    }
    // 真正要压缩了
    const compactedMessages = await runCompaction(messages, provider);
    const after = countMessagesTokens(compactedMessages);
    return { messages: compactedMessages, compacted: true, before, after };
}
/**
 * 不看阈值，直接执行一次压缩。供 `/compact` 手动命令使用。
 *
 * 与 autoCompactIfNeeded 共用同一段 runCompaction 逻辑；区别仅在于此函数
 * 永远跑压缩、不返回 `compacted` 字段（由调用方决定如何反馈）。
 */
export async function forceCompact(messages, provider) {
    const before = countMessagesTokens(messages);
    const compactedMessages = await runCompaction(messages, provider);
    const after = countMessagesTokens(compactedMessages);
    return { messages: compactedMessages, before, after };
}
/**
 * 真正执行压缩（一轮非流式 LLM 调用）。
 *
 * 输入：原始历史
 * 输出：[原 system, "会话延续指令 + 摘要" 的 user 消息, 最后 N 条原消息（保证完整性）]
 *
 * ✅ 关键修复：使用 findTailWithCompleteToolChains() 替代简单的 slice(-N)
 * 保证压缩后的消息结构符合 OpenAI 协议要求（tool 消息必须有对应的 tool_call 父消息）。
 */
async function runCompaction(messages, provider) {
    // 1. 找出原 system（保留它，避免 agent 失忆）
    const systemMsg = messages.find((m) => m.role === 'system');
    const nonSystem = messages.filter((m) => m.role !== 'system');
    // 2. 构造给"压缩用"的请求：完整历史 + 9 段式压缩 prompt
    //    （即使最后 N 条会被原样保留在新历史里，也送给 LLM 看 —— 让它写得出
    //     高质量的 "Context for Continuing Work" 节，知道当前停在哪儿。）
    const compactRequest = [
        ...(systemMsg ? [systemMsg] : []),
        ...nonSystem,
        {
            role: 'user',
            content: buildCompactPrompt(),
        },
    ];
    // 3. 调 LLM（用同一个 chat() 但不传工具，避免模型尝试调用工具）
    let summary = '';
    try {
        for await (const ev of chat({
            provider,
            messages: compactRequest,
            tools: [],
        })) {
            if (ev.type === 'text_delta')
                summary += ev.delta;
        }
    }
    catch (e) {
        // 压缩失败时降级：保留原历史的最后 N 条 + 一条用 user 消息说明压缩失败
        // ✅ 即使降级也要使用智能截取保证完整性
        return [
            ...(systemMsg ? [systemMsg] : []),
            {
                role: 'user',
                content: `(自动压缩失败：${e.message}，已直接截断旧历史)`,
            },
            ...findTailWithCompleteToolChains(nonSystem),
        ];
    }
    const cleanSummary = formatCompactSummary(summary);
    // ✅ 核心修复：智能截取尾部，保证 tool 消息完整性
    // 不再使用固定的 slice(-KEEP_RECENT_MESSAGES)
    const recentTail = findTailWithCompleteToolChains(nonSystem);
    // 4. 组装新历史：用"会话延续"包装把摘要做成 user 消息
    return [
        ...(systemMsg ? [systemMsg] : []),
        {
            role: 'user',
            content: buildContinuationUserMessage({
                summary: cleanSummary,
                recentMessagesPreserved: recentTail.length > 0,
            }),
        },
        ...recentTail,
    ];
}
