"""Session-Level 评价回顾 (#145 Phase C)

maintenance 子进程中执行，产出 session_record fact。
纯函数模块，零外部依赖。4 个消费者共用：
1. maintenance._step_session_review() — 主调用方
2. awaken() — 注入上次 session_record 的 incomplete_work
3. session_context.py — 跨会话连续性
4. 未来 dashboard — 会话质量可视化

L0 纯规则 fallback 与 LLM 输出同 schema，消费者无需区分来源。
"""

from __future__ import annotations

import json
import logging
import re
from collections import defaultdict

logger = logging.getLogger(__name__)

# ── 常量 ──────────────────────────────────────────────────────

MIN_EVENTS_FOR_REVIEW = 8  # 短会话跳过（<8 条无跨事件因果可挖掘）
TOP_K_EVENTS = 5  # 详情阶段取 top-K 条
MAX_PER_BUCKET = 2  # 每个多样性桶最多 2 条

# 预编译正则
_RE_TEST_RESULT = re.compile(r'(\d+)\s*(passed|failed|error)', re.IGNORECASE)


# ── 多样性分桶 ────────────────────────────────────────────────


def _classify_event_bucket(event: dict) -> str:
    """将 raw_event 分入多样性桶"""
    tool = event.get("tool_name", "")
    summary = (event.get("summary", "") or "").lower()
    importance = event.get("importance", 0.0) or 0.0

    # error 桶：importance >= 0.9 或 summary 含错误关键词
    if importance >= 0.9 or any(
        kw in summary for kw in ("error", "traceback", "failed", "panic", "fatal")
    ):
        return "error"

    # test 桶：importance >= 0.8 或含测试关键词
    if importance >= 0.8 or any(
        kw in summary for kw in ("passed", "pytest", "test result")
    ):
        return "test"

    # create 桶
    if tool == "Write":
        return "create"

    # edit 桶
    if tool in ("Edit", "MultiEdit"):
        return "edit"

    # 默认 explore
    return "explore"


def select_diverse_events(
    events: list[dict], top_k: int = TOP_K_EVENTS,
) -> list[dict]:
    """多样性分桶 + importance 排序选 top-K

    每个 bucket 最多 MAX_PER_BUCKET 条，剩余名额按 importance 填充。
    返回按 event_id 升序排列（时间顺序）的事件列表。
    """
    if not events:
        return []

    # 按 bucket 分组
    buckets: dict[str, list[dict]] = defaultdict(list)
    for e in events:
        bucket = _classify_event_bucket(e)
        buckets[bucket].append(e)

    # 每个 bucket 按 importance 排序，取 top MAX_PER_BUCKET
    selected: list[dict] = []
    for _bucket_name, bucket_events in buckets.items():
        sorted_events = sorted(
            bucket_events, key=lambda x: x.get("importance", 0) or 0, reverse=True,
        )
        selected.extend(sorted_events[:MAX_PER_BUCKET])

    # 如果不够 top_k，用剩余最高 importance 事件填充
    if len(selected) < top_k:
        selected_ids = {e.get("event_id") for e in selected}
        remaining = [e for e in events if e.get("event_id") not in selected_ids]
        remaining.sort(key=lambda x: x.get("importance", 0) or 0, reverse=True)
        for e in remaining:
            if len(selected) >= top_k:
                break
            selected.append(e)

    # 截取到 top_k，按 event_id 排序（时间顺序）
    selected = selected[:top_k]
    selected.sort(key=lambda x: x.get("event_id", 0))
    return selected


# ── 未完成工作检测（L0 纯规则）──────────────────────────────


def detect_incomplete_work(events: list[dict]) -> list[str]:
    """L0 检测未完成工作

    规则：
    - 最后 N 条事件中有 error/fail 且后续无 fix/pass
    - 测试失败未恢复
    """
    if not events:
        return []

    incomplete: list[str] = []

    # 反向扫描，找最后一个错误事件
    last_error_idx = -1
    last_pass_idx = -1
    for i, e in enumerate(events):
        summary = (e.get("summary", "") or "").lower()
        importance = e.get("importance", 0.0) or 0.0
        if importance >= 0.9 or "error" in summary or "failed" in summary:
            last_error_idx = i
        if "passed" in summary or importance == 0.8:
            last_pass_idx = i

    # 最后一个错误在最后一个通过之后 → 未修复
    if last_error_idx > last_pass_idx and last_error_idx >= 0:
        error_event = events[last_error_idx]
        summary = error_event.get("summary", "")
        tool = error_event.get("tool_name", "?")
        incomplete.append(f"[{tool}] {summary[:80]}" if summary else f"[{tool}] error unresolved")

    return incomplete


# ── L0 Session Review（纯规则 fallback）────────────────────


