/**
 * ============================================================
 *  src/context/snipCompact.ts —— 轻量"砍头"压缩（Path 3）
 * ------------------------------------------------------------
 *  对齐 kakadeai 主项目 services/compact/snipCompact.ts 的设计：
 *  纯规则、零 LLM 调用，直接砍掉最老的 N% 消息，用作 LLM 压缩失败时
 *  的兜底（"穷人的压缩"）。
 *
 *  压缩强度光谱：
 *    micro  —— 工具结果级别（SHA-1 + 长度截断，每条消息内部）
 *    snip   —— 对话级别（按比例砍最老消息，无 LLM）   ← 本文件
 *    auto   —— LLM 全量重写摘要（9 段式模板）
 *
 *  设计要点：
 *    1. system 消息（messages[0]）永远不动
 *    2. 默认 < 10 条历史不动（除非 force=true）
 *    3. 砍头时严格保护 tool_call / tool_result 配对：切口落在 tool
 *       消息上会自动向后微调，避免造成"孤儿 tool"导致 API 400
 *    4. 替换为一条 user marker 消息，告诉模型"前面有 N 条被自动删除"
 * ============================================================
 */
import { countMessagesTokens } from './tokenCounter.js';
// ==================== 配置常量 ====================
const DEFAULT_SNIP_PERCENT = 0.2;
const DEFAULT_MIN_KEEP = 5;
const DEFAULT_MAX_SNIP = 50;
/** 小于这个总长度时除非 force 不动 */
const DEFAULT_THRESHOLD = 10;
// ==================== 核心函数 ====================
/**
 * 砍掉最老的 N% 消息，用一条 user marker 消息替代。
 *
 * 切口保护：如果按比例算出的切点落在 tool 消息上，会自动向后微调
 * 直到切点不在 tool 上——避免造成孤儿 tool（其父 assistant.tool_calls
 * 被砍掉而它自己被保留，OpenAI 协议下 API 会 400 "tool id not found"）。
 */
export function snipCompactIfNeeded(messages, options = {}) {
    const force = options.force ?? false;
    const snipPercent = options.snipPercent ?? DEFAULT_SNIP_PERCENT;
    const minKeep = options.minMessagesToKeep ?? DEFAULT_MIN_KEEP;
    const maxSnip = options.maxMessagesToSnip ?? DEFAULT_MAX_SNIP;
    if (messages.length === 0) {
        return { messages: [], messagesRemoved: 0, tokensFreed: 0 };
    }
    // 拆出 system 消息（只可能在 index 0；最多一条）
    const systemMsg = messages[0].role === 'system' ? messages[0] : null;
    const rest = systemMsg ? messages.slice(1) : messages.slice();
    // 阈值检查（< 10 条且非 force → 不动）
    if (!force && rest.length < DEFAULT_THRESHOLD) {
        return { messages: messages.slice(), messagesRemoved: 0, tokensFreed: 0 };
    }
    // 算应该砍多少条
    const byPercent = Math.floor(rest.length * snipPercent);
    const proposedSnipCount = Math.min(byPercent, maxSnip, Math.max(0, rest.length - minKeep));
    if (proposedSnipCount <= 0) {
        return { messages: messages.slice(), messagesRemoved: 0, tokensFreed: 0 };
    }
    // 切点保护：避免落在 tool 消息上造成孤儿
    const actualCut = findHeadCutpoint(rest, proposedSnipCount);
    if (actualCut === 0 || actualCut >= rest.length) {
        return { messages: messages.slice(), messagesRemoved: 0, tokensFreed: 0 };
    }
    const snipped = rest.slice(0, actualCut);
    const remaining = rest.slice(actualCut);
    const tokensFreed = countMessagesTokens(snipped);
    const marker = {
        role: 'user',
        content: `[已自动删除最早的 ${actualCut} 条对话以节省 token（约释放 ${tokensFreed.toLocaleString()} tokens）。` +
            `如需完整历史请用 /new 重启会话或 /compact 触发 LLM 摘要压缩。]`,
    };
    return {
        messages: [...(systemMsg ? [systemMsg] : []), marker, ...remaining],
        messagesRemoved: actualCut,
        tokensFreed,
    };
}
/**
 * 找到一个安全的"砍头切点"：从 proposedCut 开始向后扫描，
 * 跳过所有会留下孤儿 tool 的位置。
 *
 * 算法（与 compact.ts 的 findTailWithCompleteToolChains 互补）：
 *   1. 扫描 messages[cut:]（即"保留区"），收集 assistant.tool_calls.id
 *      到 knownIds Set 中（向前扫描，每遇到 assistant 就累加）
 *   2. 遇到 tool 消息且其 tool_call_id 不在 knownIds → 这是孤儿
 *      → 把切点向后推一位（即 cut++）并重新扫描
 *   3. 直到保留区中所有 tool 消息都有父级，或 cut 越界
 *
 * 复杂度：O(N²) 最坏（N 个消息），实际 N 通常 < 100、扩展次数 < 5 → 极快
 */
export function findHeadCutpoint(messages, proposedCut) {
    let cut = Math.max(0, Math.min(proposedCut, messages.length));
    // 反复扫描直到切点稳定
    while (cut < messages.length) {
        const knownIds = new Set();
        let foundOrphan = false;
        for (let i = cut; i < messages.length; i++) {
            const msg = messages[i];
            if (msg.role === 'assistant' && msg.tool_calls) {
                for (const tc of msg.tool_calls)
                    knownIds.add(tc.id);
            }
            else if (msg.role === 'tool' && !knownIds.has(msg.tool_call_id)) {
                foundOrphan = true;
                break;
            }
        }
        if (!foundOrphan)
            return cut;
        cut++;
    }
    return cut;
}
