"""
Diff Renderer - Visualize file changes

Provides diff visualization with:
- Side-by-side and unified diff formats
- Syntax highlighting for code
- Line number display
- Change statistics
- Hunk headers

Examples:
    # Create diff renderer
    renderer = DiffRenderer()

    # Render unified diff
    diff_text = renderer.render_unified(old_content, new_content, filename="main.py")
    print(diff_text)

    # Render side-by-side
    diff_text = renderer.render_side_by_side(old_content, new_content)
    print(diff_text)

    # Get diff stats
    stats = renderer.get_stats(old_content, new_content)
    print(f"+{stats.lines_added} -{stats.lines_removed}")
"""

from dataclasses import dataclass
from difflib import unified_diff, SequenceMatcher
from enum import Enum
from typing import Optional


class DiffFormat(Enum):
    """Diff output format"""
    UNIFIED = "unified"
    SIDE_BY_SIDE = "side_by_side"
    INLINE = "inline"


@dataclass
class DiffStats:
    """Statistics about a diff"""
    lines_added: int = 0
    lines_removed: int = 0
    lines_changed: int = 0
    total_lines: int = 0

    @property
    def lines_unchanged(self) -> int:
        """Calculate unchanged lines"""
        return self.total_lines - self.lines_changed

    @property
    def change_percentage(self) -> float:
        """Calculate percentage of lines changed"""
        if self.total_lines == 0:
            return 0.0
        return (self.lines_changed / self.total_lines) * 100


@dataclass
class DiffHunk:
    """A hunk (section) of a diff"""
    old_start: int
    old_count: int
    new_start: int
    new_count: int
    lines: list[tuple[str, str]]  # (prefix, content)