def session_review_l0(
    events: list[dict],
    collection: str,
    session_id: str | None = None,
) -> dict:
    """L0 纯规则 session review — 产出同 LLM schema 的 JSON

    从 raw_events 的 summary + importance + signals_json 聚合。
    无需外部服务，永远可用。
    """
    files_changed: set[str] = set()
    key_changes: list[dict] = []

    # 从 signals_json 和 summary 提取信息
    for e in events:
        signals = _parse_signals(e)
        file_path = signals.get("file", "")
        if file_path:
            files_changed.add(file_path)

        # 从高 importance 事件提取 key_changes
        importance = e.get("importance", 0.0) or 0.0
        if importance >= 0.5:
            change: dict = {}
            if file_path:
                change["file"] = file_path
            tool = e.get("tool_name", "")
            if tool == "Write":
                change["action"] = "create"
            elif tool in ("Edit", "MultiEdit"):
                change["action"] = "edit"
            elif tool == "Bash":
                change["action"] = "run"
            else:
                change["action"] = tool.lower() if tool else "unknown"

            # 从 signals_json 提取 identifiers
            ids: list[str] = []
            for key in ("added_fn", "removed_fn", "added_import", "added_const"):
                ids.extend(signals.get(key, []))
            if ids:
                change["identifiers"] = ids[:5]

            # tags
            tags: list[str] = []
            if signals.get("new_file"):
                tags.append("new_file")
            if signals.get("added_fn"):
                tags.append("structural")
            if importance >= 0.9:
                tags.append("error")
            if tags:
                change["tags"] = tags

            if change.get("file") or change.get("identifiers"):
                key_changes.append(change)

    # 提取 outcome（最后的测试结果）
    outcome = _extract_outcome(events)

    # 未完成工作
    incomplete = detect_incomplete_work(events)

    return {
        "session_record": {
            "files_changed": sorted(files_changed),
            "key_changes": key_changes[:10],
            "causal_chain": [],  # L0 无法推断因果
            "patterns": _detect_patterns(events),
            "outcome": outcome,
            "incomplete_work": incomplete,
            "evaluation": {
                "went_well": None,
                "went_wrong": None,
                "next_time": None,
            },
            "_source": "l0",
        },
    }


def _parse_signals(event: dict) -> dict:
    """安全解析 signals_json"""
    raw = event.get("signals_json", "{}")
    if not raw:
        return {}
    try:
        return json.loads(raw) if isinstance(raw, str) else raw
    except (json.JSONDecodeError, TypeError):
        return {}


def _extract_outcome(events: list[dict]) -> str:
    """从事件列表提取最终测试结果"""
    # 反向搜索最后的测试结果
    for e in reversed(events):
        summary = e.get("summary", "") or ""
        m = _RE_TEST_RESULT.search(summary)
        if m:
            return summary.strip()
    return ""


def _detect_patterns(events: list[dict]) -> list[str]:
    """L0 模式检测（简单规则）"""
    patterns: list[str] = []
    has_error = False
    has_fix = False
    has_create = False
    has_read = False

    for e in events:
        importance = e.get("importance", 0.0) or 0.0
        tool = e.get("tool_name", "")
        if importance >= 0.9:
            has_error = True
        if tool in ("Edit", "MultiEdit") and importance >= 0.4:
            has_fix = True
        if tool == "Write":
            has_create = True
        if tool in ("Read", "Grep", "Glob"):
            has_read = True

    if has_error and has_fix:
        patterns.append("error-fix")
    if has_read and has_fix:
        patterns.append("investigate-fix")
    if has_create:
        patterns.append("create")
    return patterns


# ── LLM Prompt 构建 ──────────────────────────────────────────

SESSION_REVIEW_PROMPT = """\
You are reviewing a coding session. Produce a structured JSON summary.

EVENT INDEX (all events, summary only):
{stage_a}

TOP EVENTS (full details):
{stage_b}

{prev_context}

Output EXACTLY this JSON structure (no markdown fences, no extra text):
{{
  "session_record": {{
    "files_changed": ["file1.py", "file2.py"],
    "key_changes": [
      {{"file": "file.py", "action": "fix|create|refactor|edit", "identifiers": ["func_name"], "tags": ["bugfix"]}}
    ],
    "causal_chain": ["read X → found Y → fixed Z → test passed"],
    "patterns": ["error-fix"],
    "outcome": "N/N tests passed",
    "incomplete_work": ["unresolved error in X"],
    "evaluation": {{
      "went_well": "quickly identified root cause",
      "went_wrong": null,
      "next_time": null
    }}
  }}
}}"""


