"""
Reconocedores mexicanos para Microsoft Presidio: CURP, RFC, CLABE y clave de elector (INE).

Regalo de la ponencia "Agentes de IA para oficiales de cumplimiento" - Vladimir Díaz · Menteia
Licencia MIT. Úselo, modifíquelo y compártalo. Sin garantía: pruébelo con sus propios datos antes de producción.

Cada reconocedor combina un patrón con su dígito verificador oficial, así que un texto que solo
"se parece" a una CURP o a una CLABE no se marca con confianza alta.

Instalación:
    pip install presidio-analyzer presidio-anonymizer
    python -m spacy download es_core_news_md

Uso rápido:
    python reconocedores_mx.py            # corre el ejemplo del final

    from reconocedores_mx import crear_analizador
    analizador = crear_analizador()
    resultados = analizador.analyze(text="Mi CURP es ...", language="es")
"""
import re
from datetime import date

# ---------------------------------------------------------------------------
# Validadores (no dependen de Presidio; se pueden usar solos)
# ---------------------------------------------------------------------------

_ENTIDADES_CURP = {
    "AS", "BC", "BS", "CC", "CL", "CM", "CS", "CH", "DF", "DG", "GT", "GR", "HG", "JC", "MC", "MN",
    "MS", "NT", "NL", "OC", "PL", "QT", "QR", "SP", "SL", "SR", "TC", "TS", "TL", "VZ", "YN", "ZS", "NE",
}
_TABLA_CURP = "0123456789ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"


def _fecha_valida(aa: str, mm: str, dd: str) -> bool:
    try:
        # El siglo no importa para saber si la fecha existe (29 de febrero se valida con un año bisiesto)
        date(2000 + int(aa), int(mm), int(dd))
        return True
    except ValueError:
        return False


def curp_valida(curp: str) -> bool:
    """Estructura + fecha + entidad + dígito verificador (posición 18)."""
    c = curp.strip().upper()
    if not re.fullmatch(r"[A-Z][AEIOUX][A-Z]{2}\d{6}[HMX][A-Z]{2}[B-DF-HJ-NP-TV-ZÑ]{3}[A-Z0-9]\d", c):
        return False
    if c[11:13] not in _ENTIDADES_CURP or not _fecha_valida(c[4:6], c[6:8], c[8:10]):
        return False
    suma = sum(_TABLA_CURP.index(ch) * (18 - i) for i, ch in enumerate(c[:17]))
    return (10 - suma % 10) % 10 == int(c[17])


_TABLA_RFC = "0123456789ABCDEFGHIJKLMN&OPQRSTUVWXYZ Ñ"


def rfc_valido(rfc: str) -> bool:
    """Persona física (13) o moral (12): estructura + fecha + dígito verificador del SAT."""
    r = rfc.strip().upper()
    if not re.fullmatch(r"[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{2}[0-9A]", r):
        return False
    pref = 4 if len(r) == 13 else 3
    if not _fecha_valida(r[pref:pref + 2], r[pref + 2:pref + 4], r[pref + 4:pref + 6]):
        return False
    base = r if len(r) == 13 else " " + r          # la persona moral se completa con un espacio
    suma = sum(_TABLA_RFC.index(ch) * (13 - i) for i, ch in enumerate(base[:12]))
    resto = suma % 11
    esperado = "0" if resto == 0 else ("A" if 11 - resto == 10 else str(11 - resto))
    return r[-1] == esperado


def clabe_valida(clabe: str) -> bool:
    """18 dígitos con ponderación 3-7-1 (estándar de Banxico)."""
    c = re.sub(r"[\s-]", "", clabe)
    if not re.fullmatch(r"\d{18}", c):
        return False
    pesos = (3, 7, 1)
    suma = sum((int(d) * pesos[i % 3]) % 10 for i, d in enumerate(c[:17]))
    return (10 - suma % 10) % 10 == int(c[17])


def clave_elector_valida(clave: str) -> bool:
    """Clave de elector de la credencial del INE: 6 letras, fecha AAMMDD, entidad 01-32, sexo H/M, 3 dígitos.
    No tiene dígito verificador público: se valida estructura, fecha y entidad."""
    c = clave.strip().upper()
    if not re.fullmatch(r"[A-Z]{6}\d{8}[HM]\d{3}", c):
        return False
    return _fecha_valida(c[6:8], c[8:10], c[10:12]) and 1 <= int(c[12:14]) <= 32


# ---------------------------------------------------------------------------
# Reconocedores de Presidio
# ---------------------------------------------------------------------------

