"""코드 리뷰가 무엇을 읽고 결과 파일을 어디에 쓰는지 해소한다.

두 모드를 한 진입점에 모아 스킬이 base 커밋도, 리뷰 파일 경로도, 회차도 다시
유도하지 않게 한다. stage 모드는 read side 에 그대로 위임하고, branch 모드만
여기서 해소한다 — caller 가 base 를 지정하지 않으면 기본 브랜치와의
merge-base 를 쓴다. 이 모듈은 인자 검증과 JSON 정형화만 하고, 경로·회차 조립은
code_review_paths 가, stage 규칙은 code_review_target_snapshot 이 소유한다.
"""
from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
from datetime import date
from pathlib import Path

from okstra_project import (
    ResolverError,
    StateError,
    resolve_project_root,
    resolve_task_identity,
)

from . import code_review_paths, stage_targets, worktree_registry
from .code_review_paths import branch_review_dir, next_branch_review
from .paths import RunRef
from .stage_map import StageMapError, load_latest_plan_stage_map
from .fixed_text import line

DEFAULT_BRANCH_CANDIDATES = ("main", "master")

_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def _git_out(repo: Path, *args: str) -> str:
    result = subprocess.run(
        ["git", "-C", str(repo), *args],
        capture_output=True,
        text=True,
        check=False,
    )
    return result.stdout.strip() if result.returncode == 0 else ""


def _default_branch_ref(repo: Path) -> str:
    """merge-base 를 잴 기준 ref — origin/HEAD 가 있으면 그것, 없으면 관례 이름."""
    remote_head = _git_out(repo, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
    if remote_head:
        return remote_head
    for candidate in DEFAULT_BRANCH_CANDIDATES:
        if _git_out(repo, "rev-parse", "--verify", "--quiet", candidate):
            return candidate
    return ""


def _merge_base(repo: Path, branch: str) -> str:
    default_ref = _default_branch_ref(repo)
    merge_base = _git_out(repo, "merge-base", default_ref, branch) if default_ref else ""
    if not merge_base:
        raise StateError(
            f"no merge-base for {branch!r} against the default branch — "
            "pass --base explicitly",
            stage="base_unresolved",
        )
    return merge_base


def _review_date(raw: str) -> str:
    """branch 결과 파일명에 쓸 날짜.

    next_branch_review 는 넘겨받은 문자열을 그대로 파일명에 쓰지만 되읽을 때는
    `YYYY-MM-DD` 만 인식한다. 어긋난 날짜를 통과시키면 회차가 늘 1 로 돌아가
    직전 리뷰 파일을 덮어쓴다.
    """
    if not raw:
        return date.today().isoformat()
    if not _DATE_RE.match(raw):
        raise StateError(f"--date must be YYYY-MM-DD, got {raw!r}", stage="args")
    return raw


def _branch_snapshot(repo: Path, branch: str, base: str, review_date: str) -> dict:
    head = _git_out(repo, "rev-parse", branch)
    if not head:
        raise StateError(f"branch {branch!r} not found", stage="branch_missing")
    base_commit = base or _merge_base(repo, branch)
    review_path, seq = next_branch_review(branch_review_dir(repo, branch), review_date)
    return {
        "mode": "branch",
        "worktreePath": str(repo),
        "branch": branch,
        "baseCommit": base_commit,
        "headCommit": head,
        "reviewPath": str(review_path),
        "round": seq,
    }


_CLI_EPILOG = r"""Usage:
  okstra code-review target --task-key <k> --stage <N> [--project-root <dir>] [--cwd <dir>] --json
  okstra code-review target --branch <name> [--base <ref>] [--date <YYYY-MM-DD>] [--project-root <dir>] --json

Output: JSON { ok, mode, worktreePath, branch, baseCommit, headCommit,
reviewPath, round }. Stage mode reads the base commit from the stage
registry row; branch mode falls back to the merge-base with the default
branch. An empty worktreePath means the review reads the branch ref
instead of a checked-out tree. This is read-only — it never creates
directories or files.
"""


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        epilog=_CLI_EPILOG,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        prog="okstra code-review",
        description="Resolve a code review's diff range and result file path."
    )
    # 서브커맨드는 하나뿐이지만 공개 명령이 `okstra code-review target` 이므로
    # 그 토큰을 파서가 직접 받는다.
    parser.add_argument("command", choices=("target",))
    parser.add_argument("--task-key", default="", help="project-id:task-group:task-id")
    parser.add_argument("--stage", type=int, default=0, help="stage number to review")
    parser.add_argument("--branch", default="", help="branch to review")
    parser.add_argument("--base", default="", help="explicit base ref for branch mode")
    parser.add_argument("--date", default="", help="YYYY-MM-DD for branch result naming")
    parser.add_argument("--project-root", default="", help="explicit project root")
    parser.add_argument("--cwd", default=".", help="cwd for project root resolution")
    output = parser.add_mutually_exclusive_group()
    output.add_argument("--json", action="store_true", help="emit JSON (default)")
    output.add_argument("--text", action="store_true", help="emit fixed text fields")
    return parser


