#!/usr/bin/env python3
"""Git subprocess wrapper for repository operations.

Provides primitives for mirror cloning, syncing, remote management,
file extraction from bare repos, and submodule management.
No pygit2 or gitpython, uses subprocess only.
"""
import logging
import subprocess
from pathlib import Path
from typing import List, Optional

logger = logging.getLogger(__name__)


class GitError(Exception):
    """A git subprocess returned a non-zero exit code."""

    def __init__(self, args: List[str], returncode: int, stderr: str):
        self.args_list = args
        self.returncode = returncode
        self.stderr = stderr
        super().__init__(
            f"git exited {returncode}: {' '.join(args)}\n{stderr}"
        )


def parse_base_url(base_url: str) -> str:
    """Extract workspace from an SCP-style Bitbucket base URL.

    The host part must be exactly ``bitbucket.org`` — bootstrapping
    requires the Bitbucket API, so other hosts are rejected.

    >>> _parse_base_url("git@bitbucket.org:byteb4rb1e")
    'byteb4rb1e'
    """
    # SCP-style: git@bitbucket.org:workspace
    if ":" not in base_url or "//" in base_url:
        raise ValueError(
            f"Expected SCP-style URL (git@bitbucket.org:workspace), "
            f"got: {base_url}"
        )
    host_part, workspace = base_url.split(":", 1)
    # host_part is e.g. "git@bitbucket.org"
    host = host_part.split("@", 1)[-1]
    if host != "bitbucket.org":
        raise ValueError(
            f"Mirror base URL must target bitbucket.org, "
            f"got host: {host}"
        )
    return Path(workspace).parent


def parse_repo_name(base_url: str) -> str:
    """Extract workspace from an SCP-style Bitbucket base URL.

    The host part must be exactly ``bitbucket.org`` — bootstrapping
    requires the Bitbucket API, so other hosts are rejected.

    >>> _parse_base_url("git@bitbucket.org:byteb4rb1e")
    'byteb4rb1e'
    """
    # SCP-style: git@bitbucket.org:workspace
    if ":" not in base_url or "//" in base_url:
        raise ValueError(
            f"Expected SCP-style URL (git@bitbucket.org:workspace), "
            f"got: {base_url}"
        )
    host_part, workspace = base_url.split(":", 1)
    # host_part is e.g. "git@bitbucket.org"
    host = host_part.split("@", 1)[-1]
    if host != "bitbucket.org":
        raise ValueError(
            f"Mirror base URL must target bitbucket.org, "
            f"got host: {host}"
        )
    return Path(workspace).name.split('.')[0]



def _run(
    args: List[str],
    cwd: Optional[Path] = None,
    capture_stdout: bool = False,
) -> subprocess.CompletedProcess:  # type: ignore[type-arg]
    """Run a git command, raising GitError on failure."""
    cmd = ["git"] + args
    logger.debug("$ %s", " ".join(cmd))
    result = subprocess.run(
        cmd,
        cwd=cwd,
        capture_output=True,
        text=True,
    )
    if result.returncode != 0:
        raise GitError(cmd, result.returncode, result.stderr.strip())
    return result


def mirror_clone(source_url: str, dest: Path) -> None:
    """Clone a repository as a bare mirror.

    Equivalent to ``git clone --mirror <source_url> <dest>``.
    The destination directory must not already exist.
    """
    _run(["clone", "--mirror", source_url, str(dest)])
    logger.info("Cloned mirror %s → %s", source_url, dest)


def add_remote(repo: Path, name: str, url: str) -> None:
    """Add a named remote to a bare repository."""
    _run(["remote", "add", name, url], cwd=repo)
    logger.debug("Added remote %s → %s in %s", name, url, repo)


def has_remote(repo: Path, name: str) -> bool:
    """Check whether a named remote exists."""
    result = _run(["remote"], cwd=repo)
    return name in result.stdout.splitlines()


def mirror_update(repo: Path) -> None:
    """Fetch all remotes in a bare mirror repository.

    Equivalent to ``git remote update`` inside the bare repo.
    """
    _run(["remote", "update"], cwd=repo)
    logger.debug("Updated remotes in %s", repo)


def fetch(repo: Path, remote: str = "origin") -> None:
    """Fetch from a single remote."""
    _run(["fetch", remote], cwd=repo)
    logger.debug("fetched %s in %s", remote, repo)


def show_ref(repo: Path) -> str:
    """Return the raw output of ``git show-ref`` (all refs + SHAs).

    Returns an empty string if the repo has no refs.
    """
    try:
        result = _run(["show-ref"], cwd=repo)
        return result.stdout
    except GitError:
        return ""


def ls_remote(repo: Path, remote: str) -> str:
    """Return the raw output of ``git ls-remote <remote>``.

    Returns an empty string if the remote has no refs or on error.
    """
    try:
        result = _run(["ls-remote", remote], cwd=repo)
        return result.stdout
    except GitError:
        return ""


def mirror_push(repo: Path, remote: str) -> None:
    """Push the full mirror to a remote.

    Equivalent to ``git push --mirror <remote>``.
    """
    _run(["push", "--mirror", remote], cwd=repo)
    logger.info("Pushed mirror to %s from %s", remote, repo)


