# -*- coding: utf-8 -*-
"""dispatch.anu_finish_preflight — task-2731 finish/closeout preflight (dev1/dev2 마찰 분리).

dev1/dev2 closeout 미완 실증 결함 중 **finish-task 끝단 마찰 3종**을 기존 guard
직접 수정 없이 별개 모듈로 분리 판정한다(expected_files 내부 유지 · canonical 0 touch):

  1. **branch regex `-rN`/`+N` 안전 추출**(dev2): `task-2730-r2` → base `task-2730`
     로만 추출하면 dev1 의 `task-2730` state/lock 과 충돌. revision suffix 를
     인식하여 base + revision + full ref 를 분리, 정체성 충돌 0.
  2. **worktree path mismatch 탐지**(dev2): `.worktrees/<task>` 기대 vs CODE_ROOT
     직접 체크아웃 불일치를 preflight 에서 탐지(경고/차단 판정).
  3. **task-lock 마찰 우회 판정**(dev2): OS runner 의 result.json 수거는 task-lock /
     worktree / tracked 여부와 **독립**(lock-free)임을 명문화 — lock 마찰로 callback
     누락 0.

순수 함수(git/subprocess 0 · 파일시스템 mutation 0 · 입력만으로 판정) → isolated
unit test 가능. 기존 guard(scripts/start_task_guard.py 등) **직접 수정 0**.

Layer A / NO-CRON: cron 0, subprocess 0, merge 0.
"""
from __future__ import annotations

import os
import re
from dataclasses import dataclass, field
from typing import List, Optional

# ── 상수 ─────────────────────────────────────────────────────────────────────
PREFLIGHT_SCHEMA = "dispatch.anu_finish_preflight.v1"

# canonical worktree 디렉토리 prefix (CODE_ROOT 직접 체크아웃 금지 — dev2 마찰).
WORKTREES_DIR = ".worktrees"

# branch task-id 추출 regex.
#   - base task id: task-<digits>  (예: task-2731)
#   - revision suffix: -r<digits> 또는 +<digits>  (예: -r2 / +1) → replacement 식별
#   - executor suffix: -dev<digits> 또는 -<word>  (예: -dev6) → executor 식별(정체성 무관)
_TASK_BASE_RE = re.compile(r"task-(\d+)")
_REVISION_RE = re.compile(r"^(?:-r(\d+)|\+(\d+))")
_EXECUTOR_RE = re.compile(r"^-(dev\d+|[a-z][a-z0-9]*)$")


# ── BranchParse ───────────────────────────────────────────────────────────────
@dataclass
class BranchParse:
    """branch/ref 에서 안전 추출한 task 정체성.

    base_task_id  : revision/executor suffix 제거한 base (예: task-2731)
    revision      : replacement revision 라벨 (예: r2 / +1) — 없으면 ""
    executor      : executor 라벨 (예: dev6) — 없으면 ""
    full_task_ref : base + revision (lock/state 정체성 — 충돌 회피용. 예: task-2730-r2)
    is_revision   : revision suffix 존재 여부 (True → base 단독 추출 시 dev1 충돌 위험)
    """

    base_task_id: str = ""
    revision: str = ""
    executor: str = ""
    full_task_ref: str = ""
    is_revision: bool = False
    reasons: List[str] = field(default_factory=list)

    @property
    def ok(self) -> bool:
        return bool(self.base_task_id)


