#!/usr/bin/env python3
"""Empuja a D1 lo que no se puede anclar por la palabra sola: se amplia el ancla
con su contexto hasta que sea unica.

De donde salen estos casos. Son 96 correcciones ya verificadas contra la imagen
del impreso y escritas en el repo, que `empujar_todas_d1.py` no puede aplicar
porque la palabra a cambiar sale varias veces en la misma fila. `appeared` tres
veces en un purport y solo una tiene que pasar a `appears`; un REPLACE las
cambiaria las tres.

Como se resuelve. El repo es la fuente: alli la correccion YA esta puesta y en su
sitio. Se localiza en el fichero, se toma contexto a los dos lados en anchuras
crecientes, y se reconstruye como decia ese mismo tramo ANTES (poniendo el texto
viejo donde ahora esta el nuevo). Ese tramo reconstruido se busca en D1: en
cuanto aparece exactamente una vez, ya se sabe cual de las ocurrencias es, y se
sustituye el tramo entero.

Tres cautelas, porque sustituir un tramo es mas invasivo que sustituir una
palabra:

  - se prueba de menos a mas (4, 8, 15, 30 palabras a cada lado) y se para en la
    primera anchura que da ancla unica, para mover lo minimo;
  - se LISTA lo que cambia de mas dentro del tramo, que es lo que se lleva por
    delante sin haberlo pedido;
  - si a la anchura maxima el tramo sigue sin ser unico, no se toca y se informa.

Uso: python3 empujar_con_contexto_d1.py [--aplicar]
"""
import json, re, os, sys, subprocess, unicodedata, pathlib, collections, difflib

HERE = pathlib.Path(__file__).resolve().parent
ASTRO = pathlib.Path('/Users/jaganat/git_projects/astro_vedabase')
REPO = pathlib.Path('/Users/jaganat/git_projects/vedabase-original')
sys.path.insert(0, str(HERE))
from sincronizar_diff import d1, q
from verificar_paridad import ref_de

ANCHURAS = (4, 8, 15, 30)
CAMPOS = (('purport', 'p'), ('translation', 't'), ('synonyms', 's'),
          ('verse_text', 'v'), ('devanagari', 'd'))


def sd(s):
    s = unicodedata.normalize('NFC', s or '')
    return ' '.join(re.sub(r'[^A-Za-z0-9ऀ-ॿঀ-৿]+', ' ', s.lower()).split())


def inverso(md, viejo, nuevo):
    """Los tramos del repo alrededor de `nuevo`, reconstruidos como estaban antes.

    Devuelve pares (tramo_antiguo, tramo_nuevo) de menor a mayor anchura. El
    tramo antiguo es lo que hay que encontrar en D1; el nuevo, lo que se escribe.
    """
    i = md.find(nuevo)
    if i < 0: return []
    ini = md.rfind('\n', 0, i) + 1
    fin = md.find('\n', i + len(nuevo))
    fin = fin if fin > 0 else len(md)
    out = []
    for n in ANCHURAS:
        # EL TRAMO LITERAL, no recompuesto. Uniendo palabras con espacios se
        # rompia justo el caso mas comun: cuando lo que cambia esta DENTRO de una
        # palabra compuesta. En `*sarva-darmān` la palabra de la izquierda es
        # `*sarva-`, y al volver a unir salia `*sarva- darmān`, que no existe en
        # ningun sitio. Aqui se cortan indices de caracteres y se respeta el
        # texto tal cual.
        izq = list(re.finditer(r'\S+', md[ini:i]))
        der = list(re.finditer(r'\S+', md[i + len(nuevo):fin]))
        a = ini + (izq[-n].start() if len(izq) >= n and n else (izq[0].start() if izq else 0))
        b = i + len(nuevo) + (der[n - 1].end() if len(der) >= n and n
                              else (der[-1].end() if der else 0))
        tramo_nuevo = md[a:b]
        # el viejo, en la misma posicion dentro del tramo
        rel = i - a
        tramo_viejo = tramo_nuevo[:rel] + viejo + tramo_nuevo[rel + len(nuevo):]
        out.append((tramo_viejo.strip(), tramo_nuevo.strip()))
    return out


def limpio(s):
    """Quita las marcas de markdown que el repo trae y D1 no siempre."""
    return re.sub(r'[*_]', '', s)


