"""定点补丁引擎：把进化输出的离散 edit 逐条应用到文本，逐条出状态报告。

借鉴 SkillOpt skill.py 的 apply 语义；守 P5：找不到锚点不静默乱改、不裸 except。
冻结区按全文坐标区间判定；append/退化追加插到最早冻结区之前（无则 EOF）。
"""

from __future__ import annotations

from loguru import logger

APPENDIX_START = "<!-- APPENDIX_START -->"
APPENDIX_END = "<!-- APPENDIX_END -->"
APPENDIX_MAX_CHARS = 2000  # appendix 区软上限（守设计「长度上限+warning，不做去重」）

MOMENTUM_START = "<!-- MOMENTUM_START -->"
MOMENTUM_END = "<!-- MOMENTUM_END -->"
MOMENTUM_MAX_CHARS = 2000  # momentum 区软上限（与 appendix 一致：超限 warning 不截断）
MOMENTUM_HEADING = "## 动量指导（每轮重写，勿手改）"  # replace_momentum 写入的固定标题行


def momentum_region_bounds(text: str) -> tuple[int, int] | None:
    """定位 momentum 受保护区的字符区间，并对损坏态显式报错（P5）。

    momentum marker 由 replace_momentum 在 epoch 末反复重写，guidance 又来自 LLM
    外部输入，因此 marker 可能出现损坏态。本函数是 momentum 路径的唯一边界判定入口，
    把配对校验集中在一处：

    - START 与 END 各恰好出现一次且 START 在 END 之前 → 返回 (start_idx, end_idx)，
      end_idx 指向 END marker 结束位置（即 content[start:end] 含完整两 marker）。
    - 两 marker 都不出现 → 返回 None（合法的"无区"态，调用方据此新建）。
    - 其余皆为损坏态（仅一个 marker、END 在 START 前、任一 marker 重复）→ raise
      ValueError，拒绝静默新建/跳过，要求人工修复。

    参数:
        text: 待检测的文本（skill 全文）。
    返回:
        (start_idx, end_idx) 表示区间，或 None 表示无 momentum 区。
    异常:
        ValueError: momentum marker 损坏/不配对。
    """
    start_count = text.count(MOMENTUM_START)
    end_count = text.count(MOMENTUM_END)
    if start_count == 0 and end_count == 0:
        return None
    if start_count != 1 or end_count != 1:
        raise ValueError(
            f"momentum marker 损坏/不配对：MOMENTUM_START 出现 {start_count} 次、"
            f"MOMENTUM_END 出现 {end_count} 次（各须恰好 1 次），需人工修复"
        )
    start_idx = text.index(MOMENTUM_START)
    end_idx = text.index(MOMENTUM_END) + len(MOMENTUM_END)
    if start_idx >= text.index(MOMENTUM_END):
        raise ValueError(
            "momentum marker 损坏/不配对：MOMENTUM_END 出现在 MOMENTUM_START 之前，需人工修复"
        )
    return start_idx, end_idx


def momentum_inner(content: str) -> str:
    """返回 momentum 受保护区的内层文本（去掉两 marker），无区返回空串。

    与 _momentum_span（含 marker 的整段）的区别：本函数只取两 marker 之间的内层正文，
    供 run_slow_momentum 的 prev_guidance 使用。prev_guidance 在 LLM 解析失败时会被
    run_slow_momentum 原样返回、再喂给 replace_momentum；replace_momentum 禁止 guidance
    含 marker 字面量，故 prev_guidance 必须是无 marker 的内层文本，否则一旦解析回退即
    在 replace_momentum 抛 ValueError。

    边界判定与配对校验统一委托 momentum_region_bounds：marker 损坏/不配对时由其 raise
    ValueError，本函数不把损坏态静默当作"无区"。

    参数:
        content: skill 全文。
    返回:
        momentum 区两 marker 之间的内层文本（已 strip）；无区返回空串。
    异常:
        ValueError: momentum marker 损坏/不配对。
    """
    bounds = momentum_region_bounds(content)
    if bounds is None:
        return ""
    start, end = bounds
    inner = content[start + len(MOMENTUM_START) : end - len(MOMENTUM_END)].strip()
    # 去掉 replace_momentum 写入的固定标题行，只回传纯指导文本，使其等价于上一轮
    # 传给 replace_momentum 的 guidance（解析回退时原样回传不会引入重复标题）。
    if inner.startswith(MOMENTUM_HEADING):
        inner = inner[len(MOMENTUM_HEADING) :].lstrip("\n")
    return inner.strip()


