"""Git worktree management for /spec isolation.

Provides ``detect``, ``create``, ``diff``, ``sync``, ``cleanup``, and
``status`` subcommands that manage isolated git worktrees used during
the /spec workflow.

Worktrees live under ``.worktrees/spec-<slug>-<hash>/`` relative to the
repository root.
"""

from __future__ import annotations

import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any

from launcher.config import get_tribunal_home, sanitize_session_id

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

WORKTREE_DIR = ".worktrees"


def _git(*args: str, cwd: str | Path | None = None) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        ["git", *args],
        capture_output=True,
        text=True,
        cwd=cwd,
        timeout=30,
    )


def _git_root() -> Path | None:
    r = _git("rev-parse", "--show-toplevel")
    if r.returncode == 0:
        return Path(r.stdout.strip())
    return None


def _worktree_path(root: Path, slug: str) -> Path:
    short_hash = hashlib.sha1(slug.encode()).hexdigest()[:8]
    return root / WORKTREE_DIR / f"spec-{slug}-{short_hash}"


def _branch_name(slug: str) -> str:
    return f"spec/{slug}"


def _current_branch() -> str:
    r = _git("rev-parse", "--abbrev-ref", "HEAD")
    return r.stdout.strip() if r.returncode == 0 else "main"


def _session_dir() -> Path:
    session_id = sanitize_session_id(os.environ.get("SF_SESSION_ID", "").strip())
    d = get_tribunal_home() / "sessions" / session_id
    d.mkdir(parents=True, exist_ok=True)
    return d


def _save_worktree_state(slug: str, wt_path: Path, branch: str, base: str) -> None:
    state = {
        "slug": slug,
        "path": str(wt_path),
        "branch": branch,
        "base_branch": base,
    }
    try:
        (_session_dir() / "worktree.json").write_text(json.dumps(state, indent=2) + "\n")
    except OSError:
        pass


def _load_worktree_state() -> dict[str, Any] | None:
    f = _session_dir() / "worktree.json"
    if not f.exists():
        return None
    try:
        return json.loads(f.read_text())
    except (json.JSONDecodeError, OSError):
        return None


def _clear_worktree_state() -> None:
    f = _session_dir() / "worktree.json"
    f.unlink(missing_ok=True)


def _ensure_gitignore(root: Path) -> None:
    gi = root / ".gitignore"
    marker = f"/{WORKTREE_DIR}/"
    try:
        if gi.exists():
            content = gi.read_text()
            if marker in content:
                return
            gi.write_text(content.rstrip("\n") + f"\n{marker}\n")
        else:
            gi.write_text(f"{marker}\n")
    except OSError:
        pass


def _output(data: dict[str, Any], *, as_json: bool) -> None:
    if as_json:
        print(json.dumps(data))
    else:
        for k, v in data.items():
            print(f"{k}: {v}")


# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------


def detect(slug: str, *, as_json: bool = False) -> None:
    """Check if a worktree for *slug* already exists."""
    root = _git_root()
    if not root:
        _output({"found": False, "error": "not a git repository"}, as_json=as_json)
        return

    wt_path = _worktree_path(root, slug)
    branch = _branch_name(slug)

    if wt_path.exists():
        state = _load_worktree_state()
        base_branch = state["base_branch"] if state and "base_branch" in state else _current_branch()
        _output(
            {
                "found": True,
                "path": str(wt_path),
                "branch": branch,
                "base_branch": base_branch,
            },
            as_json=as_json,
        )
    else:
        _output({"found": False}, as_json=as_json)


def create(slug: str, *, as_json: bool = False) -> None:
    """Create a new worktree for *slug* and register it with the session."""
    root = _git_root()
    if not root:
        _output({"success": False, "error": "not a git repository"}, as_json=as_json)
        sys.exit(1)

    # Check for dirty working tree
    status = _git("status", "--porcelain", cwd=root)
    if status.stdout.strip():
        _output(
            {
                "success": False,
                "error": "dirty",
                "detail": "Working tree has uncommitted changes",
            },
            as_json=as_json,
        )
        sys.exit(1)

    wt_path = _worktree_path(root, slug)
    branch = _branch_name(slug)
    base = _current_branch()

    if wt_path.exists():
        _output(
            {
                "success": True,
                "path": str(wt_path),
                "branch": branch,
                "base_branch": base,
                "note": "already exists",
            },
            as_json=as_json,
        )
        _save_worktree_state(slug, wt_path, branch, base)
        return

    _ensure_gitignore(root)

    # Create the worktree with a new branch
    r = _git("worktree", "add", "-b", branch, str(wt_path), cwd=root)
    if r.returncode != 0:
        _output(
            {
                "success": False,
                "error": "git_error",
                "detail": r.stderr.strip(),
            },
            as_json=as_json,
        )
        sys.exit(1)

    _save_worktree_state(slug, wt_path, branch, base)
    _output(
        {
            "success": True,
            "path": str(wt_path),
            "branch": branch,
            "base_branch": base,
        },
        as_json=as_json,
    )


