"""worktree 를 실제로 만든다 — task 하나에 하나, stage 하나에 하나.

이 파일만 부수 효과를 낸다: 레지스트리 예약, worktree 추가, sync 링크 설치,
실패 시 되감기. 나머지 층은 전부 이 두 함수가 쓰려고 나뉘어 있다.
`provision_task_worktree` 는 같은 task-key 로 다시 들어오면 기존 worktree 를
그대로 돌려주고, `provision_stage_worktree` 는 base_commit 을 반드시 요구한다.
"""
from __future__ import annotations

import sys
from dataclasses import dataclass
from pathlib import Path

from .. import worktree_registry
from .decisions import preview_worktree_decision, resolve_stage_worktree_decision
from .git_ops import (
    _branch_exists,
    _branch_exists_message,
    _git,
    _head_sha,
    _resolve_commit_sha,
    main_worktree_path,
    remove_worktree_force,
)
from .linking import (
    _copy_snapshot_files,
    _link_sync_dirs,
    _link_sync_files,
    describe_sync_key_drift,
    sync_file_key_names,
    _seed_worktree_settings_symlink,
)
from .naming import _safe_segment


@dataclass
class WorktreeProvision:
    """Result of `provision_task_worktree`.

    status:
      - "created": fresh worktree at `path` on `branch`
      - "reused": registry already had this task-key; same path/branch
        returned and no new `git worktree add` was executed
      - "skipped-in-worktree": project_root is itself a non-main
        worktree; the run reuses `project_root` and no new worktree is
        materialised (registry NOT updated — that caller is already
        isolated by virtue of its own worktree)
      - "skipped-not-git": project_root has no `.git` (worktree path
        cannot be provisioned; degrade gracefully)
    """
    status: str
    path: str = ""          # absolute path of the task worktree (or project_root when reused)
    branch: str = ""        # branch checked out in the worktree (empty when reused / not-git)
    base_ref: str = ""      # commit SHA the worktree was branched from (empty when not created)
    note: str = ""          # human-readable explanation, surfaced in team-state / manifests


def _sync_key_drift_note(main_root: Path, entry) -> str:
    """재사용되는 worktree 에 붙일 공유 sync 파일 키 드리프트 한 줄.

    앞에 `; ` 를 붙인 형태로 돌려주므로 note 문자열에 그대로 이어 붙일 수 있고,
    드리프트가 없으면 빈 문자열이라 종전 note 와 바이트가 같다.
    """
    if entry is None:
        return ""
    message = describe_sync_key_drift(
        entry.sync_file_keys, sync_file_key_names(main_root)
    )
    if not message:
        return ""
    print(f"okstra-worktree: {message}", file=sys.stderr)
    return f"; {message}"


