"""§8.8 Phase 2:阈值结构化摘要 —— 长会话窗口中段折叠。

Phase 1(context_base_idx 软重置)解决"跨话题累积";单个分段内持续对话,窗口仍只增
不减 —— 50% 门槛的旧 tool 压缩(core/context.py)只摘工具正文,user/assistant 对话
本体一字不少,最终仍会撞 reliable_context。本模块在 run 起点检测窗口体量,达
reliable_context × FOLD_TRIGGER_RATIO 时把窗口中段(保留尾部约 FOLD_TAIL_RATIO)
折叠为一份固定模板的结构化摘要。

设计要点(取舍详 DESIGN §8.8):
- **边界而非删除**:原文全量留 messages 表,只写 tasks.context_summary + 推进
  context_base_idx;web 端照旧翻完整历史。
- **增量更新**:旧摘要作为注入消息本就在被折叠的前缀里,摘要指令要求合并 ——
  新摘要 = f(旧摘要, 新折叠段),不重读全史。
- **前缀缓存友好**:摘要调用复用会话自身的消息前缀(prepare 同款压缩后取 [:切点]),
  末尾追加一条摘要指令 user 消息 —— 前缀与上一轮 chat 的缓存字节一致,几乎全程
  cache hit;折叠后的窗口(system + 摘要 + 尾段)在下次折叠前字节稳定,缓存恢复全暖。
- **精确值原文保留**:模板硬性要求路径/ID/数值不得改写(mem0 实测自由摘要会静默丢
  精确值),守 §8.2 质量边界(确认过的需求/结论不删)。
- **失败零阻塞**:摘要调用失败只 warn + 跳过本次折叠,run 照常跑(85% 距硬上限有垫)。
"""
from __future__ import annotations

from typing import Any, Callable, List, Optional
from uuid import UUID

from sqlalchemy import update

from .context import (
    CHARS_PER_TOKEN,
    COMPACT_CONTEXT_RATIO,
    _message_chars,
    prepare_messages_with_stats,
)
from .storage import session_scope
from .storage.models import Task
from .storage.usage import record_chat_usage

# 窗口体量达 reliable_context 的此比例 → 触发折叠(50% 压缩、85% 折叠双层门槛)。
FOLD_TRIGGER_RATIO = 0.85
# 折叠后保留的尾部体量占比(近期对话原文,续聊手感;其余折进摘要)。
FOLD_TAIL_RATIO = 0.30
# 可折叠段少于这么多条消息就不折 —— 摘要调用本身有成本,段太短折了省不了几个字。
MIN_FOLD_MESSAGES = 8

SUMMARY_NOTE_HEADER = "[前情摘要]"

# 摘要指令:作为最后一条 user 消息追加在会话前缀之后(前缀缓存复用,见模块注释)。
FOLD_INSTRUCTION = (
    "请暂停手头任务,现在只做一件事:把以上全部对话内容压缩成一份结构化摘要,"
    "它将替代这些消息作为后续对话的上下文。若开头已有「[前情摘要]」,把它合并进来。\n"
    "严格按以下模板输出,除模板内容外不要输出任何别的文字:\n"
    "## 目标\n## 已确认的约束与决定\n## 进展\n## 待办\n## 关键路径 / ID / 数值\n"
    "硬性要求:文件路径、URL、ID、数字、代码符号、命令等精确值必须原文保留,"
    "不得改写、四舍五入或省略;用户明确确认过的需求与结论必须保留;"
    "不确定的内容标注[待确认]。"
)


def build_summary_note(summary: str) -> dict:
    """把摘要包成注入消息(user 角色,仅内存不落库;Session.load 与折叠现场共用)。"""
    return {
        "role": "user",
        "content": (
            f"{SUMMARY_NOTE_HEADER} 以下是本对话更早内容的结构化摘要"
            "(原始消息已归档,不在当前上下文中);请把它当作已发生的事实,"
            "直接继续对话,不必复述:\n\n" + summary
        ),
    }


def find_cutoff(messages: List[dict], head_len: int) -> Optional[int]:
    """选折叠切点:返回内存 index(切点本身保留在窗口里),不可折返回 None。

    从尾部向前累计体量,第一条使尾部 ≥ FOLD_TAIL_RATIO 的 **user 消息**即切点 ——
    切在 user 消息上天然不劈开 assistant tool_calls / tool 配对,与 Phase 1 软重置
    的锚点语义一致(窗口永远以 user 消息开头)。user 消息稀疏时尾部占比可能超过
    FOLD_TAIL_RATIO,保守方向,可接受。
    """
    n = len(messages)
    if n <= head_len:
        return None
    total = sum(_message_chars(m) for m in messages[head_len:])
    tail_target = total * FOLD_TAIL_RATIO
    acc = 0
    cutoff: Optional[int] = None
    for i in range(n - 1, head_len - 1, -1):
        acc += _message_chars(messages[i])
        if acc >= tail_target and messages[i].get("role") == "user":
            cutoff = i
            break
    if cutoff is None or cutoff - head_len < MIN_FOLD_MESSAGES:
        return None
    return cutoff


