#!/usr/bin/env python3
"""Deduce el orden REAL de los ficheros de un libro localizandolos en el OCR.

Mejoras sobre deducir_orden.py:
  - Concatena TODOS los volumenes declarados en books.json en un solo texto
    global (la version anterior indexaba por 'p0001' y los volumenes
    COLISIONABAN: el vol.2 pisaba al vol.1).
  - Prueba VARIAS sondas por fichero (no solo la primera frase) y exige que la
    sonda aparezca UNA sola vez en todo el escaneo.
  - Vota entre las sondas: se queda con la posicion mas votada (mediana del
    cluster), asi una sonda que casa con una cita repetida no descarrila nada.

Uso: deducir_orden.py <slug> [...]
"""
import json, os, re, sys, unicodedata
from pathlib import Path
from statistics import median

HERE = Path('/Users/jaganat/git_projects/astro_vedabase/scripts/scan_audit')
CFG = json.loads((HERE / 'books.json').read_text())
SUR = HERE / 'surya'
RO = Path(CFG['_repo_root'])
SCRATCH = Path(os.environ.get('TMPDIR', '/tmp'))


def fold(s):
    s = unicodedata.normalize('NFKD', s.lower())
    s = ''.join(c for c in s if not unicodedata.combining(c))
    s = s.replace('’', "'").replace('‘', "'")
    return ' '.join(re.sub(r'[^a-z0-9 ]', ' ', s).split())


def natural(p):
    return [int(x) if x.isdigit() else x for x in re.split(r'(\d+)', p.name)]


def orden_actual(book, files):
    exact = CFG.get('_file_order_exact', {}).get(book)
    order = CFG.get('_file_order', {}).get(book, [])
    tail = CFG.get('_file_order_tail', {}).get(book, [])

    def rank(p):
        if exact:
            return exact.index(p.name) if p.name in exact else len(exact)
        if p.name in order:
            return order.index(p.name) - len(order)
        if p.name in tail:
            return 1 + tail.index(p.name)
        return 0
    return sorted(files, key=lambda p: (rank(p), natural(p)))


def build_scan(book):
    """Texto global del escaneo + indice palabra->pagina.

    Devuelve (texto_plano, lista_de_(offset_palabra, pageref)) donde el texto
    es una cadena de palabras separadas por espacio y la lista permite mapear
    una posicion de caracter a la pagina.
    """
    bdir = SUR / book
    declared = {Path(rel).stem for rel in CFG['books'].get(book, [])}
    partes, marcas = [], []   # marcas: (offset_char_inicio, pageref)
    pos = 0
    for stemdir in sorted(bdir.iterdir()):
        if not stemdir.is_dir():
            continue
        if declared and stemdir.name not in declared:
            continue
        for pf in sorted(stemdir.glob('*.surya.txt')):
            t = fold(pf.read_text(errors='ignore'))
            if not t:
                continue
            pageref = f"{stemdir.name}#{pf.stem.replace('.surya','')}"
            marcas.append((pos, pageref))
            partes.append(t)
            pos += len(t) + 1
    return ' '.join(partes), marcas


def page_of(marcas, off):
    lo, hi = 0, len(marcas) - 1
    best = marcas[0][1]
    while lo <= hi:
        mid = (lo + hi) // 2
        if marcas[mid][0] <= off:
            best = marcas[mid][1]
            lo = mid + 1
        else:
            hi = mid - 1
    return best


def cuerpo(f):
    """Texto plegado del fichero, sin encabezados markdown ni citas en verso."""
    lns = []
    for l in f.read_text().split('\n'):
        s = l.strip()
        if not s or s.startswith('#') or s.startswith('>') or s.startswith('!['):
            continue
        lns.append(s)
    return fold(' '.join(lns))


NPROBE = 14      # sondas repartidas por el fichero
PLEN = 9         # palabras por sonda


def localizar(f, scan):
    """Devuelve (offset|None, motivo, detalle).

    `motivo` distingue los tres modos de fallo, que piden acciones distintas:
      CORTO     el fichero no da ni para una sonda (title.md y similares).
                No es un fallo: colocalo a mano donde toque.
      AUSENTE   se probaron N sondas y NINGUNA aparece en el escaneo. El
                fichero NO esta en ese escaneo — casi siempre porque el repo
                y el impreso son ediciones distintas (topmost-yoga: el 1970
                trae 6 capitulos, el repo 9). No es ruido de OCR.
      DISPERSO  hay sondas que casan pero no forman cluster: OCR malo, texto
                muy repetitivo, o volumenes mal declarados en books.json.
    """
    ws = cuerpo(f).split()
    if len(ws) < PLEN + 2:
        return None, 'CORTO', f'{len(ws)}w'
    # sondas repartidas uniformemente por el fichero
    maxi = len(ws) - PLEN
    step = max(1, maxi // NPROBE) if maxi else 1
    starts = list(range(0, maxi + 1, step))[:NPROBE * 2]
    hits = []
    tried = vistas = 0
    for i in starts:
        probe = ' '.join(ws[i:i + PLEN])
        if len(probe) < 35:
            continue
        tried += 1
        a = scan.find(probe)
        if a < 0:
            continue
        vistas += 1
        if scan.find(probe, a + 1) >= 0:
            continue                       # no es unica: se descarta
        # posicion estimada del ARRANQUE del fichero: restamos lo ya avanzado
        hits.append(a - i * 6)             # ~6 chars/palabra, aproximacion
    if not hits:
        motivo = 'AUSENTE' if vistas == 0 else 'DISPERSO'
        return None, motivo, f'sondas={tried} halladas={vistas}'
    hits.sort()
    m = median(hits)
    # cluster: sondas coherentes entre si (dentro de +-40k chars de la mediana)
    clus = [h for h in hits if abs(h - m) < 40000]
    if len(clus) < 2:
        return None, 'DISPERSO', f'sondas={tried} halladas={vistas} clus={len(hits)}'
    return int(median(clus)), 'OK', f'clus={len(clus)}/{tried}'


for book in sys.argv[1:]:
    bdir = RO / book
    scan, marcas = build_scan(book)
    files = sorted(bdir.rglob('*.md'))
    print(f'== {book}: {len(files)} ficheros, {len(marcas)} paginas OCR, '
          f'{len(scan):,} chars')
    pos, fallos = {}, []
    for f in files:
        off, motivo, det = localizar(f, scan)
        if off is None:
            fallos.append((f.name, motivo, det))
        else:
            pos[f.name] = off
    real = [n for n, _ in sorted(pos.items(), key=lambda kv: kv[1])]
    actual = [p.name for p in orden_actual(book, files) if p.name in pos]
    print(f'   localizados {len(pos)}/{len(files)}   orden '
          + ('IGUAL' if real == actual else 'DISTINTO'))
    for etiqueta in ('CORTO', 'AUSENTE', 'DISPERSO'):
        gr = [(n, d) for n, m, d in fallos if m == etiqueta]
        if gr:
            print(f'   {etiqueta}: ' + ', '.join(f'{n}({d})' for n, d in gr))
    print('   --- orden real deducido ---')
    for n in real:
        print(f'      {n:60} {page_of(marcas, pos[n])}')
    SCRATCH.joinpath(f'orden2_{book}.json').write_text(
        json.dumps({'real': real,
                    'paginas': {n: page_of(marcas, pos[n]) for n in real},
                    'sin_localizar': {n: f'{m} {d}' for n, m, d in fallos}},
                   ensure_ascii=False, indent=1))
