#!/usr/bin/env python3
"""Lee las parejas «clave—glosa» del impreso tal cual vienen, sin trocearlas.

Por que otra pasada. `comparar_glosas.py` partia el texto del impreso usando las
CLAVES del repo como cortes, porque daba por hecho que el OCR llegaba en crudo,
sin puntuacion. No es asi: Surya conserva los guiones largos, los puntos y coma
y los diacriticos, y el impreso se lee entero tal como esta:

    śrī sūtaḥ uvāca—Śrī Sūta Gosvāmī said; viduraḥ—Vidura; tīrtha-yātrāyām—…

Con eso las parejas del impreso salen solas y no hay que suponer donde acaba
cada glosa. Importa: trocear con las claves del repo hace que una clave que el
impreso NO tiene se lleve por delante la glosa de la siguiente, y eso inventaba
divergencias que no existen.

Lo que se compara es la glosa de la MISMA clave en los dos sitios. Las claves
que solo estan en uno de los dos lados se dejan aparte, que son otro problema.

Uso:
    python3 glosas_impreso.py srimad-bhagavatam/canto-01/chapter-14/sb-1.14.19.md
    python3 glosas_impreso.py --lista            # los versos con candidatos abiertos
"""
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 LIBRO, REPO, SURYA  # noqa: E402

GUION = '[—–]'
_crudo = {}


def crudo(libro):
    """El OCR sin normalizar: hacen falta los guiones y los puntos y coma."""
    if libro not in _crudo:
        d = SURYA / LIBRO[libro]
        t = '\n'.join(f.read_text(encoding='utf-8', errors='ignore')
                      for f in sorted(d.rglob('*.surya.txt'))) if d.is_dir() else ''
        # el OCR corta lineas a mitad de glosa; se unen para no perder parejas
        _crudo[libro] = re.sub(r'\s*\n\s*', ' ', t)
    return _crudo[libro]


def pel(s):
    """Quita marcas, diacriticos y mayusculas: solo para COMPARAR, no para mostrar."""
    s = str(s or '').replace('*', '').replace('’', "'")
    s = ''.join(c for c in unicodedata.normalize('NFD', s) if not unicodedata.combining(c))
    return re.sub(r'\s+', ' ', re.sub(r"[^a-zA-Z' ]", ' ', s)).strip().lower()


def parejas(texto):
    """[(clave, glosa)] de un texto con la forma «clave—glosa; clave—glosa»."""
    fuera = []
    for t in re.split(r'[;\n]', texto):
        m = re.match(r'\s*([^—–]{1,60}?)\s*' + GUION + r'\s*(.+)', t.strip())
        if m:
            fuera.append((pel(m.group(1)), m.group(2).strip().rstrip('.'), m.group(1).strip()))
    return fuera


def plano(libro):
    """El impreso sin diacriticos ni mayusculas pero CON la puntuacion.

    Hace falta que sea una sola cadena y no dos: para localizar el verso hay que
    buscar sin diacriticos —el repo escribe «triṣṭup» y el OCR puede traer otra
    cosa— pero para leer las parejas hacen falta los guiones y los puntos y coma.
    Quitando solo las marcas de acento se conservan las dos cosas y las
    posiciones siguen valiendo para cortar.
    """
    if ('*plano*', libro) not in _crudo:
        s = unicodedata.normalize('NFD', crudo(libro))
        _crudo[('*plano*', libro)] = ''.join(
            c for c in s if not unicodedata.combining(c)).lower()
    return _crudo[('*plano*', libro)]


def bloque_impreso(libro, f, primera=None, primera_glosa=None):
    """El tramo del impreso con las glosas de ESTE verso.

    Anclado en la TRANSLITERACION, que es unica en el libro. Anclar en la
    primera clave del repo no vale: «tasmāt—» encabeza glosas en decenas de
    versos y el bloque salia de otro sitio, con lo que las diferencias que
    aparecian eran de un verso distinto.
    """
    t = plano(libro)
    # Todas las lineas de la transliteracion juntas, no solo la ultima. Los
    # versos se parten en cuatro renglones cortos y el ultimo suele tener dos o
    # tres palabras: pidiendo cuatro a esa linea sola, 72 versos se quedaban sin
    # localizar por una razon que no tiene nada que ver con el OCR.
    lineas = [l.strip() for l in f.read_text(encoding='utf-8').splitlines()
              if l.strip().startswith('>') and not re.search(r'[ऀ-ॿঀ-৿]', l.strip())]
    for corte in range(len(lineas), 0, -1):
        piezas = pel(' '.join(x.lstrip('> ') for x in lineas[:corte])).split()
        if len(piezas) < 4:
            continue
        # El ancla tiene que casar UNA sola vez. Con las ultimas cuatro palabras
        # a secas, en sb/1/13/37 casaba tambien en el verso de al lado y el
        # bloque salia de alli. Intentar validar el bloque despues no sirve: los
        # versos vecinos comparten particulas y el bloque equivocado puntuaba
        # MAS alto que los buenos. Se prueban ventanas de mas a menos y se
        # acepta la primera que sea inequivoca.
        for n in (len(piezas), 9, 7, 6, 5, 4):
            if n > len(piezas):
                continue
            pat = (r'(?<![\w])' + r'[^\w]+'.join(re.escape(p) for p in piezas[-n:])
                   + r'(?![\w])')
            ap = list(re.finditer(pat, t))
            if len(ap) != 1:
                continue
            tramo = t[ap[0].end():ap[0].end() + 3000]
            corte = re.search(r'(?<![\w])(translation|purport)(?![\w])', tramo)
            tramo = tramo[:corte.start()] if corte else tramo
            # Detras del ancla tiene que empezar el bloque de glosas, y su
            # PRIMERA clave tiene que ser la primera del repo. Sin esto, en
            # sb/7/13/7 el ancla casaba dentro de un purport que cita la
            # traduccion del verso, y el «bloque» era prosa de otro sitio: de
            # ahi salio un cambio equivocado que hubo que revertir. Es barato y
            # es tajante: la primera glosa de un verso es su primera palabra.
            ps = parejas(tramo)
            # Un bloque SIN ninguna pareja no es un bloque de glosas: pasa
            # cuando el ancla cae a mitad del verso y lo que sigue es el resto
            # del verso. Exigir «si hay parejas, que la primera cuadre» dejaba
            # colar justo ese caso.
            if not ps or (primera and ps[0][0] != primera):
                continue
            return tramo
    return por_primera_pareja(t, primera, primera_glosa)


