"""Code duplication finder using hash-based near-duplicate detection.

Uses a rolling hash over normalized code blocks to detect duplicated
segments across files. Normalizes whitespace and variable names to
catch near-duplicates, not just exact copies.
"""

from __future__ import annotations

import hashlib
import re
from collections import defaultdict
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


@dataclass
class DuplicateBlock:
    """A block of code that appears in multiple locations."""

    block_hash: str
    line_count: int
    locations: list[dict[str, Any]] = field(default_factory=list)
    normalized_text: str = ""

    @property
    def duplication_count(self) -> int:
        return len(self.locations)

    def to_dict(self) -> dict[str, Any]:
        return {
            "block_hash": self.block_hash,
            "line_count": self.line_count,
            "duplication_count": self.duplication_count,
            "locations": self.locations,
        }


class DuplicationAnalyzer:
    """Detect code duplication using content-hash fingerprinting."""

    SUPPORTED_EXTENSIONS = {".py", ".js", ".ts", ".jsx", ".tsx"}

    def __init__(self, min_block_lines: int = 6, min_duplicates: int = 2) -> None:
        """Initialize duplication analyzer.

        Args:
            min_block_lines: Minimum number of lines for a block to be considered.
            min_duplicates: Minimum occurrences to count as duplication.
        """
        self.min_block_lines = min_block_lines
        self.min_duplicates = min_duplicates

    def _normalize_line(self, line: str) -> str:
        """Normalize a line of code for comparison.

        - Strip whitespace
        - Replace string literals with placeholder
        - Replace numeric literals with placeholder
        - Lowercase
        """
        line = line.strip()
        if not line or line.startswith("#") or line.startswith("//"):
            return ""
        # Remove string literals
        line = re.sub(r'''(["'])(?:(?!\1).)*\1''', '"_STR_"', line)
        # Remove numeric literals
        line = re.sub(r'\b\d+\.?\d*\b', '_NUM_', line)
        # Collapse whitespace
        line = re.sub(r'\s+', ' ', line)
        return line.lower()

    def _hash_block(self, lines: list[str]) -> str:
        """Create a hash of a normalized code block."""
        content = "\n".join(lines)
        return hashlib.md5(content.encode("utf-8")).hexdigest()

    def analyze_file(self, file_path: str | Path) -> list[tuple[str, int, int, list[str]]]:
        """Extract hashable blocks from a single file.

        Returns list of (hash, start_line, end_line, normalized_lines).
        """
        file_path = Path(file_path)
        try:
            raw_lines = file_path.read_text(encoding="utf-8", errors="replace").splitlines()
        except OSError:
            return []

        normalized = [self._normalize_line(line) for line in raw_lines]
        blocks: list[tuple[str, int, int, list[str]]] = []

        # Sliding window of size min_block_lines
        window_size = self.min_block_lines
        for start in range(len(normalized) - window_size + 1):
            window = normalized[start: start + window_size]
            # Skip blocks that are mostly empty
            non_empty = [l for l in window if l]
            if len(non_empty) < window_size * 0.6:
                continue
            block_hash = self._hash_block(non_empty)
            blocks.append((block_hash, start + 1, start + window_size, non_empty))

        return blocks

    def analyze_directory(
        self,
        directory: str | Path,
        exclude_patterns: list[str] | None = None,
    ) -> list[DuplicateBlock]:
        """Find all duplicate code blocks in a directory.

        Returns:
            List of DuplicateBlock objects for each duplicated segment.
        """
        directory = Path(directory)
        exclude_patterns = exclude_patterns or [
            "venv", "node_modules", ".git", "__pycache__", ".tox",
            "build", "dist", "migrations",
        ]

        # hash -> list of (file_path, start_line, end_line, normalized)
        hash_map: dict[str, list[dict[str, Any]]] = defaultdict(list)

        for ext in self.SUPPORTED_EXTENSIONS:
            for f in directory.rglob(f"*{ext}"):
                parts = f.relative_to(directory).parts
                if any(excl in parts for excl in exclude_patterns):
                    continue
                blocks = self.analyze_file(f)
                for block_hash, start, end, norm_lines in blocks:
                    hash_map[block_hash].append({
                        "file_path": str(f),
                        "start_line": start,
                        "end_line": end,
                    })

        # Filter to only actual duplicates
        duplicates: list[DuplicateBlock] = []
        seen_hashes: set[str] = set()

        for block_hash, locations in hash_map.items():
            if len(locations) < self.min_duplicates:
                continue
            if block_hash in seen_hashes:
                continue

            # Deduplicate overlapping blocks in the same file
            unique_locations = self._deduplicate_locations(locations)
            if len(unique_locations) < self.min_duplicates:
                continue

            seen_hashes.add(block_hash)
            duplicates.append(
                DuplicateBlock(
                    block_hash=block_hash,
                    line_count=self.min_block_lines,
                    locations=unique_locations,
                )
            )

        # Sort by number of duplications
        duplicates.sort(key=lambda d: d.duplication_count, reverse=True)
        return duplicates

    def _deduplicate_locations(
        self, locations: list[dict[str, Any]]
    ) -> list[dict[str, Any]]:
        """Remove overlapping blocks in the same file."""
        by_file: dict[str, list[dict[str, Any]]] = defaultdict(list)
        for loc in locations:
            by_file[loc["file_path"]].append(loc)

        result: list[dict[str, Any]] = []
        for file_path, locs in by_file.items():
            locs.sort(key=lambda l: l["start_line"])
            merged: list[dict[str, Any]] = []
            for loc in locs:
                if merged and loc["start_line"] <= merged[-1]["end_line"]:
                    # Overlapping — extend previous
                    merged[-1]["end_line"] = max(merged[-1]["end_line"], loc["end_line"])
                else:
                    merged.append(dict(loc))
            result.extend(merged)

        return result

    def summary(self, duplicates: list[DuplicateBlock]) -> dict[str, Any]:
        """Generate duplication summary."""
        if not duplicates:
            return {
                "duplicate_block_count": 0,
                "total_duplicate_lines": 0,
                "files_with_duplicates": 0,
                "worst_offender": None,
            }

        files_with_dups: set[str] = set()
        total_lines = 0
        for dup in duplicates:
            for loc in dup.locations:
                files_with_dups.add(loc["file_path"])
                total_lines += dup.line_count

        worst = duplicates[0] if duplicates else None
        return {
            "duplicate_block_count": len(duplicates),
            "total_duplicate_lines": total_lines,
            "files_with_duplicates": len(files_with_dups),
            "worst_offender": worst.to_dict() if worst else None,
        }
