"""Unified diff patch parser."""

from __future__ import annotations

import re
from dataclasses import dataclass, field


@dataclass
class Hunk:
    """A single hunk within a patch."""

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


@dataclass
class Patch:
    """A parsed unified diff patch."""

    file_path: str
    hunks: list[Hunk] = field(default_factory=list)
    raw_diff: str = ""

    @property
    def lines_changed(self) -> int:
        """Count of lines added or removed."""
        return self.additions + self.deletions

    @property
    def additions(self) -> int:
        """Count of lines added."""
        count = 0
        for hunk in self.hunks:
            for line in hunk.lines:
                if line.startswith("+") and not line.startswith("+++"):
                    count += 1
        return count

    @property
    def deletions(self) -> int:
        """Count of lines removed."""
        count = 0
        for hunk in self.hunks:
            for line in hunk.lines:
                if line.startswith("-") and not line.startswith("---"):
                    count += 1
        return count


def extract_patches(text: str) -> list[Patch]:
    """Extract unified diff patches from text."""
    patches = []
    seen_diffs = set()

    # Find diff blocks in markdown code blocks
    diff_pattern = r"```diff\s*\n(.*?)\n```"
    matches = re.finditer(diff_pattern, text, re.DOTALL)

    for match in matches:
        diff_text = match.group(1)
        # Normalize for deduplication
        normalized = diff_text.strip()
        if normalized in seen_diffs:
            continue
        seen_diffs.add(normalized)

        patch = parse_patch(diff_text)
        if patch:
            patches.append(patch)

    # Also try to find raw diffs (not in code blocks) - skip if we already found patches
    # This prevents duplicates when the same diff appears both in and outside code blocks
    if not patches:
        raw_diff_pattern = r"^---\s+a/(.+)\n\+\+\+\s+b/(.+)\n((?:@@.*@@.*\n(?:[ +-].*\n)*)+)"
        for match in re.finditer(raw_diff_pattern, text, re.MULTILINE):
            diff_text = match.group(0)
            patch = parse_patch(diff_text)
            if patch:
                patches.append(patch)

    return patches


def parse_patch(diff_text: str) -> Patch | None:
    """Parse a single unified diff into a Patch."""
    lines = diff_text.strip().split("\n")

    if len(lines) < 3:
        return None

    # Find file paths
    file_path = None
    for line in lines[:4]:
        if line.startswith("---"):
            # Extract path from --- a/path or --- path
            match = re.match(r"---\s+(?:a/)?(.+)", line)
            if match:
                file_path = match.group(1).strip()
        elif line.startswith("+++"):
            # Prefer +++ path if --- didn't give us one
            if not file_path:
                match = re.match(r"\+\+\+\s+(?:b/)?(.+)", line)
                if match:
                    file_path = match.group(1).strip()

    if not file_path:
        return None

    patch = Patch(file_path=file_path, raw_diff=diff_text)

    # Parse hunks
    current_hunk = None
    hunk_header_pattern = r"@@\s*-(\d+)(?:,(\d+))?\s*\+(\d+)(?:,(\d+))?\s*@@"

    for line in lines:
        # Check for hunk header
        match = re.match(hunk_header_pattern, line)
        if match:
            if current_hunk:
                patch.hunks.append(current_hunk)

            current_hunk = Hunk(
                old_start=int(match.group(1)),
                old_count=int(match.group(2) or 1),
                new_start=int(match.group(3)),
                new_count=int(match.group(4) or 1),
            )
        elif current_hunk is not None:
            # Add line to current hunk
            if line.startswith((" ", "+", "-")):
                current_hunk.lines.append(line)

    if current_hunk:
        patch.hunks.append(current_hunk)

    # Strip line-number prefixes that LLMs sometimes copy from numbered
    # content (e.g. " 39| \usepackage{...}" → " \usepackage{...}").
    _strip_line_number_prefixes(patch)

    # Recalculate old_count / new_count from actual hunk lines.
    # LLMs frequently get the @@ header counts wrong.
    for hunk in patch.hunks:
        actual_old = 0  # context + removed
        actual_new = 0  # context + added
        for hl in hunk.lines:
            if hl.startswith(" "):
                actual_old += 1
                actual_new += 1
            elif hl.startswith("-"):
                actual_old += 1
            elif hl.startswith("+"):
                actual_new += 1
        if actual_old > 0:
            hunk.old_count = actual_old
        if actual_new > 0:
            hunk.new_count = actual_new

    return patch if patch.hunks else None


# Matches a diff line prefix (space/+/-) followed by a line-number prefix
# like "  39| " that LLMs copy from numbered content.
_LINE_NUM_RE = re.compile(r"^([ +-])\s*\d+\|\s?")


def _strip_line_number_prefixes(patch: Patch) -> None:
    """Remove line-number prefixes from hunk lines.

    When the LLM is shown numbered content (``  39| \\usepackage{...}``), it
    sometimes copies the prefix into the diff.  This corrupts both context
    matching and the final file content.  Strip them early so downstream
    code sees clean lines.
    """
    for hunk in patch.hunks:
        cleaned: list[str] = []
        for line in hunk.lines:
            m = _LINE_NUM_RE.match(line)
            if m:
                # Keep the diff prefix (space/+/-) but drop the number prefix
                line = m.group(1) + line[m.end():]
            cleaned.append(line)
        hunk.lines = cleaned