def _user_ordinal(messages: List[dict], idx: int) -> int:
    """messages[idx] 是从头数第几条 user 消息(1-based)。"""
    return sum(1 for m in messages[: idx + 1] if m.get("role") == "user")

def _kth_user_index(messages: List[dict], k: int) -> Optional[int]:
    """从头数第 k 条(1-based)user 消息的 index;不存在返回 None。"""
    seen = 0
    for i, m in enumerate(messages):
        if m.get("role") == "user":
            seen += 1
            if seen == k:
                return i
    return None


def persist_fold(task_id: UUID, new_base: int, summary: str) -> None:
    """折叠结果落库:摘要 + 窗口起点,一个事务。先于内存变更执行 —— DB 写失败时
    内存未动,本次折叠白跑一次摘要调用但状态零污染,下次 run 重试。"""
    with session_scope() as s:
        s.execute(
            update(Task)
            .where(Task.task_id == task_id)
            .values(context_base_idx=new_base, context_summary=summary)
        )


def maybe_fold(
    session: Any,
    llm: Any,
    caps: Any,
    *,
    user_id: UUID,
    emit: Callable[[dict], None],
) -> Optional[dict]:
    """run 起点调用:体量达阈值则折叠,返回统计 dict;未触发/不可折返回 None。

    session 鸭子类型,需要:messages / task_id / context_head_len / context_base /
    apply_fold(cutoff, summary)。异常向上抛,由 loop 包 try 兜(失败零阻塞纪律)。
    """
    msgs = session.messages
    before_chars = sum(_message_chars(m) for m in msgs)
    threshold = int(caps.reliable_context * FOLD_TRIGGER_RATIO * CHARS_PER_TOKEN)
    if before_chars < threshold:
        return None
    head_len = session.context_head_len
    cutoff = find_cutoff(msgs, head_len)
    if cutoff is None:
        return None
    folded_count = cutoff - head_len
    emit({"type": "context_fold", "phase": "start", "folding_messages": folded_count})

    # 复用 loop 同款 prepare(50% 门槛旧 tool 压缩)再取前缀 —— 字节与上一轮 chat 的
    # 缓存前缀一致,摘要调用吃到 cache hit。切点在两个列表间用「第 k 条 user 消息」对齐:
    # prepare 只改写 tool 内容 / 补占位 tool 消息,user 消息原样且相对顺序不变。
    compact_threshold = int(caps.reliable_context * COMPACT_CONTEXT_RATIO * CHARS_PER_TOKEN)
    prepared, _ = prepare_messages_with_stats(msgs, compact_threshold_chars=compact_threshold)
    p_cut = _kth_user_index(prepared, _user_ordinal(msgs, cutoff))
    if p_cut is None:  # 理论不可达,防御
        return None
    fold_input = prepared[:p_cut] + [{"role": "user", "content": FOLD_INSTRUCTION}]

    response = llm.chat(messages=fold_input)
    choices = getattr(response, "choices", None) or []
    summary = ((choices[0].message.content if choices else "") or "").strip()
    if not summary:
        emit({"type": "warn", "msg": "context fold: 摘要调用返回空,本轮跳过折叠"})
        return None

    usage = getattr(response, "usage", None)
    try:
        record_chat_usage(
            task_id=session.task_id,
            user_id=user_id,
            message_id=None,  # 摘要不入 messages,event 挂 task 维度
            model_profile=f"{caps.family}.{caps.variant}",
            prompt_tokens=getattr(usage, "prompt_tokens", 0) or 0,
            completion_tokens=getattr(usage, "completion_tokens", 0) or 0,
            input_cny_per_mtoken=caps.input_cny_per_mtoken,
            output_cny_per_mtoken=caps.output_cny_per_mtoken,
            response=response,
            # 独立 kind:按 kind 对账"折叠烧多少钱",且折叠次数 = count(该 kind)
            # (任务详情 context_folds 字段的数据源,免加计数列)
            kind="context_fold",
        )
    except Exception:
        pass  # 记账失败不阻塞折叠(与 loop 记账同纪律)

    new_base = session.context_base + folded_count
    persist_fold(session.task_id, new_base, summary)
    session.apply_fold(cutoff, summary)
    after_chars = sum(_message_chars(m) for m in session.messages)
    stats = {
        "type": "context_fold",
        "phase": "done",
        "folded_messages": folded_count,
        "before_chars": before_chars,
        "after_chars": after_chars,
        "new_base": new_base,
    }
    emit(stats)
    return stats
