"""Utilities for detecting changed line ranges from git diffs."""

from __future__ import annotations

import logging
import re
import subprocess
from dataclasses import dataclass, field

logger = logging.getLogger(__name__)


@dataclass
class ChangedLineRange:
    """A contiguous range of changed lines in a file (1-indexed, inclusive)."""

    start: int
    end: int


@dataclass
class ChangedFile:
    """A file changed in a git diff, with its changed line ranges."""

    path: str
    changed_ranges: list[ChangedLineRange] = field(default_factory=list)


def _resolve_base_ref(ref: str, cwd: str | None = None) -> str:
    """Ensure *ref* is available locally, fetching from origin if needed.

    Returns the ref string to use in git commands (may be ``origin/<ref>``).
    """
    # Already a remote ref or commit hash — use as-is
    if ref.startswith("origin/") or ref.startswith("refs/"):
        return ref

    # Try to fetch the branch from origin so it's available locally
    try:
        subprocess.run(
            ["git", "fetch", "--no-tags", "origin", f"refs/heads/{ref}:refs/remotes/origin/{ref}"],
            capture_output=True,
            text=True,
            cwd=cwd,
            check=True,
        )
        return f"origin/{ref}"
    except (subprocess.CalledProcessError, FileNotFoundError):
        # Fetch failed — fall back to using the ref as-is;
        # the diff command will report the real error if it's missing.
        return ref


def get_changed_line_ranges(
    base_ref: str, file_paths: list[str], cwd: str | None = None
) -> dict[str, list[ChangedLineRange]]:
    """Return changed line ranges for specific files relative to a base ref.

    Uses ``git diff -U0 base_ref...HEAD -- <files>`` to get only the hunks
    for the requested files.  Returns a dict mapping relative file path to
    its list of changed line ranges.
    """
    if not file_paths:
        return {}

    # Ensure the base ref is available locally (e.g. in CI shallow clones)
    resolved_ref = _resolve_base_ref(base_ref, cwd=cwd)

    try:
        result = subprocess.run(
            ["git", "diff", "-U0", f"{resolved_ref}...HEAD", "--"] + file_paths,
            capture_output=True,
            text=True,
            cwd=cwd,
            check=True,
        )
    except subprocess.CalledProcessError as exc:
        logger.warning(
            "git diff -U0 failed: %s. Changed-function detection is disabled — "
            "all functions in changed files will be treated as changed. "
            "Fix: add 'fetch-depth: 0' to your checkout step in the workflow.",
            exc.stderr.strip(),
        )
        return {}

    parsed = _parse_unified_diff(result.stdout)
    return {cf.path: cf.changed_ranges for cf in parsed}


# Matches hunk headers like @@ -a,b +c,d @@
_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")


def _parse_unified_diff(diff_output: str) -> list[ChangedFile]:
    """Parse ``git diff -U0`` output into ChangedFile objects."""
    files: list[ChangedFile] = []
    current_file: ChangedFile | None = None

    for line in diff_output.splitlines():
        # New file header: +++ b/path/to/file
        if line.startswith("+++ b/"):
            path = line[6:]  # strip "+++ b/"
            current_file = ChangedFile(path=path)
            files.append(current_file)
            continue

        if current_file is None:
            continue

        m = _HUNK_RE.match(line)
        if m:
            start = int(m.group(1))
            count = int(m.group(2)) if m.group(2) is not None else 1
            if count == 0:
                # Pure deletion hunk — no new lines added
                continue
            current_file.changed_ranges.append(ChangedLineRange(start=start, end=start + count - 1))

    return files


def ranges_overlap(func_start: int, func_end: int, changed_ranges: list[ChangedLineRange]) -> bool:
    """Check if a function's line span overlaps any changed line range."""
    return any(r.start <= func_end and r.end >= func_start for r in changed_ranges)