def main():
    aplicar = '--aplicar' in sys.argv
    faltan = json.loads(pathlib.Path('/tmp/siguen_faltando.json').read_text(encoding='utf-8'))
    print(f'correcciones que faltan por colocar: {len(faltan)}')

    inv = {}
    for f in subprocess.run(['git', 'ls-files'], capture_output=True, text=True,
                            cwd=str(REPO)).stdout.splitlines():
        if f.endswith('.md') and not f.startswith('translations/'):
            r = ref_de(f)
            if r: inv.setdefault(re.sub(r'^ejotp/', 'ej/', r), f)

    res = collections.Counter(); colaterales = []; sin = []
    porref = collections.defaultdict(list)
    for x in faltan: porref[x['ref']].append(x)

    for ref in sorted(porref):
        fich = inv.get(ref)
        if not fich or not (REPO / fich).exists():
            res['sin fichero en el repo'] += len(porref[ref]); continue
        md = (REPO / fich).read_text(encoding='utf-8')
        filas = d1("SELECT " + ', '.join(f"COALESCE({c},'') {k}" for c, k in CAMPOS) +
                   f" FROM verses WHERE ref={q(ref)} AND lang='en'", js=True)
        if not filas:
            res['sin fila en D1'] += len(porref[ref]); continue
        r0 = filas[0]; stmts = []
        for x in porref[ref]:
            viejo, nuevo = x['viejo'], x['nuevo']
            if sd(nuevo) in sd(' '.join(r0[k] for _, k in CAMPOS)):
                res['ya estaba'] += 1; continue
            puesto = False
            for va, vn in inverso(md, viejo, nuevo):
                for campo, k in CAMPOS:
                    val = r0[k]
                    for cand_a, cand_n in ((va, vn), (limpio(va), limpio(vn))):
                        if not cand_a or val.count(cand_a) != 1: continue
                        sm = difflib.SequenceMatcher(None, cand_a.split(), cand_n.split())
                        dif = [(' '.join(cand_a.split()[i1:i2]), ' '.join(cand_n.split()[j1:j2]))
                               for op, i1, i2, j1, j2 in sm.get_opcodes() if op != 'equal']
                        colaterales.append({'ref': ref, 'campo': campo,
                                            'palabras_de_contexto': len(cand_a.split()),
                                            'cambios': dif})
                        stmts.append(f"UPDATE verses SET {campo}=REPLACE({campo},{q(cand_a)},"
                                     f"{q(cand_n)}) WHERE ref={q(ref)} AND lang='en';")
                        puesto = True; break
                    if puesto: break
                if puesto: break
            if puesto: res['colocadas'] += 1
            else:
                res['sin ancla ni con 30 palabras de contexto'] += 1
                sin.append(x)
        if not stmts: continue
        if aplicar:
            f = HERE / '_push_ctx.sql'
            f.write_text('\n'.join(stmts) + '\n', encoding='utf-8')
            p = subprocess.run(['npx', 'wrangler', 'd1', 'execute', 'vedabase-search-db',
                                '--remote', '--file', str(f)], capture_output=True, text=True,
                               cwd=str(ASTRO),
                               env={**os.environ, 'CF_API_TOKEN': '', 'CLOUDFLARE_API_TOKEN': ''})
            f.unlink(missing_ok=True)
            if 'rows_written' not in p.stdout:
                res['FALLO en D1'] += 1; print(f'  FALLO {ref}')

    print()
    for k, v in res.most_common(): print(f'  {v:5}  {k}')
    json.dump(colaterales, open(HERE / 'colaterales_contexto.json', 'w'),
              ensure_ascii=False, indent=1)
    json.dump(sin, open(HERE / 'sin_colocar.json', 'w'), ensure_ascii=False, indent=1)
    extra = sum(max(0, len(c['cambios']) - 1) for c in colaterales)
    print(f'\n  cambios que viajan de mas dentro del contexto: {extra}'
          f'  (detalle en colaterales_contexto.json)')
    for c in colaterales:
        if len(c['cambios']) > 1:
            print(f"    {c['ref']} ({c['palabras_de_contexto']} palabras):")
            for a, b in c['cambios'][:3]: print(f"        {a[:40]!r} -> {b[:40]!r}")
    if not aplicar: print('\n(ensayo: anade --aplicar)')
    return 0


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