"""Safe wrapper around git CLI.

Provides read-only git operations: clone, tag listing, checkout,
diff, log. Used for fetching CloudSense source repos and performing
version-to-version diff analysis.
"""

import logging
import subprocess
from pathlib import Path

logger = logging.getLogger(__name__)


class GitCliError(Exception):
    """Raised when a git command fails."""

    def __init__(self, message: str, command: str, exit_code: int, stderr: str):
        super().__init__(message)
        self.command = command
        self.exit_code = exit_code
        self.stderr = stderr


def run_git(
    args: list[str],
    *,
    cwd: Path | str | None = None,
    timeout: int = 300,
) -> str:
    """Run a git command and return stdout.

    Args:
        args: Git arguments (without the 'git' prefix).
        cwd: Working directory.
        timeout: Timeout in seconds.

    Returns:
        Stripped stdout string.

    Raises:
        GitCliError on any failure.
    """
    cmd = ["git"] + args
    logger.info("Running: %s (cwd=%s)", " ".join(cmd), cwd or ".")

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            cwd=str(cwd) if cwd else None,
            timeout=timeout,
        )
    except subprocess.TimeoutExpired:
        raise GitCliError(
            f"git command timed out after {timeout}s",
            command=" ".join(cmd),
            exit_code=-1,
            stderr="TimeoutExpired",
        )
    except FileNotFoundError:
        raise GitCliError(
            "git is not installed or not on PATH.",
            command=" ".join(cmd),
            exit_code=-1,
            stderr="FileNotFoundError",
        )

    if result.returncode != 0:
        stderr = result.stderr.strip() or result.stdout.strip()
        raise GitCliError(
            f"git command failed (exit {result.returncode}): {stderr}",
            command=" ".join(cmd),
            exit_code=result.returncode,
            stderr=stderr,
        )

    return result.stdout.strip()


def clone_shallow(
    url: str,
    dest: Path,
    *,
    branch: str | None = None,
    depth: int = 1,
    timeout: int = 300,
) -> Path:
    """Shallow-clone a repo."""
    args = ["clone", "--depth", str(depth)]
    if branch:
        args.extend(["--branch", branch])
    args.extend([url, str(dest)])
    run_git(args, timeout=timeout)
    return dest


def clone_sparse(
    url: str,
    dest: Path,
    files: list[str],
    *,
    timeout: int = 120,
) -> Path:
    """Clone a repo and check out only specific files (minimal disk use)."""
    run_git(
        ["clone", "--depth", "1", "--filter=blob:none",
         "--no-checkout", url, str(dest)],
        timeout=timeout,
    )
    run_git(
        ["checkout", "HEAD", "--"] + files,
        cwd=dest,
        timeout=30,
    )
    return dest


def list_tags(
    repo_dir: Path, pattern: str | None = None
) -> list[str]:
    """List tags, optionally filtered by glob pattern."""
    args = ["tag", "--list"]
    if pattern:
        args.append(pattern)
    output = run_git(args, cwd=repo_dir)
    if not output:
        return []
    return output.splitlines()


def checkout(repo_dir: Path, ref: str) -> None:
    """Check out a specific ref (tag, branch, commit)."""
    run_git(["checkout", ref], cwd=repo_dir)


def diff_stat(
    repo_dir: Path, from_ref: str, to_ref: str
) -> str:
    """Return diff --stat between two refs."""
    return run_git(["diff", "--stat", from_ref, to_ref], cwd=repo_dir)


def diff_name_status(
    repo_dir: Path, from_ref: str, to_ref: str
) -> list[dict[str, str]]:
    """Return file-level changes (added/modified/deleted) between refs."""
    output = run_git(
        ["diff", "--name-status", from_ref, to_ref],
        cwd=repo_dir,
    )
    changes = []
    for line in output.splitlines():
        parts = line.split("\t", 1)
        if len(parts) == 2:
            changes.append({"status": parts[0], "file": parts[1]})
    return changes