def _reconocedores(idioma: str = "es"):
    from presidio_analyzer import Pattern, PatternRecognizer

    class _ConValidador(PatternRecognizer):
        validador = None

        def validate_result(self, pattern_text: str):
            # True sube la confianza al máximo; False descarta la coincidencia
            return bool(type(self).validador(pattern_text))

    class CurpRecognizer(_ConValidador):
        validador = staticmethod(curp_valida)

    class RfcRecognizer(_ConValidador):
        validador = staticmethod(rfc_valido)

    class ClabeRecognizer(_ConValidador):
        validador = staticmethod(clabe_valida)

    class ClaveElectorRecognizer(_ConValidador):
        validador = staticmethod(clave_elector_valida)

    return [
        CurpRecognizer(
            supported_entity="MX_CURP", supported_language=idioma, name="Reconocedor CURP",
            patterns=[Pattern("CURP", r"\b[A-Z][AEIOUX][A-Z]{2}\d{6}[HMX][A-Z]{2}[B-DF-HJ-NP-TV-ZÑ]{3}[A-Z0-9]\d\b", 0.5)],
            context=["curp", "clave única", "registro de población", "renapo"],
        ),
        RfcRecognizer(
            supported_entity="MX_RFC", supported_language=idioma, name="Reconocedor RFC",
            patterns=[
                Pattern("RFC persona física", r"\b[A-ZÑ&]{4}\d{6}[A-Z0-9]{2}[0-9A]\b", 0.4),
                Pattern("RFC persona moral", r"\b[A-ZÑ&]{3}\d{6}[A-Z0-9]{2}[0-9A]\b", 0.3),
            ],
            context=["rfc", "registro federal", "contribuyente", "sat", "factura", "cfdi"],
        ),
        ClabeRecognizer(
            supported_entity="MX_CLABE", supported_language=idioma, name="Reconocedor CLABE",
            patterns=[Pattern("CLABE", r"\b\d{3}[\s-]?\d{3}[\s-]?\d{11}[\s-]?\d\b", 0.3)],
            context=["clabe", "cuenta", "transferencia", "spei", "banco", "depósito"],
        ),
        ClaveElectorRecognizer(
            supported_entity="MX_INE_CLAVE_ELECTOR", supported_language=idioma, name="Reconocedor clave de elector",
            patterns=[Pattern("Clave de elector", r"\b[A-Z]{6}\d{8}[HM]\d{3}\b", 0.4)],
            context=["ine", "ife", "clave de elector", "credencial", "votar"],
        ),
    ]


def crear_analizador(modelo_spacy: str = "es_core_news_md"):
    """AnalyzerEngine en español con los cuatro reconocedores mexicanos ya registrados."""
    from presidio_analyzer import AnalyzerEngine
    from presidio_analyzer.nlp_engine import NlpEngineProvider

    nlp = NlpEngineProvider(nlp_configuration={
        "nlp_engine_name": "spacy",
        "models": [{"lang_code": "es", "model_name": modelo_spacy}],
    }).create_engine()
    analizador = AnalyzerEngine(nlp_engine=nlp, supported_languages=["es"])
    for r in _reconocedores("es"):
        analizador.registry.add_recognizer(r)
    return analizador


ENTIDADES_MX = ["MX_CURP", "MX_RFC", "MX_CLABE", "MX_INE_CLAVE_ELECTOR"]


if __name__ == "__main__":
    # Datos de ejemplo (ficticios o de documentación pública), nunca de clientes reales
    texto = (
        "El cliente con CURP HEGG560427MVZRRL04 y RFC GODE561231GR8 pidió que el depósito se haga "
        "a la CLABE 002010077777777771. Su clave de elector es GMVLMR80070501M100."
    )
    try:
        from presidio_anonymizer import AnonymizerEngine
        analizador = crear_analizador()
        hallazgos = analizador.analyze(text=texto, language="es", entities=ENTIDADES_MX)
        for h in hallazgos:
            print(f"{h.entity_type:<22} {texto[h.start:h.end]:<20} confianza {h.score:.2f}")
        print("\nTexto enmascarado antes de mandarlo a la IA:\n")
        print(AnonymizerEngine().anonymize(text=texto, analyzer_results=hallazgos).text)
    except ImportError:
        print("Presidio no está instalado; pruebo solo los validadores.")
        print("CURP ", curp_valida("HEGG560427MVZRRL04"))
        print("RFC  ", rfc_valido("GODE561231GR8"))
        print("CLABE", clabe_valida("002010077777777771"))
        print("INE  ", clave_elector_valida("GMVLMR80070501M100"))