def provision_task_worktree(
    *,
    task_type: str,
    project_root: Path,
    project_id: str,
    task_group_segment: str,
    task_id_segment: str,
    work_category: str,
    base_ref: str = "",
    require_base_ref: bool = False,
) -> WorktreeProvision:
    """Materialise (or reuse) the task worktree for this run.

    First phase of a task-key creates the worktree on a new branch.
    Subsequent phases of the same task-key look up the registry and
    return the existing path + branch unchanged.

    ``base_ref`` is the ref (branch name, tag, or commit SHA) to branch
    the new worktree from on first phase. When empty, the main worktree's
    current ``HEAD`` is used (legacy default; the CLI enforces a
    non-empty value on first phase so callers go through the
    AskUserQuestion menu in the okstra-run skill). Subsequent phases
    ignore ``base_ref`` — the registered entry's base is reused.

    Concurrency: callers must hold ``locks.worktree_provision_mutex`` for
    this task-key (run.py's prepare flow does). The exists/branch pre-checks
    and ``git worktree add`` here are not internally locked — only the
    registry reserve row is — so unlocked concurrent calls race (TOCTOU).
    flock is non-reentrant, hence the lock lives at the caller.

    Raises:
        RuntimeError when worktree creation fails (path clash on disk
        that the registry does not know about, branch clash with a
        different task-key, `git worktree add` non-zero). The caller
        (`run.py`) catches and re-raises as PrepareError to keep a
        single error surface.
    """
    decision = preview_worktree_decision(
        project_root=project_root, project_id=project_id,
        task_group_segment=task_group_segment, task_id_segment=task_id_segment,
        work_category=work_category, base_ref=base_ref,
    )

    if decision.status == "skipped-not-git":
        return WorktreeProvision(
            status="skipped-not-git",
            path=decision.path,
            note=(
                "worktree provisioning skipped: project_root is not inside a git "
                "repository; task will operate directly on project_root"
            ),
        )

    if decision.status == "skipped-in-worktree":
        return WorktreeProvision(
            status="skipped-in-worktree",
            path=decision.path,
            note=(
                "worktree provisioning skipped: project_root is already inside a "
                "non-main git worktree; task reuses the caller's worktree"
            ),
        )

    safe_project = _safe_segment(project_id)
    safe_group = _safe_segment(task_group_segment)
    safe_task = _safe_segment(task_id_segment)

    if decision.status == "reused":
        worktree_registry.touch_phase(safe_project, safe_group, safe_task, task_type)
        _seed_worktree_settings_symlink(Path(decision.path))
        drift = _sync_key_drift_note(
            main_worktree_path(project_root),
            worktree_registry.lookup(safe_project, safe_group, safe_task),
        )
        return WorktreeProvision(
            status="reused",
            path=decision.path,
            branch=decision.branch,
            base_ref=decision.base_ref,
            note=(
                f"task worktree reused at {decision.path} on branch "
                f"{decision.branch} (base {decision.base_ref[:12]}); phase {task_type}"
                f"{drift}"
            ),
        )

    # decision.status == "new" — proceed with creation
    worktree_path = Path(decision.path)
    branch = decision.branch

    if worktree_path.exists():
        raise RuntimeError(
            f"task worktree path already exists but is not in the registry: "
            f"{worktree_path}. Remove it with `git worktree remove <path>` "
            "(or `rm -rf` if it is not a registered worktree) before retrying."
        )
    if _branch_exists(project_root, branch):
        raise RuntimeError(_branch_exists_message(project_root, branch, "task"))

    main_root = main_worktree_path(project_root)
    requested_base = (base_ref or "").strip()
    if not requested_base and require_base_ref:
        raise RuntimeError(
            "first-phase task worktree requires an explicit base ref; "
            "pass `--base-ref <branch|tag|sha>` (or invoke through the "
            "okstra-run skill which collects this interactively)"
        )
    if requested_base:
        resolved_sha = _resolve_commit_sha(main_root, requested_base)
        if not resolved_sha:
            raise RuntimeError(
                f"could not resolve base ref `{requested_base}` in main worktree "
                f"({main_root}); ensure the branch/tag/SHA exists locally"
            )
        resolved_base_ref = resolved_sha
        base_origin = requested_base
    else:
        resolved_base_ref = _head_sha(main_root)
        if not resolved_base_ref:
            raise RuntimeError(
                "could not resolve HEAD sha in main worktree; cannot create task worktree"
            )
        base_origin = "HEAD"

    worktree_path.parent.mkdir(parents=True, exist_ok=True)
    res = _git(
        main_root,
        "worktree", "add", "-b", branch, str(worktree_path), resolved_base_ref,
    )
    if res.returncode != 0:
        raise RuntimeError(
            f"`git worktree add` failed (exit={res.returncode}): "
            f"{(res.stderr or res.stdout).strip()}"
        )

    # Sync dirs sourced from the MAIN worktree so every task sees the
    # same shared state regardless of which checkout invoked okstra.
    linked = _link_sync_dirs(main_root, worktree_path)
    linked_files = _link_sync_files(main_root, worktree_path)
    sync_file_keys = sync_file_key_names(main_root)
    snapshot_files = _copy_snapshot_files(main_root, worktree_path)
    linked_parts: list[str] = []
    if linked:
        linked_parts.append(f"linked {', '.join(linked)}")
    if linked_files:
        linked_parts.append(f"linked-files {', '.join(linked_files)}")
    if snapshot_files:
        linked_parts.append(f"snapshot {', '.join(snapshot_files)}")
    linked_suffix = ("; " + "; ".join(linked_parts)) if linked_parts else ""

    try:
        worktree_registry.reserve(
            project_id=safe_project,
            task_group=safe_group,
            task_id=safe_task,
            worktree_path=str(worktree_path),
            branch=branch,
            base_ref=resolved_base_ref,
            phase=task_type,
            sync_file_keys=sync_file_keys,
        )
    except RuntimeError:
        # Roll back the on-disk worktree so the next attempt is not
        # blocked by the lingering directory / branch.
        remove_worktree_force(main_root, worktree_path)
        _git(main_root, "branch", "-D", branch)
        raise

    _seed_worktree_settings_symlink(worktree_path)

    base_label = (
        f"{base_origin} @ {resolved_base_ref[:12]}"
        if base_origin != "HEAD"
        else f"HEAD @ {resolved_base_ref[:12]}"
    )
    return WorktreeProvision(
        status="created",
        path=str(worktree_path),
        branch=branch,
        base_ref=resolved_base_ref,
        note=(
            f"task worktree created at {worktree_path} on branch {branch} "
            f"(base {base_label}; phase {task_type}){linked_suffix}"
        ),
    )


