#!/usr/bin/env python3
"""Aplica una correccion en LA OCURRENCIA CONCRETA, no en todo el fichero.

El problema que resuelve. La evidencia del escaneo es de UNA pagina, pero hasta
ahora la sustitucion era de fichero entero, asi que solo podia aplicarse cuando
el recuento cuadraba (tantas ocurrencias en el fichero como casos senalados). Si
no cuadraba, el par se quedaba sin decidir aunque los dos motores de OCR
estuvieran de acuerdo: 226 pares bloqueados por eso. Y cuando se salto esa
garantia, `words`->`word` arraso un capitulo entero.

La salida estaba en los propios candidatos: traen `context`, el texto
normalizado alrededor con la posicion marcada entre dobles corchetes:

    'dedication to my father gour [[mohan]] de 1849 1930 a pure devotee'

Basta con localizar ese contexto en el fichero y cambiar solo ahi.

Como se localiza. El contexto viene NORMALIZADO (minusculas, sin diacriticos,
sin marcado de cursiva, espacios colapsados) y el fichero no. Se construye una
version normalizada del fichero **guardando, para cada caracter, de que posicion
del original viene**; se busca el contexto en esa version y el mapa devuelve los
offsets reales. Asi el cambio cae exactamente donde la evidencia lo respalda.

Si el contexto aparece mas de una vez, o ninguna, se aparta: sin posicion unica
no hay sustitucion. Ese es todo el criterio.

Uso: python3 aplicar_posicional.py [libro] [--aplicar]
"""
import json, re, sys, unicodedata, pathlib, collections

HERE = pathlib.Path(__file__).resolve().parent
REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
FECHA = '2026-08-09'


def normaliza(t):
    """Devuelve (texto_normalizado, mapa) con mapa[i] = indice en el original."""
    out = []; mapa = []
    espacio = True
    for i, c in enumerate(t):
        d = ''.join(x for x in unicodedata.normalize('NFD', c)
                    if unicodedata.category(x) != 'Mn').lower()
        if not d:                            # el caracter era solo un diacritico
            continue
        if not d.isalnum():
            # El alineador convierte TODO lo que no es letra o cifra en espacio,
            # no lo borra: '1849-1930' le queda como '1849 1930'. Copiarlo mal
            # dejaba 281 de 290 contextos sin localizar.
            if espacio: continue
            out.append(' '); mapa.append(i); espacio = True
            continue
        espacio = False
        for x in d:
            out.append(x); mapa.append(i)
    return ''.join(out), mapa


