#!/usr/bin/env python3
"""Comprueba que cada cambio hecho HOY en el repo sigue casando con el escaneo.

Por que hace falta. `build_ledger.py` da 0 abiertos, pero se construye sobre la
lista de candidatos ya generada: no vuelve a pasar el diff. Si una correccion de
hoy metio en el repo algo que el papel no dice, el balance sigue en 0 y no se
entera nadie.

Y no todas las correcciones de hoy nacieron de leer el impreso. Varias salieron
de cotejar el repo contra D1, y ahi el criterio fue «el repo es la fuente
adjudicada» o «el impreso da la razon a D1». Esas son justo las que conviene
volver a comprobar contra el papel.

Como comprueba. Para cada par `old -> new` del registro, se busca en el OCR del
libro la forma NUEVA con su contexto izquierdo, en cascada de 5, 4, 3 y 2
palabras. Si aparece la nueva y no la vieja, el cambio esta respaldado. Si
aparece la vieja y no la nueva, el cambio va contra el papel y hay que
deshacerlo. Si no aparece ninguna, se informa: puede ser que el OCR lea mal esa
zona, y entonces toca la imagen.

Uso:
    python3 verificar_cambios_contra_escaneo.py                # los de hoy
    python3 verificar_cambios_contra_escaneo.py --desde 2026-08-14
"""
import json
import pathlib
import re
import subprocess
import sys

HERE = pathlib.Path(__file__).resolve().parent
REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
sys.path.insert(0, str(HERE))
from arbitrar_deriva import texto_escaneo, norm  # noqa: E402
from verificar_paridad import ref_de  # noqa: E402

ENTRE = r'(?:[\s\*_,;:.!?()\[\]"\'’‘“”—–-]|</?em>|</?i>|\\)+'
_MAPA = {}


def fichero_de(x, ref):
    """El .md del repo, del registro o deducido de la referencia.

    Las entradas que escribio `deshacer_reemplazo_pasado.py` guardan `ref: null`
    y las de compuestas guardan `file: null`. Con una de las dos basta.
    """
    rel = x.get('file') or ''
    if rel.startswith(str(REPO)):
        rel = rel[len(str(REPO)) + 1:]
    if rel and (REPO / rel).is_file():
        return REPO / rel
    if not ref:
        return None
    carp = {'bg': 'bhagavad-gita-as-it-is', 'sb': 'srimad-bhagavatam',
            'cc': 'sri-caitanya-caritamrta', 'kb': 'krsna-the-supreme-personality-of-godhead',
            'tlc': 'teachings-of-lord-caitanya', 'iso': 'isopanisad',
            'nod': 'nectar-of-devotion', 'ssr': 'science-of-self-realization',
            'tqk': 'teachings-of-queen-kunti'}.get(ref.split('/')[0])
    if not carp:
        return None
    if carp not in _MAPA:
        m = {}
        for g in (REPO / carp).rglob('*.md'):
            r = ref_de(str(g.relative_to(REPO)))
            if r:
                m[r] = g
        _MAPA[carp] = m
    return _MAPA[carp].get(ref)


def pares_desde(fecha):
    """Los pares old/new añadidos a corrections.jsonl desde esa fecha."""
    commits = subprocess.run(['git', 'log', f'--since={fecha} 00:00', '--format=%H'],
                             capture_output=True, text=True, cwd=str(REPO)).stdout.split()
    if not commits:
        return []
    base = subprocess.run(['git', 'show', f'{commits[-1]}~1:corrections.jsonl'],
                          capture_output=True, text=True, cwd=str(REPO)).stdout.splitlines()
    ahora = (REPO / 'corrections.jsonl').read_text(encoding='utf-8').splitlines()
    fuera = []
    for linea in ahora[len(base):]:
        try:
            x = json.loads(linea)
        except Exception:
            continue
        par = (x.get('replace') or {}).get('en')
        if par:
            fuera.append({'ref': x.get('ref'), 'file': x.get('file'),
                          'old': par[0], 'new': par[1], 'note': x.get('note', '')})
    return fuera


