"""
Git worktree utilities for worker session isolation.

Provides functions to create, remove, and list git worktrees, enabling
each worker session to operate in its own isolated working directory
while sharing the same repository history.

Two worktree strategies are supported:

1. External worktrees (legacy):
   ~/.maniple/worktrees/{repo-path-hash}/{worker-name}-{timestamp}/
   - Created outside the target repo to avoid polluting it
   - No .gitignore modifications needed

2. Local worktrees (preferred):
   {repo}/.worktrees/{issue-annotation}/ or {name-uuid-annotation}/
   - Kept within the repo for easier discovery and cleanup
   - Automatically adds .worktrees to .gitignore
"""

import hashlib
import re
import subprocess
import time
import uuid
from pathlib import Path
from typing import Optional


# Local worktree directory name within repos
LOCAL_WORKTREE_DIR = ".worktrees"


def slugify(text: str) -> str:
    """
    Convert text to a URL/filesystem-friendly slug.

    Converts to lowercase, replaces spaces and special chars with dashes,
    and removes consecutive dashes.

    Args:
        text: The text to slugify

    Returns:
        A lowercase, dash-separated string safe for filenames/URLs

    Example:
        slugify("Add local worktrees support")  # "add-local-worktrees-support"
        slugify("Fix Bug #123")                 # "fix-bug-123"
    """
    # Convert to lowercase
    text = text.lower()
    # Replace spaces and underscores with dashes
    text = re.sub(r"[\s_]+", "-", text)
    # Remove any characters that aren't alphanumeric or dashes
    text = re.sub(r"[^a-z0-9-]", "", text)
    # Collapse multiple dashes
    text = re.sub(r"-+", "-", text)
    # Strip leading/trailing dashes
    text = text.strip("-")
    return text


def short_slug(text: str, max_length: int = 30) -> str:
    """
    Create a slug suitable for compact identifiers.

    Truncates long slugs to keep branch and directory names short,
    while preserving the leading portion of the slug.
    """
    slug = slugify(text)
    if len(slug) <= max_length:
        return slug
    return slug[:max_length].rstrip("-")



def ensure_gitignore_entry(repo_path: Path, entry: str) -> bool:
    """
    Ensure an entry exists in the repository's .gitignore file.

    Creates the .gitignore file if it doesn't exist. Adds the entry
    on a new line if not already present.

    Args:
        repo_path: Path to the repository root
        entry: The gitignore entry to add (e.g., ".worktrees")

    Returns:
        True if the entry was added, False if it already existed

    Example:
        ensure_gitignore_entry(Path("/path/to/repo"), ".worktrees")
    """
    gitignore_path = Path(repo_path) / ".gitignore"

    # Check if entry already exists
    if gitignore_path.exists():
        content = gitignore_path.read_text()
        lines = content.splitlines()

        # Check for exact match (with or without trailing slash)
        entry_variants = {entry, entry + "/", entry.rstrip("/")}
        for line in lines:
            stripped = line.strip()
            if stripped in entry_variants:
                return False

        # Entry not found, append it
        # Ensure there's a newline before our entry if file doesn't end with one
        if content and not content.endswith("\n"):
            content += "\n"
        content += f"{entry}\n"
        gitignore_path.write_text(content)
        return True
    else:
        # Create new .gitignore with the entry
        gitignore_path.write_text(f"{entry}\n")
        return True


class WorktreeError(Exception):
    """Raised when a git worktree operation fails."""

    pass


def get_repo_hash(repo_path: Path) -> str:
    """
    Generate a short hash from a repository path.

    Used to create unique subdirectories for each repo's worktrees.

    Args:
        repo_path: Absolute path to the repository

    Returns:
        8-character hex hash of the repo path
    """
    return hashlib.sha256(str(repo_path).encode()).hexdigest()[:8]


def get_worktree_base_for_repo(repo_path: Path) -> Path:
    """
    Get the base directory for a repo's worktrees.

    Args:
        repo_path: Path to the main repository

    Returns:
        Path to ~/.maniple/worktrees/{repo-hash}/
    """
    repo_path = Path(repo_path).resolve()
    repo_hash = get_repo_hash(repo_path)
    from maniple.paths import resolve_data_dir

    return resolve_data_dir() / "worktrees" / repo_hash