def provision_stage_worktree(
    *,
    project_root: Path,
    project_id: str,
    task_group_segment: str,
    task_id_segment: str,
    work_category: str,
    stage_number: int,
    base_commit: str,
) -> WorktreeProvision:
    """Materialise an isolated worktree for one implementation stage.

    Unlike `provision_task_worktree` (one worktree per task-key shared
    across phases), this provisions a per-stage worktree branched from
    `base_commit` at `<task-key>--stage-<N>` (a sibling of the task worktree) on
    branch `<prefix>-<task>-s<N>`.
    The stage-key (`<task-key>#stage-<N>`) is reserved atomically through
    `worktree_registry`; re-entry of the same stage-key returns the
    existing entry. Branch / on-disk conflicts roll back the worktree
    before re-raising so a retry is not blocked.

    Concurrency: callers must hold ``locks.worktree_provision_mutex`` for
    the task-key, acquired BEFORE the Stage Run Claim reads the registry
    (run.py does) — otherwise two `--stage auto` runs can select the same
    stage and the loser silently enters the winner's worktree via the
    "reused" path. flock is non-reentrant, hence the lock lives at the
    caller.
    """
    if not base_commit:
        raise RuntimeError("provision_stage_worktree requires a base_commit")

    decision = resolve_stage_worktree_decision(
        project_id=project_id,
        task_group_segment=task_group_segment,
        task_id_segment=task_id_segment,
        work_category=work_category,
        stage_number=stage_number,
    )
    if decision.status == "reused":
        drift = _sync_key_drift_note(
            main_worktree_path(project_root),
            worktree_registry.lookup(
                _safe_segment(project_id),
                _safe_segment(task_group_segment),
                _safe_segment(task_id_segment),
                stage_number=stage_number,
            ),
        )
        return WorktreeProvision(
            status="reused",
            path=decision.path,
            branch=decision.branch,
            base_ref=decision.base_ref,
            note=(
                f"stage {stage_number} worktree reused at "
                f"{decision.path} on branch {decision.branch} "
                f"(base {decision.base_ref[:12]}){drift}"
            ),
        )

    safe_project = _safe_segment(project_id)
    safe_group = _safe_segment(task_group_segment)
    safe_task = _safe_segment(task_id_segment)
    worktree_path = Path(decision.path)
    branch = decision.branch

    if worktree_path.exists():
        raise RuntimeError(
            f"stage worktree path already exists but is not in the registry: "
            f"{worktree_path}. Remove it before retrying."
        )
    if _branch_exists(project_root, branch):
        raise RuntimeError(_branch_exists_message(project_root, branch, "stage"))

    main_root = main_worktree_path(project_root)
    resolved_sha = _resolve_commit_sha(main_root, base_commit)
    if not resolved_sha:
        raise RuntimeError(
            f"could not resolve base_commit `{base_commit}` in main worktree "
            f"({main_root}); ensure the commit exists locally"
        )

    worktree_path.parent.mkdir(parents=True, exist_ok=True)
    res = _git(
        main_root,
        "worktree", "add", "-b", branch, str(worktree_path), resolved_sha,
    )
    if res.returncode != 0:
        raise RuntimeError(
            f"`git worktree add` failed (exit={res.returncode}): "
            f"{(res.stderr or res.stdout).strip()}"
        )

    _link_sync_dirs(main_root, worktree_path)
    _link_sync_files(main_root, worktree_path)
    _copy_snapshot_files(main_root, worktree_path)
    sync_file_keys = sync_file_key_names(main_root)

    try:
        worktree_registry.reserve(
            project_id=safe_project,
            task_group=safe_group,
            task_id=safe_task,
            worktree_path=str(worktree_path),
            branch=branch,
            base_ref=resolved_sha,
            phase="implementation",
            stage_number=stage_number,
            sync_file_keys=sync_file_keys,
        )
    except RuntimeError:
        remove_worktree_force(main_root, worktree_path)
        _git(main_root, "branch", "-D", branch)
        raise

    _seed_worktree_settings_symlink(worktree_path)

    return WorktreeProvision(
        status="created", path=str(worktree_path),
        branch=branch, base_ref=resolved_sha,
        note=(
            f"stage {stage_number} worktree created at {worktree_path} "
            f"on branch {branch} (base {resolved_sha[:12]})"
        ),
    )
