"""Git utility functions for MergeGuide hooks.

This module provides utility functions for interacting with git repositories,
primarily used by the hooks subsystem to query repository state and file
information during pre-commit and pre-push hooks.

All functions execute git commands via subprocess and handle errors
consistently using the GitError exception class.

Functions:
    is_git_repo: Check if current directory is in a git repository
    get_repo_root: Get the repository root directory path
    get_git_hooks_dir: Get the .git/hooks directory path
    get_staged_files: Get list of files staged for commit
    get_push_range_files: Get files changed between commits
    get_current_branch: Get the current branch name
    get_user_email: Get the configured git user email

Example Usage:
    >>> from mergeguide.hooks.git_utils import is_git_repo, get_staged_files
    >>> if is_git_repo():
    ...     staged = get_staged_files()
    ...     print(f"Staged files: {staged}")

Note:
    These functions require git to be installed and available in PATH.
    Operations on non-git directories will raise GitError.
"""

from __future__ import annotations

import subprocess
from pathlib import Path


class GitError(Exception):
    """Exception raised when a git command fails.

    This exception wraps subprocess errors from git commands to provide
    a consistent error handling interface. The error message typically
    includes the stderr output from the failed git command.

    Attributes:
        message: Human-readable description of the error.
    """

    pass


def is_git_repo() -> bool:
    """Check if the current directory is within a git repository.

    Uses `git rev-parse --git-dir` to determine if we're inside a
    git repository. This works from any subdirectory of the repo.

    Returns:
        True if the current directory is in a git repository,
        False otherwise (including if git is not installed).

    Example:
        >>> import os
        >>> os.chdir("/tmp")
        >>> is_git_repo()
        False
        >>> os.chdir("/path/to/git/repo")
        >>> is_git_repo()
        True
    """
    try:
        subprocess.run(
            ["git", "rev-parse", "--git-dir"],
            capture_output=True,
            check=True,
        )
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False


def get_repo_root() -> Path:
    """Get the root directory of the current git repository.

    Uses `git rev-parse --show-toplevel` to find the repository root.
    This is useful for resolving relative paths to absolute paths.

    Returns:
        Path object pointing to the repository root directory.

    Raises:
        GitError: If not in a git repository, git is not installed,
            or the git command fails for any other reason.

    Example:
        >>> root = get_repo_root()
        >>> print(root)
        PosixPath('/home/user/projects/my-repo')
    """
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            capture_output=True,
            text=True,
            check=True,
        )
        return Path(result.stdout.strip())
    except subprocess.CalledProcessError as e:
        raise GitError(f"Failed to get repo root: {e.stderr}") from e
    except FileNotFoundError as e:
        raise GitError("Git is not installed or not in PATH") from e


def get_git_hooks_dir() -> Path:
    """Get the path to the git hooks directory.

    Uses `git rev-parse --git-dir` to find the .git directory, then
    appends "hooks" to get the hooks directory path. This handles
    worktrees and non-standard git directory locations correctly.

    Returns:
        Path object pointing to the hooks directory (typically .git/hooks).

    Raises:
        GitError: If not in a git repository, git is not installed,
            or the git command fails for any other reason.

    Example:
        >>> hooks_dir = get_git_hooks_dir()
        >>> print(hooks_dir)
        PosixPath('/home/user/projects/my-repo/.git/hooks')
    """
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--git-dir"],
            capture_output=True,
            text=True,
            check=True,
        )
        return Path(result.stdout.strip()) / "hooks"
    except subprocess.CalledProcessError as e:
        raise GitError(f"Failed to get git directory: {e.stderr}") from e
    except FileNotFoundError as e:
        raise GitError("Git is not installed or not in PATH") from e