def main():
    libros = [a for a in sys.argv[1:] if not a.startswith('--')]
    aplicar = '--aplicar' in sys.argv
    confirmados = json.load(open(HERE / 'confirmados_pendientes.json', encoding='utf-8'))

    abiertos = {(r['file'], r['idx']) for r in map(json.loads, open(HERE / 'ledger.jsonl', encoding='utf-8'))
                if r['estado'] == 'ABIERTO'}
    quiero = {(c['file'], c['repo'], c['scan']) for c in confirmados}
    # 1. Nada que la IMAGEN ya haya desmentido puede volver a proponerse. Sin
    #    esto reaparecio 'ever unsatisfied'->'satisfied' en sb-3.28.16, que el
    #    impreso ya habia contradicho.
    rev = HERE / 'revocados_imagen.json'
    if rev.exists():
        for f, a, b in json.load(open(rev, encoding='utf-8')):
            quiero.discard((f, a, b))
    # 2. Solo pares de UNA palabra contra UNA palabra. Los multipalabra van
    #    siempre a imagen: es donde el desajuste de pagina del alineador rompe
    #    la frase ('does not accept a material body, but returns' -> 'and
    #    return', "one's" -> 'his').
    quiero = {(f, a, b) for f, a, b in quiero if ' ' not in a and ' ' not in b}

    porfich = collections.defaultdict(list)
    for cf in sorted((HERE / 'out_surya').glob('*.candidates.jsonl')):
        libro = cf.name.replace('.candidates.jsonl', '')
        if libros and libro not in libros: continue
        for i, l in enumerate(open(cf, encoding='utf-8')):
            d = json.loads(l)
            a = (d.get('repo') or '').strip().lower(); b = (d.get('scan') or '').strip().lower()
            if (d['file'], i) not in abiertos or (d['file'], a, b) not in quiero: continue
            if not d.get('context'): continue
            porfich[d['file']].append((a, b, d['context'], d.get('page', '')))

    res = collections.Counter(); hechas = []; n = 0
    for fich, casos in sorted(porfich.items()):
        p = REPO / fich
        if not p.exists(): res['sin fichero'] += len(casos); continue
        txt = p.read_text(encoding='utf-8')
        # se procesa de atras hacia delante para que los offsets no se muevan
        pendientes = []
        for a, b, ctx, pag in casos:
            m = re.search(r'\[\[(.+?)\]\]', ctx)
            if not m: res['contexto sin marca'] += 1; continue
            izq = ctx[:m.start()].strip(); der = ctx[m.end():].strip()
            palabra = m.group(1).strip()
            if palabra != a: res['la marca no es la palabra del repo'] += 1; continue
            buscado = ' '.join(x for x in (izq, palabra, der) if x)
            norm, mapa = normaliza(txt)
            pos = [x.start() for x in re.finditer(re.escape(buscado), norm)]
            if len(pos) != 1:
                res[f'contexto {"ausente" if not pos else "repetido"}'] += 1; continue
            # offset de la palabra dentro del contexto localizado
            ini_n = pos[0] + (len(izq) + 1 if izq else 0)
            fin_n = ini_n + len(palabra)
            ini, fin = mapa[ini_n], mapa[fin_n - 1] + 1
            real = txt[ini:fin]
            if normaliza(real)[0] != palabra:
                res['el mapa no cuadra'] += 1; continue
            nuevo = b.capitalize() if real[:1].isupper() else b
            # GUARDARRAIL DE DIACRITICOS. El OCR se los come constantemente, asi
            # que un cambio que QUITA diacriticos casi siempre es dano de
            # reconocimiento, no la grafia del impreso. Paso el 2026-08-09:
            # 'Bhaumāsura' -> 'Baumasura' y 'yajña-tapofāṁ' -> 'tapasam' en una
            # linea de verso. Se aparta y se decide por imagen.
            def dia(s):
                return sum(1 for c in unicodedata.normalize('NFD', s)
                           if unicodedata.category(c) == 'Mn')
            if dia(nuevo) < dia(real):
                res['quita diacriticos: requiere imagen'] += 1; continue
            pendientes.append((ini, fin, real, nuevo, a, b, pag))
        for ini, fin, real, nuevo, a, b, pag in sorted(pendientes, reverse=True):
            txt = txt[:ini] + nuevo + txt[fin:]
            n += 1
            hechas.append({'file': fich, 'repo': a, 'scan': b, 'page': pag,
                           'antes': real, 'despues': nuevo})
        res['APLICADO en su posicion'] += len(pendientes)
        if aplicar and pendientes:
            p.write_text(txt, encoding='utf-8')

    for k, v in res.most_common(): print(f'  {v:5}  {k}')
    print('\nsustituciones posicionales:', n, 'en', len(porfich), 'ficheros')
    if aplicar:
        json.dump(hechas, open(HERE / 'posicionales.json', 'w'), ensure_ascii=False, indent=1)
        pares = collections.defaultdict(set)
        for h in hechas: pares[h['file']].add((h['repo'], h['scan']))
        json.dump({k: sorted(v) for k, v in pares.items()},
                  open(HERE / 'ultimas_grafias_posicional.json', 'w'), ensure_ascii=False, indent=1)
        with open(HERE / 'verdicts.jsonl', 'a') as fh:
            for h in hechas:
                fh.write(json.dumps({"repo": h['repo'], "scan": h['scan'], "estado": "APLICADO",
                                     "razon": "confirmado por los dos motores de OCR y aplicado en LA "
                                              "ocurrencia que senala el contexto del alineador, "
                                              "no en todo el fichero",
                                     "fecha": FECHA}, ensure_ascii=False) + '\n')
        print('veredictos escritos:', len(hechas))
    return 0


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