"""Git diff analyzer for change categorization and summarization.

This module provides tools for analyzing git diffs, categorizing changes,
and generating meaningful summaries for commit messages.
"""

from __future__ import annotations

import re
import subprocess
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Optional


class ChangeType(Enum):
    """Type of change detected in a file."""

    ADDED = "added"
    MODIFIED = "modified"
    DELETED = "deleted"
    RENAMED = "renamed"
    COPIED = "copied"


class ChangeCategory(Enum):
    """Category of changes for commit message generation."""

    FEATURE = "feat"  # New feature
    FIX = "fix"  # Bug fix
    REFACTOR = "refactor"  # Code refactoring
    DOCS = "docs"  # Documentation
    STYLE = "style"  # Formatting, missing semicolons, etc.
    TEST = "test"  # Adding or modifying tests
    CHORE = "chore"  # Maintenance tasks
    PERF = "perf"  # Performance improvements
    BUILD = "build"  # Build system changes
    CI = "ci"  # CI configuration


@dataclass
class DiffHunk:
    """Represents a single hunk in a diff."""

    old_start: int
    old_count: int
    new_start: int
    new_count: int
    header: str
    lines: list[str] = field(default_factory=list)
    additions: int = 0
    deletions: int = 0


@dataclass
class FileChange:
    """Represents changes to a single file."""

    file_path: str
    change_type: ChangeType
    old_path: Optional[str] = None  # For renames
    hunks: list[DiffHunk] = field(default_factory=list)
    additions: int = 0
    deletions: int = 0
    binary: bool = False

    @property
    def total_changes(self) -> int:
        """Total number of changed lines."""
        return self.additions + self.deletions


@dataclass
class ChangeAnalysis:
    """Analysis result of all changes."""

    files: list[FileChange] = field(default_factory=list)
    category: ChangeCategory = ChangeCategory.CHORE
    scope: Optional[str] = None
    summary: str = ""
    breaking: bool = False

    @property
    def total_additions(self) -> int:
        return sum(f.additions for f in self.files)

    @property
    def total_deletions(self) -> int:
        return sum(f.deletions for f in self.files)

    @property
    def total_files(self) -> int:
        return len(self.files)