def append_to_appendix(content: str, notes: list[str]) -> str:
    """把 LAPSE 提醒追加到文件尾的 appendix 受保护区；区不存在则创建。

    护栏：appendix 区超过 APPENDIX_MAX_CHARS 时 logger.warning（不静默截断，
    提示人工压缩；不做自动去重——YAGNI，见设计）。

    参数:
        content: 原文。
        notes: 待追加的提醒文本列表。
    返回:
        含 appendix 区的新文本。
    """
    if not notes:
        return content
    bullet = "\n".join(f"- {n.strip()}" for n in notes if n.strip())
    if not bullet:
        return content
    if APPENDIX_START in content and APPENDIX_END in content:
        head, rest = content.split(APPENDIX_START, 1)
        inner, tail = rest.split(APPENDIX_END, 1)
        new_inner = f"{inner.rstrip()}\n{bullet}"
        out = f"{head}{APPENDIX_START}{new_inner}\n{APPENDIX_END}{tail}"
    else:
        new_inner = f"\n## 执行提醒（自动累积，勿手改）\n{bullet}"
        out = f"{content.rstrip()}\n\n{APPENDIX_START}{new_inner}\n{APPENDIX_END}\n"
    if len(new_inner) > APPENDIX_MAX_CHARS:
        logger.warning(
            "appendix 区长度 {} 超过上限 {}，建议人工压缩",
            len(new_inner),
            APPENDIX_MAX_CHARS,
        )
    return out


def appendix_region_bounds(text: str) -> tuple[int, int] | None:
    """定位 appendix 受保护区的字符区间，对损坏态显式报错（P5，对称 momentum_region_bounds）。

    appendix marker 由 append_to_appendix 维护、consolidation 回写，可能出现损坏态。
    本函数是 appendix 路径的唯一边界判定入口，把配对校验集中一处：

    - START 与 END 各恰好一次且 START 在 END 之前 → 返回 (start_idx, end_idx)，
      end_idx 指向 END marker 结束位置（content[start:end] 含完整两 marker）。
    - 两 marker 都不出现 → 返回 None（合法的「无区」态）。
    - 其余（仅一个 marker、END 在 START 前、任一 marker 重复）→ raise ValueError，
      拒绝静默按字符串切片处理而误拼/吞掉区外正文。

    参数:
        text: 待检测文本（skill 全文）。
    返回:
        (start_idx, end_idx) 表示区间，或 None 表示无 appendix 区。
    异常:
        ValueError: appendix marker 损坏/不配对。
    """
    start_count = text.count(APPENDIX_START)
    end_count = text.count(APPENDIX_END)
    if start_count == 0 and end_count == 0:
        return None
    if start_count != 1 or end_count != 1:
        raise ValueError(
            f"appendix marker 损坏/不配对：APPENDIX_START 出现 {start_count} 次、"
            f"APPENDIX_END 出现 {end_count} 次（各须恰好 1 次），需人工修复"
        )
    start_idx = text.index(APPENDIX_START)
    end_idx = text.index(APPENDIX_END) + len(APPENDIX_END)
    if start_idx >= text.index(APPENDIX_END):
        raise ValueError(
            "appendix marker 损坏/不配对：APPENDIX_END 出现在 APPENDIX_START 之前，需人工修复"
        )
    return start_idx, end_idx


def extract_appendix_notes(content: str) -> list[str]:
    """从 appendix 受保护区解析出 bullet 提醒列表；无区返回空列表。

    功能:
        取 appendix 区内每行以 "- " 起头的文本为一条 note（去 "- " 前缀与首尾空白），
        区内标题行（## 执行提醒…）不计。供 consolidation 读取现有 notes。
    参数:
        content: skill 全文。
    返回:
        note 字符串列表；无 appendix 区返回 []。
    异常:
        ValueError: appendix marker 损坏/不配对（经 appendix_region_bounds，不静默切片）。
    关键实现细节:
        边界判定统一委托 appendix_region_bounds，只取两 marker 之间内层正文逐行解析。
    """
    bounds = appendix_region_bounds(content)
    if bounds is None:
        return []
    start, end = bounds
    inner = content[start + len(APPENDIX_START) : end - len(APPENDIX_END)]
    notes: list[str] = []
    for line in inner.splitlines():
        stripped = line.strip()
        if stripped.startswith("- "):
            note = stripped[2:].strip()
            if note:
                notes.append(note)
    return notes


