#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
contact_classifier.py

Modulo compartido que detecta contactos de test/prueba/E3 a partir de
first_name, last_name, email y tags. Es la fuente de verdad de los
patrones; tanto el script `scripts/find_test_contacts.py` como el endpoint
`/api/contacts/{location_id}` lo usan para mantener consistencia entre el
reporte CLI y la marca visual del dashboard.

Funciones publicas:
  - classify_contact(first_name, last_name, email, tags_list) -> (reasons, classes)
  - is_test_contact(contact_dict) -> bool
  - annotate_contact(contact_dict) -> dict   (agrega is_test y test_reasons)
"""

import json
import re
import unicodedata


# Patrones (mismos del script find_test_contacts.py)
SPANISH_PATTERNS = [
    r'prueba', r'pruebas', r'probando', r'testeo', r'ejemplo', r'ejemplos',
    r'demostracion', r'borrador', r'probar', r'pruebita'
]

ENGLISH_PATTERNS = [
    r'test', r'testing', r'tester', r'tests', r'dummy', r'fake', r'mock',
    r'example', r'examples', r'demo', r'trial', r'sandbox'
]

E3_PATTERNS = [
    r'e3', r'e-3'
]

GENERIC_PATTERNS = [
    r'temporal', r'temp', r'generico', r'sdasd', r'asdasd', r'qwerty', r'12345',
    r'testea', r'ficticio'
]

# Correos conocidos del equipo E3 que siempre se consideran test, aunque el
# nombre/etiquetas no disparen ningun patron.
TEST_EMAILS = {
    "servandobra@gmail.com",
    "uriel.conse3@gmail.com",
}


def normalize_text(text):
    if not text:
        return ""
    nfkd = unicodedata.normalize("NFD", str(text))
    clean = "".join(c for c in nfkd if unicodedata.category(c) != "Mn")
    return " ".join(clean.lower().split())


def _word_match(pattern, text):
    """
    True si `pattern` aparece como palabra completa en `text` (limites \\b).

    Evita falsos positivos por subcadena: p.ej. 'temp' NO debe matchear dentro
    de 'tlatempa', ni 'test' dentro de 'protesta'. Una palabra solo cuenta si
    esta delimitada por inicio/fin de cadena, espacios o puntuacion (incluido
    el '-' de tags como 'qa-test' o el '@' de un email 'test@...').
    """
    if not text:
        return False
    return re.search(rf'\b{re.escape(pattern)}\b', text, re.IGNORECASE) is not None


def classify_contact(first_name, last_name, email, tags_list):
    """
    Devuelve (reasons, classifications). Si reasons es vacio, el contacto NO es de prueba.
    """
    first_name_lower = (first_name or "").lower()
    last_name_lower = (last_name or "").lower()
    email_lower = (email or "").lower()
    tags_lower = [t.lower() for t in tags_list] if tags_list else []

    reasons = []
    classifications = set()

    # 0. Correos conocidos del equipo E3 (match exacto).
    if email_lower in TEST_EMAILS:
        reasons.append(f"Email en lista E3 ({email_lower})")
        classifications.add("E3")

    # 1. E3 (regex con limites para evitar falsos positivos)
    for p in E3_PATTERNS:
        pattern_regex = re.compile(rf'\b{p}\b|{p}[-_]|[-_]{p}', re.IGNORECASE)
        if pattern_regex.search(first_name_lower) or pattern_regex.search(last_name_lower):
            reasons.append(f"Nombre contiene '{p.upper()}'")
            classifications.add("E3")
        elif p in first_name_lower or p in last_name_lower:
            reasons.append(f"Nombre contiene subcadena '{p.upper()}'")
            classifications.add("E3")

        email_pattern = re.compile(rf'\b{p}\b|^{p}|{p}@|{p}[-_]|[-_]{p}', re.IGNORECASE)
        if email_pattern.search(email_lower):
            reasons.append(f"Email contiene '{p.upper()}'")
            classifications.add("E3")

        if any(p in t for t in tags_lower):
            reasons.append(f"Etiqueta contiene '{p.upper()}'")
            classifications.add("E3")

    # 2. Espanol (palabra completa para evitar falsos positivos por subcadena)
    for p in SPANISH_PATTERNS:
        if _word_match(p, first_name_lower) or _word_match(p, last_name_lower):
            reasons.append(f"Nombre contiene '{p}'")
            classifications.add("Test/Prueba Espanol")
        if _word_match(p, email_lower):
            reasons.append(f"Email contiene '{p}'")
            classifications.add("Test/Prueba Espanol")
        if any(_word_match(p, t) for t in tags_lower):
            reasons.append(f"Etiqueta contiene '{p}'")
            classifications.add("Test/Prueba Espanol")

    # 3. Ingles (palabra completa para evitar falsos positivos por subcadena)
    for p in ENGLISH_PATTERNS:
        if _word_match(p, first_name_lower) or _word_match(p, last_name_lower):
            reasons.append(f"Nombre contiene '{p}'")
            classifications.add("Test/Prueba Ingles")
        if _word_match(p, email_lower):
            reasons.append(f"Email contiene '{p}'")
            classifications.add("Test/Prueba Ingles")
        if any(_word_match(p, t) for t in tags_lower):
            reasons.append(f"Etiqueta contiene '{p}'")
            classifications.add("Test/Prueba Ingles")

    # 4. Generico (palabra completa: 'temp' no debe matchear dentro de 'tlatempa')
    for p in GENERIC_PATTERNS:
        if _word_match(p, first_name_lower) or _word_match(p, last_name_lower):
            reasons.append(f"Nombre contiene temporal '{p}'")
            classifications.add("Temporal/Generico")
        if _word_match(p, email_lower):
            reasons.append(f"Email contiene temporal '{p}'")
            classifications.add("Temporal/Generico")
        if any(_word_match(p, t) for t in tags_lower):
            reasons.append(f"Etiqueta contiene temporal '{p}'")
            classifications.add("Temporal/Generico")

    # 5. Nombre o apellido puramente numerico
    if (first_name_lower and first_name_lower.isdigit()) or (last_name_lower and last_name_lower.isdigit()):
        reasons.append("Nombre o Apellido es numerico")
        classifications.add("Temporal/Generico")

    return sorted(list(set(reasons))), sorted(list(classifications))


def _coerce_tags(raw):
    """Acepta tags como lista, JSON string o None y devuelve lista de strings."""
    if raw is None:
        return []
    if isinstance(raw, list):
        return [str(t) for t in raw if t is not None]
    if isinstance(raw, str):
        try:
            parsed = json.loads(raw)
            if isinstance(parsed, list):
                return [str(t) for t in parsed if t is not None]
        except (ValueError, TypeError):
            pass
    return []


def annotate_contact(contact):
    """
    Recibe un dict de contacto (como lo devuelve db.get_contacts) y le agrega
    los campos `is_test` (bool) y `test_reasons` (lista de strings).
    Devuelve el mismo dict mutado.
    """
    tags = _coerce_tags(contact.get("tags"))
    reasons, _classes = classify_contact(
        contact.get("first_name"),
        contact.get("last_name"),
        contact.get("email"),
        tags,
    )
    contact["is_test"] = bool(reasons)
    contact["test_reasons"] = reasons
    return contact


def is_test_contact(contact):
    """Atajo: True/False sin mutar el dict."""
    tags = _coerce_tags(contact.get("tags"))
    reasons, _ = classify_contact(
        contact.get("first_name"),
        contact.get("last_name"),
        contact.get("email"),
        tags,
    )
    return bool(reasons)
