# src/fmatch/profiling/exotic_data_detector.py
"""
Lightweight detector for non-Western and special data patterns.
Designed to be fast (<1ms per column) and cache-friendly.
"""

import re
import unicodedata
import hashlib
from functools import lru_cache
from typing import Dict, Tuple
import pandas as pd
from collections import Counter


class ExoticDataDetector:
    """Detects script types, encoding issues, and semantic patterns in data."""

    # Script detection patterns
    SCRIPT_PATTERNS = {
        "latin": re.compile(r"[a-zA-Z]"),
        "cyrillic": re.compile(r"[\u0400-\u04FF]"),
        "arabic": re.compile(r"[\u0600-\u06FF\u0750-\u077F]"),
        "hebrew": re.compile(r"[\u0590-\u05FF]"),
        "cjk": re.compile(r"[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]"),
        "devanagari": re.compile(r"[\u0900-\u097F]"),
        "thai": re.compile(r"[\u0E00-\u0E7F]"),
        "greek": re.compile(r"[\u0370-\u03FF]"),
    }

    # Semantic pattern detection
    SEMANTIC_PATTERNS = {
        "email": re.compile(
            r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", re.IGNORECASE
        ),
        "phone": re.compile(r"^[\+\-\(\)\s\d]{10,}$"),
        "url": re.compile(r"^https?://|^www\.", re.IGNORECASE),
        "postal_code": re.compile(
            r"^\d{5}(-\d{4})?$|^[A-Z]\d[A-Z]\s?\d[A-Z]\d$|^\d{4,6}$"
        ),
        "product_code": re.compile(r"^[A-Z0-9]{6,}$|^[A-Z]{2,3}-\d{3,}|^\d{8,}$"),
        "date_iso": re.compile(r"^\d{4}-\d{2}-\d{2}"),
        "ssn_like": re.compile(r"^\d{3}-\d{2}-\d{4}$|^\d{9}$"),
    }

    # Emoji pattern
    EMOJI_PATTERN = re.compile(
        "[\U0001f600-\U0001f64f"  # emoticons
        "\U0001f300-\U0001f5ff"  # symbols & pictographs
        "\U0001f680-\U0001f6ff"  # transport & map symbols
        "\U0001f1e0-\U0001f1ff"  # flags
        "\U00002702-\U000027b0"
        "\U000024c2-\U0001f251]+"
    )

    @staticmethod
    @lru_cache(maxsize=1000)
    def _detect_script(text_sample: str, sample_size: int = 512) -> Tuple[str, float]:
        """Detect the primary script in a text sample."""
        if not text_sample:
            return "unknown", 0.0

        # Take first N chars for efficiency
        sample = text_sample[:sample_size]

        # Count characters by script
        script_counts = Counter()
        total_alpha = 0

        for char in sample:
            if char.isspace() or char.isdigit() or char in ".,;:!?-_":
                continue  # Skip common punctuation

            for script_name, pattern in ExoticDataDetector.SCRIPT_PATTERNS.items():
                if pattern.search(char):
                    script_counts[script_name] += 1
                    total_alpha += 1
                    break

        if total_alpha == 0:
            return "unknown", 0.0

        # Determine primary script
        if not script_counts:
            return "unknown", 0.0

        primary_script = script_counts.most_common(1)[0][0]
        confidence = script_counts[primary_script] / total_alpha

        # Check for mixed scripts
        if len(script_counts) > 1 and confidence < 0.8:
            return "mixed", confidence

        return primary_script, confidence

    @staticmethod
    def detect_characteristics(series_sample: pd.Series, max_sample: int = 512) -> Dict:
        """
        Main entry point: analyzes a pandas Series and returns characteristics.
        Designed to complete in <1ms for typical data.
        """
        result = {
            "script": "unknown",
            "script_confidence": 0.0,
            "has_emoji": False,
            "encoding_issues": False,
            "semantic_type": None,
            "mixed_types": False,
            "avg_length": 0.0,
            "special_chars_ratio": 0.0,
        }

        # Handle empty series
        if series_sample.empty:
            return result

        # Take a sample of non-null values
        non_null = series_sample.dropna()
        if non_null.empty:
            return result

        # Sample for performance
        if len(non_null) > max_sample:
            sample = non_null.sample(n=max_sample, random_state=42)
        else:
            sample = non_null

        # Convert to strings
        str_sample = sample.astype(str)

        # 1. Script detection (on concatenated sample for speed)
        combined_text = " ".join(str_sample.head(100))
        text_hash = hashlib.md5(combined_text.encode("utf-8")).hexdigest()
        script, confidence = ExoticDataDetector._detect_script(combined_text)
        result["script"] = script
        result["script_confidence"] = confidence

        # 2. Emoji detection
        emoji_count = sum(
            1
            for s in str_sample.head(100)
            if ExoticDataDetector.EMOJI_PATTERN.search(s)
        )
        result["has_emoji"] = emoji_count > len(str_sample.head(100)) * 0.05

        # 3. Encoding issues detection
        encoding_issues = 0
        for s in str_sample.head(50):
            # Look for mojibake patterns
            if any(pattern in s for pattern in ["Ã¢", "Ã©", "â€™", "â€œ", "Ã", "ï¿½"]):
                encoding_issues += 1
            # Check for replacement characters
            if "\ufffd" in s or "?" * 3 in s:
                encoding_issues += 1

        result["encoding_issues"] = encoding_issues > 2

        # 4. Semantic type detection
        semantic_matches = Counter()
        for semantic_type, pattern in ExoticDataDetector.SEMANTIC_PATTERNS.items():
            matches = sum(1 for s in str_sample.head(100) if pattern.match(s))
            if matches > len(str_sample.head(100)) * 0.5:
                semantic_matches[semantic_type] = matches

        if semantic_matches:
            result["semantic_type"] = semantic_matches.most_common(1)[0][0]

        # 5. Mixed types detection
        type_variety = len(set(str_sample.apply(lambda x: type(x).__name__)))
        result["mixed_types"] = type_variety > 2

        # 6. String characteristics
        lengths = str_sample.str.len()
        result["avg_length"] = float(lengths.mean())

        # Special characters ratio
        special_chars = str_sample.str.count(r"[^a-zA-Z0-9\s]").sum()
        total_chars = lengths.sum()
        result["special_chars_ratio"] = (
            special_chars / total_chars if total_chars > 0 else 0.0
        )

        return result


class EncodingFixer:
    """Utilities for handling encoding issues in data."""

    @staticmethod
    def try_fix_encoding(text: str) -> str:
        """Attempt to fix common encoding issues."""
        if not isinstance(text, str):
            return str(text)

        # Common mojibake fixes
        replacements = {
            "Ã¢â‚¬â„¢": "'",
            "â€™": "'",
            "â€œ": '"',
            "â€": '"',
            "Ã©": "é",
            "Ã¨": "è",
            "Ã ": "à",
            "Ã¢": "â",
            "Ã§": "ç",
            "Ã±": "ñ",
            "Ã¼": "ü",
            "Ã¶": "ö",
        }

        fixed = text
        for bad, good in replacements.items():
            fixed = fixed.replace(bad, good)

        return fixed

    @staticmethod
    def safe_unicode_normalize(text: str, form: str = "NFKD") -> str:
        """Safely normalize Unicode text."""
        try:
            return unicodedata.normalize(form, text)
        except Exception:
            return text