def busca(esc, izq, forma):
    """Cuantas veces sale esa forma tras ese contexto, probando de mas a menos."""
    pal = izq.split()
    for k in (5, 4, 3, 2):
        if len(pal) < k:
            continue
        ctx = norm(' '.join(pal[-k:]))
        if len(ctx.split()) < 2:
            continue
        # Con limite de palabra detras: sin el, «vibration» cuenta dentro de
        # «vibrations» y «universe» dentro de «universes», asi que la vieja y la
        # nueva salen empatadas y el caso se da por indeciso. Pasaba en 7 de 10.
        n = len(re.findall(re.escape(f'{ctx} {norm(forma)}') + r'(?![\w])', esc))
        if n:
            return n, k
    return 0, None


def main():
    fecha = (sys.argv[sys.argv.index('--desde') + 1] if '--desde' in sys.argv
             else '2026-08-15')
    pares = pares_desde(fecha)
    print(f'  cambios en el repo desde {fecha}: {len(pares)}')
    res = {'respaldado': [], 'CONTRA EL PAPEL': [], 'sin localizar': []}
    for x in pares:
        ref = x['ref'] or ref_de(x['file'] or '')
        libro = (ref or '').split('/')[0]
        esc = texto_escaneo(libro) if libro else ''
        if not esc:
            res['sin localizar'].append({**x, 'motivo': f'sin escaneo de {libro!r}'})
            continue
        # Algunas entradas guardan `file: null` (las escribio el deshacedor) y
        # otras la ruta absoluta. Sin este arreglo, `REPO / ''` da el directorio
        # del repo y `read_text` revienta con IsADirectoryError.
        f = fichero_de(x, ref)
        if f is None:
            res['sin localizar'].append({**x, 'motivo': 'sin fichero ni ref utilizable'})
            continue
        t = f.read_text(encoding='utf-8')
        # El registro guarda la frase SIN marcado y el .md lo lleva pegado
        # («hari-sabhā*  may»), asi que la busqueda literal falla. Se admite
        # marcado y puntuacion entre palabras.
        pat = ENTRE.join(re.escape(w) for w in x['new'].split())
        m = re.search(pat, t)
        if not m:
            res['sin localizar'].append({**x, 'motivo': 'la forma nueva no esta en el repo'})
            continue
        izq = ' '.join(t[:m.start()].split()[-5:])
        n_nueva, k = busca(esc, izq, x['new'])
        n_vieja, _ = busca(esc, izq, x['old'])
        if not n_nueva and not n_vieja:
            # Segundo intento sin contexto izquierdo: la frase entera. Vale
            # cuando la correccion cae en una glosa, donde el contexto es el par
            # anterior y el OCR lo lee de otra manera. Solo se acepta si la
            # frase tiene al menos dos palabras: con una sola no distingue nada.
            if len(x['new'].split()) >= 2:
                cola = lambda s: norm(' '.join(s.split()[-4:]))
                n_nueva = len(re.findall(re.escape(cola(x['new'])) + r'(?![\w])', esc))
                n_vieja = len(re.findall(re.escape(cola(x['old'])) + r'(?![\w])', esc))
        if n_nueva and not n_vieja:
            res['respaldado'].append(x)
        elif n_vieja and not n_nueva:
            res['CONTRA EL PAPEL'].append({**x, 'ctx': izq})
        else:
            res['sin localizar'].append({**x, 'motivo': 'el OCR no distingue aqui'})
    for k, v in res.items():
        print(f'  {len(v):4}  {k}')
    (HERE / 'cambios_contra_escaneo.json').write_text(
        json.dumps(res, ensure_ascii=False, indent=1), encoding='utf-8')
    for k in ('CONTRA EL PAPEL', 'sin localizar'):
        if not res[k]:
            continue
        print(f'\n  {k}:')
        for x in res[k]:
            print(f"    {(x['ref'] or x['file'] or '')[:24]:26} {x['old'][:26]!r:30} -> "
                  f"{x['new'][:26]!r:30} {x.get('motivo','')}")
    return 0


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