#!/usr/bin/env python3
"""Lleva a D1 y a content/en cualquier cambio ya hecho en el repo, sea cual sea.

`sincronizar_posicional.py` necesita un par (palabra vieja, palabra nueva). Una
SUPRESION no lo tiene: la palabra nueva es la cadena vacia, y `anclas()` busca
`\\bnueva\\b`, que con '' no encuentra nada. De ahi esta herramienta.

Aqui no se parte de una lista de cambios sino del DIFF: se comparan la version
de git y la del disco, y de cada tramo que difiere se saca un par de fragmentos
—el viejo y el nuevo— con unas palabras de contexto a cada lado. Eso sirve igual
para sustituir, para borrar y para insertar.

Detalles que ya costaron caros y aqui se respetan:
  - ANCLA UNICA o no se toca. Si el fragmento viejo no aparece exactamente una
    vez en el campo de D1, se informa y se deja como esta. Una divergencia
    detectada es mejor que una sustitucion a ciegas.
  - ESPACIADO FLEXIBLE. El repo parte las lineas donde D1 no, asi que el patron
    tolera cualquier blanco donde el texto ya tenia uno. Sin esto fallaban 10 de
    cada 23 anclas.
  - REPLACE, no UPDATE del campo entero: `kb/86` tiene 190 KB de significado y
    un UPDATE completo da SQLITE_TOOBIG.
  - Se prueban varias anchuras de contexto: una larga puede cruzar la frontera
    entre `translation` y `purport`, que en D1 son columnas distintas.

Uso:
  python3 sincronizar_diff.py [--desde HEAD^] [--aplicar]
"""
import json, re, sys, os, difflib, subprocess, pathlib, collections

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 verificar_paridad import ref_de
q = lambda s: "'" + s.replace("'", "''") + "'"
dec = json.JSONDecoder()


def git(*a):
    return subprocess.run(['git', *a], capture_output=True, text=True, cwd=str(REPO)).stdout


def d1(sql, js=False):
    args = ['npx', 'wrangler', 'd1', 'execute', 'vedabase-search-db', '--remote']
    if js: args.append('--json')
    args += ['--command', sql]
    for _ in range(3):
        p = subprocess.run(args, capture_output=True, text=True, cwd=str(ASTRO),
                           env={**os.environ, 'CF_API_TOKEN': '', 'CLOUDFLARE_API_TOKEN': ''})
        if js:
            for m in re.finditer(r'\[\s*\{', p.stdout):
                try: o, _ = dec.raw_decode(p.stdout[m.start():])
                except ValueError: continue
                if isinstance(o, list) and o and 'results' in o[0]: return o[0]['results']
        elif 'rows_written' in p.stdout:
            return True
    return [] if js else False


def pares(viejo, nuevo, lados=9):
    """Fragmentos (viejo, nuevo) de cada tramo que cambio, con contexto.

    Se compara por PALABRAS, no por caracteres: a nivel de caracter el
    alineador parte las palabras por la mitad y los fragmentos salen sin
    sentido. Se conservan los separadores para poder reconstruir el texto tal
    cual, con sus saltos de linea."""
    tv = re.split(r'(\s+)', viejo)
    tn = re.split(r'(\s+)', nuevo)
    sm = difflib.SequenceMatcher(None, tv, tn, autojunk=False)
    out = []
    def estira(t, i1, i2):
        """Contexto a los lados, PARANDO en el salto de parrafo.

        Sin este freno el ancla se lleva por delante la frontera entre campos:
        en D1 `translation` y `purport` son columnas distintas, y un fragmento
        que arrastraba '...bondage.\\n\\n**Paṇḍita Gaṅgādāsa...' no aparecia en
        ninguna de las dos. Fallaban 19 de 96 por esto."""
        a = i1
        for _ in range(lados * 2):
            if a == 0 or '\n\n' in t[a - 1]: break
            a -= 1
        b = i2
        for _ in range(lados * 2):
            if b >= len(t) or '\n\n' in t[b]: break
            b += 1
        return ''.join(t[a:b]).strip()

    for op, i1, i2, j1, j2 in sm.get_opcodes():
        if op == 'equal': continue
        v = estira(tv, i1, i2)
        n = estira(tn, j1, j2)
        if v and n and v != n: out.append((v, n))
    return out


def patron(s):
    """Escapa el fragmento pero deja libre el espaciado que ya tenia."""
    return re.compile(re.sub(r'(\\\s)+', r'\\s+', re.escape(s)))


