"""Git operations for tracking commits and diffs."""

from __future__ import annotations

import subprocess
from pathlib import Path


class GitError(Exception):
    """Error running git command."""

    pass


def run_git(
    *args: str,
    cwd: Path | None = None,
    check: bool = True,
) -> subprocess.CompletedProcess:
    """Run a git command.

    Args:
        *args: Git command arguments.
        cwd: Working directory.
        check: Whether to raise on non-zero exit.

    Returns:
        Completed process with stdout/stderr.

    Raises:
        GitError: If check=True and command fails.
    """
    result = subprocess.run(
        ["git", *args],
        cwd=cwd,
        capture_output=True,
        text=True,
    )

    if check and result.returncode != 0:
        raise GitError(f"git {' '.join(args)} failed: {result.stderr}")

    return result


def get_current_commit(cwd: Path | None = None, short: bool = True) -> str:
    """Get the current HEAD commit SHA.

    Args:
        cwd: Working directory.
        short: If True, return short SHA (7 chars).

    Returns:
        Commit SHA string.
    """
    args = ["rev-parse"]
    if short:
        args.append("--short")
    args.append("HEAD")

    result = run_git(*args, cwd=cwd)
    return result.stdout.strip()


def get_full_commit(short_sha: str, cwd: Path | None = None) -> str | None:
    """Expand a short SHA to full SHA.

    Args:
        short_sha: Short commit SHA.
        cwd: Working directory.

    Returns:
        Full commit SHA, or None if not found.
    """
    result = run_git("rev-parse", short_sha, cwd=cwd, check=False)
    if result.returncode != 0:
        return None
    return result.stdout.strip()


def has_changes_since(
    filepath: str,
    since_commit: str | None,
    cwd: Path | None = None,
) -> bool:
    """Check if a file has changed since a given commit.

    Args:
        filepath: Path to the file (relative to repo root).
        since_commit: Commit SHA to compare against.
        cwd: Working directory.

    Returns:
        True if the file has changes, False otherwise.
    """
    if not since_commit:
        return True  # Never reviewed means "has changes"

    full_commit = get_full_commit(since_commit, cwd=cwd)
    if not full_commit:
        return True  # Can't find commit, assume changes

    result = run_git(
        "diff",
        "--quiet",
        full_commit,
        "HEAD",
        "--",
        filepath,
        cwd=cwd,
        check=False,
    )

    return result.returncode != 0  # Non-zero means changes exist


def get_remote_url(cwd: Path | None = None) -> str | None:
    """Get the origin remote URL.

    Args:
        cwd: Working directory.

    Returns:
        Remote URL or None.
    """
    result = run_git("remote", "get-url", "origin", cwd=cwd, check=False)
    if result.returncode != 0:
        return None
    return result.stdout.strip()


def get_authenticated_push_url(cwd: Path | None = None) -> str | None:
    """Build an authenticated push URL for HTTPS remotes using GitLab token.

    For SSH remotes, returns None (SSH keys handle auth).
    For HTTPS remotes, embeds the token as oauth2:TOKEN in the URL.

    Args:
        cwd: Working directory.

    Returns:
        Authenticated URL, or None if not needed or not possible.
    """
    import os

    url = get_remote_url(cwd=cwd)
    if not url or not url.startswith("https://"):
        return None  # SSH or no remote — no URL rewriting needed

    token = (
        os.environ.get("GITLAB_PRIVATE_TOKEN")
        or os.environ.get("GITLAB_TOKEN")
        or os.environ.get("CI_JOB_TOKEN")
    )
    if not token:
        return None

    # https://host/path → https://oauth2:TOKEN@host/path
    return url.replace("https://", f"https://oauth2:{token}@", 1)


def get_gitlab_project(cwd: Path | None = None) -> str | None:
    """Extract GitLab project path from remote URL.

    Args:
        cwd: Working directory.

    Returns:
        Project path (e.g., "user/project") or None.
    """
    url = get_remote_url(cwd=cwd)
    if not url:
        return None

    # Handle SSH: git@git.ligo.org:user/project.git
    if url.startswith("git@"):
        path = url.split(":")[-1]
    # Handle HTTPS: https://git.ligo.org/user/project.git
    elif "://" in url:
        path = "/".join(url.split("/")[-2:])
    else:
        return None

    return path.removesuffix(".git")


