"""리드의 `PROGRESS:` 체크포인트를 lead-events 원장에 기록한다.

`PROGRESS:` 는 원래 리드가 사용자에게 그대로 보여 주는 대화 텍스트다.
Claude Code 리드는 그 줄이 세션 전사에 남아 검증기가 읽을 수 있지만, 나머지
호스트는 회계 방식이 `artifact-only` 라 대화 텍스트가 어디에도 보존되지 않는다.
그 호스트에서 체크포인트가 검증에 잡히려면 `leadEventsPath` 에 `progress`
이벤트가 있어야 하는데, 그 행을 만들 명령이 없었다 — `okstra agent-activity
append` 는 activity 원장 전용이고 `--phase` 도 `--detail` 도 받지 않는다.
그래서 워커를 실제로 띄우고 거둔 run 도 "체크포인트 없음"으로 보고됐다
(실측: dev-10784 error-analysis, advisory 22건 중 6건이 이 이유 하나).

이 모듈이 그 명령이다. 한 번의 호출이 두 가지를 낸다 — 원장에 남는 `progress`
이벤트와, 리드가 사용자에게 그대로 내보낼 `PROGRESS:` 한 줄.
"""
from __future__ import annotations

import argparse
import json
import sys
from collections.abc import Mapping, Sequence
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object
from okstra_ctl.lead_events import LeadEvent, append_lead_event


class ProgressRecordError(ValueError):
    """PROGRESS 체크포인트를 기록할 수 없을 때."""


# 정본은 `prompts/lead/okstra-lead-contract.md` "Progress reporting (BLOCKING)"
# 의 Required checkpoints 목록이다. 이 튜플이 그 목록과 어긋나면
# `tests/contract/test_lead_progress_cli.py` 가 잡는다.
PROGRESS_PHASES: tuple[str, ...] = (
    "phase-1-intake",
    "phase-2-prompts",
    "phase-3-team-create",
    "phase-4-dispatch",
    "phase-5-poll",
    "phase-5-collect",
    "phase-5-stage",
    "phase-5-stage-complete",
    "phase-5.5-convergence",
    "phase-5.6-critic",
    "phase-5.5.9-plan-verify",
    "phase-batch-cleanup",
    "phase-6-synthesis",
    "user-confirm",
    "phase-7-persist",
    "phase-7-teardown",
    "complete",
)

# 계약이 문구까지 고정한 체크포인트. `--detail` 없이 phase 만 줘도 계약대로
# 렌더된다 — 리드가 문구를 다시 지어내면 검사와 어긋날 자리가 생긴다.
_DEFAULT_DETAIL: Mapping[str, str] = {
    "phase-1-intake": "reading task bundle",
    "phase-2-prompts": "preparing worker prompts",
    "phase-3-team-create": "using implicit team",
    "phase-6-synthesis": "dispatching report-writer-worker",
    "phase-7-persist": "updating manifests",
    "phase-7-teardown": "shutting-down-workers",
}


def _utc_now() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _read_json_object(path: Path, artifact: str) -> dict[str, Any]:
    try:
        return load_owned_object(path, artifact=artifact)
    except JsonBoundaryError as exc:
        raise ProgressRecordError(str(exc)) from exc


def _require_string(payload: Mapping[str, Any], key: str) -> str:
    value = payload.get(key)
    if not isinstance(value, str) or not value.strip():
        raise ProgressRecordError(f"required string missing: {key}")
    return value.strip()


def _resolve_project_path(project_root: Path, value: str | Path) -> Path:
    path = Path(value)
    return path if path.is_absolute() else project_root / path


def _manifest_run_seq(manifest: Mapping[str, Any]) -> str:
    sequences = manifest.get("runSequencesByCategory")
    if not isinstance(sequences, Mapping):
        raise ProgressRecordError(
            "run manifest has no runSequencesByCategory object"
        )
    return _require_string(sequences, "manifests")


def _roster_workers(project_root: Path, manifest: Mapping[str, Any]) -> list[dict]:
    value = manifest.get("teamStatePath")
    if not isinstance(value, str) or not value.strip():
        return []
    path = _resolve_project_path(project_root, value.strip())
    if not path.is_file():
        return []
    state = _read_json_object(path, "team-state")
    workers = state.get("workers")
    if not isinstance(workers, list):
        return []
    return [row for row in workers if isinstance(row, Mapping)]


def _worker_aliases(worker: Mapping[str, Any]) -> set[str]:
    """명단 한 행이 받아 줄 이름들 — 검증기 `_worker_roles` 와 같은 확장."""
    aliases: set[str] = set()
    for raw in (worker.get("role"), worker.get("workerId")):
        name = str(raw or "").strip().casefold()
        if not name:
            continue
        aliases.add(name)
        aliases.add(name.replace(" ", "-"))
        if name.endswith(" worker"):
            aliases.add(name[: -len(" worker")])
        if name.endswith("-worker"):
            aliases.add(name[: -len("-worker")])
    return aliases