def read_file(
    repo: Path,
    filepath: str,
    ref: str = "HEAD",
) -> Optional[str]:
    """Extract a file's contents from a bare repo without checkout.

    Returns the file content as a string, or None if the file does
    not exist at the given ref.
    """
    try:
        result = _run(
            ["show", f"{ref}:{filepath}"],
            cwd=repo,
            capture_stdout=True,
        )
        return result.stdout
    except GitError:
        return None


# -------------------------------------------------------------------
# Ref / tag primitives
# -------------------------------------------------------------------

def list_tags(repo: Path) -> List[str]:
    """List all tags in a repository."""
    result = _run(["tag", "-l"], cwd=repo)
    return [t for t in result.stdout.splitlines() if t]


def resolve_ref(repo: Path, ref: str) -> str:
    """Resolve a ref to a full SHA.

    Raises GitError if the ref cannot be resolved.
    """
    result = _run(
        ["rev-parse", ref], cwd=repo, capture_stdout=True,
    )
    return result.stdout.strip()


def head_ref(repo: Path) -> str:
    """Return the full SHA of HEAD."""
    return resolve_ref(repo, "HEAD")


# -------------------------------------------------------------------
# Pull-through bare clone cache
# -------------------------------------------------------------------

def bare_path_for_url(url: str, cache_dir: Path) -> Path:
    """Derive a cache path from a clone URL.

    Strips scheme/host, keeps the path component, appends ``.git``.

    Examples::

        https://github.com/h5p/h5p-multi-choice
            → cache_dir / h5p / h5p-multi-choice.git
        git@github.com:h5p/h5p-multi-choice.git
            → cache_dir / h5p / h5p-multi-choice.git
    """
    # Handle SCP-style URLs (git@host:path)
    if ":" in url and "//" not in url:
        path_part = url.split(":", 1)[1]
    else:
        # Strip scheme + host
        from urllib.parse import urlparse
        parsed = urlparse(url)
        path_part = parsed.path.lstrip("/")

    # Strip trailing .git if present, then re-add it
    if path_part.endswith(".git"):
        path_part = path_part[:-4]

    return cache_dir / (path_part + ".git")


def ensure_bare_clone(url: str, cache_dir: Path) -> Path:
    """Ensure a bare mirror clone exists in *cache_dir*.

    If the bare repo already exists, fetches updates via
    ``mirror_update``.  Otherwise, creates a new mirror clone.
    Returns the path to the bare repo.
    """
    bare_path = bare_path_for_url(url, cache_dir)
    if bare_path.exists():
        mirror_update(bare_path)
        logger.debug("Updated existing cache %s", bare_path)
    else:
        bare_path.parent.mkdir(parents=True, exist_ok=True)
        mirror_clone(url, bare_path)
        logger.info("Cached new bare clone %s", bare_path)
    return bare_path


# -------------------------------------------------------------------
# Submodule operations
# -------------------------------------------------------------------

def has_submodule(repo: Path, path: str) -> bool:
    """Check whether a submodule is registered at *path*.

    Reads ``.gitmodules`` to determine whether the submodule exists.
    *path* is resolved relative to *repo*, then compared against
    the repository root so the check works when *repo* is a
    subdirectory of the actual git working tree.
    Returns False if ``.gitmodules`` does not exist.
    """
    try:
        toplevel = Path(
            _run(
                ["rev-parse", "--show-toplevel"], cwd=repo,
            ).stdout.strip()
        )
    except GitError:
        return False
    gitmodules = toplevel / ".gitmodules"
    if not gitmodules.is_file():
        return False
    # Resolve the full path relative to the repo root
    full_path = (repo / path).resolve()
    try:
        rel_path = str(full_path.relative_to(toplevel.resolve()))
    except ValueError:
        return False
    try:
        result = _run(
            ["config", "--file", str(gitmodules),
             "--get-regexp", r"submodule\..*\.path"],
            cwd=toplevel,
        )
    except GitError:
        return False
    for line in result.stdout.splitlines():
        parts = line.split(None, 1)
        if len(parts) == 2 and parts[1] == rel_path:
            return True
    return False


def submodule_add(repo: Path, url: str, path: str) -> None:
    """Add a git submodule at *path* pointing to *url*.

    Equivalent to ``git submodule add <url> <path>`` inside *repo*.
    """
    _run(["submodule", "add", url, path], cwd=repo)
    logger.info("Added submodule %s → %s", url, path)


def submodule_update(repo: Path, path: str) -> None:
    """Fetch and update a submodule to the latest remote HEAD.

    Enters the submodule directory, fetches origin, and checks out
    the latest commit on the remote default branch.
    """
    sub_path = repo / path
    _run(["fetch", "origin"], cwd=sub_path)
    # Determine default branch from remote HEAD
    result = _run(
        ["symbolic-ref", "refs/remotes/origin/HEAD",
         "--short"],
        cwd=sub_path,
    )
    default_branch = result.stdout.strip()
    _run(["checkout", default_branch], cwd=sub_path)
    logger.info("Updated submodule %s to %s", path, default_branch)


def submodule_checkout(repo: Path, path: str, ref: str) -> None:
    """Fetch and checkout a specific ref in a submodule."""
    sub_path = repo / path
    _run(["fetch", "origin"], cwd=sub_path)
    _run(["checkout", ref], cwd=sub_path)
    logger.info("Checked out submodule %s at %s", path, ref)
