#!/usr/bin/env python3
"""Comprueba que el indio y la transliteracion del repo dicen lo mismo.

De donde sale. Las tres erratas que encontro el alineador de verso
—santosa/sankoca, ei/ek, pache/pacne— compartian la misma firma: el fichero SE
CONTRADECIA A SI MISMO. El devanagari o el bengali decia una cosa y la linea de
transliteracion, otra. Eso se puede detectar SIN el escaneo, y sin heredar el
ruido del OCR, que en escritura india es demasiado alto para servir (61 % de
las lineas del Caitanya-caritamrta no casan letra a letra con el OCR, y no
porque el texto difiera).

Aqui se translitera el indio del propio fichero a IAST y se compara con la
linea que el fichero ya trae transliterada. Lo que no cuadre es una
incoherencia interna, y la incoherencia interna es la unica excepcion que
admite la regla de respetar el impreso.

La transliteracion no busca ser perfecta —el sandhi y las conjuntas la
complican— sino comparable: se normaliza a un esqueleto consonantico que
sobrevive a las diferencias de notacion. Por eso el umbral no es la igualdad
sino el parecido.

Uso: python3 coherencia_verso.py <libro|--todos> [--min 0.80]
"""
import json, re, sys, difflib, unicodedata, pathlib, collections

HERE = pathlib.Path(__file__).resolve().parent
REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
OUT = HERE / 'out_coherencia'

DEVA = re.compile(r'[ऀ-ॿ]')
BENG = re.compile(r'[ঀ-৿]')
INDIO = re.compile(r'[ऀ-ॿঀ-৿]')

# consonantes: devanagari y bengali comparten el orden del alfabeto, asi que la
# misma tabla sirve desplazando el bloque
# El bloque de consonantes va de 0x15 a 0x39: 37 huecos. Devanagari y bengali
# comparten el orden, asi que la misma tabla sirve desplazando la base. Las
# series retrofleja y dental se colapsan a proposito (t/th/d/dh/n dos veces):
# lo que se compara es un ESQUELETO, no una transliteracion academica.
CONS = ['k','kh','g','gh','n',      # 0x15 ka kha ga gha nga
        'c','ch','j','jh','n',      # 0x1a ca cha ja jha nya
        't','th','d','dh','n',      # 0x1f ta tha da dha na (retroflejas)
        't','th','d','dh','n',      # 0x24 ta tha da dha na (dentales)
        'n',                        # 0x29 nnna — la que desalineaba la tabla
        'p','ph','b','bh','m',      # 0x2a pa pha ba bha ma
        'y','r','r','l','l','l','v',# 0x2f ya ra rra la lla llla va
        's','s','s','h']            # 0x36 sha ssa sa ha

VOC = {0x05: 'a', 0x06: 'a', 0x07: 'i', 0x08: 'i', 0x09: 'u', 0x0A: 'u',
       0x0B: 'r', 0x0F: 'e', 0x10: 'ai', 0x13: 'o', 0x14: 'au'}
MAT = {0x3E: 'a', 0x3F: 'i', 0x40: 'i', 0x41: 'u', 0x42: 'u', 0x43: 'r',
       0x47: 'e', 0x48: 'ai', 0x4B: 'o', 0x4C: 'au'}


def a_iast(s):
    """Esqueleto consonantico del indio. No es transliteracion academica: es
    una forma comparable, que es lo que hace falta."""
    out = []
    base = 0x0900 if DEVA.search(s) else 0x0980
    i = 0
    while i < len(s):
        c = ord(s[i]) - base
        if 0x15 <= c <= 0x39:
            out.append(CONS[c - 0x15])
            # 'a' implicita salvo virama o matra detras
            sig = ord(s[i + 1]) - base if i + 1 < len(s) else -1
            if sig != 0x4D and sig not in MAT: out.append('a')
        elif c in MAT: out.append(MAT[c])
        elif c in VOC: out.append(VOC[c])
        elif c == 0x02: out.append('m')          # anusvara
        elif c == 0x03: out.append('h')          # visarga
        i += 1
    return ''.join(out)