def replace_appendix_notes(content: str, notes: list[str]) -> str:
    """用 notes 整体替换 appendix 区内容；notes 为空则删除整个 appendix 区。

    功能:
        consolidation 回写压缩后 notes 的替换语义（区别于 append_to_appendix 累积）：
        区存在则整体覆盖区内 bullet；notes 空则连 marker 一并删除、保留区外正文；
        区不存在且 notes 非空则按 append_to_appendix 格式新建。
    参数:
        content: 原文（可能含 appendix 区）。
        notes: 压缩后的提醒列表；空列表表示删区。
    返回:
        替换后的全文。
    异常:
        ValueError: appendix marker 损坏/不配对（经 appendix_region_bounds）。
    关键实现细节:
        边界经 appendix_region_bounds 显式校验，按 (start,end) 切出 head/tail 拼接，
        不做两次独立 split（避免损坏态误拼/吞掉区外正文）。
    """
    bounds = appendix_region_bounds(content)
    if bounds is not None:
        start, end = bounds
        head = content[:start]
        tail = content[end:]
        if not notes:
            return head.rstrip() + ("\n" + tail.lstrip("\n") if tail.strip() else "\n")
        bullet = "\n".join(f"- {n.strip()}" for n in notes if n.strip())
        new_inner = f"\n## 执行提醒（自动累积，勿手改）\n{bullet}"
        return f"{head}{APPENDIX_START}{new_inner}\n{APPENDIX_END}{tail}"
    if not notes:
        return content
    return append_to_appendix(content, notes)


def replace_momentum(content: str, guidance: str) -> str:
    """把「动量指导」整体写入文件尾的 momentum 受保护区；区不存在则创建。

    与 append_to_appendix 的累积语义不同，momentum 是**替换**语义：慢更新周期每
    epoch 末整体重写一段动量指导，旧指导被完全覆盖（不保留历史）。momentum 区与
    appendix 区独立共存——本函数只触碰 momentum marker，不破坏已有 appendix 区。

    护栏：momentum 区超过 MOMENTUM_MAX_CHARS 时 logger.warning（不静默截断，与
    appendix 对齐）。

    关键实现细节：
        - 替换非追加：区已存在时用 guidance 整体覆盖 marker 内 inner，旧动量不残留。
        - 创建位置在文件尾（append_to_appendix 同样在文件尾，但两区 marker 不同，
          split 按各自 marker 定位，互不干扰）。

    空 guidance 决策：与 appendix 的累积语义不同，momentum 是「每轮整体重写」，空
    guidance 表示「本轮无动量指导」，属合法语义——照常写入（区内仅留标题，旧动量被清空），
    而非返回原文保留旧动量。

    参数:
        content: 原文（可能已含 appendix 区）。
        guidance: 本轮动量指导全文（整体覆盖旧动量）。
    返回:
        含 momentum 区的新文本。
    异常:
        ValueError: guidance 含 momentum marker 字面量（外部输入注入），或原文 momentum
            marker 损坏/不配对。
    """
    if MOMENTUM_START in guidance or MOMENTUM_END in guidance:
        raise ValueError(
            "guidance 不得包含 momentum marker 字面量"
            f"（{MOMENTUM_START} / {MOMENTUM_END}），否则会破坏 marker 配对"
        )
    bounds = momentum_region_bounds(content)
    new_inner = f"\n## 动量指导（每轮重写，勿手改）\n{guidance.strip()}"
    if bounds is not None:
        start_idx, end_idx = bounds
        head = content[:start_idx]
        tail = content[end_idx:]
        out = f"{head}{MOMENTUM_START}{new_inner}\n{MOMENTUM_END}{tail}"
    else:
        out = f"{content.rstrip()}\n\n{MOMENTUM_START}{new_inner}\n{MOMENTUM_END}\n"
    if len(new_inner) > MOMENTUM_MAX_CHARS:
        logger.warning(
            "momentum 区长度 {} 超过上限 {}，建议人工压缩",
            len(new_inner),
            MOMENTUM_MAX_CHARS,
        )
    return out


def _protected_ranges(content: str, spans: list[str]) -> list[tuple[int, int]]:
    """把冻结文本块映射成 content 中的 [start, end) 坐标区间。"""
    ranges: list[tuple[int, int]] = []
    for span in spans:
        idx = content.find(span)
        if idx != -1:
            ranges.append((idx, idx + len(span)))
    return ranges


def _in_ranges(pos: int, ranges: list[tuple[int, int]]) -> bool:
    """判断位置 pos 是否落在任意冻结区间内。"""
    return any(start <= pos < end for start, end in ranges)


