"""Foundation layer: git helpers, repo detection, exceptions.

No dependencies on config or Rich — this is the lowest layer.
"""

from __future__ import annotations

import re
import subprocess
from dataclasses import dataclass
from pathlib import Path


# ── Exceptions ──────────────────────────────────────────────────────────────


class GwtError(Exception):
    """Base exception for all gwt errors."""


class NotARepoError(GwtError):
    """Not inside a git repository."""


class WorktreeNotFoundError(GwtError):
    """Named worktree does not exist."""


class WorktreeExistsError(GwtError):
    """Worktree or branch already exists."""


class DirtyWorktreeError(GwtError):
    """Worktree has uncommitted changes."""


class GitCommandError(GwtError):
    """A git command failed."""

    def __init__(self, cmd: list[str], returncode: int, stderr: str) -> None:
        self.cmd = cmd
        self.returncode = returncode
        self.stderr = stderr
        super().__init__(f"git {' '.join(cmd)} failed (rc={returncode}): {stderr.strip()}")


# ── Data classes ────────────────────────────────────────────────────────────


@dataclass(frozen=True)
class RepoContext:
    """Information about the current git repository."""

    repo_name: str
    main_worktree: Path
    git_dir: Path
    is_inside_worktree: bool


@dataclass(frozen=True)
class WorktreeInfo:
    """Parsed output from `git worktree list --porcelain`."""

    path: Path
    head: str
    branch: str | None  # None for detached HEAD
    is_bare: bool


# ── Git helpers ─────────────────────────────────────────────────────────────


def run_git(*args: str, cwd: Path | None = None) -> str:
    """Execute a git command and return stdout.

    Raises GitCommandError on non-zero exit.
    """
    cmd = ["git", *args]
    result = subprocess.run(
        cmd,
        cwd=cwd,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        raise GitCommandError(list(args), result.returncode, result.stderr)
    return result.stdout.strip()


def get_repo_context(cwd: Path | None = None) -> RepoContext:
    """Detect the git repository context from the given directory.

    Works from both the main worktree and linked worktrees.
    """
    cwd = cwd or Path.cwd()

    try:
        toplevel = run_git("rev-parse", "--show-toplevel", cwd=cwd)
    except GitCommandError:
        raise NotARepoError(f"Not a git repository: {cwd}")

    try:
        git_dir = Path(run_git("rev-parse", "--git-dir", cwd=cwd)).resolve()
    except GitCommandError:
        raise NotARepoError(f"Cannot determine git dir: {cwd}")

    # For linked worktrees, git-dir is .git/worktrees/<name> — resolve to main
    git_common_dir = Path(run_git("rev-parse", "--git-common-dir", cwd=cwd)).resolve()

    # The main worktree is the parent of .git (or git-common-dir)
    if git_common_dir.name == ".git":
        main_worktree = git_common_dir.parent
    else:
        main_worktree = Path(toplevel)

    # If we're inside a linked worktree, get the main worktree path
    try:
        main_wt = run_git(
            "worktree", "list", "--porcelain", cwd=cwd
        ).split("\n")[0]
        if main_wt.startswith("worktree "):
            main_worktree = Path(main_wt.split(" ", 1)[1])
    except GitCommandError:
        pass

    repo_name = main_worktree.name

    is_inside = run_git("rev-parse", "--is-inside-work-tree", cwd=cwd) == "true"

    return RepoContext(
        repo_name=repo_name,
        main_worktree=main_worktree,
        git_dir=git_dir,
        is_inside_worktree=is_inside,
    )


def list_worktrees(repo_path: Path) -> list[WorktreeInfo]:
    """Parse `git worktree list --porcelain` output."""
    raw = run_git("worktree", "list", "--porcelain", cwd=repo_path)
    if not raw:
        return []

    worktrees: list[WorktreeInfo] = []
    current: dict[str, str] = {}

    for line in raw.split("\n"):
        if not line:
            if current:
                worktrees.append(_parse_worktree_block(current))
                current = {}
            continue

        if line.startswith("worktree "):
            current["path"] = line.split(" ", 1)[1]
        elif line.startswith("HEAD "):
            current["head"] = line.split(" ", 1)[1]
        elif line.startswith("branch "):
            current["branch"] = line.split(" ", 1)[1]
        elif line == "bare":
            current["bare"] = "true"
        elif line == "detached":
            current["detached"] = "true"

    # Handle last block (no trailing newline)
    if current:
        worktrees.append(_parse_worktree_block(current))

    return worktrees


def _parse_worktree_block(block: dict[str, str]) -> WorktreeInfo:
    branch_ref = block.get("branch")
    # Strip refs/heads/ prefix
    branch = branch_ref.removeprefix("refs/heads/") if branch_ref else None

    return WorktreeInfo(
        path=Path(block["path"]),
        head=block.get("head", ""),
        branch=branch,
        is_bare="bare" in block,
    )


def get_branch_status(repo_path: Path, branch: str) -> dict[str, int]:
    """Get ahead/behind counts and dirty file count for a branch."""
    result: dict[str, int] = {"ahead": 0, "behind": 0, "dirty": 0}

    # Ahead/behind relative to upstream
    try:
        counts = run_git(
            "rev-list", "--left-right", "--count",
            f"{branch}@{{upstream}}...{branch}",
            cwd=repo_path,
        )
        parts = counts.split("\t")
        if len(parts) == 2:
            result["behind"] = int(parts[0])
            result["ahead"] = int(parts[1])
    except GitCommandError:
        pass  # No upstream configured

    # Dirty files
    try:
        status = run_git("status", "--porcelain", cwd=repo_path)
        if status:
            result["dirty"] = len(status.strip().split("\n"))
    except GitCommandError:
        pass

    return result


def is_branch_merged(repo_path: Path, branch: str, into: str) -> bool:
    """Check if `branch` has been merged into `into`."""
    try:
        merged = run_git("branch", "--merged", into, cwd=repo_path)
        for b in merged.split("\n"):
            # Strip leading markers: * (current), + (worktree), whitespace
            name = b.strip().lstrip("*+ ").strip()
            if name == branch:
                return True
        return False
    except GitCommandError:
        return False


def validate_worktree_name(name: str) -> str:
    """Validate and return a sanitized worktree name.

    Allows alphanumeric, hyphens, underscores, dots.
    """
    if not name:
        raise ValueError("Worktree name cannot be empty")

    if not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*$", name):
        raise ValueError(
            f"Invalid worktree name '{name}': "
            "must start with alphanumeric and contain only alphanumeric, hyphens, underscores, dots"
        )

    if name in ("main", "master"):
        raise ValueError(f"'{name}' is reserved — use 'gwtm cd {name}' to go to the main worktree")

    return name
