"""Generate composite keys for duplicate prevention."""

from typing import Dict, Any, List, Optional
import re
import hashlib
import unicodedata

__transform_id__ = "generate_dedup_key"
__version__ = "1.1.0"
__updated__ = "2024-09-20"


def generate_dedup_key(data: Dict[str, Any]) -> Dict[str, Any]:
    """
    Generate composite deduplication keys for record matching.

    Creates multiple key types for different matching strategies:
    - email_based: For individual person records
    - company_based: For account-level deduplication
    - phone_based: For contact matching
    - composite: Combined multi-field key
    - fuzzy: Normalized key for fuzzy matching

    Input:
    {
        "email": "john.smith@acme.com",
        "first_name": "John",
        "last_name": "Smith",
        "company": "Acme Corp",
        "company_domain": "acme.com",
        "phone": "+14155551234",
        "address": "123 Main St, San Francisco, CA"
    }

    Returns:
    {
        "value": "john.smith@acme.com",  # Primary key
        "keys": {
            "email": "john.smith@acme.com",
            "email_domain": "john.smith@acme.com|acme.com",
            "domain_name": "acme.com|john|smith",
            "company_name": "acmecorp|john|smith",
            "phone": "+14155551234",
            "composite": "acme:john:smith:415555",
            "fuzzy": "acm:joh:smi",
            "hash": "a3f2b891c..."
        },
        "match_confidence": {
            "email": 1.0,  # Exact email match
            "email_domain": 0.95,  # Same person, same company
            "domain_name": 0.85,  # Same company, same name
            "company_name": 0.75,  # Similar company, same name
            "phone": 0.9,  # Same phone
            "composite": 0.8,
            "fuzzy": 0.6
        },
        "recommended_strategy": "email_first"
    }
    """
    if not data:
        return {"value": None, "keys": {}, "reason": "no_data"}

    keys = {}
    match_confidence = {}

    # Extract and clean input fields
    email = _clean_email(data.get("email"))
    first_name = _clean_name(data.get("first_name"))
    last_name = _clean_name(data.get("last_name"))
    company = _clean_company(data.get("company"))
    domain = _clean_domain(data.get("company_domain") or data.get("domain"))
    phone = _clean_phone(data.get("phone"))

    # If no domain but have email, extract it
    if not domain and email and "@" in email:
        domain = email.split("@")[1].lower()

    # 1. Email-based keys (highest confidence for individuals)
    if email:
        keys["email"] = email
        match_confidence["email"] = 1.0

        if domain:
            keys["email_domain"] = f"{email}|{domain}"
            match_confidence["email_domain"] = 0.95

    # 2. Domain + Name keys (good for B2B matching)
    if domain:
        if first_name and last_name:
            keys["domain_name"] = f"{domain}|{first_name}|{last_name}"
            match_confidence["domain_name"] = 0.85

        if first_name or last_name:
            name_part = f"{first_name}|{last_name}".strip("|")
            keys["domain_partial"] = f"{domain}|{name_part}"
            match_confidence["domain_partial"] = 0.75

    # 3. Company + Name keys (when domain unknown)
    if company:
        if first_name and last_name:
            keys["company_name"] = f"{company}|{first_name}|{last_name}"
            match_confidence["company_name"] = 0.75

        # Company-only key for account-level dedup
        keys["company"] = company
        match_confidence["company"] = 0.6

    # 4. Phone-based key
    if phone:
        keys["phone"] = phone
        match_confidence["phone"] = 0.9

        # Phone + name for higher confidence
        if first_name or last_name:
            name_part = f"{first_name}{last_name}".replace(" ", "")
            keys["phone_name"] = f"{phone}|{name_part}"
            match_confidence["phone_name"] = 0.95

    # 5. Composite key (multiple signals)
    composite_parts = []
    if company:
        composite_parts.append(company[:3])  # First 3 chars of company
    if first_name:
        composite_parts.append(first_name[:3])
    if last_name:
        composite_parts.append(last_name[:3])
    if phone:
        composite_parts.append(phone[-6:])  # Last 6 digits

    if len(composite_parts) >= 2:
        keys["composite"] = ":".join(composite_parts)
        match_confidence["composite"] = 0.7 + (0.05 * len(composite_parts))

    # 6. Fuzzy matching key (for approximate matches)
    fuzzy_parts = []
    if company:
        fuzzy_parts.append(_create_fuzzy_key(company))
    if first_name:
        fuzzy_parts.append(_create_fuzzy_key(first_name))
    if last_name:
        fuzzy_parts.append(_create_fuzzy_key(last_name))

    if fuzzy_parts:
        keys["fuzzy"] = ":".join(fuzzy_parts)
        match_confidence["fuzzy"] = 0.5

    # 7. Hash key (for exact full-record matching)
    if keys:
        hash_input = "|".join(
            sorted(
                [
                    email or "",
                    first_name or "",
                    last_name or "",
                    company or "",
                    domain or "",
                    phone or "",
                ]
            )
        )
        # Use SHA256 instead of MD5 for better collision resistance
        keys["hash"] = hashlib.sha256(hash_input.encode()).hexdigest()[:16]
        match_confidence["hash"] = 1.0 if all([email, first_name, last_name]) else 0.8

    # Determine primary key and strategy
    primary_key, strategy = _determine_primary_key(keys, data)

    # Generate match rules
    match_rules = _generate_match_rules(keys, match_confidence)

    return {
        "value": primary_key,
        "keys": keys,
        "match_confidence": match_confidence,
        "recommended_strategy": strategy,
        "match_rules": match_rules,
        "data_quality": _assess_data_quality(data),
    }