def build_review_prompt(
    events: list[dict],
    top_events: list[dict],
    prev_record: dict | None = None,
) -> str:
    """构建两阶段 LLM session review prompt

    Stage A (~2KB): 所有事件的 summary 拼接
    Stage B (~10KB): top-5 详情的完整 payload
    """
    # Stage A: 事件索引
    stage_a_lines: list[str] = []
    for e in events:
        tool = e.get("tool_name", "?")
        summary = e.get("summary", "")
        importance = e.get("importance", 0.0) or 0.0
        if summary:
            stage_a_lines.append(f"[{tool}] (imp={importance:.1f}) {summary}")
    stage_a = "\n".join(stage_a_lines)
    # 截断 Stage A 到 ~2KB
    if len(stage_a) > 2048:
        stage_a = stage_a[:2048] + "\n... (truncated)"

    # Stage B: Top 事件详情
    stage_b_lines: list[str] = []
    for e in top_events:
        tool = e.get("tool_name", "?")
        importance = e.get("importance", 0.0) or 0.0
        signals = _parse_signals(e)
        # 从 payload 提取关键信息
        payload_str = e.get("payload", "{}")
        try:
            payload = json.loads(payload_str) if isinstance(payload_str, str) else payload_str
        except (json.JSONDecodeError, TypeError):
            payload = {}

        tool_input = payload.get("tool_input", {})
        tool_response = str(payload.get("tool_response", ""))[:500]

        entry = f"--- [{tool}] importance={importance:.2f} ---\n"
        if signals:
            entry += f"signals: {json.dumps(signals, ensure_ascii=False)}\n"
        if isinstance(tool_input, dict):
            # 精简 tool_input（只保留关键字段）
            compact_input = {}
            for k in ("file_path", "command", "content", "old_string", "new_string"):
                if k in tool_input:
                    v = str(tool_input[k])
                    compact_input[k] = v[:300] if len(v) > 300 else v
            if compact_input:
                entry += f"input: {json.dumps(compact_input, ensure_ascii=False)}\n"
        if tool_response and tool_response != "{}":
            entry += f"output: {tool_response[:500]}\n"
        stage_b_lines.append(entry)

    stage_b = "\n".join(stage_b_lines)
    if len(stage_b) > 10240:
        stage_b = stage_b[:10240] + "\n... (truncated)"

    # 跨会话连续性
    prev_context = ""
    if prev_record:
        prev_sr = prev_record.get("session_record", prev_record)
        incomplete = prev_sr.get("incomplete_work", [])
        key_changes = prev_sr.get("key_changes", [])
        if incomplete or key_changes:
            parts: list[str] = ["PREVIOUS SESSION CONTEXT:"]
            if incomplete:
                parts.append(f"Incomplete work: {json.dumps(incomplete, ensure_ascii=False)}")
            if key_changes:
                parts.append(f"Previous key changes: {json.dumps(key_changes[:3], ensure_ascii=False)}")
            prev_context = "\n".join(parts)

    return SESSION_REVIEW_PROMPT.format(
        stage_a=stage_a,
        stage_b=stage_b,
        prev_context=prev_context,
    )


def parse_review_output(raw: str) -> dict | None:
    """解析 LLM JSON 输出

    4 层容错：
    1. 直接 JSON parse
    2. 提取 ```json 块
    3. 查找 { } 边界
    4. 返回 None（降级到 L0）
    """
    if not raw or not raw.strip():
        return None

    text = raw.strip()

    # Layer 1: 直接解析
    try:
        result = json.loads(text)
        if isinstance(result, dict) and "session_record" in result:
            return result
    except json.JSONDecodeError:
        pass

    # Layer 2: 提取 ```json 块
    m = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', text, re.DOTALL)
    if m:
        try:
            result = json.loads(m.group(1))
            if isinstance(result, dict):
                return result
        except json.JSONDecodeError:
            pass

    # Layer 3: 查找 { } 边界
    start = text.find("{")
    end = text.rfind("}")
    if start >= 0 and end > start:
        try:
            result = json.loads(text[start:end + 1])
            if isinstance(result, dict):
                return result
        except json.JSONDecodeError:
            pass

    return None


def make_session_record_fact(
    record: dict,
    collection: str,
    session_id: str | None,
) -> dict:
    """将 session_record 转为 fact dict（用于 insert_fact）"""
    sr = record.get("session_record", record)

    files = sr.get("files_changed", [])
    outcome = sr.get("outcome", "")

    # assertion — 可读摘要 + keyword-dense 尾部（FTS5 检索用）
    file_count = len(files)
    change_count = len(sr.get("key_changes", []))
    assertion = f"Session: {file_count} files changed, {change_count} key changes"
    if outcome:
        assertion += f", {outcome[:60]}"
    # 附加关键词（文件名 + 标识符 + 模式），insert_fact 会截断到 300 字符
    kw_parts: list[str] = []
    for f in files[:3]:
        kw_parts.append(f.rsplit("/", 1)[-1])  # 只取文件名
    for kc in sr.get("key_changes", [])[:3]:
        for ident in kc.get("identifiers", [])[:2]:
            kw_parts.append(ident)
    for p in sr.get("patterns", []):
        kw_parts.append(p)
    if kw_parts:
        assertion += " | " + " ".join(kw_parts)

    source = "session_review_l0" if sr.get("_source") == "l0" else "session_review_llm"

    return {
        "assertion": assertion,
        "category": "session_record",
        "confidence": 0.9 if source == "session_review_llm" else 0.7,
        "source": source,
        "collection": collection,
        "session_id": session_id or "",
        "context_json": json.dumps(record, ensure_ascii=False),
        "scope": "project",
    }
