"""
Content Scrubber

Removes AI watermarks, invisible Unicode characters, and normalizes em-dashes
with context-appropriate punctuation.
"""

import re
import unicodedata

from pydantic import BaseModel


# Known AI watermark characters
WATERMARK_CHARS = [
    "\u200B",  # Zero-width space
    "\uFEFF",  # Byte Order Mark (BOM) / Zero-width no-break space
    "\u200C",  # Zero-width non-joiner
    "\u2060",  # Word joiner
    "\u00AD",  # Soft hyphen
    "\u202F",  # Narrow no-break space
]

# Character names for reporting
WATERMARK_NAMES = {
    "\u200B": "Zero-width space",
    "\uFEFF": "Byte Order Mark",
    "\u200C": "Zero-width non-joiner",
    "\u2060": "Word joiner",
    "\u00AD": "Soft hyphen",
    "\u202F": "Narrow no-break space",
}

# Conjunctive adverbs that indicate independent clauses
CONJUNCTIVE_ADVERBS = {
    "however",
    "therefore",
    "moreover",
    "furthermore",
    "consequently",
    "nevertheless",
    "meanwhile",
    "otherwise",
    "thus",
    "hence",
    "accordingly",
    "indeed",
    "instead",
    "likewise",
    "similarly",
    "still",
    "then",
    "also",
    "besides",
    "finally",
}

# Attribution verbs that indicate quotes/citations
ATTRIBUTION_WORDS = {
    "said",
    "says",
    "wrote",
    "writes",
    "noted",
    "notes",
    "stated",
    "states",
    "according",
    "via",
    "per",
    "explained",
    "explains",
    "added",
    "adds",
    "claimed",
    "claims",
}


class EmDashReplacement(BaseModel):
    """Details of an em-dash replacement."""

    line: int
    original_context: str  # Short snippet around the dash
    replacement: str  # The punctuation used (comma, period, semicolon)
    reason: str  # Why this replacement was chosen


class ScrubResult(BaseModel):
    """Result of content scrubbing."""

    original_length: int
    scrubbed_length: int
    watermarks_removed: int
    watermark_types: dict[str, int]  # Character name -> count
    em_dashes_replaced: int
    replacements: list[EmDashReplacement]
    scrubbed_content: str


def _count_watermarks(content: str) -> dict[str, int]:
    """Count occurrences of each watermark character type."""
    counts: dict[str, int] = {}

    for char in WATERMARK_CHARS:
        count = content.count(char)
        if count > 0:
            name = WATERMARK_NAMES.get(char, f"U+{ord(char):04X}")
            counts[name] = count

    # Also count other format control characters (category Cf)
    other_cf = 0
    for char in content:
        if unicodedata.category(char) == "Cf" and char not in WATERMARK_CHARS:
            other_cf += 1
    if other_cf > 0:
        counts["Other format control"] = other_cf

    return counts


def _remove_watermarks(content: str) -> tuple[str, int]:
    """
    Remove watermark characters from content.

    Returns:
        Tuple of (cleaned_content, total_removed_count)
    """
    total_removed = 0

    # First, replace zero-width spaces between word characters with regular spaces
    # This preserves word separation that may have been hidden
    result = re.sub(r"(\w)\u200B(\w)", r"\1 \2", content)
    zwsp_replaced = content.count("\u200B") - result.count("\u200B")

    # Remove all known watermark characters
    for char in WATERMARK_CHARS:
        count = result.count(char)
        if count > 0:
            result = result.replace(char, "")
            total_removed += count

    total_removed += zwsp_replaced

    # Remove other format control characters (Unicode category Cf)
    cleaned_chars = []
    for char in result:
        if unicodedata.category(char) == "Cf":
            total_removed += 1
        else:
            cleaned_chars.append(char)

    return "".join(cleaned_chars), total_removed


def _is_attribution_context(before: str, after: str) -> bool:
    """Check if em-dash is in attribution context (quotes, said, etc.)."""
    before_lower = before.lower().strip()
    after_lower = after.lower().strip()

    # Check for attribution words before or after
    for word in ATTRIBUTION_WORDS:
        if before_lower.endswith(word) or after_lower.startswith(word):
            return True

    # Check for quote marks nearby
    if '"' in before[-20:] or '"' in after[:20]:
        return True

    return False


def _is_independent_clause(after: str) -> bool:
    """Check if text after dash starts with capital letter (independent clause)."""
    after_stripped = after.lstrip()
    if after_stripped:
        return after_stripped[0].isupper()
    return False