def parse_branch(branch: str) -> BranchParse:
    """branch/ref 문자열에서 task 정체성을 **안전 추출**한다(`-rN`/`+N` 인식).

    예시:
      - ``task/task-2731-dev6``  → base=task-2731, executor=dev6, full=task-2731
      - ``task-2731-r2``         → base=task-2731, revision=r2, full=task-2731-r2 (is_revision)
      - ``task-2730-r2``         → base=task-2730, revision=r2, full=task-2730-r2
                                    (★ base 단독이면 dev1 task-2730 충돌 → full_task_ref 사용)
      - ``task/task-2730+1``     → base=task-2730, revision=+1, full=task-2730+1
      - ``main`` / ""            → ok=False (task ref 아님)

    lock/state 정체성은 **full_task_ref** 를 사용해야 revision 이 base 와 충돌하지
    않는다(dev2 → dev1 state 충돌 방지)."""
    s = str(branch or "").strip()
    if not s:
        return BranchParse(reasons=["빈 branch — task ref 아님."])
    # ``task/`` prefix 제거 후 마지막 segment 가 task ref.
    seg = s.rsplit("/", 1)[-1]
    m = _TASK_BASE_RE.search(seg)
    if not m:
        return BranchParse(reasons=[f"task-<N> 패턴 부재: {seg!r} — task ref 아님."])
    base = f"task-{m.group(1)}"
    rest = seg[m.end():]  # base 뒤 잔여(suffix 후보)

    revision = ""
    executor = ""
    reasons: List[str] = []

    # revision suffix(-rN / +N) 우선 인식.
    rmatch = _REVISION_RE.match(rest)
    if rmatch:
        rev_num = rmatch.group(1) or rmatch.group(2)
        # -rN → "rN", +N → "+N" 로 라벨링(원형 보존 — full_task_ref 가 정확히 복원되게).
        revision = f"r{rev_num}" if rest.startswith("-r") else f"+{rev_num}"
        rest = rest[rmatch.end():]
        reasons.append(
            f"revision suffix 인식: {revision} — base 단독({base})은 원본 task state/"
            f"lock 과 충돌 가능 → full_task_ref 사용 권장."
        )

    # executor suffix(-dev6 등) 인식.
    if rest:
        ematch = _EXECUTOR_RE.match(rest)
        if ematch:
            executor = ematch.group(1)
        else:
            reasons.append(f"잔여 suffix 미인식(무시): {rest!r}")

    # full_task_ref 복원: base + revision(원형).
    if revision.startswith("r"):
        full = f"{base}-{revision}"
    elif revision.startswith("+"):
        full = f"{base}{revision}"
    else:
        full = base

    return BranchParse(
        base_task_id=base,
        revision=revision,
        executor=executor,
        full_task_ref=full,
        is_revision=bool(revision),
        reasons=reasons,
    )


# ── WorktreeCheck ─────────────────────────────────────────────────────────────
@dataclass
class WorktreeCheck:
    """worktree path mismatch 판정 결과."""

    mismatch: bool
    expected_subpath: str
    actual_path: str
    reasons: List[str] = field(default_factory=list)

    @property
    def ok(self) -> bool:
        return not self.mismatch


def expected_worktree_subpath(task_ref: str, executor: str = "") -> str:
    """기대 worktree 상대경로: ``.worktrees/<task_ref>[-<executor>]``.
    예: task_ref=task-2731, executor=dev6 → ``.worktrees/task-2731-dev6``."""
    name = task_ref if not executor else f"{task_ref}-{executor}"
    return os.path.join(WORKTREES_DIR, name)


def detect_worktree_mismatch(
    actual_path: str,
    *,
    task_ref: str,
    executor: str = "",
    code_root: str = "",
) -> WorktreeCheck:
    """실 작업 경로가 기대 worktree(`.worktrees/<task_ref>[-executor]`)인지 판정.

    dev2 마찰: executor 가 CODE_ROOT 에 직접 체크아웃(`.worktrees/...` 미사용) →
    start_task_guard 기대 경로 불일치로 finish 미완. 본 함수는 그 mismatch 를
    preflight 에서 탐지한다(경고/차단 판정). 파일시스템 mutation 0."""
    expected = expected_worktree_subpath(task_ref, executor)
    actual = os.path.normpath(str(actual_path or ""))
    reasons: List[str] = []

    # 기대 worktree segment 가 실제 경로에 포함되어야 한다.
    expected_name = os.path.basename(expected)
    in_worktrees = (os.sep + WORKTREES_DIR + os.sep) in (actual + os.sep)
    name_match = expected_name and (
        actual.endswith(os.sep + expected_name) or os.path.basename(actual) == expected_name
    )

    mismatch = not (in_worktrees and name_match)
    if mismatch:
        if not in_worktrees:
            reasons.append(
                f"worktree mismatch: 실 경로가 {WORKTREES_DIR}/ 하위가 아님 "
                f"(CODE_ROOT 직접 체크아웃 의심) — 기대={expected}, 실제={actual}."
            )
        elif not name_match:
            reasons.append(
                f"worktree mismatch: worktree 이름 불일치 — 기대 segment={expected_name}, "
                f"실제={os.path.basename(actual)}."
            )
        if code_root and actual == os.path.normpath(code_root):
            reasons.append(
                "★ 실 경로 == CODE_ROOT — dev2 와 동일 마찰(직접 체크아웃). "
                f"{expected} 형태 worktree 분기 필요."
            )
    else:
        reasons.append(f"worktree OK: {actual} 이(가) 기대 worktree({expected}) 와 일치.")

    return WorktreeCheck(
        mismatch=mismatch,
        expected_subpath=expected,
        actual_path=actual,
        reasons=reasons,
    )


# ── TaskLockBypass ────────────────────────────────────────────────────────────
@dataclass
class TaskLockBypass:
    """task-lock 마찰 우회 판정 결과(result.json 수거 lock 독립)."""

    bypass_ok: bool
    lock_contended: bool
    result_json_present: bool
    reasons: List[str] = field(default_factory=list)

    @property
    def ok(self) -> bool:
        return self.bypass_ok


