"""repo_sync.py — T04 Push Seal: closed-loop git sync for consistency runs.

The **push-seal invariant**
---------------------------
Every ``fix_repo()`` run that creates commits must push them to remote before
returning.  This guarantees the next run starts with ``local ≡ remote``,
making the 15-minute timer idempotent and preventing the open-loop commit
pile-up that occurs when automated writebacks accumulate without being pushed.

Three checks enforce the invariant end-to-end:

  C-16  repo-behind-remote      local is behind remote; skip writes, pull first
  C-17  repo-ahead-push-failed  local has unpushed commits and push failed; skip writes
  T04   push-seal               at end of every fix run, push all commits made this run

Stable state the timer converges to
-------------------------------------
  repo clean AND local ≡ remote
  → ``_report_needs_action`` returns False
  → repo skipped entirely
  → no git activity, no divergence

Public API
----------
pull_ff(repo_path)         git pull --ff-only  →  (ok: bool, message: str)
push_ff(repo_path)         git push --ff-only  →  (ok: bool, message: str)
count_remote_ahead(path)   commits remote has that local lacks  (C-16 input)
count_local_ahead(path)    commits local has that remote lacks  (C-17 / T04 input)

All functions are **best-effort**: errors return 0 / (False, "…") rather than
raising, so the consistency engine is never blocked by transient network issues.
The caller decides whether a 0 / False result is a problem.
"""
from __future__ import annotations

import subprocess


def pull_ff(repo_path: str) -> tuple[bool, str]:
    """Run ``git pull --ff-only`` on *repo_path*.

    Returns ``(success, message)`` describing the outcome.  Never raises.
    """
    try:
        result = subprocess.run(
            ["git", "-C", repo_path, "pull", "--ff-only"],
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode == 0:
            return True, result.stdout.strip() or "already up to date"
        return False, result.stderr.strip() or "pull failed"
    except Exception as exc:
        return False, str(exc)


def push_ff(repo_path: str) -> tuple[bool, str]:
    """Run ``git push`` on *repo_path*.

    ``git push`` already rejects non-fast-forward updates by default (the
    remote refuses them), so no ``--force`` flag is needed or used.  The
    function name signals the *semantic* guarantee — we never force-push —
    not a literal CLI flag.

    Returns ``(success, message)`` describing the outcome.  Never raises.
    """
    try:
        result = subprocess.run(
            ["git", "-C", repo_path, "push"],
            capture_output=True, text=True, timeout=30,
        )
        if result.returncode == 0:
            return True, result.stdout.strip() or "pushed"
        return False, result.stderr.strip() or "push failed"
    except Exception as exc:
        return False, str(exc)


def count_remote_ahead(repo_path: str) -> int:
    """Return how many commits remote has that local lacks (the *behind* count).

    Runs ``git fetch --quiet origin`` first so the result reflects the actual
    current remote state, not a stale tracking branch.  Returns 0 on any
    error (network down, no upstream configured, etc.) so C-16 is never
    spuriously triggered by connectivity issues.
    """
    try:
        subprocess.run(
            ["git", "-C", repo_path, "fetch", "--quiet", "origin"],
            capture_output=True, timeout=15,
        )
        result = subprocess.run(
            ["git", "-C", repo_path, "rev-list", "--count", "HEAD..@{u}"],
            capture_output=True, text=True, timeout=5,
        )
        if result.returncode != 0:
            return 0
        return int(result.stdout.strip() or "0")
    except Exception:
        return 0


def count_local_ahead(repo_path: str) -> int:
    """Return how many commits local has that remote lacks (the *ahead* count).

    Uses the already-fetched tracking branch — call after ``count_remote_ahead``
    (which runs fetch) for the most accurate result, or accept a one-run lag.
    Returns 0 on any error so C-17 is never spuriously triggered.
    """
    try:
        result = subprocess.run(
            ["git", "-C", repo_path, "rev-list", "--count", "@{u}..HEAD"],
            capture_output=True, text=True, timeout=5,
        )
        if result.returncode != 0:
            return 0
        return int(result.stdout.strip() or "0")
    except Exception:
        return 0
