/**
 * ============================================================
 *  src/context/reactiveCompact.ts —— 反应式压缩（错误自救）
 * ------------------------------------------------------------
 *  对齐 kakadeai 主项目 services/compact/reactiveCompact.ts：
 *  当 API 返回 "prompt too long" 类错误时，自动触发压缩重试。
 *
 *  典型场景：
 *    用户灌了一大段上下文 → 调 LLM → 返回 400 prompt_too_long
 *    → 系统不直接报错，而是先调 forceCompact 把上下文摘要一遍
 *    → 摘要失败再用 snipCompact 砍头兜底
 *    → 把新上下文交还给调用方，调用方重试一次 chat()
 *
 *  防爆约束（circuit breaker）：
 *    连续自救失败 MAX_CONSECUTIVE_REACTIVE_FAILURES 次才熔断。
 *    任意一次自救成功（LLM 压缩或 snip 兜底）→ 计数清零，
 *    允许后续再次触发。行为：压缩成功→继续→再溢出→再压→正常往复。
 *    用户 /new 重启会话后 consecutiveFailures 也归零。
 * ============================================================
 */
import { forceCompact } from './compact.js';
import { snipCompactIfNeeded } from './snipCompact.js';
import { countMessagesTokens } from './tokenCounter.js';
export function createReactiveCompactState() {
    return { consecutiveFailures: 0 };
}
/** 连续失败几次触发熔断，拒绝继续自救 */
export const MAX_CONSECUTIVE_REACTIVE_FAILURES = 3;
const defaultState = createReactiveCompactState();
/** /new 时调用，允许下一个 session 再次自救 */
export function resetReactiveCompactState(state = defaultState) {
    state.consecutiveFailures = 0;
}
/**
 * 测试 / 调试用：查询 circuit breaker 是否已熔断（连续失败达上限）。
 *
 * 原签名 hasAttemptedReactiveCompact 在新语义下映射为「电路是否已断开」：
 * 单次成功就清零，只有连续失败 ≥ MAX_CONSECUTIVE_REACTIVE_FAILURES 才返回 true。
 * 效果等价于原先的 attempted 旗标语义的"超集"（原先=1次，新=N次）。
 * 别名 isReactiveCircuitOpen 供新代码使用，两者完全等价。
 */
export function hasAttemptedReactiveCompact(state = defaultState) {
    return state.consecutiveFailures >= MAX_CONSECUTIVE_REACTIVE_FAILURES;
}
/** hasAttemptedReactiveCompact 的语义化别名 */
export const isReactiveCircuitOpen = hasAttemptedReactiveCompact;
// ==================== 错误识别 ====================
/**
 * 判断一个错误是否是"提示词太长"类错误。
 *
 * 兼容多家 provider 的错误消息格式：
 *   - OpenAI:   "This model's maximum context length is X tokens..."
 *   - Anthropic: "prompt is too long"
 *   - MiniMax:  "input length exceeds the context window"
 *   - DeepSeek: "context_length_exceeded"
 *   - 通用:     含 "prompt" / "context" / "token" 且含 "long"/"exceed"/"limit"
 */
export function isPromptTooLongError(error) {
    const msg = errorMessage(error).toLowerCase();
    if (!msg)
        return false;
    // 宽松匹配：提及 prompt/context/token 且提及 long/exceed/limit/max
    const hasSubject = /prompt|context|token|input length/.test(msg);
    const hasIssue = /too long|exceed|limit|maximum|over/.test(msg);
    return hasSubject && hasIssue;
}
function errorMessage(error) {
    if (typeof error === 'string')
        return error;
    if (error instanceof Error)
        return error.message;
    if (error && typeof error === 'object' && 'message' in error) {
        return String(error.message ?? '');
    }
    return String(error ?? '');
}
/**
 * 如果当前错误是 prompt_too_long 且 circuit breaker 未熔断，
 * 执行一次"先 LLM 压缩、失败兜底 snip"的恢复流程。
 *
 * circuit breaker 规则：
 *   - 任意成功（LLM 压缩 or snip 兜底）→ consecutiveFailures 清零
 *   - 两个步骤都失败 → consecutiveFailures +1
 *   - consecutiveFailures ≥ MAX_CONSECUTIVE_REACTIVE_FAILURES → 熔断拒绝
 *
 * @param messages 当前历史（不修改）
 * @param provider 当前 provider（用于 LLM 压缩）
 * @param error    刚刚抛出的错误
 * @param state    可选的独立状态（默认使用进程级单例）
 */
export async function reactiveCompactIfApplicable(messages, provider, error, state = defaultState) {
    // 非 prompt_too_long 错误：直接短路，不消耗计数
    if (!isPromptTooLongError(error)) {
        return { recovered: false, messages, reason: 'not a prompt-too-long error' };
    }
    // 电路已熔断：连续失败达上限，拒绝继续
    if (state.consecutiveFailures >= MAX_CONSECUTIVE_REACTIVE_FAILURES) {
        return {
            recovered: false,
            messages,
            reason: '反应式压缩已熔断（连续失败达上限）——请 /new 或手动 /compact',
        };
    }
    // Step 1: 先试 LLM 全量压缩
    try {
        const r = await forceCompact(messages, provider);
        // 救活成功 → 清零计数，让后续继续可用
        state.consecutiveFailures = 0;
        return {
            recovered: true,
            messages: r.messages,
            reason: `LLM 压缩成功（${r.before} → ${r.after} tokens）`,
            before: r.before,
            after: r.after,
        };
    }
    catch {
        // 压缩失败 → 走 snip 兜底
    }
    // Step 2: snip 兜底（更激进 40%）
    const beforeSnip = countMessagesTokens(messages);
    const snipped = snipCompactIfNeeded(messages, { force: true, snipPercent: 0.4 });
    if (snipped.messagesRemoved > 0) {
        // snip 也算救活 → 清零计数
        state.consecutiveFailures = 0;
        const afterSnip = countMessagesTokens(snipped.messages);
        return {
            recovered: true,
            messages: snipped.messages,
            reason: `snip 兜底成功（删除 ${snipped.messagesRemoved} 条最老消息，释放 ~${snipped.tokensFreed} tokens）`,
            before: beforeSnip,
            after: afterSnip,
        };
    }
    // 两步都没救活 → 失败计数 +1
    state.consecutiveFailures++;
    return {
        recovered: false,
        messages,
        reason: '反应式压缩失败：LLM 压缩抛错且 snip 也没东西可砍',
    };
}