def assess_task_lock_bypass(
    *,
    result_json_present: bool,
    lock_contended: bool = False,
    lock_path: str = "",
) -> TaskLockBypass:
    """OS runner 의 result.json 수거가 task-lock 마찰과 **독립**임을 판정한다.

    dev2 마찰: task-lock 생성/요구가 worktree 경로·tracked 여부에 묶여 finish 끝단이
    막힘 → callback 미발사. 본 모듈은 result.json 수거 경로가 lock 과 **무관**(lock-free)
    임을 명문화한다:

      - result.json 존재 → lock 마찰 여부와 무관하게 수거 가능(bypass_ok=True).
      - lock 이 contended 여도 result.json 수거/closeout/callback launch 는 진행
        (OS runner 는 lock 을 잡지 않는다 — ANCHOR-1).
      - result.json 부재 → 수거 대상 없음(bypass_ok=False, 단 lock 과 무관한 사유).

    파일시스템 mutation 0 · lock acquire 0 (판정만)."""
    reasons: List[str] = []
    if not result_json_present:
        reasons.append(
            "result.json 부재 — 수거 대상 없음(lock 과 무관). executor 미작성 또는 경로 오류."
        )
        return TaskLockBypass(
            bypass_ok=False,
            lock_contended=lock_contended,
            result_json_present=False,
            reasons=reasons,
        )
    if lock_contended:
        reasons.append(
            f"task-lock 마찰 감지(lock={lock_path or 'unknown'}) — 그러나 OS runner 의 "
            "result.json 수거는 lock-free(lock 독립). 수거·closeout·callback launch 진행."
        )
    else:
        reasons.append(
            "task-lock 마찰 없음 — result.json 수거 진행(lock 독립 경로)."
        )
    return TaskLockBypass(
        bypass_ok=True,
        lock_contended=lock_contended,
        result_json_present=True,
        reasons=reasons,
    )


# ── 통합 preflight ────────────────────────────────────────────────────────────
@dataclass
class FinishPreflight:
    """finish/closeout preflight 통합 결과(판정만 — mutation 0)."""

    schema: str
    branch: BranchParse
    worktree: Optional[WorktreeCheck]
    lock_bypass: Optional[TaskLockBypass]
    reasons: List[str] = field(default_factory=list)

    @property
    def collectible(self) -> bool:
        """OS runner 가 result.json 을 수거 가능한가.
        worktree mismatch / branch revision / lock 마찰이 있어도 result.json 존재 시
        수거 가능(ANCHOR-1) — lock_bypass.bypass_ok 가 단일 게이트."""
        return bool(self.lock_bypass and self.lock_bypass.bypass_ok)


def preflight(
    *,
    branch: str,
    actual_path: str = "",
    executor: str = "",
    code_root: str = "",
    result_json_present: bool = False,
    lock_contended: bool = False,
    lock_path: str = "",
) -> FinishPreflight:
    """branch 안전 추출 + worktree mismatch + task-lock 우회 통합 판정.

    ★ 핵심 불변: worktree mismatch/branch revision/lock 마찰은 **수거를 막지 않는다**.
    이들은 경고(reasons)로 surface 하되, result.json 이 존재하면 OS runner 가 수거하여
    callback launch 를 강제한다(finish-task 끝단 마찰과 callback 발사의 분리 — ANCHOR-1)."""
    bp = parse_branch(branch)
    exec_label = executor or bp.executor
    task_ref = bp.full_task_ref or bp.base_task_id

    wt: Optional[WorktreeCheck] = None
    if actual_path and task_ref:
        wt = detect_worktree_mismatch(
            actual_path, task_ref=task_ref, executor=exec_label, code_root=code_root
        )

    lb = assess_task_lock_bypass(
        result_json_present=result_json_present,
        lock_contended=lock_contended,
        lock_path=lock_path,
    )

    reasons: List[str] = []
    if bp.is_revision:
        reasons.append(
            f"branch revision({bp.revision}) — lock/state 는 full_task_ref="
            f"{bp.full_task_ref} 사용(base {bp.base_task_id} 단독 시 충돌)."
        )
    if wt and wt.mismatch:
        reasons.append("worktree mismatch 경고 — 단 result.json 수거는 lock/worktree 독립(진행).")

    return FinishPreflight(
        schema=PREFLIGHT_SCHEMA,
        branch=bp,
        worktree=wt,
        lock_bypass=lb,
        reasons=reasons,
    )


__all__ = [
    "PREFLIGHT_SCHEMA",
    "WORKTREES_DIR",
    "BranchParse",
    "parse_branch",
    "WorktreeCheck",
    "expected_worktree_subpath",
    "detect_worktree_mismatch",
    "TaskLockBypass",
    "assess_task_lock_bypass",
    "FinishPreflight",
    "preflight",
]