def create_worktree(
    repo_path: Path,
    worktree_name: str,
    branch: Optional[str] = None,
    timestamp: Optional[int] = None,
) -> Path:
    """
    Create a git worktree for a worker.

    Creates a new worktree at:
        ~/.maniple/worktrees/{repo-hash}/{worktree_name}-{timestamp}/

    If a branch is specified and doesn't exist, it will be created from HEAD.
    If no branch is specified, creates a detached HEAD worktree.

    Args:
        repo_path: Path to the main repository
        worktree_name: Name for the worktree (worker name, e.g., "John-abc123")
        branch: Branch to checkout (creates new branch from HEAD if doesn't exist)
        timestamp: Unix timestamp for directory name (defaults to current time)

    Returns:
        Path to the created worktree

    Raises:
        WorktreeError: If the git worktree command fails

    Example:
        path = create_worktree(
            repo_path=Path("/path/to/repo"),
            worktree_name="John-abc123",
            branch="John-abc123"
        )
        # Returns: Path("~/.maniple/worktrees/a1b2c3d4/John-abc123-1703001234")
    """
    repo_path = Path(repo_path).resolve()

    # Generate worktree path outside the repo
    if timestamp is None:
        timestamp = int(time.time())
    worktree_dir_name = f"{worktree_name}-{timestamp}"
    base_dir = get_worktree_base_for_repo(repo_path)
    worktree_path = base_dir / worktree_dir_name

    # Ensure base directory exists
    base_dir.mkdir(parents=True, exist_ok=True)

    # Check if worktree already exists
    if worktree_path.exists():
        raise WorktreeError(f"Worktree already exists at {worktree_path}")

    # Build the git worktree add command
    cmd = ["git", "-C", str(repo_path), "worktree", "add"]

    if branch:
        # Check if branch exists
        branch_check = subprocess.run(
            ["git", "-C", str(repo_path), "rev-parse", "--verify", f"refs/heads/{branch}"],
            capture_output=True,
            text=True,
        )

        if branch_check.returncode == 0:
            # Branch exists, check it out
            cmd.extend([str(worktree_path), branch])
        else:
            # Branch doesn't exist, create it with -b
            cmd.extend(["-b", branch, str(worktree_path)])
    else:
        # No branch specified, create detached HEAD
        cmd.extend(["--detach", str(worktree_path)])

    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode != 0:
        raise WorktreeError(f"Failed to create worktree: {result.stderr.strip()}")

    return worktree_path


def _resolve_worktree_base(repo_path: Path, base: str) -> str:
    # Resolve base ref to a commit hash to avoid worktree-locked branch refs.
    def _rev_parse(ref: str) -> Optional[str]:
        result = subprocess.run(
            ["git", "-C", str(repo_path), "rev-parse", "--verify", ref],
            capture_output=True,
            text=True,
        )
        if result.returncode == 0:
            return result.stdout.strip()
        return None

    commit = _rev_parse(f"{base}^{{commit}}")
    if commit:
        return commit

    normalized_base = base.removeprefix("refs/heads/")
    try:
        for worktree in list_git_worktrees(repo_path):
            if worktree.get("branch") == normalized_base and worktree.get("commit"):
                return worktree["commit"]
    except WorktreeError:
        pass

    raise WorktreeError(
        f"Base ref not found: {base}. Ensure it exists locally or fetch it."
    )