def diff(slug: str, *, as_json: bool = False) -> None:
    """List changed files in the worktree vs its base branch."""
    root = _git_root()
    if not root:
        _output({"error": "not a git repository"}, as_json=as_json)
        sys.exit(1)

    wt_path = _worktree_path(root, slug)
    if not wt_path.exists():
        _output({"error": "worktree not found"}, as_json=as_json)
        sys.exit(1)

    state = _load_worktree_state()
    base = state["base_branch"] if state else "main"

    r = _git("diff", "--name-status", base, cwd=wt_path)
    files = []
    for line in r.stdout.strip().splitlines():
        parts = line.split("\t", 1)
        if len(parts) == 2:
            files.append({"status": parts[0], "path": parts[1]})

    if as_json:
        print(json.dumps({"files": files, "count": len(files)}))
    else:
        for f in files:
            print(f"{f['status']}\t{f['path']}")
        print(f"\n{len(files)} files changed")


def sync(slug: str, *, as_json: bool = False) -> None:
    """Squash merge worktree changes back to the base branch."""
    root = _git_root()
    if not root:
        _output({"success": False, "error": "not a git repository"}, as_json=as_json)
        sys.exit(1)

    wt_path = _worktree_path(root, slug)
    if not wt_path.exists():
        _output({"success": False, "error": "worktree not found"}, as_json=as_json)
        sys.exit(1)

    state = _load_worktree_state()
    base = state["base_branch"] if state else "main"
    branch = _branch_name(slug)

    # Switch back to base branch in the main repo
    r = _git("checkout", base, cwd=root)
    if r.returncode != 0:
        _output({"success": False, "error": "checkout_failed", "detail": r.stderr.strip()}, as_json=as_json)
        sys.exit(1)

    # Squash merge
    r = _git("merge", "--squash", branch, cwd=root)
    if r.returncode != 0:
        _output({"success": False, "error": "merge_failed", "detail": r.stderr.strip()}, as_json=as_json)
        sys.exit(1)

    # Commit (may fail if merge resulted in no changes)
    r = _git("commit", "--no-edit", cwd=root)
    if r.returncode != 0:
        _output(
            {
                "success": False,
                "error": "nothing_to_commit",
                "detail": r.stderr.strip() or "Squash merge produced no changes",
            },
            as_json=as_json,
        )
        sys.exit(1)

    h = _git("rev-parse", "--short", "HEAD", cwd=root)
    commit_hash = h.stdout.strip()

    # Count changed files using diff-tree (works even on the initial commit).
    r = _git("diff-tree", "--no-commit-id", "-r", "--name-only", "HEAD", cwd=root)
    files_changed = len(r.stdout.strip().splitlines()) if r.returncode == 0 else 0

    _output(
        {
            "success": True,
            "files_changed": files_changed,
            "commit_hash": commit_hash,
        },
        as_json=as_json,
    )


def cleanup(slug: str, *, as_json: bool = False, force: bool = False) -> None:
    """Remove worktree and its branch.

    Checks for uncommitted changes before removal unless *force* is True.
    """
    root = _git_root()
    if not root:
        _output({"success": False, "error": "not a git repository"}, as_json=as_json)
        sys.exit(1)

    wt_path = _worktree_path(root, slug)
    branch = _branch_name(slug)

    # Guard: check for uncommitted changes in the worktree
    if wt_path.exists() and not force:
        status = _git("status", "--porcelain", cwd=wt_path)
        if status.returncode == 0 and status.stdout.strip():
            _output(
                {
                    "success": False,
                    "error": "uncommitted_changes",
                    "detail": "Worktree has uncommitted changes. Use --force to remove anyway.",
                    "path": str(wt_path),
                },
                as_json=as_json,
            )
            sys.exit(1)

    # Remove worktree (use --force only when explicitly requested)
    if wt_path.exists():
        args = ["worktree", "remove"]
        if force:
            args.append("--force")
        args.append(str(wt_path))
        _git(*args, cwd=root)

    # Prune stale worktree refs
    _git("worktree", "prune", cwd=root)

    # Delete branch
    _git("branch", "-D", branch, cwd=root)

    _clear_worktree_state()
    _output({"success": True, "removed": str(wt_path)}, as_json=as_json)


def status(*, as_json: bool = False) -> None:
    """Show active worktree info for the current session."""
    state = _load_worktree_state()
    if state is None:
        _output({"active": False}, as_json=as_json)
    else:
        wt_path = Path(state.get("path", ""))
        _output(
            {
                "active": wt_path.exists(),
                "slug": state.get("slug", ""),
                "path": state.get("path", ""),
                "branch": state.get("branch", ""),
                "base_branch": state.get("base_branch", ""),
            },
            as_json=as_json,
        )