def get_staged_files() -> list[Path]:
    """Get list of files staged for commit.

    Returns files that have been added to the git index (staged area)
    and are ready for commit. Only includes Added, Copied, and Modified
    files - deleted files are excluded since there's no content to check.

    This is primarily used by the pre-commit hook to determine which
    files need policy validation.

    Returns:
        List of Path objects for staged files, relative to repo root.
        Empty list if no files are staged.

    Raises:
        GitError: If not in a git repository, git is not installed,
            or the git command fails.

    Example:
        >>> staged = get_staged_files()
        >>> for file in staged:
        ...     print(f"Staged: {file}")
        Staged: src/main.py
        Staged: tests/test_main.py
    """
    try:
        # --diff-filter=ACM: Added, Copied, Modified (exclude Deleted)
        result = subprocess.run(
            ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
            capture_output=True,
            text=True,
            check=True,
        )
        files = result.stdout.strip().split("\n")
        return [Path(f) for f in files if f]
    except subprocess.CalledProcessError as e:
        raise GitError(f"Failed to get staged files: {e.stderr}") from e
    except FileNotFoundError as e:
        raise GitError("Git is not installed or not in PATH") from e


def get_push_range_files(remote_sha: str, local_sha: str) -> list[Path]:
    """Get files changed between remote and local commits.

    Determines which files were modified in the commits being pushed.
    This is used by the pre-push hook to validate only the files that
    are actually being changed on the remote.

    Handles two scenarios:
    1. New branch (remote_sha is all zeros): Lists files in the commit tree
    2. Existing branch: Lists files changed between remote and local

    Args:
        remote_sha: The SHA of the remote branch tip, or 40 zeros if
            this is a new branch being pushed for the first time.
        local_sha: The SHA of the local commit being pushed.

    Returns:
        List of Path objects for changed files, relative to repo root.
        Empty list if no files changed.

    Raises:
        GitError: If git is not installed or the command fails.

    Example:
        >>> # Existing branch - get files changed since remote
        >>> files = get_push_range_files("abc123", "def456")
        >>> # New branch - get all files in the commit
        >>> files = get_push_range_files("0" * 40, "def456")
    """
    try:
        # Handle new branch case where remote SHA is all zeros
        if remote_sha == "0" * 40:
            # New branch: list all files in the commit tree
            result = subprocess.run(
                ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", local_sha],
                capture_output=True,
                text=True,
                check=True,
            )
        else:
            # Existing branch: diff between remote and local
            result = subprocess.run(
                ["git", "diff", "--name-only", f"{remote_sha}..{local_sha}"],
                capture_output=True,
                text=True,
                check=True,
            )
        files = result.stdout.strip().split("\n")
        return [Path(f) for f in files if f]
    except subprocess.CalledProcessError as e:
        raise GitError(f"Failed to get push range files: {e.stderr}") from e
    except FileNotFoundError as e:
        raise GitError("Git is not installed or not in PATH") from e


def get_current_branch() -> str:
    """Get the name of the currently checked out branch.

    Uses `git rev-parse --abbrev-ref HEAD` to get a human-readable
    branch name. Returns "HEAD" if in detached HEAD state.

    Returns:
        The current branch name (e.g., "main", "feature/new-api"),
        or "HEAD" if in detached HEAD state.

    Raises:
        GitError: If not in a git repository, git is not installed,
            or the git command fails.

    Example:
        >>> branch = get_current_branch()
        >>> print(f"On branch: {branch}")
        On branch: main
    """
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            capture_output=True,
            text=True,
            check=True,
        )
        return result.stdout.strip()
    except subprocess.CalledProcessError as e:
        raise GitError(f"Failed to get current branch: {e.stderr}") from e
    except FileNotFoundError as e:
        raise GitError("Git is not installed or not in PATH") from e


def get_user_email() -> str:
    """Get the configured git user email address.

    Retrieves the user.email configuration value. This is useful for
    audit trails to identify who performed an action.

    Returns:
        The configured email address as a string, or empty string
        if user.email is not configured.

    Note:
        Unlike other functions in this module, this function does not
        raise GitError when git is not available or the config is not
        set. It returns an empty string instead for convenience.

    Example:
        >>> email = get_user_email()
        >>> if email:
        ...     print(f"User: {email}")
        ... else:
        ...     print("Email not configured")
    """
    try:
        result = subprocess.run(
            ["git", "config", "user.email"],
            capture_output=True,
            text=True,
            check=True,
        )
        return result.stdout.strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return ""
