"""Git operations: fetch, status, pull-safe with conflict detection."""

from __future__ import annotations

import asyncio
import re
import subprocess
from pathlib import Path

from ..core.models import GitStatus, PullStatus, RepoResult

_REF_PATTERN = re.compile(r"^[a-zA-Z0-9._/@{}\-]+$")
_GIT_SEMAPHORE = asyncio.Semaphore(10)


class GitVersionError(RuntimeError):
    """Raised when the installed git version is below the minimum required."""


def check_git_version(minimum: tuple[int, int] = (2, 38)) -> None:
    """Parse `git --version` and raise GitVersionError if the version is below minimum."""
    result = subprocess.run(
        ["git", "--version"],
        capture_output=True,
        text=True,
        check=True,
    )
    match = re.search(r"(\d+)\.(\d+)", result.stdout)
    if not match:
        raise GitVersionError(f"Could not parse git version from: {result.stdout!r}")
    major, minor = int(match.group(1)), int(match.group(2))
    if (major, minor) < minimum:
        raise GitVersionError(
            f"git {major}.{minor} is too old; minimum required is {minimum[0]}.{minimum[1]}"
        )


def git_status(repo_path: Path) -> GitStatus:
    """Return a GitStatus for the given repo path."""
    # Branch
    branch_result = subprocess.run(
        ["git", "symbolic-ref", "--short", "HEAD"],
        cwd=repo_path,
        capture_output=True,
        text=True,
    )
    branch = branch_result.stdout.strip() if branch_result.returncode == 0 else None

    # Dirty = modified/staged files (untracked files don't block pull)
    porcelain_result = subprocess.run(
        ["git", "status", "--porcelain"],
        cwd=repo_path,
        capture_output=True,
        text=True,
        check=True,
    )
    dirty = any(
        not line.startswith("??")
        for line in porcelain_result.stdout.strip().splitlines()
    )

    # Ahead / behind — returns 0,0 if no upstream
    ahead, behind = 0, 0
    revlist_result = subprocess.run(
        ["git", "rev-list", "--left-right", "--count", "HEAD...@{upstream}"],
        cwd=repo_path,
        capture_output=True,
        text=True,
    )
    if revlist_result.returncode == 0:
        parts = revlist_result.stdout.strip().split()
        if len(parts) == 2:
            ahead, behind = int(parts[0]), int(parts[1])

    # Stale detection: upstream configured but remote branch gone
    stale = False
    if branch:
        remote_cfg = subprocess.run(
            ["git", "config", f"branch.{branch}.remote"],
            cwd=repo_path, capture_output=True, text=True,
        )
        if remote_cfg.returncode == 0 and remote_cfg.stdout.strip():
            remote = remote_cfg.stdout.strip()
            ref_check = subprocess.run(
                ["git", "rev-parse", "--verify", f"refs/remotes/{remote}/{branch}"],
                cwd=repo_path, capture_output=True, text=True,
            )
            stale = ref_check.returncode != 0

    # Remote branches (strip "origin/" prefix)
    refs_result = subprocess.run(
        ["git", "for-each-ref", "--format=%(refname:short)", "refs/remotes/"],
        cwd=repo_path, capture_output=True, text=True,
    )
    remote_branches = None
    if refs_result.returncode == 0:
        remote_branches = [
            ref.split("/", 1)[1]
            for ref in refs_result.stdout.strip().splitlines()
            if "/" in ref and not ref.endswith("/HEAD")
        ]

    return GitStatus(branch=branch, ahead=ahead, behind=behind, dirty=dirty,
                     stale=stale, remote_branches=remote_branches)


def _validate_ref(ref: str) -> None:
    """Raise ValueError if ref contains unexpected characters."""
    if not _REF_PATTERN.match(ref):
        raise ValueError(f"Invalid git ref: {ref!r}")


def ahead_behind_ref(repo_path: Path, ref: str) -> tuple[int, int]:
    """Return (ahead, behind) of HEAD relative to a given ref (e.g. 'origin/main')."""
    _validate_ref(ref)
    result = subprocess.run(
        ["git", "rev-list", "--left-right", "--count", f"HEAD...{ref}"],
        cwd=repo_path,
        capture_output=True,
        text=True,
    )
    if result.returncode == 0:
        parts = result.stdout.strip().split()
        if len(parts) == 2:
            return int(parts[0]), int(parts[1])
    return 0, 0


