"""Git operations for the delivery pipeline."""

from __future__ import annotations

import subprocess
from pathlib import Path

from loguru import logger

from message_delivery.errors import GitError


class GitOperations:
    """Perform git operations needed by the delivery pipeline."""

    def __init__(self) -> None:
        self._project_root_cache: Path | None = None

    def find_project_root(self, start: Path | None = None) -> Path:
        """Return the top-level directory of the current git repo.

        *start* optionally specifies the directory to search from (defaults
        to the current working directory).  Caches the result.  Raises
        :class:`GitError` if not inside a git repository.
        """
        if self._project_root_cache is not None:
            return self._project_root_cache

        try:
            result = subprocess.run(
                ["git", "rev-parse", "--show-toplevel"],
                capture_output=True,
                text=True,
                check=True,
                cwd=str(start) if start is not None else None,
            )
        except subprocess.CalledProcessError as exc:
            raise GitError(f"Not in a git repository: {exc.stderr.strip() or exc}") from exc
        except FileNotFoundError as exc:
            raise GitError("git executable not found") from exc

        toplevel = result.stdout.strip()
        if not toplevel:
            raise GitError("git rev-parse returned an empty path")

        root = Path(toplevel)
        self._project_root_cache = root
        return root

    def commit(self, path: Path, message: str) -> bool:
        """Stage *path* and commit with *message*.

        Returns ``True`` if a commit was created, ``False`` if there was
        nothing to commit.  Raises :class:`GitError` for other failures.
        """
        path_str = str(path)
        cwd = str(path.parent) if path.parent.exists() else None
        try:
            subprocess.run(
                ["git", "add", path_str],
                capture_output=True,
                text=True,
                check=True,
                cwd=cwd,
            )
        except subprocess.CalledProcessError as exc:
            raise GitError(f"git add failed: {exc.stderr.strip() or exc}") from exc
        except FileNotFoundError as exc:
            raise GitError("git executable not found") from exc

        try:
            result = subprocess.run(
                ["git", "commit", "-m", message],
                capture_output=True,
                text=True,
                cwd=cwd,
            )
        except FileNotFoundError as exc:
            raise GitError("git executable not found") from exc

        # Nothing to commit — git exits with code 1 and prints a message
        # like "nothing to commit, working tree clean".
        if result.returncode == 1:
            combined = (result.stdout + result.stderr).lower()
            if "nothing to commit" in combined or "no changes added" in combined:
                logger.debug("Nothing to commit for {}", path_str)
                return False

        if result.returncode != 0:
            raise GitError(
                f"git commit failed: {result.stderr.strip() or result.stdout.strip() or result}"
            )

        return True

    def push(self) -> None:
        """Run ``git push``.

        Logs a warning if there is no remote configured.  Raises
        :class:`GitError` if the push fails.
        """
        cwd = str(self._project_root_cache) if self._project_root_cache is not None else None

        # Check for a remote first so we can warn rather than error.
        try:
            remote_result = subprocess.run(
                ["git", "remote"],
                capture_output=True,
                text=True,
                check=True,
                cwd=cwd,
            )
        except subprocess.CalledProcessError as exc:
            raise GitError(f"git remote failed: {exc.stderr.strip() or exc}") from exc
        except FileNotFoundError as exc:
            raise GitError("git executable not found") from exc

        if not remote_result.stdout.strip():
            logger.warning("No git remote configured; skipping push.")
            return

        try:
            result = subprocess.run(
                ["git", "push"],
                capture_output=True,
                text=True,
                cwd=cwd,
            )
        except FileNotFoundError as exc:
            raise GitError("git executable not found") from exc

        if result.returncode != 0:
            raise GitError(
                f"git push failed: {result.stderr.strip() or result.stdout.strip() or result}"
            )