def _resolve(args: argparse.Namespace, project_root: Path) -> dict:
    if args.task_key:
        if args.stage < 1:
            raise StateError("--task-key needs --stage <N>, N >= 1", stage="args")
        return code_review_target_snapshot(project_root, args.task_key, args.stage)
    if args.branch:
        return _branch_snapshot(
            project_root, args.branch, args.base, _review_date(args.date)
        )
    raise StateError("pass either --task-key --stage or --branch", stage="args")


def render_code_review_target_text(payload: dict) -> str:
    """모델이 소비할 리뷰 범위와 결과 경로만 고정 줄로 투영한다."""
    rows = ["Okstra code review target\n"]
    rows.append(line("Status", "ready" if payload.get("ok") else "error"))
    fields = (
        ("Project root", "projectRoot"), ("Mode", "mode"),
        ("Worktree path", "worktreePath"), ("Branch", "branch"),
        ("Base commit", "baseCommit"), ("Head commit", "headCommit"),
        ("Review path", "reviewPath"), ("Round", "round"),
        ("Task key", "taskKey"), ("Task root", "taskRoot"), ("Stage", "stage"),
        ("Failure stage", "stage"), ("Failure reason", "reason"),
    )
    for label, key in fields:
        failure_field = label.startswith("Failure")
        if key in payload and failure_field != bool(payload.get("ok")):
            rows.append(line(label, payload.get(key)))
    return "".join(rows)


def main(argv: list[str] | None = None) -> int:
    args = _build_parser().parse_args(argv)
    try:
        project_root = resolve_project_root(
            explicit_root=args.project_root, cwd=args.cwd
        )
    except ResolverError as exc:
        payload = {"ok": False, "stage": "resolve", "reason": str(exc)}
        print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
        return 2

    try:
        snapshot = _resolve(args, Path(project_root))
    except StateError as exc:
        payload = {
            "ok": False,
            "stage": exc.stage or "target",
            "reason": str(exc),
        }
        print(render_code_review_target_text(payload) if args.text else json.dumps(payload))
        return 1

    payload = {"ok": True, "projectRoot": str(project_root), **snapshot}
    print(
        render_code_review_target_text(payload)
        if args.text
        else json.dumps(payload, ensure_ascii=False, indent=2)
    )
    return 0


# ---- stage 모드 해소 -------------------------------------------------------
# 모듈 도크스트링이 "stage 규칙은 code_review_target_snapshot 이 소유한다" 고
# 적어 두었지만 실제 함수는 okstra_project.state 에 있었고, 거기서 okstra_ctl 을
# 함수 본문에서 지연 import 했다. 선언대로 소유자를 여기로 옮긴다.
# 옮기면서 state.py 가 따로 갖고 있던 _git_out 사본(본문 동일)은 이 파일의
# 것으로 합쳐진다.

