"""worktree 로 옮겨 갈 경로 목록을 결정한다 — 그 목록을 쓰지는 않는다.

`git worktree add` 는 추적되지 않는 최상위 항목을 가져오지 않는다. 어떤
디렉터리·파일이 메인 체크아웃에서 새 worktree 로 따라와야 하는지가 여기서
정해진다. 우선순위는 세 단계 — 환경 변수, `project.json`, 내장 기본값 — 이고
앞의 것이 뒤를 REPLACE 한다(병합이 아니다).

이 층은 git 을 부르지 않고 심링크도 걸지 않는다. 목록만 답한다.
"""
from __future__ import annotations

import os
from pathlib import Path
from typing import Optional

from okstra_project.dirs import project_json_path

from ..json_boundary import JsonBoundaryError, load_owned_object


# Project-root directories that hold okstra task state, ignored by git, or
# otherwise required for the executor to operate but NOT carried across by
# `git worktree add`. Each is symlinked from the MAIN worktree into the new
# worktree at provision time. Symlinks (not copies) so every task sees the
# live shared state and disk/CPU cost stays near zero; the trade-off is
# that any write through the link reaches the main worktree, which is
# acceptable because okstra writes only to paths it owns: its task-scoped
# subdirectory (e.g. `.okstra/tasks/<task-id>/runs/...`) and, for a
# branch-mode code review, `.project-docs/code-reviews/<branch>/`.
#
# Override precedence (most-specific first):
#   1. `OKSTRA_WORKTREE_SYNC_DIRS` env var — colon-separated list, REPLACES
#      defaults. Empty string disables the feature entirely. One-off
#      operator override.
#   2. `worktreeSyncDirs` array in `.okstra/project.json` —
#      project-level config, persists across runs. Same semantics: array
#      REPLACES defaults, empty array disables.
#   3. The built-in `DEFAULT_WORKTREE_SYNC_DIRS` below.
DEFAULT_WORKTREE_SYNC_DIRS: tuple[str, ...] = (
    ".project-docs",
    ".scratch",
    "graphify-out",
    ".claude",
)


# Sync dirs materialised as a REAL directory whose children are symlinked one
# by one, instead of a single symlink standing in for the whole directory.
#
# Why the split exists: git does not follow a symlink, so a symlinked directory
# is one *file* to it. A project that ignores host config by its contents
# (`.claude/*`) matches every child but never the bare `.claude` path, so the
# symlink lands in `git status` as
# `?? .claude` while the same directory is invisible in the main checkout. Any
# plan step asserting a clean worktree then fails on okstra's own provisioning.
# Linking the children instead reproduces the main checkout's shape, so
# whatever the project's ignore rules do there, they do here too.
#
# Only `.claude` qualifies. The other sync dirs are shared okstra state that
# okstra WRITES into, and a directory symlink is what makes a newly created
# top-level entry land in the main checkout rather than diverging inside the
# worktree. `.claude` is host configuration okstra reads; its one okstra write
# is the fixed `settings.local.json` child seeded by
# `_seed_worktree_settings_symlink`.
CHILD_LINKED_SYNC_DIRS: tuple[str, ...] = (".claude",)


# Project-root-relative FILES (not dirs) symlinked from MAIN → task worktree
# at provision time. Same symlink semantics as `DEFAULT_WORKTREE_SYNC_DIRS`:
# every task sees the live shared file. The split exists because the original
# `_link_sync_dirs` helper only walked directories — a `.env` (or any
# top-level file outside `.git`'s tracking) would otherwise be lost on every
# new worktree, blocking verifier dispatches that depend on environment-
# resolved secrets.
#
# Override precedence mirrors `DEFAULT_WORKTREE_SYNC_DIRS`:
#   1. `OKSTRA_WORKTREE_SYNC_FILES` env var (colon-separated, REPLACES).
#   2. `worktreeSyncFiles` array in `.okstra/project.json`.
#   3. The built-in `DEFAULT_WORKTREE_SYNC_FILES` below.
DEFAULT_WORKTREE_SYNC_FILES: tuple[str, ...] = (
    ".env",
)


