"""직전 implementation-planning run 이 남긴 계획 맥락을 packet 용으로 좁힌다.

계획 run 이 실패한 뒤 `--selected-direction` 으로 다시 도는 경우, 새 run 은 직전
run 의 산출물을 하나도 받지 못했다. analysis packet 에 그것을 실을 자리가 없었고
(`analysis_packet.build_analysis_packet` 에 직전 finding·판정·리포트를 받는 인자가
없었다), 판정 이월은 답변된 clarification 이 있는 경로에만 있다
(`incremental_scope.decide_scope` 는 답변된 `C-NNN` 이 없으면 `full` 을 되돌린다).
그래서 planner 들이 brief 에서 전부 다시 유도했고, 같은 이견이 새 `P-*` 번호로
다시 열렸다.

이 모듈은 그 재유도를 줄일 만큼만 모은다 — clarification 행, Stage Map 한 줄씩,
직전 수렴이 못 가른 finding, 그리고 나머지를 위한 리포트 경로. 리포트 전문은
싣지 않는다: 그 중복이 실측 283K 리포트로 892K 중복 읽기를 만들어 report-writer 를
timeout 시킨 적이 있다(`clarification_items.carry` 참고).

판정은 소유하지 않는다. 이 블록은 사실 기록이고, 승인되지 않은 계획을 그대로
복사하라는 지시가 아니다 — 그 문장은 packet 블록 머리글이 직접 말한다
(`analysis_packet._prior_planning_block`).
"""
from __future__ import annotations

import re
from pathlib import Path
from typing import Any, Mapping

from .clarification_items import carried_clarification_rows
from .final_report_paths import final_report_data_path
from .json_boundary import JsonBoundaryError, load_owned_object
from .paths import RunRef, infer_project_root, project_rel


PLANNING_TASK_TYPE = "implementation-planning"
# 직전 run 이 끝내지 못한 두 상태. `convergence_engine._CLASSIFICATION_COUNT_KEYS`
# 의 어휘를 그대로 쓴다 — 새 어휘를 만들면 같은 finding 이 run 마다 다르게 읽힌다.
UNSETTLED_CLASSIFICATIONS = ("contested", "worker-unique")
_SUMMARY_LIMIT = 200
_WHITESPACE_RE = re.compile(r"\s+")


def build_prior_planning_summary(task_root: Path) -> str:
    """이 task 의 직전 계획 run 요약. 직전 run 이 없으면 빈 문자열.

    직전 리포트 해소는 wizard / interactive 진입점이 쓰는 것과 같다
    (`RunRef.latest_under`: mtime 최신, 동률이면 basename, `.data.json` 우선).
    같은 task-type 으로 좁히므로 다른 phase 의 리포트를 계획 맥락으로 집지 않는다.
    """
    task_root = Path(task_root)
    ref = RunRef.latest_under(task_root, (PLANNING_TASK_TYPE,))
    if ref is None:
        return ""
    report = ref.report
    project_root = infer_project_root(task_root)
    sections = [f"- Prior planning report: `{project_rel(report, project_root)}`"]
    rows = carried_clarification_rows(report)
    if rows:
        sections.append(f"### Prior Clarification Items\n\n{rows}")
    stage_map = _stage_map_lines(report)
    if stage_map:
        sections.append(
            "### Prior Stage Map\n\n"
            "`stage | title | depends-on`, as the prior plan wrote it.\n\n"
            + "\n".join(stage_map)
        )
    unsettled = _unsettled_finding_lines(ref)
    if unsettled:
        sections.append(
            "### Findings the prior run left unsettled\n\n"
            + "\n".join(unsettled)
        )
    return "\n\n".join(sections)


def _stage_map_lines(report: Path) -> list[str]:
    """직전 계획의 Stage Map 을 스테이지당 한 줄로.

    `stageMap` 이 정본이고 `stages` 는 그것이 없는 리포트를 위한 폴백이다 —
    `plan_items._planning_stage_rows` 가 쓰는 것과 같은 우선순위다.
    """
    planning = _planning(report)
    rows = planning.get("stageMap")
    if not isinstance(rows, list) or not rows:
        rows = planning.get("stages")
    if not isinstance(rows, list):
        return []
    lines = []
    for row in rows:
        if not isinstance(row, Mapping):
            continue
        stage = row.get("stage")
        if not isinstance(stage, int) or isinstance(stage, bool):
            continue
        title = _one_line(row.get("title")) or "(untitled)"
        depends = _one_line(row.get("dependsOn")) or "(unrecorded)"
        lines.append(f"- {stage} | {title} | depends-on: {depends}")
    return lines


def _unsettled_finding_lines(ref: RunRef) -> list[str]:
    """직전 수렴이 `contested` / `worker-unique` 로 닫은 finding 한 줄씩.

    상태 파일이 없거나 못 읽으면 빈 목록이다. 없는 것이 정상 분기다 — 수렴을
    돌기 전에 죽은 run, 그리고 디렉터리별 seq 가 갈린 run 이 둘 다 여기로 온다
    (`RunRef.convergence_state` 참고).

    seq 를 못 읽은 ref 도 같다. 옛 타임스탬프 이름의 리포트가 그 경우이고, 그
    이름에서는 상태 파일 이름을 재구성할 수 없다.
    """
    if ref.seq is None:
        return []
    state = _load_object(ref.convergence_state)
    findings = state.get("findings")
    if not isinstance(findings, list):
        return []
    lines = []
    for finding in findings:
        if not isinstance(finding, Mapping):
            continue
        classification = finding.get("classification")
        if classification not in UNSETTLED_CLASSIFICATIONS:
            continue
        finding_id = _one_line(finding.get("findingId"))
        if not finding_id:
            continue
        summary = _one_line(finding.get("summary")) or "(no summary recorded)"
        lines.append(f"- `{finding_id}` ({classification}) — {summary}")
    return lines


def _planning(report: Path) -> Mapping[str, Any]:
    data = _load_object(final_report_data_path(report))
    planning = data.get("implementationPlanning")
    return planning if isinstance(planning, Mapping) else {}


def _load_object(path: Path) -> Mapping[str, Any]:
    """소유한 JSON 객체, 못 읽으면 빈 매핑.

    이 블록은 전부 최선 노력이다. 직전 run 의 산출물이 깨졌다고 새 run 의 준비를
    막으면, 다시 도는 이유였던 그 실패가 재실행 자체를 못 하게 만든다.
    """
    try:
        data = load_owned_object(path, artifact="prior planning artifact")
    except (JsonBoundaryError, OSError):
        # expected-miss: 직전 run 이 남기지 못했거나 깨뜨린 파일. 이 블록은
        # 맥락일 뿐이라 없는 상태가 정상 분기고, 소비처는 해당 하위 블록을
        # 생략한다.
        return {}
    return data if isinstance(data, Mapping) else {}


def _one_line(value: object) -> str:
    if not isinstance(value, str):
        return ""
    collapsed = _WHITESPACE_RE.sub(" ", value).strip()
    if len(collapsed) <= _SUMMARY_LIMIT:
        return collapsed
    return collapsed[: _SUMMARY_LIMIT - 1].rstrip() + "…"