def create_local_worktree(
    repo_path: Path,
    worker_name: str,
    issue_id: Optional[str] = None,
    annotation: Optional[str] = None,
    branch: Optional[str] = None,
    base: Optional[str] = None,
) -> Path:
    """
    Create a git worktree in the repo's .worktrees/ directory.

    Creates a new worktree at:
        {repo}/.worktrees/{issue_id}-{annotation}/  (if issue_id provided)
        {repo}/.worktrees/{worker_name}-{uuid}-{annotation}/  (otherwise)

    The branch name matches the worktree directory name for consistency unless
    an explicit branch is provided.
    Automatically adds .worktrees to .gitignore if not present.

    If a generated worktree path or branch already exists, appends an
    incrementing suffix (-1, -2, etc.) until an available name is found.
    This allows multiple workers to work on the same issue in parallel.
    When an explicit branch is provided, pre-existing branches are treated
    as an error.

    Args:
        repo_path: Path to the main repository
        worker_name: Name of the worker (used in fallback naming)
        issue_id: Optional issue tracker ID (e.g., "cic-abc123")
        annotation: Optional annotation for the worktree
        branch: Optional branch name to create for the worktree
        base: Optional base ref/branch for the new branch

    Returns:
        Path to the created worktree

    Raises:
        WorktreeError: If the git worktree command fails

    Example:
        # With issue ID
        path = create_local_worktree(
            repo_path=Path("/path/to/repo"),
            worker_name="Groucho",
            issue_id="cic-abc",
            annotation="Add local worktrees"
        )
        # Returns: Path("/path/to/repo/.worktrees/cic-abc-add-local-worktrees")

        # If called again with same issue/annotation:
        # Returns: Path("/path/to/repo/.worktrees/cic-abc-add-local-worktrees-1")

        # Without issue ID
        path = create_local_worktree(
            repo_path=Path("/path/to/repo"),
            worker_name="Groucho",
            annotation="Fix bug"
        )
        # Returns: Path("/path/to/repo/.worktrees/groucho-a1b2c3d4-fix-bug")
    """
    repo_path = Path(repo_path).resolve()

    # Build the worktree directory name
    if issue_id:
        # Issue-based naming: {issue_id}-{annotation}
        if annotation:
            dir_name = f"{issue_id}-{short_slug(annotation)}"
        else:
            dir_name = issue_id
    else:
        # Fallback naming: {worker_name}-{uuid}-{annotation}
        short_uuid = uuid.uuid4().hex[:8]
        name_slug = slugify(worker_name)
        if annotation:
            dir_name = f"{name_slug}-{short_uuid}-{short_slug(annotation)}"
        else:
            dir_name = f"{name_slug}-{short_uuid}"

    # Worktree path inside the repo
    worktrees_dir = repo_path / LOCAL_WORKTREE_DIR

    # Ensure .worktrees is in .gitignore
    ensure_gitignore_entry(repo_path, LOCAL_WORKTREE_DIR)

    # Ensure .worktrees directory exists
    worktrees_dir.mkdir(parents=True, exist_ok=True)

    # Find an available name, handling collisions with incrementing suffix.
    # Check both path existence and branch existence (git won't allow the same
    # branch checked out in multiple worktrees).
    def branch_exists(name: str) -> bool:
        result = subprocess.run(
            ["git", "-C", str(repo_path), "rev-parse", "--verify", f"refs/heads/{name}"],
            capture_output=True,
            text=True,
        )
        return result.returncode == 0

    base_dir_name = dir_name
    worktree_path = worktrees_dir / dir_name
    suffix = 0

    if branch:
        if branch_exists(branch):
            raise WorktreeError(f"Branch already exists: {branch}")
        while worktree_path.exists():
            suffix += 1
            dir_name = f"{base_dir_name}-{suffix}"
            worktree_path = worktrees_dir / dir_name
        branch_name = branch
    else:
        while worktree_path.exists() or branch_exists(dir_name):
            suffix += 1
            dir_name = f"{base_dir_name}-{suffix}"
            worktree_path = worktrees_dir / dir_name
        branch_name = dir_name

    resolved_base = None
    if base:
        resolved_base = _resolve_worktree_base(repo_path, base)

    # Build the git worktree add command.
    # Branch is guaranteed not to exist (collision loop checked for it).
    cmd = ["git", "-C", str(repo_path), "worktree", "add", "-b", branch_name, str(worktree_path)]
    if resolved_base:
        cmd.append(resolved_base)

    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode != 0:
        raise WorktreeError(f"Failed to create local worktree: {result.stderr.strip()}")

    return worktree_path


def remove_worktree(
    repo_path: Path,
    worktree_path: Path,
    force: bool = True,
) -> bool:
    """
    Remove a worktree directory (does NOT delete the branch).

    The branch is intentionally kept alive so that commits can be
    cherry-picked before manual cleanup.

    Args:
        repo_path: Path to the main repository
        worktree_path: Full path to the worktree to remove
        force: If True, force removal even with uncommitted changes

    Returns:
        True if worktree was successfully removed

    Raises:
        WorktreeError: If the git worktree remove command fails

    Example:
        success = remove_worktree(
            repo_path=Path("/path/to/repo"),
            worktree_path=Path("~/.maniple/worktrees/a1b2c3d4/John-abc123-1703001234")
        )
    """
    repo_path = Path(repo_path).resolve()
    worktree_path = Path(worktree_path).resolve()

    cmd = ["git", "-C", str(repo_path), "worktree", "remove"]

    if force:
        cmd.append("--force")

    cmd.append(str(worktree_path))

    result = subprocess.run(cmd, capture_output=True, text=True)

    if result.returncode != 0:
        # Check if worktree doesn't exist (not an error)
        if "is not a working tree" in result.stderr or "No such file" in result.stderr:
            return True
        raise WorktreeError(f"Failed to remove worktree: {result.stderr.strip()}")

    # Also run prune to clean up stale worktree references
    subprocess.run(
        ["git", "-C", str(repo_path), "worktree", "prune"],
        capture_output=True,
        text=True,
    )

    return True