def _append_at(content: str, ranges: list[tuple[int, int]]) -> int:
    """append/退化追加落点：最早一个 start>0 的冻结区之前；无则文末（头部 frontmatter 不计）。"""
    starts = [start for start, _ in ranges if start > 0]
    return min(starts) if starts else len(content)


def _insert_at(content: str, at: int, payload: str) -> str:
    """在 at 位置插入 payload，自动补换行保持段落格式。"""
    head, tail = content[:at].rstrip(), content[at:].lstrip("\n")
    if tail:
        return head + "\n\n" + payload + "\n\n" + tail
    return head + "\n\n" + payload + "\n"


def _do_append(content: str, payload: str, ranges: list[tuple[int, int]]) -> tuple[str, str]:
    """执行 append 操作，返回更新后内容与状态字符串。"""
    return _insert_at(content, _append_at(content, ranges), payload), "applied_append"


def _do_insert_after(
    content: str, target: str, payload: str, ranges: list[tuple[int, int]]
) -> tuple[str, str]:
    """执行 insert_after 操作，处理退化追加与冻结区跳过。"""
    pos = content.find(target) if target else -1
    if pos == -1:
        logger.warning("insert_after 锚点缺失，退化为追加 target={}", target[:80])
        return (
            _insert_at(content, _append_at(content, ranges), payload),
            "applied_insert_after_fallback",
        )
    if _in_ranges(pos, ranges):
        logger.warning("insert_after 目标在冻结区，跳过 target={}", target[:80])
        return content, "skipped_protected"
    at = pos + len(target)
    nl = content.find("\n", at)
    at = nl + 1 if nl != -1 else len(content)
    return content[:at] + payload + "\n" + content[at:], "applied_insert_after"


def _do_replace_delete(
    op: str,
    content: str,
    target: str,
    payload: str,
    ranges: list[tuple[int, int]],
) -> tuple[str, str]:
    """执行 replace 或 delete 操作，返回更新后内容与状态字符串。"""
    if not target:
        return content, "skipped_missing_target"
    pos = content.find(target)
    if pos == -1:
        logger.warning("{} 锚点缺失，跳过 target={}", op, target[:80])
        return content, "skipped_target_not_found"
    if _in_ranges(pos, ranges):
        logger.warning("{} 目标在冻结区，跳过 target={}", op, target[:80])
        return content, "skipped_protected"
    new_content = content.replace(target, payload if op == "replace" else "", 1)
    return new_content, "applied_" + op


def _apply_one(content: str, edit: dict, ranges: list[tuple[int, int]]) -> tuple[str, dict]:
    """应用单条 edit，返回 (更新后内容, 状态报告)。"""
    if not isinstance(edit, dict):
        return content, {
            "op": "",
            "target": "",
            "content_preview": "",
            "status": "error",
            "error": f"edit 非 dict: {type(edit).__name__}",
        }
    op = str(edit.get("op", ""))
    target = str(edit.get("target", "") or "")
    payload = str(edit.get("content", "") or "").strip()
    report = {
        "op": op,
        "target": target[:200],
        "content_preview": payload[:200],
        "status": "unknown",
    }

    if op == "append":
        content, report["status"] = _do_append(content, payload, ranges)
        return content, report

    if op == "insert_after":
        content, report["status"] = _do_insert_after(content, target, payload, ranges)
        return content, report

    if op in ("replace", "delete"):
        content, report["status"] = _do_replace_delete(op, content, target, payload, ranges)
        return content, report

    logger.warning("未知 op，跳过: {}", op)
    report["status"] = "skipped_unknown_op"
    return content, report


def apply_patch_with_report(
    content: str,
    edits: list[dict],
    protected_spans: list[str] | None = None,
) -> tuple[str, list[dict]]:
    """顺序应用 edit 列表，返回 (新内容, 逐条状态报告)。

    参数:
        content: 原始文本。
        edits: 每条 {op, target, content}。
        protected_spans: 冻结文本块列表；目标落入其坐标区间即跳过，append 插到其前。

    返回:
        (应用后文本, reports)；reports 每条含 op/target/content_preview/status/index。
    """
    spans = protected_spans or []
    reports: list[dict] = []
    for i, edit in enumerate(edits, 1):
        try:
            ranges = _protected_ranges(content, spans)
            content, report = _apply_one(content, edit, ranges)
        except (KeyError, TypeError, ValueError, AttributeError) as exc:
            report = {
                "op": "",
                "target": "",
                "content_preview": "",
                "status": "error",
                "error": str(exc),
            }
            logger.exception("补丁应用异常 index={}", i)
        report["index"] = i
        reports.append(report)
    return content, reports
