#!/usr/bin/env python3
"""Aplica, para un libro, las divergencias de grafia de UNA PALABRA contra el impreso.

Consolida el metodo que funciono en el TLC. Cuatro filtros encadenados, de mas
barato a mas caro, y una garantia final:

  1. El par tiene que ser dos palabras alfabeticas de >=4 letras y parecidas.
  2. RECUENTO en todas las paginas del escaneo: la forma del repo no puede
     aparecer NI UNA VEZ y la del impreso al menos dos.
  3. Si el impreso usa AMBAS formas, se mira el tramo de paginas de ese fichero:
     solo se aplica si ahi no aparece la forma del repo.
  4. CONSENSO de OCR: las tres lecturas (scan, scan6, scan4) deben coincidir.
  5. GARANTIA: el numero de ocurrencias en el fichero tiene que coincidir con el
     de casos que senala el alineador. Si no, se aparta. Esto es lo que impide
     arrasar un capitulo por una palabra inglesa corriente ('numbers'->'number').

REGLA APRENDIDA A GOLPES (2026-08-08): usar SIEMPRE limite de palabra, tambien
en D1. Si en el repo se usa \\b y en D1 un REPLACE por subcadena, las palabras
derivadas divergen ('Vrajabhumi'->'Brajabhumi') y hasta se corrompen
('nothing'->'nnothing'). Este script escribe las tres copias con el mismo
criterio.

Uso:  python3 aplicar_grafias.py <libro-escaneo> <carpeta-repo> <prefijo-ref> [--aplicar]
Ej.:  python3 aplicar_grafias.py nectar-of-devotion nectar-of-devotion nod --aplicar
"""
import json, re, sys, glob, os, collections, pathlib, subprocess, difflib

HERE = pathlib.Path(__file__).resolve().parent
REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
ASTRO = pathlib.Path('/Users/jaganat/git_projects/astro_vedabase')
PAL = re.compile(r'^[a-zāīūṛṅñṭḍṇśṣḥṁ]{4,}$')

# Diccionario ingles, para el guardarrail del punto 6 (abajo).
DIC = set()
_d = pathlib.Path('/usr/share/dict/words')
if _d.exists():
    DIC = {w.strip().lower() for w in _d.read_text(errors='ignore').splitlines() if w.strip()}
# grafias britanicas/antiguas que el impreso si usa y el diccionario no siempre
PERMITIDAS = {'benefitted', 'travelled', 'alloted', 'enamoured', 'advertize', 'despatched',
              'criticise', 'worshipable', 'neighbourhood', 'wilfully', 'fourty'}
def en_dic(w):
    """El diccionario del sistema no trae plurales ni formas verbales, asi que
    'earrings' no figuraba y se colaba 'earrings' -> 'carrings' como si el
    impreso mandara. Se prueban tambien las formas base."""
    if w in DIC: return True
    for suf, base in (('s', ''), ('es', ''), ('ed', ''), ('ed', 'e'), ('ing', ''), ('ing', 'e'),
                      ('ies', 'y'), ('d', '')):
        if w.endswith(suf) and (w[:-len(suf)] + base) in DIC: return True
    return False


q = lambda s: "'" + s.replace("'", "''") + "'"
dec = json.JSONDecoder()


def wrangler(sql):
    for _ in range(3):
        p = subprocess.run(['npx', 'wrangler', 'd1', 'execute', 'vedabase-search-db',
                            '--remote', '--command', sql],
                           capture_output=True, text=True, cwd=str(ASTRO),
                           env={**os.environ, 'CF_API_TOKEN': '', 'CLOUDFLARE_API_TOKEN': ''})
        if 'rows_written' in p.stdout:
            return True
    return False


def rep_palabra(txt, viejo, nuevo):
    """Sustituye respetando el limite de palabra y la mayuscula inicial."""
    def f(m):
        s = m.group(0)
        return nuevo.capitalize() if s[:1].isupper() else nuevo
    return re.subn(rf'\b{re.escape(viejo)}\b', f, txt, flags=re.I)