def resolve_roster_role(
    workers: Sequence[Mapping[str, Any]], value: str
) -> tuple[str, str | None]:
    """`--worker` 가 가리키는 명단 역할을 돌려준다.

    체크포인트의 `worker=` 는 명단(`workers[].role`)과 대조되므로, 국면별
    기능 라벨(`Codex executor`)을 그대로 쓰면 아무 워커도 지목하지 못한다.
    호출자가 준 이름이 명단의 어느 행을 가리키면 그 행의 `role` 로 바꿔 적는다.
    아무 행도 가리키지 못하면 준 값을 그대로 쓰고, 어떤 이름이 있는지 알린다 —
    기록 자체를 막으면 남는 것이 없다.
    """
    wanted = value.strip().casefold()
    for worker in workers:
        role = str(worker.get("role") or "").strip()
        if role and wanted in _worker_aliases(worker):
            return role, None
    if not workers:
        return value.strip(), None
    known = ", ".join(sorted(
        str(row.get("role") or "").strip()
        for row in workers
        if str(row.get("role") or "").strip()
    ))
    return value.strip(), (
        f"worker `{value.strip()}` names no roster worker; "
        f"the checkpoint is matched against: {known}"
    )


def _parse_field(raw: str) -> tuple[str, str]:
    name, separator, field_value = raw.partition("=")
    if not separator or not name.strip():
        raise ProgressRecordError(
            f"--field expects NAME=VALUE, got: {raw}"
        )
    return name.strip(), field_value.strip()


def render_progress_line(
    phase: str,
    fields: Sequence[tuple[str, str]] = (),
    detail: str | None = None,
) -> str:
    """계약이 정한 `PROGRESS: <phase-id> <verb-phrase>` 한 줄."""
    tokens = [f"{name}={field_value}" for name, field_value in fields]
    text = detail if detail is not None else _DEFAULT_DETAIL.get(phase)
    if text:
        tokens.append(text.strip())
    rest = " ".join(token for token in tokens if token)
    return f"PROGRESS: {phase} {rest}".strip()


def record_progress(
    project_root: Path,
    run_manifest_path: Path,
    phase: str,
    *,
    worker: str | None = None,
    fields: Sequence[tuple[str, str]] = (),
    detail: str | None = None,
    timestamp: str | None = None,
) -> tuple[LeadEvent, str | None]:
    """체크포인트 한 건을 원장에 남기고, 그 이벤트와 경고를 돌려준다."""
    manifest = _read_json_object(run_manifest_path, "run manifest")
    warning: str | None = None
    ordered: list[tuple[str, str]] = []
    if worker is not None and worker.strip():
        role, warning = resolve_roster_role(
            _roster_workers(project_root, manifest), worker
        )
        ordered.append(("worker", role))
    ordered.extend(fields)
    line = render_progress_line(phase, ordered, detail)
    event = LeadEvent(
        event_type="progress",
        lead_runtime=_require_string(manifest, "leadRuntime"),
        task_key=_require_string(manifest, "taskKey"),
        task_type=_require_string(manifest, "taskType"),
        run_seq=_manifest_run_seq(manifest),
        timestamp=timestamp or _utc_now(),
        details={
            "phase": phase,
            "line": line,
            **({name: field_value for name, field_value in ordered}),
        },
    )
    events_path = _resolve_project_path(
        project_root, _require_string(manifest, "leadEventsPath")
    )
    append_lead_event(events_path, event)
    return event, warning


def _append(args: argparse.Namespace) -> int:
    fields = [_parse_field(raw) for raw in args.field]
    event, warning = record_progress(
        args.project_root,
        args.run_manifest,
        args.phase,
        worker=args.worker,
        fields=fields,
        detail=args.detail,
    )
    if warning:
        print(f"okstra lead-progress: {warning}", file=sys.stderr)
    payload = {
        "ok": True,
        "phase": args.phase,
        "progressLine": event.details["line"],
    }
    print(json.dumps(payload, ensure_ascii=False, indent=2))
    return 0


_CLI_EPILOG = r"""Usage:
  okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-1-intake
  okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-4-dispatch --worker codex-worker --field model=gpt-5.6-sol
  okstra lead-progress append --project-root <dir> --run-manifest <path> --phase phase-5-collect --worker codex-worker --field status=completed

`progressLine` in the output is the line to emit to the user, verbatim.
"""
_CLI_DESCRIPTION = (
    "Record a lead PROGRESS checkpoint in the run's lead-events ledger."
)


def _parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description=_CLI_DESCRIPTION,
        epilog=_CLI_EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        prog="okstra lead-progress")
    subparsers = parser.add_subparsers(dest="command", required=True)
    append_parser = subparsers.add_parser("append")
    append_parser.add_argument("--project-root", type=Path, required=True)
    append_parser.add_argument("--run-manifest", type=Path, required=True)
    append_parser.add_argument(
        "--phase", choices=PROGRESS_PHASES, required=True
    )
    append_parser.add_argument(
        "--worker",
        help="roster role this checkpoint is about (rendered as `worker=`)")
    append_parser.add_argument(
        "--field", action="append", default=[], metavar="NAME=VALUE",
        help="extra `NAME=VALUE` token, in the order given")
    append_parser.add_argument(
        "--detail",
        help="verb phrase after the fields; contract wording is the default")
    return parser


def main(argv: list[str] | None = None) -> int:
    args = _parser().parse_args(argv)
    try:
        return _append(args)
    except ProgressRecordError as exc:
        print(f"okstra lead-progress: {exc}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main(sys.argv[1:]))
