"""task-manifest.json 의 workStatus 를 갱신하는 단일 CLI 진입점.

okstra-inspect status.4 가 Edit 도구로 하던 수동 JSON 편집(키 순서·개행 보존
규칙을 프롬프트로 강제)을 CLI 로 수렴시킨다. 직렬화는 render._write_json 과
동일한 json.dumps(indent=2, ensure_ascii=False) + "\n" 이므로 재렌더와 byte
규칙이 일치한다. discovery/task-catalog.json 은 여기서 재생성하지 않는다 —
다음 run 렌더가 manifest 를 재투영할 때까지 stale 할 수 있다.
"""
from __future__ import annotations

import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

from okstra_ctl.ids import slugify_task_segment
from okstra_ctl.fixed_text import line, scalar
from okstra_ctl.paths import task_dir, task_manifest_file
from okstra_ctl.json_boundary import load_owned_object, write_owned_object_atomic
from okstra_project import (
    ResolverError,
    StateError,
    resolve_project_root,
    resolve_task_reference,
)

ALLOWED_WORK_STATUSES = ("todo", "in-progress", "blocked", "done")


def _emit(payload: dict, *, text: bool = False) -> None:
    if not text:
        print(json.dumps(payload, ensure_ascii=False, indent=2))
        return
    lines = ["# Okstra Work Status Result", ""]
    for key, label in (
        ("ok", "OK"), ("stage", "Stage"), ("reason", "Reason"),
        ("taskKey", "Task key"), ("previousWorkStatus", "Previous work status"),
        ("workStatus", "Work status"), ("workStatusUpdatedAt", "Updated at"),
        ("workStatusNote", "Note"), ("taskManifestPath", "Task manifest"),
    ):
        if key in payload:
            lines.append(line(label, payload.get(key)).rstrip("\n"))
    for match in payload.get("matches", []) if isinstance(payload.get("matches"), list) else []:
        if isinstance(match, dict):
            lines.append(f"- Match: `{scalar(match.get('taskKey'))}`")
    print("\n".join(lines))


def _manifest_path(project_root: Path, entry: dict) -> Path:
    rel = entry.get("taskManifestPath") or ""
    if rel:
        candidate = Path(rel)
        return candidate if candidate.is_absolute() else project_root / candidate
    group_seg = entry.get("taskGroupPathSegment") or slugify_task_segment(
        entry.get("taskGroup", "")
    )
    id_seg = entry.get("taskIdPathSegment") or slugify_task_segment(
        entry.get("taskId", "")
    )
    return task_manifest_file(task_dir(project_root, group_seg, id_seg))


def _parse_args(argv: list[str] | None):
    parser = argparse.ArgumentParser(
        description="Set a task's user-managed workStatus in task-manifest.json."
    )
    parser.add_argument(
        "token",
        help="full task-key (<project-id>:<task-group>:<task-id>) or bare task-id",
    )
    parser.add_argument("status", choices=ALLOWED_WORK_STATUSES)
    parser.add_argument(
        "--note",
        default=None,
        help="set workStatusNote; omit to leave any existing note untouched",
    )
    parser.add_argument(
        "--task-group", default="", help="scope a bare task-id match to this task-group"
    )
    parser.add_argument("--project-root", default="", help="project root for catalog lookup")
    parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
    parser.add_argument("--json", action="store_true", help="emit JSON (always on)")
    parser.add_argument("--text", action="store_true", help="emit fixed text fields")
    return parser.parse_args(argv)


def _resolve_entry(args):
    try:
        project_root = resolve_project_root(explicit_root=args.project_root, cwd=args.cwd)
    except ResolverError as exc:
        _emit({"ok": False, "stage": "resolve", "reason": str(exc)}, text=args.text)
        return None, None, 2

    token = args.token
    task_group = args.task_group
    if token.count(":") == 2:
        _, key_group, key_id = token.split(":")
        token = key_id
        task_group = key_group or task_group

    try:
        matches = resolve_task_reference(
            project_root, token, task_group=task_group or None
        )
    except StateError as exc:
        _emit({"ok": False, "stage": "catalog", "reason": str(exc)}, text=args.text)
        return None, None, 2

    if not matches:
        _emit({"ok": False, "stage": "not-found", "token": args.token}, text=args.text)
        return None, None, 1
    if len(matches) > 1:
        _emit({"ok": False, "stage": "ambiguous", "matches": matches}, text=args.text)
        return None, None, 2
    return project_root, matches[0], 0


def _updated_payload(manifest_path: Path, manifest: dict, entry: dict, args) -> dict:
    previous = manifest.get("workStatus", "")
    manifest["workStatus"] = args.status
    manifest["workStatusUpdatedAt"] = datetime.now(timezone.utc).strftime(
        "%Y-%m-%dT%H:%M:%SZ"
    )
    if args.note is not None:
        manifest["workStatusNote"] = args.note
    write_owned_object_atomic(manifest_path, manifest, artifact="task manifest")
    return {
        "ok": True, "taskKey": entry.get("taskKey", ""),
        "previousWorkStatus": previous, "workStatus": args.status,
        "workStatusUpdatedAt": manifest["workStatusUpdatedAt"],
        "workStatusNote": manifest.get("workStatusNote", ""),
        "taskManifestPath": str(manifest_path),
    }


def main(argv: list[str] | None = None) -> int:
    args = _parse_args(argv)
    project_root, entry, resolution_exit = _resolve_entry(args)
    if resolution_exit:
        return resolution_exit
    assert project_root is not None and entry is not None
    manifest_path = _manifest_path(project_root, entry)
    if not manifest_path.exists():
        _emit(
            {
                "ok": False,
                "stage": "manifest-missing",
                "taskKey": entry.get("taskKey", ""),
                "taskManifestPath": str(manifest_path),
            },
            text=args.text,
        )
        return 1
    try:
        manifest = load_owned_object(manifest_path, artifact="task manifest")
    except ValueError as exc:
        _emit(
            {
                "ok": False,
                "stage": "manifest-invalid",
                "taskManifestPath": str(manifest_path),
                "reason": str(exc),
            },
            text=args.text,
        )
        return 1

    _emit(_updated_payload(manifest_path, manifest, entry, args), text=args.text)
    return 0


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