def _has_conjunctive_adverb(after: str) -> bool:
    """Check if text after dash starts with a conjunctive adverb."""
    after_lower = after.lower().strip()
    for adverb in CONJUNCTIVE_ADVERBS:
        if after_lower.startswith(adverb):
            # Make sure it's a complete word
            rest = after_lower[len(adverb) :]
            if not rest or not rest[0].isalpha():
                return True
    return False


def _determine_replacement(before: str, after: str) -> tuple[str, str]:
    """
    Determine the appropriate replacement for an em-dash.

    Returns:
        Tuple of (replacement_punctuation, reason)
    """
    # Rule 1: Attribution context -> comma
    if _is_attribution_context(before, after):
        return ", ", "attribution"

    # Rule 2: Conjunctive adverb after -> semicolon
    if _has_conjunctive_adverb(after):
        return "; ", "conjunctive_adverb"

    # Rule 3: Independent clause (capital letter) -> period
    if _is_independent_clause(after):
        # Check if before ends with proper sentence structure
        before_stripped = before.rstrip()
        if before_stripped and before_stripped[-1] not in ".!?":
            return ". ", "independent_clause"

    # Rule 4: List/series context (commas nearby) -> comma
    if "," in before[-30:] or "," in after[:30]:
        return ", ", "list_context"

    # Rule 5: Short content after (parenthetical aside) -> comma
    after_stripped = after.strip()
    if len(after_stripped) < 30:
        return ", ", "short_aside"

    # Default -> comma (most common in parenthetical use)
    return ", ", "default"


def _get_line_number(content: str, position: int) -> int:
    """Get the line number for a position in the content."""
    return content[:position].count("\n") + 1


def _get_context(content: str, position: int, window: int = 30) -> str:
    """Get a context snippet around a position."""
    start = max(0, position - window)
    end = min(len(content), position + window)
    snippet = content[start:end]
    # Clean up for display
    snippet = snippet.replace("\n", " ").strip()
    if start > 0:
        snippet = "..." + snippet
    if end < len(content):
        snippet = snippet + "..."
    return snippet


def _replace_em_dashes(content: str) -> tuple[str, list[EmDashReplacement]]:
    """
    Replace em-dashes with context-appropriate punctuation.

    Returns:
        Tuple of (modified_content, list_of_replacements)
    """
    replacements: list[EmDashReplacement] = []

    # Match em-dash (U+2014) or two hyphens used as em-dash
    em_dash_pattern = re.compile(r"\u2014|--")

    result = []
    last_end = 0

    for match in em_dash_pattern.finditer(content):
        start = match.start()
        end = match.end()

        # Get text before and after the em-dash
        before = content[last_end:start]
        # Look ahead for context (next 100 chars)
        after_start = end
        after_end = min(len(content), end + 100)
        after = content[after_start:after_end]

        # Determine replacement
        replacement, reason = _determine_replacement(before, after)

        # Track the replacement
        line_num = _get_line_number(content, start)
        context = _get_context(content, start)

        replacements.append(
            EmDashReplacement(
                line=line_num,
                original_context=context,
                replacement=replacement.strip(),
                reason=reason,
            )
        )

        # Add the content before and the replacement
        result.append(before)
        result.append(replacement)

        last_end = end

    # Add remaining content
    result.append(content[last_end:])

    return "".join(result), replacements


def scrub_content(
    content: str,
    remove_watermarks: bool = True,
    replace_em_dashes: bool = True,
) -> ScrubResult:
    """
    Scrub content of AI watermarks and optionally normalize em-dashes.

    Args:
        content: Text to scrub
        remove_watermarks: Whether to remove watermark characters (default True)
        replace_em_dashes: Whether to replace em-dashes with context-appropriate
            punctuation (default True)

    Returns:
        ScrubResult with scrubbed content and details of changes
    """
    original_length = len(content)
    result_content = content
    watermarks_removed = 0
    watermark_types: dict[str, int] = {}
    em_dashes_replaced = 0
    replacements: list[EmDashReplacement] = []

    # Count watermarks before removal (for reporting)
    if remove_watermarks:
        watermark_types = _count_watermarks(content)
        result_content, watermarks_removed = _remove_watermarks(result_content)

    # Replace em-dashes
    if replace_em_dashes:
        result_content, replacements = _replace_em_dashes(result_content)
        em_dashes_replaced = len(replacements)

    return ScrubResult(
        original_length=original_length,
        scrubbed_length=len(result_content),
        watermarks_removed=watermarks_removed,
        watermark_types=watermark_types,
        em_dashes_replaced=em_dashes_replaced,
        replacements=replacements,
        scrubbed_content=result_content,
    )