def _clean_email(email: Any) -> Optional[str]:
    """Clean and normalize email."""
    if not email:
        return None
    return str(email).strip().lower()


def _clean_name(name: Any) -> Optional[str]:
    """
    Clean and normalize name, preserving international characters.

    Supports names in all scripts (CJK, Cyrillic, Arabic, Hebrew, etc.)
    - "José García" -> "joségarcia"
    - "李明" -> "李明"
    - "Владимир" -> "владимир"
    """
    if not name:
        return None
    # Normalize Unicode and lowercase
    s = str(name)
    s = unicodedata.normalize("NFKC", s)
    s = s.lower()
    # Remove non-word characters (preserves Unicode letters/digits)
    cleaned = re.sub(r"[^\w]", "", s, flags=re.UNICODE)
    return cleaned if cleaned else None


def _clean_company(company: Any) -> Optional[str]:
    """
    Clean and normalize company name, preserving international characters.

    Supports company names in all scripts:
    - "Société Générale Inc" -> "societegenerale"
    - "日本電信電話株式会社" -> "日本電信電話"
    - "Яндекс ООО" -> "яндекс"
    """
    if not company:
        return None

    # Normalize Unicode
    cleaned = unicodedata.normalize("NFKC", str(company))
    cleaned = cleaned.lower()

    # Remove common suffixes (Latin and international)
    suffixes = [
        " inc",
        " llc",
        " ltd",
        " corp",
        " corporation",
        " company",
        " co",
        " gmbh",
        " sa",
        " plc",
        " ag",
        " ооо",
        " ао",
        " пао",  # Russian
        " 株式会社",
        " 有限会社",  # Japanese
        " 주식회사",  # Korean
    ]
    for suffix in suffixes:
        if cleaned.endswith(suffix):
            cleaned = cleaned[: -len(suffix)]
            break  # Only remove one suffix

    # Remove special characters but preserve Unicode letters/digits
    cleaned = re.sub(r"[^\w]", "", cleaned, flags=re.UNICODE)
    return cleaned if cleaned else None


def _clean_domain(domain: Any) -> Optional[str]:
    """Clean and normalize domain."""
    if not domain:
        return None

    cleaned = str(domain).lower()
    # Remove protocol if present
    cleaned = re.sub(r"https?://", "", cleaned)
    # Remove www
    cleaned = re.sub(r"^www\.", "", cleaned)
    # Remove path
    cleaned = cleaned.split("/")[0]
    # Remove port
    cleaned = cleaned.split(":")[0]

    return cleaned if cleaned else None


def _clean_phone(phone: Any) -> Optional[str]:
    """Clean and normalize phone number."""
    if not phone:
        return None

    # Keep only digits and +
    cleaned = re.sub(r"[^\d+]", "", str(phone))

    # Ensure it starts with + for international format
    if cleaned and not cleaned.startswith("+"):
        # Assume US if 10 digits
        if len(cleaned) == 10:
            cleaned = "+1" + cleaned
        elif len(cleaned) == 11 and cleaned.startswith("1"):
            cleaned = "+" + cleaned

    return cleaned if len(cleaned) >= 10 else None