def get_gitlab_host(cwd: Path | None = None) -> str | None:
    """Extract GitLab host from remote URL.

    Args:
        cwd: Working directory.

    Returns:
        Host (e.g., "git.ligo.org") or None.
    """
    url = get_remote_url(cwd=cwd)
    if not url:
        return None

    # Handle SSH: git@git.ligo.org:user/project.git
    if url.startswith("git@"):
        # Extract host between @ and :
        host_and_path = url.split("@", 1)[1]
        colon_parts = host_and_path.split(":", 1)
        return colon_parts[0] if colon_parts[0] else None
    # Handle HTTPS: https://git.ligo.org/user/project.git
    elif "://" in url:
        parts = url.split("/")
        return parts[2] if len(parts) > 2 else None
    else:
        return None


def get_current_branch(cwd: Path | None = None) -> str:
    """Get the current branch name.

    Args:
        cwd: Working directory.

    Returns:
        Branch name string.

    Raises:
        GitError: If in detached HEAD state or git command fails.
    """
    result = run_git("rev-parse", "--abbrev-ref", "HEAD", cwd=cwd)
    branch = result.stdout.strip()
    if branch == "HEAD":
        raise GitError("Detached HEAD state - not on a branch")
    return branch


def create_branch(branch_name: str, cwd: Path | None = None) -> None:
    """Create and checkout a new branch.

    Args:
        branch_name: Name of the branch to create.
        cwd: Working directory.

    Raises:
        GitError: If branch creation fails (e.g., already exists).
    """
    run_git("checkout", "-b", branch_name, cwd=cwd)


def checkout_branch(branch_name: str, cwd: Path | None = None) -> None:
    """Checkout an existing branch.

    Args:
        branch_name: Name of the branch to checkout.
        cwd: Working directory.

    Raises:
        GitError: If checkout fails.
    """
    run_git("checkout", branch_name, cwd=cwd)


def commit_all_changes(message: str, cwd: Path | None = None) -> str:
    """Stage modifications to tracked files and commit.

    Only stages changes to already-tracked files (git add -u).
    Untracked files are intentionally excluded to avoid
    accidentally committing secrets or unrelated files.

    Args:
        message: Commit message.
        cwd: Working directory.

    Returns:
        The commit SHA (short) of the new commit.

    Raises:
        GitError: If there are no changes to commit or commit fails.
    """
    run_git("add", "-u", cwd=cwd)

    # Check if there are staged changes
    result = run_git("diff", "--cached", "--quiet", cwd=cwd, check=False)
    if result.returncode == 0:
        raise GitError("No changes to commit")

    run_git("commit", "-m", message, cwd=cwd)
    return get_current_commit(cwd=cwd)


def push_branch(
    branch_name: str,
    cwd: Path | None = None,
    remote: str = "origin",
) -> None:
    """Push a branch to a remote with upstream tracking.

    Args:
        branch_name: Name of the branch to push.
        cwd: Working directory.
        remote: Remote name or URL to push to.

    Raises:
        GitError: If push fails.
    """
    run_git("push", "-u", remote, branch_name, cwd=cwd)


def has_uncommitted_changes(cwd: Path | None = None) -> bool:
    """Check if the working tree has uncommitted changes.

    Args:
        cwd: Working directory.

    Returns:
        True if there are uncommitted changes, False otherwise.
    """
    result = run_git("status", "--porcelain", "-uno", cwd=cwd)
    return bool(result.stdout.strip())


def count_lines(filepath: str, cwd: Path | None = None) -> int:
    """Count the number of lines in a file.

    Args:
        filepath: Path to the file.
        cwd: Working directory.

    Returns:
        Number of lines, or 0 if file doesn't exist.
    """
    full_path = (cwd or Path.cwd()) / filepath
    if not full_path.exists():
        return 0

    with open(full_path) as f:
        return sum(1 for _ in f)