def esqueleto(s):
    """La transliteracion latina reducida a lo mismo: sin diacriticos, sin
    distinguir las series que la tabla de arriba colapsa."""
    s = unicodedata.normalize('NFD', s.lower())
    s = ''.join(ch for ch in s if unicodedata.category(ch) != 'Mn')
    s = re.sub(r'[^a-z]', '', s)
    for a, b in (('sh', 's'), ('ri', 'r'), ('w', 'v')):
        s = s.replace(a, b)
    # El bengali NO distingue b de v: la misma letra ব vale para las dos, y la
    # transliteracion elige segun la palabra. 'biruddha'/'viruddha',
    # 'sbatah'/'svatah' son la misma cosa.
    s = s.replace('b', 'v')
    # Vocal doblada: la sale de juntar la matra con la 'a' implicita
    # ('prakasiyaa' por 'prakasiya'). No es una diferencia de texto.
    s = re.sub(r'([aeiou])\1+', r'\1', s)
    # ANUSVARA. El indio lo escribe con un solo signo y la transliteracion elige
    # entre m, n, ng o ny segun la consonante que siga. No es diferencia de
    # texto: 131 de las discrepancias eran esto.
    s = s.replace('n', 'm')
    return s


def bloques(md):
    """Pares (linea india, linea transliterada) de cada verso del fichero."""
    ind = []; lat = []
    for l in md.splitlines():
        s = l.strip()
        if not s.startswith('>'): continue
        s = s.lstrip('> ').rstrip('\\').strip()
        if not s: continue
        if INDIO.search(s): ind.append(s)
        elif re.search(r'[a-zA-Zāīūṛṁṅñṇṣśṭḍḥ]', s): lat.append(s)
    return ind, lat


def main():
    if len(sys.argv) < 2: print(__doc__); return 1
    minimo = float(sys.argv[sys.argv.index('--min') + 1]) if '--min' in sys.argv else 0.80
    OUT.mkdir(exist_ok=True)
    libros = ([d.name for d in REPO.iterdir() if d.is_dir()
               and any(INDIO.search(f.read_text(encoding='utf-8')[:4000])
                       for f in list(d.rglob('*.md'))[:3])]
              if sys.argv[1] == '--todos' else [sys.argv[1]])
    tot = 0
    for b in sorted(libros):
        casos = []; n = 0
        for f in sorted((REPO / b).rglob('*.md')):
            ind, lat = bloques(f.read_text(encoding='utf-8'))
            if not ind or not lat: continue
            # PALABRA A PALABRA, no verso a verso. Comparando la linea entera,
            # una silaba cambiada mueve el parecido de 1,000 a 0,939 y se
            # esconde entre el ruido del sandhi. Alineadas las palabras, la que
            # falla queda sola.
            A = [esqueleto(a_iast(w)) for w in ' '.join(ind).split()]
            C = [esqueleto(w) for w in ' '.join(lat).split()]
            A = [w for w in A if w]; C = [w for w in C if w]
            if not A or not C: continue
            n += 1
            sm = difflib.SequenceMatcher(None, A, C, autojunk=False)
            for op, i1, i2, j1, j2 in sm.get_opcodes():
                if op != 'replace' or (i2 - i1) != 1 or (j2 - j1) != 1: continue
                a, c = A[i1], C[j1]
                r = difflib.SequenceMatcher(None, a, c).ratio()
                # muy distintas = palabras diferentes de verdad (sandhi, corte
                # de compuesto); casi iguales = una letra, que es lo que busco
                if not (0.55 <= r < 0.95): continue
                # UNA SOLA LETRA. Lo que separa el hallazgo del ruido: si las dos
                # formas tienen la misma longitud y difieren en un unico caracter,
                # es una errata. Si difieren en mas, casi siempre es sandhi, un
                # compuesto partido de otra manera, o una conjunta que el
                # transliterador no resuelve. De 6.823 candidatos en el
                # Caitanya-caritamrta, esto deja 54 — y ahi estaban los tres
                # reales.
                if len(a) != len(c): continue
                dif = [(u, v) for u, v in zip(a, c) if u != v]
                if len(dif) != 1: continue
                casos.append({'file': str(f.relative_to(REPO)), 'parecido': round(r, 3),
                              'letra': f'{dif[0][0]} -> {dif[0][1]}',
                              'del_indio': a, 'de_la_translit': c,
                              'contexto': ' '.join(C[max(0, j1 - 3):j1 + 4]),
                              'region': 'coherencia'})
        print(f'  {b:42} {n:6} versos | incoherentes: {len(casos)}')
        if casos:
            with open(OUT / f'{b}.candidates.jsonl', 'w', encoding='utf-8') as fh:
                for x in casos: fh.write(json.dumps(x, ensure_ascii=False) + '\n')
        tot += len(casos)
    print(f'\ntotal: {tot}')
    return 0


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