#!/usr/bin/env python3
"""Cierra los candidatos de glosas con pruebas comprobables, no por parecido.

De donde vienen. `build_ledger.py --sin-cubetas` desactiva la regla que daba por
terminados 2.314 candidatos de tipo «sinonimos» sin mirarlos. Quedan 1.138
abiertos. De los 42 en que repo y escaneo dicen palabras realmente distintas ya
salieron tres errores del repo (17-ago), asi que el resto merece una
clasificacion con evidencia, no otra cubeta a ciegas.

Las pruebas, en orden. Cada una deja en el veredicto el numero que la sostiene,
para que se pueda comprobar despues:

  REORDENADO  la misma glosa figura como «sobra en el repo» Y como «falta en el
              repo» dentro del mismo verso. No sobra ni falta: esta en los dos
              lados y solo cambia de sitio, porque el alineador compara en linea
              recta y el impreso no siempre lista las glosas en ese orden.

  COMPUESTO   un lado viene partido y el otro entero, y juntos coinciden casi
              del todo. Puede ser el repo separando los miembros del compuesto
              —«mṛga-indraiḥ» frente a «mṛgendraiḥ» del impreso— o el OCR
              rompiendo una palabra entera —«creation» leido «creati n»—. La
              razon guardada dice cual de las dos, segun quien venga partido.

  OCR         la forma del repo sale al menos 3 veces en los 21 escaneos y la
              del escaneo como mucho 2, Y las dos se parecen al menos un 75%.
              Las dos condiciones hacen falta. Solo la proporcion cerraba
              «arjuna»/«arjunah», y las dos son palabras reales: que una forma
              sea frecuente no dice que sea la de ESTA linea. Solo el parecido
              cerraba «anuṣṭup»/«anuṣṭabh», que son las dos formas reales del
              mismo metro y son divergencia de verdad (sb/3/12/45).

  SIN_PRUEBA  lo que no pasa ninguna. Queda ABIERTO a proposito: es donde puede
              haber un error de verdad, y ahi hay que mirar de una en una.

Uso:
    python3 cerrar_glosas.py                 # ensayo, con el reparto
    python3 cerrar_glosas.py --aplicar       # escribe en verdicts.jsonl
"""
import collections
import difflib
import json
import pathlib
import re
import sys
import unicodedata

HERE = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from arbitrar_deriva import texto_escaneo, LIBRO  # noqa: E402

MIN_REPO = 3        # veces que la forma del repo debe salir en el impreso
MAX_SCAN = 2        # y cuantas veces, como mucho, puede salir la del escaneo
MIN_PARECIDO = 0.75  # y cuanto deben parecerse las dos formas


def sd(s):
    return ''.join(c for c in unicodedata.normalize('NFD', str(s or ''))
                   if not unicodedata.combining(c)).lower()


def junta(s):
    return re.sub(r'[\s\-]', '', sd(s))


_cache = {}


def cuenta_corpus(palabra):
    """Igual, pero sobre los 21 libros escaneados a la vez.

    Un termino que sale UNA vez en su libro no se puede arbitrar contando ahi:
    «pradadhmatuh» aparece una vez en el Bhagavatam y su mala lectura
    «nradadhmatuh» tambien, asi que empatan 1-1. En el corpus entero la forma
    buena reaparece en el Gita y en el Caitanya y la mala no reaparece nunca.
    Es la misma prueba con mas muestra, no una prueba mas debil.
    """
    if not palabra.strip():
        return 0
    # Recuento de palabras hecho UNA vez, no una busqueda por candidato: 1.138
    # candidatos x 2 formas x una expresion regular sobre los 21 libros juntos
    # no termina en diez minutos. Con el recuento, cada consulta es inmediata.
    if '*corpus*' not in _cache:
        c = collections.Counter()
        for k in LIBRO:
            c.update(sd(texto_escaneo(k) or '').split())
        _cache['*corpus*'] = c
    c = _cache['*corpus*']
    piezas = sd(palabra).split()
    return min((c.get(p, 0) for p in piezas), default=0)


def cuenta(libro, palabra):
    """Veces que esa palabra aparece suelta en el OCR del libro."""
    if not palabra.strip():
        return 0
    if libro not in _cache:
        # `norm()` de arbitrar_deriva baja a minusculas y quita puntuacion pero
        # CONSERVA los diacriticos, y aqui se compara contra formas ya
        # despojadas. Sin igualar los dos lados, «samsayah» no casa nunca con
        # «saṁśayaḥ» del impreso y todo saldria «sin prueba».
        _cache[libro] = collections.Counter(sd(texto_escaneo(libro) or '').split())
    c = _cache[libro]
    return min((c.get(p, 0) for p in sd(palabra).split()), default=0)