def por_primera_pareja(t, primera, glosa):
    """Ancla de reserva: la PRIMERA pareja completa del verso.

    Cuando el OCR estropea la transliteracion no hay manera de localizar el
    verso por ahi, y 32 versos se quedaban sin poder comprobar —entre ellos
    cc/madhya/8/219, donde se cambiaron nueve glosas—. Pero «clave —glosa» junta
    es muy distintiva: «bada sakha —the big branches» solo aparece en su verso.
    Se usa la primera pareja porque es la que menos probabilidades tiene de
    haberse tocado: los cambios salen del cotejo, y si la primera hubiera
    cambiado no serviria de referencia.
    """
    if not primera or not glosa:
        return None
    pat = (r'(?<![\w])' + re.escape(primera) + r'\s*' + GUION + r'\s*'
           + r'[^\w]*'.join(re.escape(w) for w in pel(glosa).split()))
    ap = list(re.finditer(pat, t))
    if len(ap) != 1:
        return None
    tramo = t[ap[0].start():ap[0].start() + 3000]
    corte = re.search(r'(?<![\w])(translation|purport)(?![\w])', tramo)
    return tramo[:corte.start()] if corte else tramo


def repo_parejas(f):
    linea = next((l for l in f.read_text(encoding='utf-8').splitlines()
                  if l.count('—') > 2), '')
    return parejas(linea)


def revisar(ruta):
    f = REPO / (ruta if ruta.endswith('.md') else ruta + '.md')
    if not f.is_file():
        return None, []
    pr = repo_parejas(f)
    if not pr:
        return None, []
    libro = next((k for k, v in LIBRO.items() if ruta.startswith(v)), None)
    if not libro:
        return None, []
    bl = bloque_impreso(libro, f, pr[0][0] if pr else None, pr[0][1] if pr else None)
    if not bl:
        return None, []
    # No se valida el bloque a posteriori: se garantiza al anclar. Comparar la
    # estructura de claves NO distingue el verso bueno del vecino —en
    # sb/1/13/37 el bloque equivocado casaba el 71% de las claves y los buenos
    # de cc/adi/5/111 y cc/antya/1/41 solo el 57%—, porque los versos contiguos
    # comparten particulas. Lo que si distingue es exigir que la transliteracion
    # case una sola vez, y de eso se ocupa `bloque_impreso`.
    if not pr:
        return None, []
    # Una clave puede repetirse dentro del MISMO verso con glosas distintas: en
    # sb/1/1/19 «svādu» sale dos veces, «relishing» y «palatable», y asi lo trae
    # tambien el impreso. Guardando solo la primera y comparandola con todas se
    # inventan divergencias que no existen —y ademas salen invertidas—. Hay que
    # emparejar la primera con la primera y la segunda con la segunda.
    pi = {}
    for c, g, _ in parejas(bl):
        pi.setdefault(c, []).append(g)
    visto = {}
    difs = []
    for c, g, crudo_k in pr:
        i = visto.get(c, 0)
        visto[c] = i + 1
        lista = pi.get(c) or []
        if i >= len(lista):
            continue
        if pel(g) != pel(lista[i]):
            difs.append((crudo_k, g, lista[i]))
    return bl, difs


def main():
    if '--lista' in sys.argv:
        ab = json.loads((HERE / 'glosas_abiertas.json').read_text(encoding='utf-8'))
        rutas = list(dict.fromkeys(x['file'] for x in ab))
    else:
        rutas = [a for a in sys.argv[1:] if not a.startswith('--')]
    fuera = []
    for r in rutas:
        _, difs = revisar(r)
        for k, v, w in difs:
            fuera.append({'file': r, 'clave': k, 'repo': v, 'impreso': w})
        if difs and '--lista' not in sys.argv:
            print(f'\n{r.split("/")[-1]}')
            for k, v, w in difs:
                print(f'    {k[:20]:22} repo={v[:42]!r:46} impreso={w[:42]!r}')
    if '--lista' in sys.argv:
        json.dump(fuera, open(HERE / 'difs_impreso.json', 'w'), ensure_ascii=False, indent=1)
        print(f'  divergencias: {len(fuera)} en {len({x["file"] for x in fuera})} versos')
    return 0


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