class DiffRenderer:
    """
    Render file diffs with various formats

    Provides unified, side-by-side, and inline diff visualization.
    Includes syntax highlighting and change statistics.
    """

    # ANSI color codes
    COLOR_ADDED = "\033[32m"  # Green
    COLOR_REMOVED = "\033[31m"  # Red
    COLOR_CHANGED = "\033[33m"  # Yellow
    COLOR_UNCHANGED = "\033[90m"  # Gray
    COLOR_HEADER = "\033[36m"  # Cyan
    COLOR_RESET = "\033[0m"

    def __init__(self, use_colors: bool = True, context_lines: int = 3):
        """
        Initialize diff renderer

        Args:
            use_colors: Whether to use ANSI colors
            context_lines: Number of context lines around changes
        """
        self.use_colors = use_colors
        self.context_lines = context_lines

    def render_unified(
        self,
        old_content: str,
        new_content: str,
        filename: str = "file",
        old_label: str = "old",
        new_label: str = "new",
    ) -> str:
        """
        Render unified diff format

        Args:
            old_content: Original file content
            new_content: New file content
            filename: Filename for header
            old_label: Label for old version
            new_label: Label for new version

        Returns:
            Unified diff string
        """
        old_lines = old_content.splitlines(keepends=True)
        new_lines = new_content.splitlines(keepends=True)

        diff = unified_diff(
            old_lines,
            new_lines,
            fromfile=f"{filename} ({old_label})",
            tofile=f"{filename} ({new_label})",
            n=self.context_lines,
        )

        lines = []
        for line in diff:
            if self.use_colors:
                if line.startswith("+"):
                    lines.append(f"{self.COLOR_ADDED}{line}{self.COLOR_RESET}")
                elif line.startswith("-"):
                    lines.append(f"{self.COLOR_REMOVED}{line}{self.COLOR_RESET}")
                elif line.startswith("@@"):
                    lines.append(f"{self.COLOR_HEADER}{line}{self.COLOR_RESET}")
                else:
                    lines.append(line)
            else:
                lines.append(line)

        return "".join(lines)

    def render_side_by_side(
        self,
        old_content: str,
        new_content: str,
        width: int = 80,
    ) -> str:
        """
        Render side-by-side diff format

        Args:
            old_content: Original file content
            new_content: New file content
            width: Total width for output

        Returns:
            Side-by-side diff string
        """
        old_lines = old_content.splitlines()
        new_lines = new_content.splitlines()

        # Calculate column width (half of total minus separator)
        col_width = (width - 3) // 2

        matcher = SequenceMatcher(None, old_lines, new_lines)
        lines = []

        # Header
        lines.append("─" * width)
        left_header = "OLD".center(col_width)
        right_header = "NEW".center(col_width)
        lines.append(f"{left_header} │ {right_header}")
        lines.append("─" * width)

        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == "equal":
                # Unchanged lines
                for i, j in zip(range(i1, i2), range(j1, j2)):
                    left = self._truncate(old_lines[i], col_width)
                    right = self._truncate(new_lines[j], col_width)
                    if self.use_colors:
                        lines.append(f"{self.COLOR_UNCHANGED}{left}{self.COLOR_RESET} │ {self.COLOR_UNCHANGED}{right}{self.COLOR_RESET}")
                    else:
                        lines.append(f"{left} │ {right}")

            elif tag == "delete":
                # Removed lines (only on left)
                for i in range(i1, i2):
                    left = self._truncate(old_lines[i], col_width)
                    right = " " * col_width
                    if self.use_colors:
                        lines.append(f"{self.COLOR_REMOVED}{left}{self.COLOR_RESET} │ {right}")
                    else:
                        lines.append(f"{left} │ {right}")

            elif tag == "insert":
                # Added lines (only on right)
                for j in range(j1, j2):
                    left = " " * col_width
                    right = self._truncate(new_lines[j], col_width)
                    if self.use_colors:
                        lines.append(f"{left} │ {self.COLOR_ADDED}{right}{self.COLOR_RESET}")
                    else:
                        lines.append(f"{left} │ {right}")

            elif tag == "replace":
                # Changed lines
                # Show old on left, new on right
                max_len = max(i2 - i1, j2 - j1)
                for k in range(max_len):
                    left = self._truncate(old_lines[i1 + k], col_width) if i1 + k < i2 else " " * col_width
                    right = self._truncate(new_lines[j1 + k], col_width) if j1 + k < j2 else " " * col_width

                    if self.use_colors:
                        left_colored = f"{self.COLOR_CHANGED}{left}{self.COLOR_RESET}" if i1 + k < i2 else left
                        right_colored = f"{self.COLOR_CHANGED}{right}{self.COLOR_RESET}" if j1 + k < j2 else right
                        lines.append(f"{left_colored} │ {right_colored}")
                    else:
                        lines.append(f"{left} │ {right}")

        lines.append("─" * width)
        return "\n".join(lines)

    def render_inline(
        self,
        old_content: str,
        new_content: str,
    ) -> str:
        """
        Render inline diff (word-level changes highlighted)

        Args:
            old_content: Original content
            new_content: New content

        Returns:
            Inline diff string
        """
        old_lines = old_content.splitlines()
        new_lines = new_content.splitlines()

        matcher = SequenceMatcher(None, old_lines, new_lines)
        lines = []

        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == "equal":
                for line in old_lines[i1:i2]:
                    lines.append(f"  {line}")
            elif tag == "delete":
                for line in old_lines[i1:i2]:
                    if self.use_colors:
                        lines.append(f"{self.COLOR_REMOVED}- {line}{self.COLOR_RESET}")
                    else:
                        lines.append(f"- {line}")
            elif tag == "insert":
                for line in new_lines[j1:j2]:
                    if self.use_colors:
                        lines.append(f"{self.COLOR_ADDED}+ {line}{self.COLOR_RESET}")
                    else:
                        lines.append(f"+ {line}")
            elif tag == "replace":
                for line in old_lines[i1:i2]:
                    if self.use_colors:
                        lines.append(f"{self.COLOR_CHANGED}~ {line}{self.COLOR_RESET}")
                    else:
                        lines.append(f"~ {line}")
                for line in new_lines[j1:j2]:
                    if self.use_colors:
                        lines.append(f"{self.COLOR_CHANGED}~ {line}{self.COLOR_RESET}")
                    else:
                        lines.append(f"~ {line}")

        return "\n".join(lines)

    def get_stats(self, old_content: str, new_content: str) -> DiffStats:
        """
        Calculate diff statistics

        Args:
            old_content: Original content
            new_content: New content

        Returns:
            DiffStats with change counts
        """
        old_lines = old_content.splitlines()
        new_lines = new_content.splitlines()

        matcher = SequenceMatcher(None, old_lines, new_lines)

        lines_added = 0
        lines_removed = 0
        lines_changed = 0

        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            if tag == "delete":
                lines_removed += i2 - i1
            elif tag == "insert":
                lines_added += j2 - j1
            elif tag == "replace":
                lines_removed += i2 - i1
                lines_added += j2 - j1
                lines_changed += max(i2 - i1, j2 - j1)

        return DiffStats(
            lines_added=lines_added,
            lines_removed=lines_removed,
            lines_changed=lines_added + lines_removed,
            total_lines=max(len(old_lines), len(new_lines)),
        )

    def render_stats(self, stats: DiffStats) -> str:
        """
        Render diff statistics

        Args:
            stats: Diff statistics

        Returns:
            Formatted stats string
        """
        if self.use_colors:
            return (
                f"{self.COLOR_ADDED}+{stats.lines_added}{self.COLOR_RESET} "
                f"{self.COLOR_REMOVED}-{stats.lines_removed}{self.COLOR_RESET} "
                f"({stats.change_percentage:.1f}% changed)"
            )
        else:
            return f"+{stats.lines_added} -{stats.lines_removed} ({stats.change_percentage:.1f}% changed)"

    def _truncate(self, text: str, width: int) -> str:
        """Truncate text to width"""
        if len(text) > width:
            return text[:width - 1] + "…"
        return text.ljust(width)


# Convenience functions

def create_diff_renderer(use_colors: bool = True, context_lines: int = 3) -> DiffRenderer:
    """
    Create a diff renderer

    Args:
        use_colors: Whether to use ANSI colors
        context_lines: Number of context lines

    Returns:
        DiffRenderer instance
    """
    return DiffRenderer(use_colors=use_colors, context_lines=context_lines)


def render_diff(
    old_content: str,
    new_content: str,
    format: DiffFormat = DiffFormat.UNIFIED,
    filename: str = "file",
) -> str:
    """
    Render diff with specified format

    Args:
        old_content: Original content
        new_content: New content
        format: Diff format to use
        filename: Filename for header

    Returns:
        Rendered diff string
    """
    renderer = DiffRenderer()

    if format == DiffFormat.UNIFIED:
        return renderer.render_unified(old_content, new_content, filename)
    elif format == DiffFormat.SIDE_BY_SIDE:
        return renderer.render_side_by_side(old_content, new_content)
    elif format == DiffFormat.INLINE:
        return renderer.render_inline(old_content, new_content)
    else:
        return renderer.render_unified(old_content, new_content, filename)


def get_diff_stats(old_content: str, new_content: str) -> DiffStats:
    """
    Get diff statistics

    Args:
        old_content: Original content
        new_content: New content

    Returns:
        DiffStats with change counts
    """
    renderer = DiffRenderer()
    return renderer.get_stats(old_content, new_content)