VENTANA = 500       # caracteres a cada lado: el verso y sus glosas caben ahi


def en_el_mismo_pasaje(libro, bueno, malo):
    """¿Estan las dos formas juntas en el impreso, la buena y la mala?

    Devuelve cierto solo si la forma rara aparece UNA vez en el libro y la del
    repo esta a menos de VENTANA caracteres. Es la pagina corrigiendose a si
    misma: el verso transliterado y la glosa dicen lo mismo, y el OCR fallo en
    una de las dos.
    """
    if not (bueno.strip() and malo.strip() and libro):
        return False
    # `sd()` sobre el libro entero cuesta lo suyo, y aqui se llama una vez por
    # candidato: sin guardarlo, el Bhagavatam se renormaliza cientos de veces y
    # la pasada no termina.
    clave = ('*texto*', libro)
    if clave not in _cache:
        _cache[clave] = sd(texto_escaneo(libro) or '')
    txt = _cache[clave]
    ap = list(re.finditer(r'(?<![\w])' + re.escape(sd(malo)) + r'(?![\w])', txt))
    if len(ap) != 1:
        return False
    m = ap[0]
    ventana = txt[max(0, m.start() - VENTANA):m.end() + VENTANA]
    return bool(re.search(r'(?<![\w])' + re.escape(sd(bueno)) + r'(?![\w])', ventana))