def list_git_worktrees(repo_path: Path) -> list[dict]:
    """
    List all worktrees registered with git for a repository.

    Parses the porcelain output of git worktree list to provide
    structured information about each worktree.

    Args:
        repo_path: Path to the repository

    Returns:
        List of dicts, each containing:
            - path: Path to the worktree
            - branch: Branch name (or None if detached HEAD)
            - commit: Current HEAD commit hash
            - bare: True if this is the bare repository entry
            - detached: True if HEAD is detached

    Raises:
        WorktreeError: If the git worktree list command fails

    Example:
        worktrees = list_git_worktrees(Path("/path/to/repo"))
        for wt in worktrees:
            print(f"{wt['path']}: {wt['branch'] or 'detached'}")
    """
    repo_path = Path(repo_path).resolve()

    result = subprocess.run(
        ["git", "-C", str(repo_path), "worktree", "list", "--porcelain"],
        capture_output=True,
        text=True,
    )

    if result.returncode != 0:
        raise WorktreeError(f"Failed to list worktrees: {result.stderr.strip()}")

    worktrees = []
    current_worktree: dict = {}

    for line in result.stdout.strip().split("\n"):
        if not line:
            # Empty line separates worktree entries
            if current_worktree:
                worktrees.append(current_worktree)
                current_worktree = {}
            continue

        if line.startswith("worktree "):
            current_worktree["path"] = Path(line[9:])
            current_worktree["branch"] = None
            current_worktree["commit"] = None
            current_worktree["bare"] = False
            current_worktree["detached"] = False
        elif line.startswith("HEAD "):
            current_worktree["commit"] = line[5:]
        elif line.startswith("branch "):
            # Branch is in format "refs/heads/branch-name"
            branch_ref = line[7:]
            if branch_ref.startswith("refs/heads/"):
                current_worktree["branch"] = branch_ref[11:]
            else:
                current_worktree["branch"] = branch_ref
        elif line == "bare":
            current_worktree["bare"] = True
        elif line == "detached":
            current_worktree["detached"] = True

    # Don't forget the last entry
    if current_worktree:
        worktrees.append(current_worktree)

    return worktrees


def list_local_worktrees(repo_path: Path) -> list[dict]:
    """
    List all local worktrees in a repository's .worktrees/ directory.

    Finds worktrees in {repo}/.worktrees/ and cross-references them
    with git's worktree list to determine registration status.

    Args:
        repo_path: Path to the repository

    Returns:
        List of dicts, each containing:
            - path: Path to the worktree directory
            - name: Worktree directory name (e.g., "cic-abc-fix-bug")
            - branch: Branch name (if found in git worktree list)
            - commit: Current HEAD commit hash (if found)
            - registered: True if git knows about this worktree
            - exists: True if the directory exists on disk

    Example:
        worktrees = list_local_worktrees(Path("/path/to/repo"))
        for wt in worktrees:
            status = "active" if wt["registered"] else "orphaned"
            print(f"{wt['name']}: {status}")
    """
    repo_path = Path(repo_path).resolve()
    local_worktrees_dir = repo_path / LOCAL_WORKTREE_DIR

    # Get git's view of worktrees
    try:
        git_worktrees = list_git_worktrees(repo_path)
    except WorktreeError:
        git_worktrees = []

    git_worktree_paths = {str(wt["path"]) for wt in git_worktrees}

    worktrees = []

    # Check if .worktrees directory exists
    if not local_worktrees_dir.exists():
        return worktrees

    # Scan the directory for worktree folders
    for item in local_worktrees_dir.iterdir():
        if not item.is_dir():
            continue

        wt_path_str = str(item.resolve())
        registered = wt_path_str in git_worktree_paths

        # Find matching git worktree info if registered
        git_info = None
        for gwt in git_worktrees:
            if str(gwt["path"]) == wt_path_str:
                git_info = gwt
                break

        worktrees.append({
            "path": item,
            "name": item.name,
            "branch": git_info["branch"] if git_info else None,
            "commit": git_info["commit"] if git_info else None,
            "registered": registered,
            "exists": True,
        })

    return worktrees