def _has_conflict(repo_path: Path) -> bool:
    """Return True if merging @{upstream} into HEAD would produce conflicts."""
    result = subprocess.run(
        ["git", "merge-tree", "--write-tree", "HEAD", "@{upstream}"],
        cwd=repo_path,
        capture_output=True,
        text=True,
    )
    return result.returncode != 0


def check_pull_status_sync(repo_path: Path) -> str | None:
    """Non-destructive pull status check (sync — call via asyncio.to_thread).

    Returns:
        "up-to-date" — nothing to pull
        "clean" — can fast-forward or merge cleanly
        "conflict" — merge-tree detected conflicts
        None — no upstream tracking branch or error
    """
    try:
        status = git_status(repo_path)
    except Exception:
        return None

    if status.behind == 0:
        return "up-to-date"

    try:
        if _has_conflict(repo_path):
            return "conflict"
        return "clean"
    except Exception:
        return None


async def fetch_one(repo_path: Path) -> RepoResult:
    """Run `git fetch --all` on one repo asynchronously."""
    repo_name = repo_path.name
    try:
        proc = await asyncio.create_subprocess_exec(
            "git", "fetch", "--all", "--prune",
            cwd=repo_path,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        _, stderr = await proc.communicate()
        if proc.returncode == 0:
            return RepoResult(repo=repo_name, status="ok")
        return RepoResult(repo=repo_name, status="error", message=stderr.decode().strip())
    except Exception as exc:
        return RepoResult(repo=repo_name, status="error", message=str(exc))


async def _bounded_fetch(repo_path: Path) -> RepoResult:
    async with _GIT_SEMAPHORE:
        return await fetch_one(repo_path)


async def fetch_all(repo_paths: list[Path]) -> list[RepoResult]:
    """Fetch all repos concurrently (bounded by _GIT_SEMAPHORE)."""
    return list(await asyncio.gather(*[_bounded_fetch(p) for p in repo_paths]))


def _check_pull_preconditions(repo_path: Path) -> RepoResult | None:
    """Return a skip/error RepoResult if pull should not proceed, or None to continue."""
    repo_name = repo_path.name
    try:
        st = git_status(repo_path)
    except Exception as exc:
        return RepoResult(repo=repo_name, status=PullStatus.ERROR, message=str(exc))

    if st.dirty:
        return RepoResult(repo=repo_name, status=PullStatus.SKIPPED_DIRTY)
    if st.behind == 0 and st.ahead == 0:
        return RepoResult(repo=repo_name, status=PullStatus.ALREADY_UP_TO_DATE)
    if st.ahead > 0 and st.behind == 0:
        return RepoResult(repo=repo_name, status=PullStatus.SKIPPED_AHEAD_ONLY)
    if st.ahead > 0 and st.behind > 0 and _has_conflict(repo_path):
        return RepoResult(repo=repo_name, status=PullStatus.SKIPPED_CONFLICT)
    return None


async def pull_safe_one(repo_path: Path, strategy: str = "merge") -> RepoResult:
    """
    Pull safely:
    1. Skip if dirty.
    2. Skip if already up-to-date or ahead-only.
    3. Skip if diverged and conflict detected.
    4. Execute pull; return UPDATED or ERROR.
    """
    repo_name = repo_path.name
    skip = await asyncio.to_thread(_check_pull_preconditions, repo_path)
    if skip is not None:
        return skip

    pull_flags = ["--rebase"] if strategy == "rebase" else ["--no-rebase"]
    try:
        proc = await asyncio.create_subprocess_exec(
            "git", "pull", *pull_flags,
            cwd=repo_path,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        _, stderr = await proc.communicate()
        if proc.returncode == 0:
            return RepoResult(repo=repo_name, status=PullStatus.UPDATED)
        return RepoResult(
            repo=repo_name,
            status=PullStatus.ERROR,
            message=stderr.decode().strip(),
        )
    except Exception as exc:
        return RepoResult(repo=repo_name, status=PullStatus.ERROR, message=str(exc))


async def _bounded_pull(repo_path: Path, strategy: str) -> RepoResult:
    async with _GIT_SEMAPHORE:
        return await pull_safe_one(repo_path, strategy=strategy)


async def pull_safe_all(
    repo_paths: list[Path], strategy: str = "merge"
) -> list[RepoResult]:
    """Pull all repos concurrently (bounded by _GIT_SEMAPHORE)."""
    return list(
        await asyncio.gather(*[_bounded_pull(p, strategy=strategy) for p in repo_paths])
    )