def main():
    if len(sys.argv) < 4:
        print(__doc__); return 1
    libro_scan, carpeta, pref = sys.argv[1], sys.argv[2], sys.argv[3]
    aplicar = '--aplicar' in sys.argv

    ocr_dirs = [d for d in (HERE / 'surya' / libro_scan).iterdir() if d.is_dir()]
    if not ocr_dirs:
        print('sin OCR para', libro_scan); return 1
    paginas = {p.stem.replace('.surya', ''): p for d in ocr_dirs for p in d.glob('*.surya.txt')}
    libro_txt = ' '.join(p.read_text(encoding='utf-8', errors='ignore') for p in paginas.values())

    abiertos = {(r['file'], r['idx']) for r in map(json.loads, open(HERE / 'ledger.jsonl', encoding='utf-8'))
                if r['estado'] == 'ABIERTO'}
    cands = []
    pags_fich = collections.defaultdict(set)
    cf = HERE / 'out_surya' / f'{libro_scan}.candidates.jsonl'
    if not cf.exists():
        print('sin candidatos para', libro_scan); return 1
    for i, l in enumerate(open(cf, encoding='utf-8')):
        d = json.loads(l); d['idx'] = i
        cands.append(d)
        pg = (d.get('page') or '').split('#')[-1]
        if pg: pags_fich[d['file']].add(pg)

    # candidatos vivos que son pares de una palabra
    porfich = collections.defaultdict(collections.Counter)
    lecturas = collections.defaultdict(set)
    for d in cands:
        if (d['file'], d['idx']) not in abiertos: continue
        a = (d.get('repo') or '').strip().lower(); b = (d.get('scan') or '').strip().lower()
        if not (PAL.match(a) and PAL.match(b) and a != b): continue
        if not (a[:3] == b[:3] or difflib.SequenceMatcher(None, a, b).ratio() >= 0.72): continue
        porfich[d['file']][(a, b)] += 1
        for k in ('scan', 'scan6', 'scan4'):
            if d.get(k): lecturas[(d['file'], a, b)].add(d[k].strip().lower())

    def tramo(fich):
        ps = sorted(int(x[1:]) for x in pags_fich.get(fich, ()) if x[1:].isdigit())
        if not ps: return ''
        return ' '.join(paginas[f'p{n:04d}'].read_text(encoding='utf-8', errors='ignore')
                        for n in range(ps[0], ps[-1] + 1) if f'p{n:04d}' in paginas)

    repo_txt_all = ' '.join(open(f, encoding='utf-8').read().lower()
                            for f in glob.glob(str(REPO / carpeta / '**/*.md'), recursive=True))
    aplicadas = collections.Counter(); apartadas = collections.Counter(); porref = collections.defaultdict(set)
    for fich, pares in sorted(porfich.items()):
        p = REPO / fich
        if not p.exists(): continue
        txt = orig = p.read_text(encoding='utf-8')
        t_tramo = None
        hall_ctx = {}
        for (aa, _bb), _n in pares.items():
            hall_ctx[(fich, aa)] = [l for l in txt.splitlines()
                                    if re.search(rf'\b{re.escape(aa)}\b', l, re.I)][:2]
        for (a, b), n in pares.items():
            na = len(re.findall(rf'\b{re.escape(a)}\b', libro_txt, re.I))
            nb = len(re.findall(rf'\b{re.escape(b)}\b', libro_txt, re.I))
            if nb < 2: apartadas['la forma del impreso aparece <2 veces'] += n; continue
            if na:                                    # el impreso usa las dos
                if t_tramo is None: t_tramo = tramo(fich)
                if re.search(rf'\b{re.escape(a)}\b', t_tramo, re.I):
                    apartadas['el impreso alterna en ese tramo'] += n; continue
            if len(lecturas.get((fich, a, b), set())) > 1:
                apartadas['las lecturas de OCR no coinciden'] += n; continue
            # 6. GUARDARRAIL DE DICCIONARIO. Si el repo tiene una palabra inglesa
            #    correcta y el impreso, segun el OCR, una que no lo es, casi
            #    siempre es dano del reconocimiento ('opulence' -> 'pulence'),
            #    no la grafia del impreso. Y si AMBAS son palabras inglesas, el
            #    cambio puede alterar el sentido ('ghastly' -> 'ghostly'): eso
            #    no se decide por recuento, hace falta la imagen.
            # 5.bis TRANSLITERACION SANSCRITA: no se decide por recuento. El OCR
            #    confunde y/v, b/v y rn/m justo aqui. Caso real: propuso
            #    'yata-ātmavan' -> 'vata-ātmavan' en una linea de sinonimos del
            #    BG, y lo correcto es 'yata' (autocontrolado). Requiere imagen.
            #    Se detecta por la LINEA: una glosa palabra-por-palabra lleva
            #    muchos '—' y muchos asteriscos. En prosa corriente no pasa.
            lineas = hall_ctx.get((fich, a), [])
            if any(l.count('—') > 3 and l.count('*') > 6 for l in lineas):
                apartadas['linea de sinonimos (sanscrito): requiere imagen'] += n; continue
            if re.search(r'[āīūṛṅñṭḍṇśṣḥṁ]', a) or re.search(r'[āīūṛṅñṭḍṇśṣḥṁ]', b):
                apartadas['palabra con diacriticos: requiere imagen'] += n; continue
            # Regla final, la mas conservadora: solo se aplica automaticamente
            # cuando el impreso trae una palabra inglesa CORRECTA y el repo no
            # (o sea, una errata pura del repo), o cuando es una de las grafias
            # britanicas ya verificadas. Todo lo demas —sanscrito, cambios de
            # palabra, dano de OCR— se aparta para adjudicar por imagen.
            # Prueba independiente del diccionario, que no trae formas
            # irregulares ('became', 'women'): si la forma del repo aparece
            # MUCHAS veces en el propio libro, no es una errata suelta sino una
            # palabra corriente, y el cambio seria de sentido. Requiere imagen.
            if repo_txt_all.count(a) > 3 and b not in PERMITIDAS:
                apartadas['la forma del repo es frecuente: requiere imagen'] += n; continue
            if DIC and b not in PERMITIDAS:
                if not en_dic(b):
                    apartadas['el impreso no es palabra inglesa: requiere imagen'] += n; continue
                if en_dic(a):
                    apartadas['ambas son palabras inglesas: requiere imagen'] += n; continue
            hall = re.findall(rf'\b{re.escape(a)}\b', txt, re.I)
            if len(hall) != n:
                apartadas['el recuento del fichero no cuadra'] += n; continue
            txt, c = rep_palabra(txt, a, b)
            if c:
                aplicadas[(a, b)] += c
                porref[fich].add((a, b))
        if aplicar and txt != orig:
            p.write_text(txt, encoding='utf-8')

    print(f'== {libro_scan}: {"APLICANDO" if aplicar else "SIMULACION"}')
    print('  sustituciones:', sum(aplicadas.values()), 'en', len(porref), 'ficheros')
    for (a, b), n in aplicadas.most_common(15): print(f'    {n:3}x  {a} -> {b}')
    print('  apartadas:', dict(apartadas))
    if aplicar:
        json.dump({k: sorted(v) for k, v in porref.items()},
                  open(HERE / f'ultimas_grafias_{pref}.json', 'w'), ensure_ascii=False, indent=1)
    return 0


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