"""worktree 가 쓰는 git 명령 한 겹.

`_git` 은 `check=False` 라서 호출부가 returncode 를 보고 판단한다. 이 층은
okstra 의 규칙을 하나도 모른다 — 브랜치가 존재하는지, HEAD 가 무엇인지,
머지가 충돌했는지 같은 git 사실만 답한다. okstra 가 무엇을 무시하기로 했는지는
`cleanliness` 가, 무엇을 옮기기로 했는지는 `sync_config` 가 안다.
"""
from __future__ import annotations

import subprocess
from pathlib import Path


def _git(cwd: Path, *args: str) -> subprocess.CompletedProcess:
    return subprocess.run(
        ["git", "-C", str(cwd), *args],
        capture_output=True, text=True, check=False,
    )


def _is_inside_non_main_worktree(project_root: Path) -> bool:
    """True iff project_root is inside a git worktree that is NOT the
    repository's main checkout. Detection rule: `--git-dir` (per-worktree
    .git pointer) differs from `--git-common-dir` (shared object store).
    """
    common = _git(project_root, "rev-parse", "--git-common-dir")
    per_tree = _git(project_root, "rev-parse", "--git-dir")
    if common.returncode != 0 or per_tree.returncode != 0:
        return False
    common_abs = (project_root / common.stdout.strip()).resolve()
    per_tree_abs = (project_root / per_tree.stdout.strip()).resolve()
    return common_abs != per_tree_abs


def is_git_work_tree(project_root: Path) -> bool:
    """project_root 가 git work tree 내부인지 판정하는 public git-introspection seam.

    git 미설치(FileNotFoundError) 를 포함한 모든 실패는 False — 호출자(migrate
    등)가 non-git 레이아웃으로 안전하게 폴백할 수 있도록 한다. 과거 migrate.py
    가 같은 판정을 자체 구현(`--show-toplevel`)했는데 이 seam 으로 통합한다."""
    try:
        res = _git(project_root, "rev-parse", "--is-inside-work-tree")
    except (OSError, FileNotFoundError):
        return False
    return res.returncode == 0 and res.stdout.strip() == "true"


def _branch_exists(project_root: Path, branch: str) -> bool:
    res = _git(project_root, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}")
    return res.returncode == 0


def _branch_checkout_path(project_root: Path, branch: str) -> str:
    """The worktree that currently has *branch* checked out, or "" when none does.

    `git branch -D` refuses to delete a checked-out branch, so an existing-branch
    error that only recommends `-D` is unfollowable whenever the branch is the
    main checkout's HEAD. Callers use this to name the extra step instead.
    """
    res = _git(project_root, "worktree", "list", "--porcelain")
    if res.returncode != 0:
        return ""
    current = ""
    for line in res.stdout.splitlines():
        if line.startswith("worktree "):
            current = line[len("worktree "):].strip()
        elif line.strip() == f"branch refs/heads/{branch}":
            return current
    return ""


def _branch_exists_message(project_root: Path, branch: str, kind: str) -> str:
    """Existing-branch error text whose remedy is actually runnable."""
    checkout = _branch_checkout_path(project_root, branch)
    if checkout:
        return (
            f"{kind} worktree branch already exists: {branch}, and it is checked "
            f"out at {checkout}. `git branch -D {branch}` will refuse while it is "
            f"checked out — switch that checkout to another branch first "
            f"(`git -C {checkout} switch <other-branch>`), then delete it, or "
            "choose a different work-category before retrying."
        )
    return (
        f"{kind} worktree branch already exists: {branch}. "
        f"Delete it (`git branch -D {branch}`) or choose a different "
        "work-category before retrying."
    )


def _head_sha(cwd: Path) -> str:
    res = _git(cwd, "rev-parse", "HEAD")
    if res.returncode != 0:
        return ""
    return res.stdout.strip()


def _resolve_commit_sha(cwd: Path, ref: str) -> str:
    """Resolve a user-supplied ref (branch, tag, short/long SHA) to a full
    commit SHA in `cwd`. Returns empty string when the ref is not resolvable
    so the caller can raise a contextual error.
    """
    res = _git(cwd, "rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}")
    if res.returncode != 0:
        return ""
    return res.stdout.strip()


def main_worktree_path(project_root: Path) -> Path:
    """Locate the repository's MAIN worktree (the original checkout).

    `git worktree list --porcelain` lists worktrees in a stable order
    where the first `worktree <path>` block is the main checkout.
    Falls back to `project_root` if parsing fails — caller still gets
    a working path, sync-dir links just point at the caller's tree
    (the prior behaviour).
    """
    res = _git(project_root, "worktree", "list", "--porcelain")
    if res.returncode != 0:
        return project_root
    for line in res.stdout.splitlines():
        if line.startswith("worktree "):
            return Path(line[len("worktree "):].strip())
    return project_root


def is_ancestor(cwd, commit: str, head: str) -> bool:
    """True iff `commit` is an ancestor of `head` (both non-empty)."""
    if not commit or not head:
        return False
    return _git(Path(cwd), "merge-base", "--is-ancestor", commit, head).returncode == 0


class MergeError(RuntimeError):
    """`git merge` 가 내용 충돌이 아닌 사유로 실패했을 때(추적되지 않은 파일
    덮어쓰기, 진행 중인 머지 잔존, 없는 브랜치 등) 던진다. 충돌 파일이 0개인
    실패를 '충돌'로 둔갑시키지 않고 git stderr 를 그대로 노출해 호출자가 올바른
    복구 경로로 안내하도록 한다."""


def merge_branch(cwd, branch: str, *, no_ff: bool) -> list[str] | None:
    """`branch` 를 cwd 의 현재 HEAD 에 머지한다. 성공이면 None, 내용 충돌이면
    abort 후 충돌 파일 목록을 반환한다(최소 1개). 충돌이 아닌 머지 실패는
    MergeError 로 던진다 — 충돌 0개를 충돌로 보고하지 않기 위함. 호출자가 충돌
    목록을 HandoffConflict/IntegrateError 등 적절한 예외로 변환한다 — 머지·
    충돌감지·abort 시퀀스의 단일 지점."""
    args = ["merge", "--no-ff", "--no-edit", branch] if no_ff \
        else ["merge", "--no-edit", branch]
    res = _git(Path(cwd), *args)
    if res.returncode == 0:
        return None
    conflicts = _git(Path(cwd), "diff", "--name-only",
                     "--diff-filter=U").stdout.split()
    _git(Path(cwd), "merge", "--abort")
    if not conflicts:
        raise MergeError((res.stderr or res.stdout).strip()
                         or f"git merge {branch} failed (exit={res.returncode})")
    return conflicts


def move_worktree(cwd, worktree_path, new_path) -> subprocess.CompletedProcess:
    """등록된 worktree 를 다른 경로로 옮긴다(`git worktree move`). 내용·브랜치·
    등록은 그대로고 경로만 바뀐다. 중첩 stage worktree 를 task worktree 밖으로
    꺼내는 단일 지점이다."""
    return _git(Path(cwd), "worktree", "move", str(worktree_path), str(new_path))


def remove_worktree_force(cwd, worktree_path) -> subprocess.CompletedProcess:
    """워크트리를 강제 제거한다. provision_*_worktree 가 워크트리에 .okstra·
    synced 디렉터리/파일 symlink(untracked) 를 깔기 때문에 plain remove 는 그
    잔여물 때문에 git 이 거부한다 — dirty 게이트로 실사용자 변경을 막은 뒤
    --force 로 okstra 잔여물만 정리하는 단일 지점."""
    return _git(Path(cwd), "worktree", "remove", "--force", str(worktree_path))
