"""Git worktree lifecycle management.

Each child task runs in an isolated git worktree so branches don't interfere.
"""

from __future__ import annotations

import asyncio
import logging
import shutil
from pathlib import Path

from cleave.orchestrator.errors import GitWorktreeError

logger = logging.getLogger(__name__)

WORKTREE_DIR = ".cleave-worktrees"


async def _run_git(
    *args: str, cwd: Path, check: bool = True
) -> tuple[int, str, str]:
    """Run a git command asynchronously.

    Returns (returncode, stdout, stderr).
    """
    proc = await asyncio.create_subprocess_exec(
        "git", *args,
        cwd=str(cwd),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout_bytes, stderr_bytes = await proc.communicate()
    stdout = stdout_bytes.decode("utf-8", errors="replace").strip()
    stderr = stderr_bytes.decode("utf-8", errors="replace").strip()
    if check and proc.returncode != 0:
        raise GitWorktreeError(
            f"git {' '.join(args)} failed (rc={proc.returncode}): {stderr}"
        )
    return proc.returncode, stdout, stderr


async def get_current_branch(repo_path: Path) -> str:
    """Get the current branch name of the repo."""
    _, stdout, _ = await _run_git("rev-parse", "--abbrev-ref", "HEAD", cwd=repo_path)
    return stdout


async def snapshot_dirty_state(repo_path: Path) -> str:
    """Commit all dirty state as a WIP snapshot, returning the pre-commit HEAD sha.

    This enables --dirty mode: uncommitted changes are snapshotted so
    worktrees created from HEAD will include them. Call restore_dirty_state()
    after merge to unwind the snapshot commit.
    """
    # Record current HEAD before we make the snapshot commit
    _, pre_head, _ = await _run_git("rev-parse", "HEAD", cwd=repo_path)

    # Stage everything (including untracked files)
    await _run_git("add", "-A", cwd=repo_path)

    # Commit — may be a no-op if nothing to commit (shouldn't happen since
    # caller verified dirty state, but guard anyway)
    rc, _, _ = await _run_git(
        "commit", "-m", "cleave: WIP snapshot [auto]",
        cwd=repo_path, check=False,
    )
    if rc != 0:
        logger.warning("WIP snapshot commit had no changes (rc=%d)", rc)

    return pre_head


async def restore_dirty_state(repo_path: Path, pre_dirty_head: str) -> None:
    """Unwind the WIP snapshot commit, leaving changes as unstaged modifications.

    Two-step reset:
    1. git reset --soft <sha> — moves HEAD back, leaves index staged
    2. git reset — unstages everything, leaving files as working tree modifications
    """
    await _run_git("reset", "--soft", pre_dirty_head, cwd=repo_path)
    await _run_git("reset", cwd=repo_path)
    logger.info("Restored dirty state: HEAD back to %s", pre_dirty_head[:12])


async def ensure_clean_worktree(repo_path: Path) -> None:
    """Verify the repo has no uncommitted changes."""
    _rc, stdout, _ = await _run_git("status", "--porcelain", cwd=repo_path, check=False)
    if stdout.strip():
        raise GitWorktreeError(
            f"Repository has uncommitted changes. Commit or stash before running:\n{stdout}"
        )


def _sanitize_label(child_label: str, child_id: int) -> str:
    """Sanitize a child label into a safe branch/path component.

    Appends child_id to guarantee uniqueness even if two labels
    collapse to the same string after sanitization.
    """
    safe = child_label.lower().replace(" ", "-")
    for ch in "/:@{}[]()!#$%^&*+=~`|\\<>,?\"'":
        safe = safe.replace(ch, "")
    # Collapse consecutive dots (invalid in git refs)
    while ".." in safe:
        safe = safe.replace("..", ".")
    safe = safe.strip("-.")[:50]

    # Guard against empty label after sanitization
    if not safe:
        safe = "child"

    return f"{safe}-{child_id}"


async def create_worktree(
    repo_path: Path,
    child_label: str,
    base_branch: str,
    child_id: int = 0,
) -> tuple[Path, str]:
    """Create a git worktree for a child task.

    Returns (worktree_path, branch_name).
    """
    safe_label = _sanitize_label(child_label, child_id)

    branch_name = f"cleave/{safe_label}"
    worktree_root = repo_path / WORKTREE_DIR
    worktree_path = worktree_root / safe_label

    # Ensure the worktree directory parent exists
    worktree_root.mkdir(parents=True, exist_ok=True)

    # Remove stale worktree if it exists
    if worktree_path.exists():
        logger.warning("Removing stale worktree at %s", worktree_path)
        await remove_worktree(repo_path, worktree_path)

    # Delete branch if it already exists (leftover from previous run).
    # Use -d (safe) first; fall back to -D only if -d fails and the
    # branch is confirmed to be a cleave branch (not user work).
    rc, _, _ = await _run_git(
        "branch", "-d", branch_name, cwd=repo_path, check=False
    )
    if rc != 0 and branch_name.startswith("cleave/"):
        # Safe delete failed (unmerged). Force only for cleave-owned branches.
        logger.warning("Force-deleting unmerged branch %s (cleave-owned)", branch_name)
        await _run_git(
            "branch", "-D", branch_name, cwd=repo_path, check=False
        )

    # Create worktree with new branch from base
    await _run_git(
        "worktree", "add", "-b", branch_name,
        str(worktree_path), base_branch,
        cwd=repo_path,
    )
    # Symlink repo .venv into worktree so relative .venv/bin/python paths work
    repo_venv = repo_path / ".venv"
    if repo_venv.is_dir() and worktree_path.is_dir():
        wt_venv = worktree_path / ".venv"
        if not wt_venv.exists():
            wt_venv.symlink_to(repo_venv.resolve())
            logger.debug("Symlinked .venv into worktree: %s -> %s", wt_venv, repo_venv)

    logger.info("Created worktree: %s (branch: %s)", worktree_path, branch_name)
    return worktree_path, branch_name


async def remove_worktree(repo_path: Path, worktree_path: Path) -> None:
    """Remove a git worktree and clean up."""
    if worktree_path.exists():
        await _run_git(
            "worktree", "remove", "--force", str(worktree_path),
            cwd=repo_path, check=False,
        )
    # If git worktree remove failed, force-remove the directory
    if worktree_path.exists():
        shutil.rmtree(worktree_path, ignore_errors=True)
    # Prune stale worktree references
    await _run_git("worktree", "prune", cwd=repo_path, check=False)


async def cleanup_worktrees(repo_path: Path) -> None:
    """Remove the entire .cleave-worktrees directory and prune."""
    worktree_root = repo_path / WORKTREE_DIR
    if worktree_root.exists():
        # List and remove each worktree properly first
        _rc, stdout, _ = await _run_git("worktree", "list", "--porcelain", cwd=repo_path, check=False)
        for line in stdout.splitlines():
            if line.startswith("worktree ") and WORKTREE_DIR in line:
                wt_path = Path(line.split(" ", 1)[1])
                await _run_git(
                    "worktree", "remove", "--force", str(wt_path),
                    cwd=repo_path, check=False,
                )
        shutil.rmtree(worktree_root, ignore_errors=True)
        await _run_git("worktree", "prune", cwd=repo_path, check=False)
        logger.info("Cleaned up all worktrees in %s", worktree_root)


async def delete_branch(repo_path: Path, branch_name: str) -> None:
    """Delete a local branch. Uses safe delete (-d) with force fallback for cleave/ branches."""
    rc, _, _ = await _run_git("branch", "-d", branch_name, cwd=repo_path, check=False)
    if rc != 0 and branch_name.startswith("cleave/"):
        await _run_git("branch", "-D", branch_name, cwd=repo_path, check=False)