def _create_fuzzy_key(text: str) -> str:
    """Create phonetic fuzzy key for approximate matching.

    Uses a simplified metaphone approach for better matching
    of names that sound similar but are spelled differently.
    """
    if not text:
        return ""

    # Normalize unicode characters
    text = unicodedata.normalize("NFKD", text)
    text = "".join(c for c in text if not unicodedata.combining(c))
    text = text.upper()

    # Common phonetic replacements for better fuzzy matching
    replacements = [
        (r"^KN", "N"),  # knife -> nife
        (r"^GN", "N"),  # gnome -> nome
        (r"^PN", "N"),  # pneumonia -> neumonia
        (r"^WR", "R"),  # write -> rite
        (r"PH", "F"),  # phone -> fone
        (r"GH", ""),  # light -> lit
        (r"CK", "K"),  # back -> bak
        (r"C([IEY])", r"S\1"),  # city -> sity
        (r"C", "K"),  # cat -> kat
        (r"Q", "K"),  # queen -> kween
        (r"X", "KS"),  # box -> boks
    ]

    result = text
    for pattern, replacement in replacements:
        result = re.sub(pattern, replacement, result)

    # Remove vowels except at start
    if result:
        first = result[0]
        rest = re.sub(r"[AEIOUYWH]", "", result[1:])
        result = first + rest

    # Remove consecutive duplicates
    deduped = []
    prev = None
    for char in result:
        if char != prev:
            deduped.append(char)
            prev = char
    result = "".join(deduped)

    return result[:4].ljust(4, "0")  # Pad to 4 chars for better discrimination


def _determine_primary_key(
    keys: Dict[str, str], data: Dict[str, Any]
) -> tuple[Optional[str], str]:
    """Determine the best primary key and matching strategy."""
    # Priority order for primary key
    if "email" in keys:
        return keys["email"], "email_first"
    elif "email_domain" in keys:
        return keys["email_domain"], "email_domain"
    elif "domain_name" in keys:
        return keys["domain_name"], "domain_name"
    elif "phone_name" in keys:
        return keys["phone_name"], "phone_name"
    elif "company_name" in keys:
        return keys["company_name"], "company_name"
    elif "phone" in keys:
        return keys["phone"], "phone_only"
    elif "composite" in keys:
        return keys["composite"], "composite"
    elif "company" in keys:
        return keys["company"], "company_only"
    else:
        return None, "no_strategy"


def _generate_match_rules(
    keys: Dict[str, str], confidence: Dict[str, float]
) -> List[Dict[str, Any]]:
    """Generate matching rules based on available keys."""
    rules = []

    # Exact match rules (confidence >= 0.9)
    for key_type, conf in confidence.items():
        if conf >= 0.9 and key_type in keys:
            rules.append(
                {
                    "type": "exact",
                    "key": key_type,
                    "confidence": conf,
                    "action": "merge",
                }
            )

    # Fuzzy match rules (confidence < 0.9)
    for key_type, conf in confidence.items():
        if 0.6 <= conf < 0.9 and key_type in keys:
            rules.append(
                {
                    "type": "fuzzy",
                    "key": key_type,
                    "confidence": conf,
                    "action": "review",
                }
            )

    # Sort by confidence
    rules.sort(key=lambda x: x["confidence"], reverse=True)

    return rules[:5]  # Return top 5 rules


def _assess_data_quality(data: Dict[str, Any]) -> Dict[str, Any]:
    """Assess the quality of data for deduplication."""
    quality_score = 0
    max_score = 0
    missing_fields = []

    # Critical fields for dedup
    critical_fields = {
        "email": 30,
        "company": 20,
        "first_name": 15,
        "last_name": 15,
        "phone": 10,
        "company_domain": 10,
    }

    for field, weight in critical_fields.items():
        max_score += weight
        if data.get(field):
            quality_score += weight
        else:
            missing_fields.append(field)

    completeness = quality_score / max_score if max_score > 0 else 0

    return {
        "score": round(completeness * 100),
        "grade": _get_quality_grade(completeness),
        "missing_critical": missing_fields[:3],  # Top 3 missing
        "recommendation": _get_quality_recommendation(completeness, missing_fields),
    }


def _get_quality_grade(completeness: float) -> str:
    """Convert completeness to letter grade."""
    if completeness >= 0.9:
        return "A"
    elif completeness >= 0.75:
        return "B"
    elif completeness >= 0.6:
        return "C"
    elif completeness >= 0.4:
        return "D"
    else:
        return "F"


def _get_quality_recommendation(completeness: float, missing: List[str]) -> str:
    """Get recommendation for improving data quality."""
    if completeness >= 0.9:
        return "Excellent data quality for deduplication"
    elif completeness >= 0.75:
        return "Good data quality, consider enriching"
    elif completeness >= 0.5:
        if "email" in missing:
            return "Missing email - critical for person-level dedup"
        elif "company" in missing:
            return "Missing company - important for B2B matching"
        else:
            return f"Enrich {', '.join(missing[:2])} for better matching"
    else:
        return "Poor data quality - significant enrichment needed"