def main():
    aplicar = '--aplicar' in sys.argv
    ab = json.loads((HERE / 'glosas_abiertas.json').read_text(encoding='utf-8'))
    CARP = {v: k for k, v in LIBRO.items()}
    # Para el reordenamiento hace falta ver el fichero entero de una vez: que
    # palabras «sobran» y cuales «faltan» dentro del mismo verso.
    sobran = collections.defaultdict(collections.Counter)
    faltan = collections.defaultdict(collections.Counter)
    for x in ab:
        r, s = sd(x.get('repo', '')), sd(x.get('scan', ''))
        if r and not s:
            sobran[x.get('file')][r] += 1
        elif s and not r:
            faltan[x.get('file')][s] += 1

    reparto = {'REORDENADO': [], 'COMPUESTO': [], 'OCR': [], 'SIN_PRUEBA': []}
    for x in ab:
        r, s = str(x.get('repo', '')), str(x.get('scan', ''))
        libro = CARP.get(x.get('libro'))
        # Una glosa que «sobra» en el repo y a la vez «falta», dentro del mismo
        # verso y con la misma palabra, no sobra ni falta: esta en los dos lados
        # y solo cambia de sitio. El alineador compara en linea recta y las
        # glosas del impreso no siempre siguen el orden del repo.
        f = x.get('file')
        if r and not s and faltan[f].get(sd(r)):
            reparto['REORDENADO'].append((x, f'«{r}» tambien esta en el escaneo de {f}'))
            continue
        if s and not r and sobran[f].get(sd(s)):
            reparto['REORDENADO'].append((x, f'«{s}» tambien esta en el repo de {f}'))
            continue
        if r and s and junta(r) == junta(s):
            reparto['COMPUESTO'].append((x, f'unidas coinciden: {junta(r)!r}'))
            continue
        # Compuesto con sandhi. El repo separa los miembros con guion y el
        # impreso los imprime fundidos, pero fundirlos en sanscrito cambia la
        # juntura: «ati»+«autsukya» da «atyautsukya», «antaḥ»+«yāmī» da
        # «antaryāmī», «aja»+«anda» da «ajāṇḍa». Juntar las letras a secas no
        # basta, y por eso COMPUESTO daba cero. Se exige que un lado venga
        # partido y el otro entero, y que juntos coincidan casi del todo: la
        # unica diferencia esta en la costura.
        pr, ps = sd(r).replace('-', ' ').split(), sd(s).replace('-', ' ').split()
        if r and s and (len(pr) > 1) != (len(ps) > 1):
            if difflib.SequenceMatcher(None, junta(r), junta(s)).ratio() >= 0.85:
                # Quien viene partido cambia la explicacion, y la razon que se
                # guarda tiene que ser la cierta: si parte el REPO es la
                # convencion de las glosas (sandhi); si parte el ESCANEO es que
                # el OCR rompio la palabra («creation» -> «creati n»).
                cual = ('el repo separa los miembros del compuesto y el impreso los funde'
                        if len(pr) > 1 else 'el OCR partio en dos una palabra entera')
                reparto['COMPUESTO'].append(
                    (x, f'{cual}: {junta(r)!r} / {junta(s)!r}'))
                continue
        if not libro:
            reparto['SIN_PRUEBA'].append((x, 'sin escaneo de ese libro'))
            continue
        if not r or not s:
            # Contar la cadena vacia da cero siempre, asi que la prueba de
            # frecuencia no dice nada aqui: pasaria cualquier cosa.
            reparto['SIN_PRUEBA'].append((x, 'un lado vacio y sin pareja que lo explique'))
            continue
        cr, cs = cuenta_corpus(r), cuenta_corpus(s)
        # La frecuencia SOLO decide si la forma del escaneo no esta atestiguada
        # en ningun sitio. «arjuna» sale 907 veces y «arjunah» 23: la prueba de
        # proporcion la daria por mala lectura, y sin embargo las dos son
        # palabras reales —vocativo y nominativo— y el impreso puede poner
        # cualquiera AHI. Que una forma sea frecuente no dice que sea la de esta
        # linea. Una mala lectura de verdad no reaparece: «samsavah»,
        # «pranamva», «nradadhmatuh» no salen en ningun otro sitio de los 21
        # libros. Ese es el limite defendible, no la proporcion.
        # Y ademas tiene que PARECERSE. Una mala lectura deforma la palabra sin
        # alejarla: «radharn» por «radham», «pumah» por «purnah», «gaucla» por
        # «gauda». Cuando las dos formas se parecen poco ya no es tipografia
        # sino otra palabra, y ahi la frecuencia no debe decidir: en sb/3/12/45
        # el repo pone «anuṣṭup» y el impreso «anuṣṭabh», que son las dos formas
        # reales del mismo metro. La regla sin este filtro lo cerraba como
        # ruido, y es divergencia de verdad.
        parecido = difflib.SequenceMatcher(None, sd(r), sd(s)).ratio()
        # Antes de la frecuencia, la prueba mas fuerte: que la pagina se
        # desmienta sola. El recuento INCLUYE la aparicion que se esta
        # examinando, asi que «pradadhmatuh» x1 / «nradadhmatuh» x1 no es un
        # empate: el impreso trae la forma buena en la linea del verso y la mala
        # solo en la glosa, a quinientos caracteres de distancia. Cuando las dos
        # estan en el mismo pasaje no hace falta el corpus para nada.
        if parecido >= MIN_PARECIDO and en_el_mismo_pasaje(libro, r, s):
            reparto['OCR'].append(
                (x, f'el impreso trae «{r}» en el mismo pasaje y «{s}» solo en la '
                    f'glosa; se parecen {parecido:.0%}'))
            continue
        if cr >= MIN_REPO and cs <= MAX_SCAN and parecido >= MIN_PARECIDO:
            reparto['OCR'].append(
                (x, f'la forma del repo sale x{cr} en los 21 escaneos y la del '
                    f'escaneo x{cs}: no esta atestiguada; se parecen {parecido:.0%}'))
        else:
            reparto['SIN_PRUEBA'].append(
                (x, f'corpus repo x{cr}/escaneo x{cs}: la del escaneo tambien esta atestiguada'
                    if cs > MAX_SCAN else f'corpus repo x{cr}/escaneo x{cs}'))
    for k, v in reparto.items():
        print(f'  {len(v):5}  {k}')
    if aplicar:
        n = 0
        with (HERE / 'verdicts.jsonl').open('a', encoding='utf-8') as fh:
            for k in ('REORDENADO', 'COMPUESTO', 'OCR'):
                for x, prueba in reparto[k]:
                    cabeza = {'REORDENADO': 'la glosa esta en los dos lados, solo cambia de orden; ',
                              'COMPUESTO': '',
                              'OCR': 'mala lectura del OCR en la glosa; '}[k]
                    razon = cabeza + prueba
                    fh.write(json.dumps({'repo': x.get('repo'), 'scan': x.get('scan'),
                                         'estado': 'DESCARTADO', 'razon': razon,
                                         'fecha': '2026-08-17'}, ensure_ascii=False) + '\n')
                    n += 1
        print(f'\n  veredictos escritos: {n}')
        print(f'  quedan ABIERTOS a proposito: {len(reparto["SIN_PRUEBA"])}')
        json.dump([x for x, _ in reparto['SIN_PRUEBA']],
                  open(HERE / 'glosas_sin_prueba.json', 'w'), ensure_ascii=False, indent=1)
    else:
        print('\n  (ensayo: anade --aplicar)')
        for k in ('REORDENADO', 'OCR', 'COMPUESTO', 'SIN_PRUEBA'):
            print(f'\n  --- {k}')
            for x, p in reparto[k][:4]:
                print(f"      repo={str(x.get('repo'))[:24]!r:28} scan={str(x.get('scan'))[:24]!r:28} {p}")
    return 0


if __name__ == '__main__':
    sys.exit(main())
