"""Change detection for GuardRail."""

import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple


@dataclass
class ChangedFile:
    """Represents a changed Python file with its modified line ranges."""

    path: Path
    changed_ranges: List[Tuple[int, int]]  # List of (start_line, end_line) tuples

    def __str__(self) -> str:
        ranges_str = ", ".join(f"{start}-{end}" for start, end in self.changed_ranges)
        return f"{self.path} (lines: {ranges_str})"


class ChangeDetector:
    """Detects changed files using git diff."""

    def __init__(self, root: Path):
        """
        Initialize change detector.

        Args:
            root: Project root directory (should be git repository root).
        """
        self.root = Path(root).resolve()

    def is_git_repo(self) -> bool:
        """
        Check if the root directory is a git repository.

        Returns:
            True if it's a git repo, False otherwise.
        """
        git_dir = self.root / ".git"
        return git_dir.exists() or git_dir.is_dir()

    def get_changed_python_files(self) -> List[ChangedFile]:
        """
        Get list of changed Python files with their line ranges.

        Returns:
            List of ChangedFile objects representing changed .py files.

        Raises:
            RuntimeError: If not in a git repository or git command fails.
        """
        if not self.is_git_repo():
            raise RuntimeError(
                f"Not a git repository: {self.root}. GuardRail requires a git repository for change detection."
            )

        # Get unstaged changes (working directory vs HEAD)
        diff_output = self._run_git_diff()
        return self._parse_diff_output(diff_output)

    def _run_git_diff(self) -> str:
        """
        Run git diff to get unstaged changes.

        Returns:
            Git diff output as string.

        Raises:
            RuntimeError: If git command fails.
        """
        try:
            result = subprocess.run(
                ["git", "diff", "--unified=0", "HEAD"],
                cwd=self.root,
                capture_output=True,
                text=True,
                check=True,
            )
            return result.stdout
        except subprocess.CalledProcessError as e:
            raise RuntimeError(f"Git diff failed: {e.stderr}") from e
        except FileNotFoundError:
            raise RuntimeError("Git is not installed or not in PATH.")

    def _parse_diff_output(self, diff_output: str) -> List[ChangedFile]:
        """
        Parse git diff output to extract changed files and line ranges.

        Git diff format:
        diff --git a/path/to/file.py b/path/to/file.py
        index ...
        --- a/path/to/file.py
        +++ b/path/to/file.py
        @@ -start,count +start,count @@
        ...

        Args:
            diff_output: Raw git diff output.

        Returns:
            List of ChangedFile objects.
        """
        if not diff_output.strip():
            return []

        changed_files: List[ChangedFile] = []
        current_file: Optional[Path] = None
        current_ranges: List[Tuple[int, int]] = []

        lines = diff_output.split("\n")
        i = 0

        while i < len(lines):
            line = lines[i]

            # Match: diff --git a/path b/path
            diff_match = re.match(r"^diff --git a/(.+?) b/(.+?)$", line)
            if diff_match:
                # Save previous file if exists
                if current_file is not None and current_ranges:
                    changed_files.append(
                        ChangedFile(path=current_file, changed_ranges=current_ranges)
                    )

                # Start new file
                file_path = diff_match.group(2)  # Use 'b' path (new version)
                current_file = self.root / file_path
                current_ranges = []
                i += 1
                continue

            # Match: @@ -old_start,old_count +new_start,new_count @@
            # We care about the new_start and new_count (the + side)
            hunk_match = re.match(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line)
            if hunk_match and current_file is not None:
                old_start = int(hunk_match.group(1))
                old_count = int(hunk_match.group(2)) if hunk_match.group(2) else 1
                new_start = int(hunk_match.group(3))
                new_count = int(hunk_match.group(4)) if hunk_match.group(4) else 1

                # We track the new file's line ranges (what was added/modified)
                # For deletions, new_count might be 0, so we still track the context
                if new_count > 0:
                    end_line = new_start + new_count - 1
                    current_ranges.append((new_start, end_line))
                elif old_count > 0:
                    # Pure deletion - track the line where deletion happened
                    current_ranges.append((new_start, new_start))

            i += 1

        # Save last file
        if current_file is not None and current_ranges:
            changed_files.append(ChangedFile(path=current_file, changed_ranges=current_ranges))

        # Filter for Python files only
        python_files = [
            cf for cf in changed_files if cf.path.suffix == ".py" and cf.path.exists()
        ]

        return python_files