# Project-root-relative files COPIED (not symlinked) from MAIN → task worktree
# at provision time, then `chmod 0o444` so the task cannot mutate the
# snapshot. Used for live-mutating fixtures (e.g. `classifications.db`)
# where the verifier needs to reproduce accuracy SQL against the same rows
# the executor saw, without sharing the writable handle that would corrupt
# the main worktree's copy. Default is empty — okstra has no opinion about
# which fixtures any given project relies on; opt in per-project via
# `worktreeSnapshotFiles` in `project.json` (or `OKSTRA_WORKTREE_SNAPSHOT_FILES`
# for one-off operator override).
DEFAULT_WORKTREE_SNAPSHOT_FILES: tuple[str, ...] = ()


def _read_project_json_field(project_root: Path, field: str) -> Optional[tuple[str, ...]]:
    """Read a string-array field from the project's okstra project.json.

    Returns None if the field is absent or the file cannot be parsed (so
    the caller falls back to defaults). Returns an empty tuple if the
    field is explicitly an empty array (caller treats this as "disable").
    A non-list value is treated as missing — we do not raise here because
    field resolution must never block worktree provisioning.
    """
    target = project_json_path(project_root)
    if not target.is_file():
        return None
    try:
        data = load_owned_object(target, artifact="project config")
    except (OSError, JsonBoundaryError):
        return None
    if not isinstance(data, dict):
        return None
    value = data.get(field)
    if not isinstance(value, list):
        return None
    cleaned = tuple(
        item.strip() for item in value
        if isinstance(item, str) and item.strip()
    )
    return cleaned


def _resolve_entries(
    *,
    env_var: str,
    project_field: str,
    default: tuple[str, ...],
    project_root: Optional[Path],
) -> tuple[str, ...]:
    """Generic resolver shared by sync-dirs / sync-files / snapshot-files.

    Precedence: env var (colon-separated, REPLACES) → project.json field →
    built-in default. An empty env value or empty JSON array disables the
    feature (returns `()`).
    """
    raw = os.environ.get(env_var)
    if raw is not None:
        raw = raw.strip()
        if not raw:
            return ()
        return tuple(part for part in (p.strip() for p in raw.split(":")) if part)
    if project_root is not None:
        from_project = _read_project_json_field(project_root, project_field)
        if from_project is not None:
            return from_project
    return default


def _resolve_sync_dirs(project_root: Optional[Path] = None) -> tuple[str, ...]:
    """Return the list of project-root-relative dirs to symlink into the
    new worktree. Precedence: env var → project.json → built-in default.
    See the comment above `DEFAULT_WORKTREE_SYNC_DIRS` for full semantics.
    """
    return _resolve_entries(
        env_var="OKSTRA_WORKTREE_SYNC_DIRS",
        project_field="worktreeSyncDirs",
        default=DEFAULT_WORKTREE_SYNC_DIRS,
        project_root=project_root,
    )


def _resolve_sync_files(project_root: Optional[Path] = None) -> tuple[str, ...]:
    """File-level counterpart to `_resolve_sync_dirs` (FU-V2)."""
    return _resolve_entries(
        env_var="OKSTRA_WORKTREE_SYNC_FILES",
        project_field="worktreeSyncFiles",
        default=DEFAULT_WORKTREE_SYNC_FILES,
        project_root=project_root,
    )


def _resolve_snapshot_files(project_root: Optional[Path] = None) -> tuple[str, ...]:
    """Read-only snapshot file list (FU-V3)."""
    return _resolve_entries(
        env_var="OKSTRA_WORKTREE_SNAPSHOT_FILES",
        project_field="worktreeSnapshotFiles",
        default=DEFAULT_WORKTREE_SNAPSHOT_FILES,
        project_root=project_root,
    )
