"""
Text sanitization utilities for the embedding pipeline.

Ported from Spelunker's text_utils.py — ensures text can be safely
processed by embedding APIs and LLMs.
"""

import logging
import re
import unicodedata

logger = logging.getLogger(__name__)


# Common PDF ligatures
_LIGATURE_MAP = {
    "\ufb01": "fi",
    "\ufb02": "fl",
    "\ufb00": "ff",
    "\ufb03": "ffi",
    "\ufb04": "ffl",
    "\ufb05": "ft",
    "\ufb06": "st",
}

# Common special characters from PDF extraction
_SPECIAL_CHAR_MAP = {
    "\u2018": "'",   # left single quotation
    "\u2019": "'",   # right single quotation
    "\u201c": '"',   # left double quotation
    "\u201d": '"',   # right double quotation
    "\u2013": "-",   # en dash
    "\u2014": "-",   # em dash
    "\u2026": "...", # horizontal ellipsis
    "\u00a0": " ",   # non-breaking space
}

# Zero-width characters
_ZERO_WIDTH_CHARS = [
    "\u200b",  # zero-width space
    "\u200c",  # zero-width non-joiner
    "\u200d",  # zero-width joiner
    "\ufeff",  # zero-width no-break space (BOM)
]

# Control characters pattern (exclude newline, tab, carriage return)
_CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f-\x9f]")


def sanitize_text(text: str, log_changes: bool = True) -> str:
    """
    Sanitize text for embedding APIs by removing problematic characters.

    Addresses common issues that cause "invalid tokens" errors:
    null bytes, control characters, zero-width characters, invalid UTF-8,
    and non-normalized Unicode.

    :param text: Text to sanitize.
    :param log_changes: Whether to log sanitization actions.
    :returns: Sanitized text safe for tokenization.
    """
    if not text:
        return text

    original_length = len(text)
    changes: list[str] = []

    # 1. Remove null bytes
    if "\x00" in text:
        text = text.replace("\x00", "")
        changes.append("removed null bytes")

    # 2. Remove control characters
    if _CONTROL_CHAR_RE.search(text):
        text = _CONTROL_CHAR_RE.sub("", text)
        changes.append("removed control characters")

    # 3. Remove zero-width characters
    for char in _ZERO_WIDTH_CHARS:
        if char in text:
            text = text.replace(char, "")
            changes.append("removed zero-width characters")
            break

    # 4. Normalize Unicode to NFC form
    normalized = unicodedata.normalize("NFC", text)
    if normalized != text:
        text = normalized
        changes.append("normalized Unicode to NFC")

    # 5. Replace invalid UTF-8 sequences
    try:
        text = text.encode("utf-8", errors="replace").decode("utf-8")
        if "\ufffd" in text:
            changes.append("replaced invalid UTF-8 sequences")
    except Exception as exc:
        logger.warning("Error during UTF-8 validation: %s", exc)

    # 6. Clean PDF artifacts (ligatures, special chars)
    cleaned = clean_pdf_artifacts(text)
    if cleaned != text:
        text = cleaned
        changes.append("cleaned PDF artifacts")

    if log_changes and changes:
        chars_removed = original_length - len(text)
        logger.info(
            "Text sanitization: %s original_length=%d final_length=%d chars_removed=%d",
            ", ".join(changes),
            original_length,
            len(text),
            chars_removed,
        )

    return text


def clean_pdf_artifacts(text: str) -> str:
    """
    Clean common PDF extraction artifacts.

    Replaces ligatures and special characters with standard equivalents.

    :param text: Text to clean.
    :returns: Cleaned text.
    """
    for ligature, replacement in _LIGATURE_MAP.items():
        text = text.replace(ligature, replacement)

    for special, replacement in _SPECIAL_CHAR_MAP.items():
        text = text.replace(special, replacement)

    return text


def remove_excessive_whitespace(text: str) -> str:
    """
    Remove excessive whitespace while preserving paragraph structure.

    :param text: Text to clean.
    :returns: Text with normalized whitespace.
    """
    text = re.sub(r" +", " ", text)
    text = re.sub(r"\n\n+", "\n\n", text)
    text = "\n".join(line.strip() for line in text.split("\n"))
    return text.strip()


def truncate_text(text: str, max_chars: int, suffix: str = "...") -> str:
    """
    Truncate text to a maximum length preserving word boundaries.

    :param text: Text to truncate.
    :param max_chars: Maximum number of characters.
    :param suffix: Suffix to add if truncated.
    :returns: Truncated text.
    """
    if len(text) <= max_chars:
        return text

    truncate_at = text.rfind(" ", 0, max_chars - len(suffix))
    if truncate_at == -1:
        truncate_at = max_chars - len(suffix)

    return text[:truncate_at] + suffix
