#!/usr/bin/env python3
"""Safe scan-vs-D1 applier (PREVIEW by default).

For a book: align the original SCAN (gold) against the cleaned D1 verse text,
take each alignment opcode as a PRECISE, POSITIONED change (exact char span in a
specific cell), then keep only changes that:
  (a) have clean GOLD (not OCR garbage),
  (b) are small + non-trivial + not a huge insert/delete (misalignment guard),
  (c) MATCH a user-approved correction (the human-vetted set).
Emits exact-offset edits. Writes SQL only with --sql.
"""
import json, re, fitz, unicodedata, difflib, sys, os
BASE=os.path.dirname(os.path.abspath(__file__))
ENG=set(w.strip().lower() for w in open('/usr/share/dict/words'))
def fold(s):
    s=unicodedata.normalize('NFKD',str(s)); return ''.join(c for c in s if not unicodedata.combining(c)).lower()
def foldsp(s): return re.sub(r'\s+',' ',fold(s)).strip()
def cfg(key):
    src=open(os.path.join(BASE,'compare.py')).read()
    b=re.search(r'"'+key+r'":\s*\{(.+?)\}',src,re.S).group(1)
    g=lambda k:re.search(k+r'":\s*"?([^,"\n]+)',b).group(1).strip()
    return g('original'),int(g('orig_start_page')),int(g('orig_end_page'))
def resolve(fn):
    for p in (os.path.join(BASE,'improved',fn),os.path.join(BASE,'originals',fn)):
        if os.path.exists(p): return p
def scan_words(key):
    fn,p0,p1=cfg(key); d=fitz.open(resolve(fn))
    t=' '.join(re.sub(r'-\n([a-z])',r'\1',d[i].get_text()) for i in range(p0,min(p1,len(d))))
    return t.split()
def clean_md(t): return re.sub(r'\[[^\]]*\]\([^)]*\)',' ',re.sub(r'[#*_>`]+',' ',t))
def natkey(ref): return [int(x) if x.isdigit() else x for x in re.split(r'(\d+)',ref)]
def d1_tokens(key,rows):
    """word tokens with (word, id, col, char_start, char_end) on the ORIGINAL (uncleaned) cell text."""
    toks=[]
    for r in sorted([x for x in rows if x['book']==key],key=lambda r:natkey(r['ref'])):
        for col in ('translation','purport'):
            raw=r.get(col) or ''
            # tokenize on the cleaned text but map offsets back to raw by walking
            for m in re.finditer(r'\S+', raw):
                w=m.group(0)
                wc=clean_md(w)
                if wc.strip(): toks.append((wc, r['id'], col, m.start(), m.end(), r['ref']))
    return toks
def garbage(words):
    toks=[w for w in words if re.search(r'[A-Za-z]',w)]
    if not toks: return True
    if sum(1 for w in toks if len(re.sub(r'[^A-Za-z]','',w))<=1)/len(toks)>=0.5: return True
    if len(toks)>=3 and sum(1 for w in toks if fold(re.sub(r'[^A-Za-z]','',w)) in ENG)/len(toks)<0.34: return True
    if any(re.search(r'[a-z][A-Z]|[A-Za-z][?;~^|{}/\\]',w) for w in words): return True
    return False
def approved(key):
    cur=None; out=[]
    for line in open(os.path.join(BASE,'correcciones_LIMPIAS.md'),encoding='utf-8'):
        h=re.match(r'^#{2,3}\s+(\w+)',line)
        if h: cur=h.group(1)
        m=re.search(r'buscar «(.+?)»\s*→\s*reemplazar por «(.+?)»',line)
        if m and cur==key: out.append((foldsp(m.group(1)),foldsp(m.group(2))))
    return out
def run(key):
    d1=json.load(open('/tmp/d1_en.json'))[0]['results']
    cellmap={(r['id'],c):(r.get(c) or '') for r in d1 if r['book']==key for c in ('translation','purport')}
    sw=scan_words(key); dt=d1_tokens(key,d1)
    appr=set(approved(key))                       # set of (afv_folded, afg_folded)
    sm=difflib.SequenceMatcher(None,[fold(w) for w in sw],[fold(t[0]) for t in dt],autojunk=False)
    edits=[]; raw=0
    for tag,i1,i2,j1,j2 in sm.get_opcodes():
        if tag=='equal': continue
        raw+=1
        gold=' '.join(sw[i1:i2]).strip(); veda=' '.join(t[0] for t in dt[j1:j2]).strip()
        if max(i2-i1,j2-j1)>8: continue            # misalignment guard
        if garbage(gold.split()): continue
        fg,fv=foldsp(gold),foldsp(veda)
        # EXACT cross-check: block must reproduce an approved (veda->gold) pair verbatim (folded)
        if (fv,fg) not in appr: continue
        # exact span in cell (require all D1 words in same id/col)
        ids={t[1] for t in dt[j1:j2]}; cols={t[2] for t in dt[j1:j2]}
        if len(ids)!=1 or len(cols)!=1 or j2==j1: continue
        cid=dt[j1][1]; col=dt[j1][2]; cs=dt[j1][3]; ce=dt[j2-1][4]; ref=dt[j1][5]
        # HARD offset gate: the raw cell span must fold to veda
        if foldsp(cellmap.get((cid,col),'')[cs:ce])!=fv: continue
        edits.append({'id':cid,'col':col,'ref':ref,'cs':cs,'ce':ce,'veda':veda,'gold':gold})
    return edits, raw, len(appr)
if __name__=='__main__':
    key=sys.argv[1]
    edits,raw,na=run(key)
    print(f'{key}: rawblocks={raw} approved={na} -> SAFE exact edits={len(edits)}')
    for e in edits[:40]:
        print(f"  [{e['ref']}/{e['col']} @{e['cs']}] «{e['veda'][:38]}» → «{e['gold'][:38]}»")
