"""Append-only reader/writer for `<task_root>/history/fix-cycles.jsonl`.

완료(release-handoff)된 task 에 재진입하는 버그 핫픽스 run 묶음(fix cycle)의
SSOT. consumers.jsonl 과 같은 idiom — append-only + dir flock + last-wins 읽기.
이 모듈이 유일한 reader/writer 이며, 소비처(analysis-packet / manifest /
final-report / okstra-brief-gen)는 모두 summarize()/packet_summary() 파생 뷰를 쓴다.

행 3종 (event 필드로 구분):
    {"event":"opened","cycle":"fc-01","target_report":...,"symptom":...,"opened_at":...}
    {"event":"run","cycle":"fc-01","task_type":...,"run_seq":...,"run_manifest":...}
    {"event":"closed","cycle":"fc-01","closed_by":...,"report":...,"closed_at":...}

idempotency 키: (cycle, event, run_manifest|None). open cycle 은 task 당 1개.
"""
from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any, Dict, List, Optional

from .run_context import dir_flock
from .jsonl import append_jsonl

FIX_CYCLES_FILENAME = "fix-cycles.jsonl"
_LOCK_FILENAME = ".fix-cycles.lock"

# 완료(release-handoff) task 에 fix-cycle 로 재진입할 수 있는 entry phase 들.
# prepare(run.py) 의 게이트와 wizard 의 감지 술어가 공유하는 SSOT.
FIX_CYCLE_ENTRY_PHASES = (
    "requirements-discovery", "error-analysis", "implementation-option-selection",
    "implementation-planning",
)


def fix_cycles_path(task_root: Path) -> Path:
    return Path(task_root) / "history" / FIX_CYCLES_FILENAME


def read_rows(task_root: Path) -> List[Dict[str, Any]]:
    """fix-cycles.jsonl 의 유효 행만 반환.

    append-only SSOT 의 손상 내성 경계: 중단된 writer 가 남긴 깨진 줄과
    `cycle` 키가 없는(스키마 드리프트) 행은 여기서 걸러, downstream
    open_cycle/summarize/packet_summary 의 r["cycle"] 접근이 항상 안전하다.
    """
    p = fix_cycles_path(task_root)
    if not p.exists():
        return []
    out: List[Dict[str, Any]] = []
    for line in p.read_text(encoding="utf-8").splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            row = json.loads(line)
        except json.JSONDecodeError:
            continue
        if isinstance(row, dict) and "cycle" in row:
            out.append(row)
    return out


def open_cycle(rows: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
    """closed 행이 없는 마지막 opened 행. open 은 동시 1개가 불변식."""
    closed = {r["cycle"] for r in rows if r.get("event") == "closed"}
    for r in reversed(rows):
        if r.get("event") == "opened" and r["cycle"] not in closed:
            return r
    return None


def _next_cycle_id(rows: List[Dict[str, Any]]) -> str:
    n = sum(1 for r in rows if r.get("event") == "opened")
    return f"fc-{n + 1:02d}"


def append_opened(task_root: Path, *, target_report: str, symptom: str,
                  opened_at: str) -> str:
    """새 cycle 을 열고 id 를 반환. open cycle 이 이미 있으면 그 id 반환(멱등)."""
    history = fix_cycles_path(task_root).parent
    with dir_flock(history, _LOCK_FILENAME):
        rows = read_rows(task_root)
        existing = open_cycle(rows)
        if existing:
            return existing["cycle"]
        cycle = _next_cycle_id(rows)
        _append_row(task_root, {
            "event": "opened", "cycle": cycle,
            "target_report": target_report, "symptom": symptom,
            "opened_at": opened_at,
        })
        return cycle


def append_run(task_root: Path, *, cycle: str, task_type: str, run_seq: int,
               run_manifest: str) -> None:
    history = fix_cycles_path(task_root).parent
    with dir_flock(history, _LOCK_FILENAME):
        for r in read_rows(task_root):
            if (r.get("event") == "run" and r.get("cycle") == cycle
                    and r.get("run_manifest") == run_manifest):
                return
        _append_row(task_root, {
            "event": "run", "cycle": cycle, "task_type": task_type,
            "run_seq": run_seq, "run_manifest": run_manifest,
        })


def append_closed(task_root: Path, *, cycle: str, closed_by: str,
                  report: str, closed_at: str) -> None:
    history = fix_cycles_path(task_root).parent
    with dir_flock(history, _LOCK_FILENAME):
        for r in read_rows(task_root):
            if r.get("event") == "closed" and r.get("cycle") == cycle:
                return
        _append_row(task_root, {
            "event": "closed", "cycle": cycle, "closed_by": closed_by,
            "report": report, "closed_at": closed_at,
        })


def _append_row(task_root: Path, record: Dict[str, Any]) -> None:
    p = fix_cycles_path(task_root)
    p.parent.mkdir(parents=True, exist_ok=True)
    append_jsonl(p, record, ensure_ascii=False, compact=False)


def summarize(rows: List[Dict[str, Any]]) -> Dict[str, Any]:
    """소비처 공용 요약: {count, openCycleId, latest:{cycle,symptom,targetReport,closedAt}}."""
    opened = [r for r in rows if r.get("event") == "opened"]
    if not opened:
        return {"count": 0, "openCycleId": None, "latest": None}
    closed_at = {r["cycle"]: r.get("closed_at")
                 for r in rows if r.get("event") == "closed"}
    open_row = open_cycle(rows)
    latest = opened[-1]
    return {
        "count": len(opened),
        "openCycleId": open_row["cycle"] if open_row else None,
        "latest": {
            "cycle": latest["cycle"],
            "symptom": latest.get("symptom", ""),
            "targetReport": latest.get("target_report", ""),
            "closedAt": closed_at.get(latest["cycle"]),
        },
    }


def packet_summary(rows: List[Dict[str, Any]]) -> str:
    """analysis-packet 의 `## Fix History` 섹션 본문 (마크다운). 행이 없으면 ''."""
    opened = [r for r in rows if r.get("event") == "opened"]
    if not opened:
        return ""
    closed = {r["cycle"]: r for r in rows if r.get("event") == "closed"}
    lines: List[str] = []
    for o in opened:
        state = "closed" if o["cycle"] in closed else "open"
        lines.append(
            f"- `{o['cycle']}` ({state}) — {o.get('symptom', '')} "
            f"(target: `{o.get('target_report', '')}`)")
        for r in rows:
            if r.get("event") == "run" and r.get("cycle") == o["cycle"]:
                lines.append(
                    f"  - run: {r.get('task_type', '')} seq {r.get('run_seq', '')}"
                    f" (`{r.get('run_manifest', '')}`)")
    return "\n".join(lines)


_REQUEST_SUMMARY_RE = re.compile(
    r"^## Request Summary\s*$", re.MULTILINE)


def derive_symptom(brief_text: str) -> str:
    """brief 의 `## Request Summary` 첫 비어있지 않은 줄 (불릿 마커 제거)."""
    m = _REQUEST_SUMMARY_RE.search(brief_text)
    if not m:
        return ""
    for line in brief_text[m.end():].splitlines():
        line = line.strip()
        if not line:
            continue
        if line.startswith("#"):
            return ""
        return line.lstrip("-* ").strip()
    return ""
