"""메인 체크아웃의 공유 상태를 새 worktree 안에 재현한다.

`sync_config` 가 고른 목록을 실제로 심링크(또는 스냅샷 복사)하는 층이다. 방식이
세 가지이고 이유가 각각 다르다 — 디렉터리 통째 심링크는 새로 생기는 최상위
항목이 메인으로 가게 하고, 자식별 심링크는 git 이 심링크된 디렉터리를 파일
하나로 보는 탓에 `.claude` 가 `?? .claude` 로 뜨는 것을 막으며, 스냅샷 복사는
쓰기 핸들을 공유하지 않으려는 픽스처용이다.
"""
from __future__ import annotations

import os
import shutil
import stat
from pathlib import Path

from ..seeding import (
    SettingsLinkError,
    ensure_project_settings_symlink,
)
from .sync_config import (
    CHILD_LINKED_SYNC_DIRS,
    _resolve_snapshot_files,
    _resolve_sync_dirs,
    _resolve_sync_files,
)


def _link_sync_dirs(source_root: Path, worktree_path: Path) -> list[str]:
    """Symlink each configured dir from `source_root` (the MAIN
    worktree) into the new worktree.

    Skip rules:
      - Source missing in `source_root` → silently skipped.
      - Target path already exists in worktree (e.g. tracked content
        checked out by `git worktree add`) → skipped to avoid clobbering
        version-controlled files.
      - Parent directories are created as needed for nested entries.

    Entries listed in `CHILD_LINKED_SYNC_DIRS` are materialised as a real
    directory of per-child symlinks instead (see that constant).

    Returns a list of human-readable notes (one per linked entry) so the
    caller can include them in the provisioning note.
    """
    notes: list[str] = []
    for rel in _resolve_sync_dirs(source_root):
        src = (source_root / rel).resolve()
        if not src.exists():
            continue
        dst = worktree_path / rel
        if dst.exists() or dst.is_symlink():
            continue
        dst.parent.mkdir(parents=True, exist_ok=True)
        if rel in CHILD_LINKED_SYNC_DIRS and src.is_dir():
            _link_dir_children(src, dst)
            notes.append(rel)
            continue
        try:
            os.symlink(src, dst)
        except FileExistsError:
            continue
        notes.append(rel)
    return notes


def _link_dir_children(src: Path, dst: Path) -> None:
    """Create `dst` as a real directory whose entries symlink to `src`'s
    children, so git sees the same directory shape it sees in the main
    checkout (`CHILD_LINKED_SYNC_DIRS`).

    Each link points at the MAIN checkout's child rather than its resolved
    target, so a child that is itself a symlink (e.g. `.claude/settings.local.json`
    → `~/.okstra/templates/settings.local.json`) keeps following whatever the
    main checkout currently points at. An unreadable source or a child that
    cannot be linked degrades to "that child is absent in this worktree" —
    provisioning is not worth failing over host config.
    """
    dst.mkdir(parents=True, exist_ok=True)
    try:
        children = sorted(src.iterdir())
    except OSError:
        return
    for child in children:
        link = dst / child.name
        if link.exists() or link.is_symlink():
            continue
        try:
            os.symlink(child, link)
        except OSError:
            continue


def _link_sync_files(source_root: Path, worktree_path: Path) -> list[str]:
    """File-level counterpart to `_link_sync_dirs` (FU-V2).

    Same skip rules: missing source → skipped silently; pre-existing dst
    (tracked content checked out by `git worktree add`) → skipped to avoid
    clobbering. Symlinks (not copies) keep secrets out of duplicate files —
    the link points back to the MAIN worktree's `.env` so rotating the file
    there is reflected in every active task without re-provisioning.
    """
    notes: list[str] = []
    for rel in _resolve_sync_files(source_root):
        src = (source_root / rel).resolve()
        if not src.exists() or not src.is_file():
            continue
        dst = worktree_path / rel
        if dst.exists() or dst.is_symlink():
            continue
        dst.parent.mkdir(parents=True, exist_ok=True)
        try:
            os.symlink(src, dst)
        except FileExistsError:
            continue
        notes.append(rel)
    return notes