def code_review_target_snapshot(
    project_root: Path, task_key: str, stage: int
) -> dict:
    """stage 코드 리뷰가 무엇을 읽고 결과를 어디에 쓰는지 해소한다.

    stage_map_read_side_snapshot 과 같은 결: caller 는 registry 행도, run-root
    레이아웃도, stage base 규칙도 보지 않는다. stage_targets 의
    StageTargetError 도 StateError 로 바꿔 던진다 — caller 가 그 예외를 잡으려고
    okstra_ctl 을 import 하지 않도록.

    base 는 stage worktree 를 만들 때 registry 행에 적힌 `base_ref` 다 — 그
    stage 가 실제로 갈라져 나온 커밋. 리뷰 시점에 규칙으로 다시 계산하면
    표류한다: 다중의존 stage 는 오늘의 task-key worktree HEAD 를 받게 되어
    whole-task final-verification 이 stage 들을 병합한 뒤에는 base..head 구간이
    비거나 뒤집히고, 단일의존 stage 는 선행 stage 가 재실행되면 어긋난다.
    """
    identity = resolve_task_identity(project_root, task_key)
    task_root = Path(identity["taskRoot"])
    try:
        stage_snapshot = load_latest_plan_stage_map(task_root)
    except StageMapError as exc:
        raise StateError(str(exc), stage=exc.code) from exc
    if stage_snapshot.state != "ready":
        raise StateError(
            "implementation-planning Stage Map is missing",
            stage="missing",
        )
    stages = stage_snapshot.stages
    selected = next((s for s in stages if s["stage_number"] == stage), None)
    if selected is None:
        raise StateError(
            f"stage {stage} is not in the Stage Map for {task_key}",
            stage="stage_missing",
        )

    coords = (identity["projectId"], identity["taskGroup"], identity["taskId"])
    stage_row = worktree_registry.get_stage_row(*coords, stage) or {}
    worktree_view = _stage_worktree_view(project_root, stage_row, stage)
    base_commit = stage_row.get("base_ref") or _legacy_stage_base_commit(
        identity, stages, selected
    )
    review_path, round_no = code_review_paths.next_stage_review(
        code_review_paths.stage_review_dir(
            project_root, identity["taskGroup"], identity["taskId"]
        ),
        stage,
    )
    return {
        "mode": "stage",
        "taskKey": identity["taskKey"],
        "taskRoot": identity["taskRoot"],
        "stage": stage,
        **worktree_view,
        "baseCommit": base_commit,
        "reviewPath": str(review_path),
        "round": round_no,
    }


def _legacy_stage_base_commit(
    identity: dict, stages: list[dict], selected: dict
) -> str:
    """`base_ref` 를 남기지 않은 옛 registry 행의 base 를 규칙에서 되살린다.

    provision 이 base_ref 를 기록하기 전에 만들어진 stage 행에만 쓰인다. 규칙
    자체는 stage_targets 가 소유하며 여기서 다시 유도하지 않는다.
    """
    coords = (identity["projectId"], identity["taskGroup"], identity["taskId"])
    plan_run_root = RunRef.from_task_root(
        Path(identity["taskRoot"]), "implementation-planning"
    ).run_dir
    lifecycle = stage_targets.read_stage_lifecycle_snapshot(
        stages, plan_run_root, recover_from_carry=True
    )
    # The anchor and the multi-dep candidate base must come from the task-key
    # worktree HEAD, where stage work accumulates — not from the invocation
    # cwd, which may sit on an older or unrelated commit.
    task_entry = worktree_registry.lookup(*coords)
    task_worktree = Path(
        (task_entry.worktree_path if task_entry else "") or identity["projectRoot"]
    )
    anchor = worktree_registry.get_implementation_base(*coords) or ""
    try:
        return stage_targets.resolve_stage_base_commit(
            selected,
            lifecycle.done_rows,
            anchor_base_commit=anchor,
            candidate_base=_git_out(task_worktree, "rev-parse", "HEAD"),
            project_root=task_worktree,
            plan_run_root=plan_run_root,
        )
    except stage_targets.StageTargetError as exc:
        raise StateError(
            f"stage {selected['stage_number']} has no recorded base_ref and its "
            f"base could not be derived: {exc}",
            stage="stage_base_unresolved",
        ) from exc


def _stage_worktree_view(project_root: Path, stage_row: dict, stage: int) -> dict:
    """한 stage 의 worktree 경로 / 브랜치 / head 커밋.

    철거된 stage worktree 는 오류가 아니다: whole-task final-verification 이
    디렉터리를 지워도 브랜치는 남고, 리뷰가 필요한 커밋은 그 브랜치에 그대로
    있다.
    """
    branch = stage_row.get("branch") or ""
    if not branch:
        raise StateError(
            f"stage {stage} has no registry branch — review it in branch mode",
            stage="stage_branch_missing",
        )
    worktree_path = ""
    if stage_row.get("status") == "active" and stage_row.get("worktree_path"):
        candidate = Path(stage_row["worktree_path"])
        if candidate.is_dir():
            worktree_path = str(candidate)
    head_source = Path(worktree_path) if worktree_path else Path(project_root)
    head_ref = "HEAD" if worktree_path else branch
    head_commit = _git_out(head_source, "rev-parse", head_ref)
    if not head_commit:
        raise StateError(
            f"stage {stage} head could not be resolved: branch {branch!r} is "
            f"not readable from {head_source}",
            stage="stage_head_unresolved",
        )
    return {
        "worktreePath": worktree_path,
        "branch": branch,
        "headCommit": head_commit,
    }


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