def main():
    desde = sys.argv[sys.argv.index('--desde') + 1] if '--desde' in sys.argv else 'HEAD^'
    aplicar = '--aplicar' in sys.argv

    # SOLO INGLES. Las traducciones viven en `translations/<idioma>/...` con el
    # MISMO nombre de fichero, y `ref_de()` las mapea a la misma referencia. Sin
    # este filtro, tocar la version rusa de cc-madhya-9.47 escribiria texto ruso
    # en la fila inglesa de D1. No ha pasado —hoy no se toco ninguna— pero el
    # camino estaba abierto.
    fich = [l.split('\t')[-1] for l in
            git('diff', '--name-only', desde, 'HEAD').splitlines()
            if l.endswith('.md') and not l.startswith('translations/')]
    print(f'ficheros cambiados desde {desde}: {len(fich)}')

    solo_ce = '--solo-content' in sys.argv
    ok = fallo = ce = 0; sinancla = []
    for f in sorted(fich):
        ref = ref_de(f)
        if not ref:
            print('  sin ref:', f); fallo += 1; continue
        viejo = git('show', f'{desde}:{f}')
        nuevo = (REPO / f).read_text(encoding='utf-8')
        cambios = pares(viejo, nuevo)
        if not cambios: continue
        # Pasada solo para content/en. Hace falta como modo aparte porque una vez
        # aplicado el cambio en D1 el ancla VIEJA ya no aparece alli, la busqueda
        # falla y el bloque que copia a content/en no se llegaba a ejecutar.
        if solo_ce:
            ce_f = ASTRO / 'content/en' / (ref + '.md')
            if not ce_f.exists(): continue
            t = ce_f.read_text(encoding='utf-8'); orig = t
            for v, n in cambios:
                ms = list(patron(v).finditer(t))
                if len(ms) == 1: t = t.replace(ms[0].group(0), n, 1)
            if t != orig:
                if aplicar: ce_f.write_text(t, encoding='utf-8')
                ce += 1
            else:
                sinancla.append({'ref': ref, 'file': f, 'viejo': cambios[0][0][:120],
                                 'nuevo': cambios[0][1][:120]})
            continue
        filas = d1(f"SELECT COALESCE(purport,'') p, COALESCE(translation,'') t, "
                   f"COALESCE(synonyms,'') s, COALESCE(verse_text,'') v, "
                   f"COALESCE(devanagari,'') d "
                   f"FROM verses WHERE ref={q(ref)} AND lang='en'", js=True)
        if not filas:
            print('  sin fila en D1:', ref); fallo += 1; continue
        r = filas[0]
        stmts = []
        # Todas las anchuras de contexto, calculadas de una vez. `pares()`
        # recorre SIEMPRE los mismos tramos —los opcodes no dependen del
        # contexto—, asi que el cambio numero k es el mismo en todas las
        # variantes y se puede ir de ancho a estrecho por indice. Emparejarlas
        # comparando prefijos, como se hacia antes, fallaba justo cuando hacia
        # falta: con una palabra de contexto el fragmento ya no empieza igual
        # que con nueve.
        anchuras = [pares(viejo, nuevo, lados=L) for L in (9, 6, 4, 3, 2, 1)]
        anchuras = [a for a in anchuras if len(a) == len(cambios)]
        for k, (v, n) in enumerate(cambios):
            hecho = False
            for var in anchuras:
                vv, nn = var[k]
                # SYNONYMS es una columna mas. Faltaba, y por eso no se podian
                # sincronizar los cambios en la glosa palabra-por-palabra: la
                # nota al pie de sb/7/9/34 vive ahi.
                for campo, val in (('purport', r['p']), ('translation', r['t']),
                                   ('synonyms', r['s']), ('verse_text', r['v']),
                                   ('devanagari', r['d'])):
                    ms = list(patron(vv).finditer(val))
                    if len(ms) != 1: continue
                    lit = ms[0].group(0)
                    if nn == lit: continue          # ya estaba aplicado
                    stmts.append(f"UPDATE verses SET {campo}=REPLACE({campo},{q(lit)},{q(nn)}) "
                                 f"WHERE ref={q(ref)} AND lang='en';")
                    hecho = True; break
                if hecho: break
            if not hecho:
                sinancla.append({'ref': ref, 'file': f, 'viejo': v[:120], 'nuevo': n[:120]})
                fallo += 1
        if not stmts: continue
        print(f'  {ref}: {len(stmts)} tramo(s) anclado(s)')
        if aplicar:
            sql = HERE / '_diff.sql'
            sql.write_text('\n'.join(stmts) + '\n', encoding='utf-8')
            r2 = subprocess.run(['npx', 'wrangler', 'd1', 'execute', 'vedabase-search-db',
                                 '--remote', '--file', str(sql)], capture_output=True, text=True,
                                cwd=str(ASTRO),
                                env={**os.environ, 'CF_API_TOKEN': '', 'CLOUDFLARE_API_TOKEN': ''})
            if 'rows_written' in r2.stdout: ok += 1
            else: print('   FALLO en D1'); fallo += 1
            sql.unlink(missing_ok=True)
            # content/en lleva el mismo texto: se aplica el mismo tramo
            # content/en refleja la referencia tal cual: cc/adi/1/3 ->
            # content/en/cc/adi/1/3.md. (La ruta 'content/en/books/...' que
            # usaba el sincronizador anterior no existe: por eso daba 0.)
            ce_f = ASTRO / 'content/en' / (ref + '.md')
            if ce_f.exists():
                t = ce_f.read_text(encoding='utf-8'); orig = t
                for v, n in cambios:
                    ms = list(patron(v).finditer(t))
                    if len(ms) == 1: t = t.replace(ms[0].group(0), n, 1)
                if t != orig: ce_f.write_text(t, encoding='utf-8'); ce += 1
    print(f'\nD1: {ok} refs | sin ancla: {len(sinancla)} | content/en: {ce}')
    if sinancla:
        json.dump(sinancla, open(HERE / 'sin_ancla.json', 'w'), ensure_ascii=False, indent=1)
        for x in sinancla[:10]:
            print(f"  {x['ref']}: {x['viejo'][:70]!r}")
    return 0


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