class DiffAnalyzer:
    """Analyzes git diffs for commit message generation."""

    # File patterns for categorization
    CATEGORY_PATTERNS = {
        ChangeCategory.TEST: [
            r"test[s]?/",
            r".*_test\.(py|js|ts|cpp|c)$",
            r".*\.test\.(js|ts|tsx|jsx)$",
            r".*\.spec\.(js|ts|tsx|jsx)$",
            r"conftest\.py$",
        ],
        ChangeCategory.DOCS: [
            r"README",
            r"CHANGELOG",
            r"\.md$",
            r"\.rst$",
            r"docs/",
            r"documentation/",
        ],
        ChangeCategory.BUILD: [
            r"Makefile$",
            r"CMakeLists\.txt$",
            r"setup\.py$",
            r"pyproject\.toml$",
            r"package\.json$",
            r"\.gradle$",
            r"pom\.xml$",
        ],
        ChangeCategory.CI: [
            r"\.github/",
            r"\.gitlab-ci",
            r"\.travis",
            r"Jenkinsfile",
            r"\.circleci/",
        ],
        ChangeCategory.STYLE: [
            r"\.eslintrc",
            r"\.prettierrc",
            r"\.editorconfig",
            r"\.flake8",
            r"ruff\.toml",
        ],
    }

    # Keywords in diff content for categorization
    CONTENT_KEYWORDS = {
        ChangeCategory.FIX: ["fix", "bug", "issue", "error", "crash", "problem"],
        ChangeCategory.FEATURE: ["add", "new", "feature", "implement", "create"],
        ChangeCategory.REFACTOR: ["refactor", "restructure", "reorganize", "clean"],
        ChangeCategory.PERF: ["performance", "optimize", "speed", "fast", "cache"],
    }

    def __init__(self, repo_path: str | Path | None = None):
        """Initialize the diff analyzer.

        Args:
            repo_path: Path to the git repository. Defaults to current directory.
        """
        self.repo_path = Path(repo_path) if repo_path else Path.cwd()

    def get_staged_diff(self) -> str:
        """Get diff of staged changes.

        Returns:
            The git diff output for staged changes.
        """
        result = subprocess.run(
            ["git", "diff", "--cached", "--no-color"],
            cwd=self.repo_path,
            capture_output=True,
            text=True,
        )
        return result.stdout

    def get_unstaged_diff(self) -> str:
        """Get diff of unstaged changes.

        Returns:
            The git diff output for unstaged changes.
        """
        result = subprocess.run(
            ["git", "diff", "--no-color"],
            cwd=self.repo_path,
            capture_output=True,
            text=True,
        )
        return result.stdout

    def get_all_diff(self) -> str:
        """Get diff of all changes (staged + unstaged).

        Returns:
            The combined git diff output.
        """
        result = subprocess.run(
            ["git", "diff", "HEAD", "--no-color"],
            cwd=self.repo_path,
            capture_output=True,
            text=True,
        )
        return result.stdout

    def parse_diff(self, diff_text: str) -> list[FileChange]:
        """Parse diff output into structured FileChange objects.

        Args:
            diff_text: Raw git diff output.

        Returns:
            List of FileChange objects representing each changed file.
        """
        if not diff_text.strip():
            return []

        files: list[FileChange] = []
        current_file: Optional[FileChange] = None
        current_hunk: Optional[DiffHunk] = None

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

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

            # New file header
            if line.startswith("diff --git"):
                if current_file:
                    files.append(current_file)

                # Parse file paths
                match = re.match(r"diff --git a/(.*) b/(.*)", line)
                if match:
                    old_path, new_path = match.groups()
                    current_file = FileChange(
                        file_path=new_path,
                        change_type=ChangeType.MODIFIED,
                        old_path=old_path if old_path != new_path else None,
                    )
                current_hunk = None

            # Check for new file
            elif line.startswith("new file mode"):
                if current_file:
                    current_file.change_type = ChangeType.ADDED

            # Check for deleted file
            elif line.startswith("deleted file mode"):
                if current_file:
                    current_file.change_type = ChangeType.DELETED

            # Check for renamed file
            elif line.startswith("rename from"):
                if current_file:
                    current_file.change_type = ChangeType.RENAMED
                    match = re.match(r"rename from (.*)", line)
                    if match:
                        current_file.old_path = match.group(1)

            # Check for binary file
            elif line.startswith("Binary files"):
                if current_file:
                    current_file.binary = True

            # Hunk header
            elif line.startswith("@@"):
                match = re.match(r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)?", line)
                if match and current_file:
                    old_start = int(match.group(1))
                    old_count = int(match.group(2)) if match.group(2) else 1
                    new_start = int(match.group(3))
                    new_count = int(match.group(4)) if match.group(4) else 1
                    header = match.group(5) or ""

                    current_hunk = DiffHunk(
                        old_start=old_start,
                        old_count=old_count,
                        new_start=new_start,
                        new_count=new_count,
                        header=header.strip(),
                    )
                    current_file.hunks.append(current_hunk)

            # Diff content lines
            elif current_hunk and (
                line.startswith("+") or line.startswith("-") or line.startswith(" ")
            ):
                current_hunk.lines.append(line)
                if line.startswith("+") and not line.startswith("+++"):
                    current_hunk.additions += 1
                    if current_file:
                        current_file.additions += 1
                elif line.startswith("-") and not line.startswith("---"):
                    current_hunk.deletions += 1
                    if current_file:
                        current_file.deletions += 1

            i += 1

        # Add last file
        if current_file:
            files.append(current_file)

        return files

    def categorize_changes(self, files: list[FileChange]) -> ChangeCategory:
        """Categorize changes based on file patterns and content.

        Args:
            files: List of FileChange objects.

        Returns:
            The most appropriate ChangeCategory.
        """
        if not files:
            return ChangeCategory.CHORE

        category_scores: dict[ChangeCategory, int] = {cat: 0 for cat in ChangeCategory}

        for file_change in files:
            # Check file path patterns
            for category, patterns in self.CATEGORY_PATTERNS.items():
                for pattern in patterns:
                    if re.search(pattern, file_change.file_path, re.IGNORECASE):
                        category_scores[category] += 2
                        break

            # Check content keywords in hunks
            content = "\n".join(
                "\n".join(hunk.lines) for hunk in file_change.hunks
            ).lower()

            for category, keywords in self.CONTENT_KEYWORDS.items():
                for keyword in keywords:
                    if keyword in content:
                        category_scores[category] += 1

        # Get the category with highest score
        max_category = max(category_scores, key=category_scores.get)  # type: ignore
        if category_scores[max_category] > 0:
            return max_category

        # Default based on change types
        add_count = sum(1 for f in files if f.change_type == ChangeType.ADDED)
        del_count = sum(1 for f in files if f.change_type == ChangeType.DELETED)

        if add_count > len(files) // 2:
            return ChangeCategory.FEATURE
        if del_count > len(files) // 2:
            return ChangeCategory.REFACTOR

        return ChangeCategory.CHORE

    def detect_scope(self, files: list[FileChange]) -> Optional[str]:
        """Detect the scope of changes based on common paths.

        Args:
            files: List of FileChange objects.

        Returns:
            Common scope/module name if detected, None otherwise.
        """
        if not files:
            return None

        # Get common path components
        paths = [Path(f.file_path).parts for f in files]
        if not paths:
            return None

        # Find common prefix
        common = []
        for parts in zip(*paths):
            if len(set(parts)) == 1:
                common.append(parts[0])
            else:
                break

        if common:
            # Skip common directories like 'src', 'lib', etc.
            skip_dirs = {"src", "lib", "app", "pkg", "internal", "ieum"}
            while common and common[0] in skip_dirs:
                common = common[1:]

            if common:
                return common[0]

        return None

    def summarize_changes(self, files: list[FileChange]) -> str:
        """Generate a human-readable summary of changes.

        Args:
            files: List of FileChange objects.

        Returns:
            A summary string describing the changes.
        """
        if not files:
            return "No changes detected"

        parts = []

        # Count by change type
        added = [f for f in files if f.change_type == ChangeType.ADDED]
        modified = [f for f in files if f.change_type == ChangeType.MODIFIED]
        deleted = [f for f in files if f.change_type == ChangeType.DELETED]
        renamed = [f for f in files if f.change_type == ChangeType.RENAMED]

        if added:
            if len(added) == 1:
                parts.append(f"add {Path(added[0].file_path).name}")
            else:
                parts.append(f"add {len(added)} files")

        if modified:
            if len(modified) == 1:
                parts.append(f"update {Path(modified[0].file_path).name}")
            else:
                parts.append(f"update {len(modified)} files")

        if deleted:
            if len(deleted) == 1:
                parts.append(f"remove {Path(deleted[0].file_path).name}")
            else:
                parts.append(f"remove {len(deleted)} files")

        if renamed:
            parts.append(f"rename {len(renamed)} files")

        return ", ".join(parts) if parts else "minor changes"

    def analyze(self, diff_text: str | None = None) -> ChangeAnalysis:
        """Perform full analysis of changes.

        Args:
            diff_text: Optional diff text. If not provided, analyzes staged changes.

        Returns:
            Complete ChangeAnalysis with categorization and summary.
        """
        if diff_text is None:
            diff_text = self.get_staged_diff()
            if not diff_text.strip():
                diff_text = self.get_unstaged_diff()

        files = self.parse_diff(diff_text)
        category = self.categorize_changes(files)
        scope = self.detect_scope(files)
        summary = self.summarize_changes(files)

        return ChangeAnalysis(
            files=files,
            category=category,
            scope=scope,
            summary=summary,
            breaking=False,  # Could be enhanced with breaking change detection
        )


def get_status() -> dict:
    """Get current git repository status.

    Returns:
        Dictionary with status information.
    """
    result = {
        "branch": "",
        "staged": [],
        "unstaged": [],
        "untracked": [],
        "is_repo": False,
    }

    try:
        # Check if in git repo
        check = subprocess.run(
            ["git", "rev-parse", "--git-dir"],
            capture_output=True,
            text=True,
        )
        if check.returncode != 0:
            return result

        result["is_repo"] = True

        # Get current branch
        branch = subprocess.run(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            capture_output=True,
            text=True,
        )
        result["branch"] = branch.stdout.strip()

        # Get status
        status = subprocess.run(
            ["git", "status", "--porcelain"],
            capture_output=True,
            text=True,
        )

        for line in status.stdout.strip().split("\n"):
            if not line:
                continue

            index_status = line[0]
            worktree_status = line[1]
            file_path = line[3:]

            if index_status in ("A", "M", "D", "R", "C"):
                result["staged"].append(file_path)
            if worktree_status in ("M", "D"):
                result["unstaged"].append(file_path)
            if index_status == "?" and worktree_status == "?":
                result["untracked"].append(file_path)

    except FileNotFoundError:
        pass  # Git not installed

    return result
