"""okstra 기준으로 worktree 가 깨끗한지 판정한다.

`git status` 가 보는 더러움과 okstra 가 신경 쓰는 더러움은 다르다. okstra 는
자기 산출물(`.okstra/`), 자기가 걸어 둔 sync 심링크, 자기가 만든 중첩 worktree
를 제외하고 남는 변경만 사용자의 것으로 본다. 그 제외 목록을 만드는 곳이
여기이고, 그래서 `sync_config`(무엇을 걸었나)와 `git_ops`(지금 무엇이 바뀌었나)
양쪽을 쓴다.
"""
from __future__ import annotations

from pathlib import Path
from typing import Optional

from .git_ops import _git
from .sync_config import (
    _resolve_snapshot_files,
    _resolve_sync_dirs,
    _resolve_sync_files,
)


def okstra_clean_gate_excludes(project_root: Optional[Path] = None) -> tuple[str, ...]:
    """Project-relative paths okstra owns and source clean gates should ignore."""
    out: list[str] = []
    seen: set[str] = set()
    for rel in (
        ".okstra",
        *_resolve_sync_dirs(project_root),
        *_resolve_sync_files(project_root),
        *_resolve_snapshot_files(project_root),
    ):
        cleaned = rel.strip().removeprefix("./").rstrip("/")
        if not cleaned or cleaned == "." or cleaned in seen:
            continue
        seen.add(cleaned)
        out.append(cleaned)
    return tuple(out)


def nested_worktree_excludes(worktree_path) -> tuple[str, ...]:
    """Relative paths of git worktrees nested under `worktree_path`.

    implementation stage worktrees used to live inside the task worktree
    (`<task-id>/stage-<N>/`), so the parent's `git status --short` reported
    each as an untracked `?? stage-N/`. New stage worktrees are siblings
    (`<task-id>--stage-<N>`, `worktree.naming`) and whole-task
    final-verification prepare moves the remaining nested ones out, so this
    normally returns an empty tuple now — it stays because a task that has not
    been through that prepare still has the nested layout. Derived from
    `git worktree list` rather than a hardcoded pattern so it tracks exactly
    what git registered.
    """
    root = Path(worktree_path).resolve()
    r = _git(root, "worktree", "list", "--porcelain")
    if r.returncode != 0:
        return ()
    out: list[str] = []
    for line in r.stdout.splitlines():
        if not line.startswith("worktree "):
            continue
        wt = Path(line[len("worktree "):].strip()).resolve()
        if wt == root:
            continue
        try:
            rel = wt.relative_to(root)
        except ValueError:
            continue
        out.append(str(rel))
    return tuple(out)


def dirty_entries_excluding_okstra(cwd) -> list[str]:
    """`git status --short` rows for changes outside okstra-owned paths.

    okstra-owned = `okstra_clean_gate_excludes` (e.g. `.okstra`, synced dirs)
    plus `nested_worktree_excludes` (stage worktrees nested under `cwd`).

    This is the clean-worktree question every okstra gate asks, and the one a
    plan step must ask through `okstra worktree-status` rather than through a
    bare `git status --porcelain`: okstra provisions `.okstra` plus the synced
    entries into every task worktree, so a bare status is never empty there and
    an assertion built on it fails on okstra's own scaffolding.
    """
    owned = (*okstra_clean_gate_excludes(Path(cwd)), *nested_worktree_excludes(cwd))
    excludes = [f":(exclude){p}" for p in owned]
    out = _git(Path(cwd), "status", "--short", "--", ".", *excludes).stdout
    return [line for line in out.splitlines() if line.strip()]


def is_dirty_excluding_okstra(cwd) -> bool:
    """True iff the worktree has changes outside okstra-owned paths."""
    return bool(dirty_entries_excluding_okstra(cwd))
