"""Git subprocess helpers for agentmux's install/update infrastructure.

All functions run git commands via ``subprocess`` (no third-party GitPython
dependency) and raise ``RuntimeError`` with human-readable context on failure.
"""

import re
import subprocess
from pathlib import Path

__all__ = [
    "clone_repo",
    "pull_repo",
    "get_head_sha",
    "get_remote_url",
    "get_remote_tracking_sha",
    "fetch_repo",
    "diff_summary",
    "derive_name_from_source",
    "is_git_url",
]


def _run_git(args: list[str], cwd: Path | None = None) -> str:
    """Run a git command and return its stdout as a stripped string.

    Raises:
        RuntimeError: If git is not installed, or if the git process exits
            with a non-zero return code (with the original stderr included).
    """
    try:
        result = subprocess.run(
            ["git", *args],
            cwd=cwd,
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        raise RuntimeError(
            "git is not installed or not found on PATH. Install git to use this feature."
        ) from None
    if result.returncode != 0:
        raise RuntimeError(
            f"git {args[0]!r} failed (exit {result.returncode}): {result.stderr.strip()}"
        )
    return result.stdout.strip()


def clone_repo(url: str, target: Path) -> Path:
    """Shallow-clone *url* into *target*.

    Uses ``--depth 1`` to minimise data transfer.  The target directory must
    not already exist; callers are responsible for ensuring uniqueness.

    Args:
        url: Remote git URL (https or ssh).
        target: Local destination path.  Parent directory must exist.

    Returns:
        The resolved *target* path after a successful clone.

    Raises:
        RuntimeError: If the ``git clone`` command fails.
    """
    _run_git(["clone", "--depth", "1", url, str(target)])
    return target.resolve()


def pull_repo(repo_dir: Path) -> str:
    """Fetch and fast-forward the current branch in *repo_dir*.

    When the repository was created with ``--depth 1`` (shallow clone), this
    function unshallows it first so that subsequent :func:`diff_summary` calls
    have access to the full commit history.

    Returns:
        The new HEAD commit SHA after the pull.

    Raises:
        RuntimeError: If ``git pull`` fails (e.g. merge conflicts, no network).
    """
    # Unshallow if needed so diff_summary can access commits prior to the pull.
    is_shallow = _run_git(["rev-parse", "--is-shallow-repository"], cwd=repo_dir)
    if is_shallow == "true":
        _run_git(["fetch", "--unshallow"], cwd=repo_dir)
    _run_git(["pull", "--ff-only"], cwd=repo_dir)
    return get_head_sha(repo_dir)


def get_head_sha(repo_dir: Path) -> str:
    """Return the full SHA of the current HEAD commit in *repo_dir*.

    Raises:
        RuntimeError: If ``git rev-parse`` fails (not a git repo, empty repo,
            etc.).
    """
    return _run_git(["rev-parse", "HEAD"], cwd=repo_dir)


def get_remote_url(repo_dir: Path) -> str:
    """Return the fetch URL of the ``origin`` remote for the repo at *repo_dir*.

    Raises:
        RuntimeError: If ``git remote get-url`` fails (no remote named
            ``origin``, not a git repo, etc.).
    """
    return _run_git(["remote", "get-url", "origin"], cwd=repo_dir)


def fetch_repo(repo_dir: Path) -> None:
    """Run ``git fetch`` in *repo_dir* to update remote-tracking refs.

    Args:
        repo_dir: Path to the git repository.

    Raises:
        RuntimeError: If the ``git fetch`` command fails.
    """
    _run_git(["fetch"], cwd=repo_dir)


def get_remote_tracking_sha(repo_dir: Path) -> str | None:
    """Return the SHA of the upstream remote-tracking branch, or ``None``.

    Uses ``git rev-parse @{u}`` which resolves the configured upstream for the
    current branch.  Returns ``None`` if no upstream is configured (e.g. a
    local-only branch or a fresh shallow clone with no tracking branch set).

    Args:
        repo_dir: Path to the git repository.
    """
    result = subprocess.run(
        ["git", "rev-parse", "@{u}"],
        cwd=repo_dir,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        return None
    sha = result.stdout.strip()
    return sha if sha else None


def diff_summary(repo_dir: Path, from_sha: str, to_sha: str) -> str:
    """Return a ``--stat`` diff between *from_sha* and *to_sha* in *repo_dir*.

    The output mirrors ``git diff --stat <from_sha>..<to_sha>`` and lists the
    files that changed along with insertion/deletion counts.  An empty string
    is returned when there are no differences between the two commits.

    Args:
        repo_dir: Path to the git repository.
        from_sha: The earlier commit SHA (e.g. the previously installed SHA).
        to_sha: The later commit SHA (e.g. the current HEAD after a pull).

    Raises:
        RuntimeError: If the ``git diff`` command fails (unknown SHAs, not a
            git repo, etc.).
    """
    return _run_git(["diff", "--stat", f"{from_sha}..{to_sha}"], cwd=repo_dir)


def is_git_url(source: str) -> bool:
    """Return ``True`` when *source* looks like a git remote URL."""
    return source.startswith(("https://", "http://", "git@", "ssh://"))


def derive_name_from_source(source: str) -> str:
    """Derive a short registry name from a git URL or local path.

    The derived name is sanitised so it is safe to use as a filesystem directory
    name: only alphanumeric characters, hyphens, and underscores are kept;
    everything else is replaced with a hyphen and consecutive hyphens are
    collapsed.

    Examples::

        https://github.com/user/my-setup      -> "my-setup"
        https://github.com/user/my-setup.git  -> "my-setup"
        /home/user/my-setup                   -> "my-setup"
        my-setup                              -> "my-setup"
        https://github.com/user/my setup      -> "my-setup"
    """
    name = source.rstrip("/").split("/")[-1]
    if name.endswith(".git"):
        name = name[:-4]
    # Sanitise: replace any character that is not alphanumeric, hyphen, or
    # underscore with a hyphen, then collapse consecutive hyphens.
    name = re.sub(r"[^a-zA-Z0-9_-]", "-", name)
    name = re.sub(r"-{2,}", "-", name).strip("-")
    return name or "setup"