def sync_file_key_names(source_root: Path) -> dict[str, list[str]]:
    """공유되는 sync 파일마다 그 안의 **키 이름**만 모은다 (값은 절대 읽지 않는다).

    `.env` 는 gitignore 대상이라 브랜치에 속하지 않고, `_link_sync_files` 가 메인
    체크아웃의 것을 각 worktree 에 심링크로 건다. 값 회전은 그 설계가 노린 것이지만
    **키 이름 변경**은 아직 옛 이름을 읽는 모든 브랜치를 한꺼번에 깨뜨린다 — 코드는
    브랜치별로 격리되지만 `.env` 는 격리되지 않기 때문이다. 그 사고를 알아보려면
    프로비저닝 시점의 이름 집합을 남겨야 한다.

    `KEY=` 꼴이 아닌 파일은 빈 목록이 되어 드리프트 판정에서 자연히 빠진다 —
    sync 파일 목록은 프로젝트가 바꿀 수 있으므로 `.env` 형식을 전제하지 않는다.
    """
    keys: dict[str, list[str]] = {}
    for rel in _resolve_sync_files(source_root):
        src = (source_root / rel).resolve()
        if not src.is_file():
            continue
        try:
            text = src.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            continue
        found = []
        for line in text.splitlines():
            stripped = line.strip().removeprefix("export ").lstrip()
            if not stripped or stripped.startswith("#"):
                continue
            name, separator, _ = stripped.partition("=")
            name = name.strip()
            if separator and name and all(
                ch.isalnum() or ch == "_" for ch in name
            ):
                found.append(name)
        keys[rel] = sorted(set(found))
    return keys


def describe_sync_key_drift(
    recorded: object, current: dict[str, list[str]]
) -> str:
    """프로비저닝 때 기록한 키 이름 집합과 지금을 대조해 한 줄로 알린다.

    기록이 없으면(그 전에 만들어진 worktree) 빈 문자열 — 알 수 없는 것을 사고로
    보고하지 않는다. 차단이 아니라 알림인 이유는 키 추가가 정상 작업이기 때문이다.
    """
    if not isinstance(recorded, dict):
        return ""
    parts: list[str] = []
    for rel in sorted(set(recorded) | set(current)):
        before = set(recorded.get(rel) or [])
        after = set(current.get(rel) or [])
        removed = sorted(before - after)
        added = sorted(after - before)
        if not removed and not added:
            continue
        detail = []
        if removed:
            detail.append(f"gone: {', '.join(removed)}")
        if added:
            detail.append(f"new: {', '.join(added)}")
        parts.append(f"{rel} ({'; '.join(detail)})")
    if not parts:
        return ""
    return (
        "shared-env drift — " + "; ".join(parts) + ". This file is symlinked "
        "from the main checkout into every task worktree, so a renamed key "
        "breaks every branch still reading the old name at once, and it shows "
        "up as verifier sessions failing to boot while the executor passes. "
        "Key names only; values are never recorded."
    )


def _seed_worktree_settings_symlink(worktree_path: Path) -> None:
    """Seed `.claude/settings.local.json` in the worker worktree so dispatched
    Claude / codex / antigravity sessions inherit the okstra read-only / write
    allowlist. Mirrors the main-project seeding done in `run.py` — needed
    because `_link_sync_dirs` skips `.claude/` whenever `git worktree add`
    already materialised the directory (e.g. tracked `.claude/handoff-*.md`).
    Failures degrade to stderr warning so worktree provisioning still
    succeeds.
    """
    try:
        link = ensure_project_settings_symlink(project_root=worktree_path)
    except SettingsLinkError as exc:
        print(
            f"okstra-settings: failed to seed worker worktree symlink at "
            f"{worktree_path / '.claude/settings.local.json'} — worker dispatch "
            f"may be blocked by Claude Code permissions. ({exc})",
            file=__import__("sys").stderr,
        )
        return
    if link is None:
        print(
            "okstra-settings: ~/.okstra/templates/settings.local.json missing — "
            "re-run 'npx okstra@latest install' (0.14.0+) to provision the "
            "symlink target.",
            file=__import__("sys").stderr,
        )


def _copy_snapshot_files(source_root: Path, worktree_path: Path) -> list[str]:
    """Copy fixture files from MAIN → task worktree as read-only snapshots
    (FU-V3).

    Unlike `_link_sync_files`, this materialises a fresh on-disk copy and
    chmods it to `0o444` so the verifier can read the same rows the
    executor saw without sharing a writable handle that would corrupt the
    main worktree's copy. Skip rules mirror the symlink helpers (missing
    source → skipped silently; pre-existing dst → skipped to avoid
    clobbering tracked content).
    """
    notes: list[str] = []
    for rel in _resolve_snapshot_files(source_root):
        src = (source_root / rel).resolve()
        if not src.exists() or not src.is_file():
            continue
        dst = worktree_path / rel
        if dst.exists() or dst.is_symlink():
            continue
        dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src, dst)
        try:
            os.chmod(dst, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
        except OSError:
            # chmod failure (exotic filesystems, root-squashed mounts) does
            # not invalidate the snapshot itself — it just means the read-
            # only contract is best-effort. Surface in notes so the operator
            # can audit if a verifier later mutates the file.
            notes.append(f"{rel} (rw-fallback)")
            continue
        notes.append(rel)
    return notes
