#!/usr/bin/env python3
"""
Aplica la lista seleccionada de diffs del usuario al repo vedabase-original.
Lee de selected_diffs.json (en el mismo directorio).

DRY_RUN = True  -> solo imprime, no escribe
DRY_RUN = False -> modifica los archivos
"""
import json, re, unicodedata
from pathlib import Path
from collections import defaultdict

REPO     = Path('/Users/jaganat/git_projects/vedabase-original')
HERE     = Path(__file__).resolve().parent
JSON_IN  = HERE / 'selected_diffs.json'

DRY_RUN  = False  # cambiar a True para dry-run

# ── normalización ──────────────────────────────────────────────────────────────
def strip_diacritics(s):
    return ''.join(c for c in unicodedata.normalize('NFKD', s)
                   if not unicodedata.combining(c))

RAW_WORD_RE = re.compile(r'[A-Za-zÀ-ɏḀ-ỿĀ-ſ0-9]+', re.UNICODE)
# destino de un wiki-link: [[cc/madhya/19/170|  ...la parte hasta la barra
WIKI_TARGET_RE = re.compile(r'\[\[[^|\]]*\|')

def blockquote_spans(raw):
    """Spans de las líneas '>' (ślokas) y '#' (headings).

    repo_text() en diff_align_surya.py las excluye del flujo de tokens, así que
    aquí hay que excluirlas también: si no, un contexto que salta por encima de
    un verso citado no se localiza (NOT FOUND).
    """
    spans, pos = [], 0
    for ln in raw.splitlines(keepends=True):
        s = ln.strip()
        if s.startswith('>') or s.startswith('#'):
            spans.append((pos, pos + len(ln)))
        pos += len(ln)
    return spans

EMPH_RE = re.compile(r'\*+')

def raw_tokens(raw):
    """Tokens del markdown crudo, con offsets al crudo.

    Se excluye lo que diff_align_surya.py tampoco ve, o los dos scripts
    tokenizan distinto y la ficha falla como NOT FOUND:
      - DESTINOS de wiki-links ([[cc/madhya/19/170|…): no son texto; el diff
        reduce cada enlace a su etiqueta.
      - Líneas de blockquote y headings: repo_text() las aparta.

    Además, los ASTERISCOS de énfasis NO separan palabras: repo_text() los
    borra antes de tokenizar, así que `*ksatriya*communities` (sin espacio, y
    los hay) es UN token para el diff y serían DOS aquí. Se fusionan los tramos
    que sólo estén separados por asteriscos.
    """
    skip = [(m.start(), m.end()) for m in WIKI_TARGET_RE.finditer(raw)]
    skip += blockquote_spans(raw)
    def in_skip(i):
        return any(a <= i < b for a, b in skip)
    toks = []
    for m in RAW_WORD_RE.finditer(raw):
        if in_skip(m.start()):
            continue
        norm = strip_diacritics(m.group()).lower()
        # ¿pegado al token anterior sólo por asteriscos? -> mismo token
        if toks and EMPH_RE.fullmatch(raw[toks[-1][2]:m.start()] or 'x') is not None \
                and raw[toks[-1][2]:m.start()] != '':
            prev = toks.pop()
            toks.append((prev[0] + norm, prev[1], m.end()))
        else:
            toks.append((norm, m.start(), m.end()))
    return toks

# ── localizar span ─────────────────────────────────────────────────────────────
def find_span_in_raw(raw, ctx):
    m = re.match(r'(.*)\[\[(.+?)\]\](.*)', ctx)
    if not m:
        return None
    left_norm  = m.group(1).strip().split()
    brac_words = m.group(2).strip().split()
    right_norm = m.group(3).strip().split()
    nb = len(brac_words)
    nl = min(5, len(left_norm))
    nr = min(4, len(right_norm))

    toks  = raw_tokens(raw)
    words = [t[0] for t in toks]

    for i in range(len(words) - nb + 1):
        if words[i:i+nb] != brac_words:
            continue
        if nl and i >= nl and words[i-nl:i] != left_norm[-nl:]:
            continue
        if nr and i+nb+nr <= len(words) and words[i+nb:i+nb+nr] != right_norm[:nr]:
            continue
        raw_start = toks[i][1]
        raw_end   = toks[i+nb-1][2]
        return (raw_start, raw_end, raw[raw_start:raw_end])
    return None

# ── calcular reemplazo: recortar solapamiento con contexto derecho ─────────────
def compute_replacement(orig_reading, right_ctx_words):
    orig_words = orig_reading.split()
    right_norm = [strip_diacritics(w).lower().strip('.,;:()') for w in right_ctx_words]
    trim = 0
    for k in range(1, len(orig_words) + 1):
        tail = [strip_diacritics(w).lower().strip('.,;:()') for w in orig_words[-k:]]
        if tail == right_norm[:k]:
            trim = k
    return ' '.join(orig_words[:len(orig_words) - trim]) if trim < len(orig_words) else orig_reading

# ── recortar solapamiento con contexto izquierdo ──────────────────────────────
def trim_left(replacement, left_ctx_words):
    rep_words = replacement.split()
    left_norm = [strip_diacritics(w).lower().strip('.,;:()') for w in left_ctx_words]
    trim = 0
    for k in range(1, len(rep_words) + 1):
        head = [strip_diacritics(w).lower().strip('.,;:()') for w in rep_words[:k]]
        if head == left_norm[-k:]:
            trim = k
    return ' '.join(rep_words[trim:]) if trim < len(rep_words) else replacement

# ── cargar entradas seleccionadas ─────────────────────────────────────────────
entries = json.loads(JSON_IN.read_text())

# Construir mapa (book, filename) -> ruta relativa dentro del repo
# (necesario porque CC y SB tienen subdirectorios: adi-lila/chapter-01/cc-adi-1.19.md)
_file_lookup: dict[tuple, str] = {}
for e in entries:
    key = (e['book'], e['file'])
    if key not in _file_lookup:
        bdir = REPO / e['book']
        if bdir.exists():
            for f in bdir.rglob('*.md'):
                if f.name == e['file']:
                    _file_lookup[key] = str(f.relative_to(REPO))
                    break

def resolve_path(e):
    return _file_lookup.get((e['book'], e['file']), f"{e['book']}/{e['file']}")

# Agrupar por archivo
per_file = defaultdict(list)
for e in entries:
    per_file[resolve_path(e)].append(e)

# ── aplicar ───────────────────────────────────────────────────────────────────
stats = {'applied': 0, 'skipped_same': 0, 'not_found': 0}

for filerel, file_entries in sorted(per_file.items()):
    fp = REPO / filerel
    if not fp.exists():
        print(f"FILE NOT FOUND: {fp}")
        continue
    raw      = fp.read_text(errors='ignore')
    modified = raw

    file_edits = []
    for e in file_entries:
        ctx  = e['context']
        orig = e['original_reading']

        m = re.match(r'(.*)\[\[(.+?)\]\](.*)', ctx)
        if not m:
            print(f"  BAD CTX: {filerel} | {ctx[:60]}")
            continue
        left_norm  = m.group(1).strip().split()
        right_norm = m.group(3).strip().split()

        replacement = compute_replacement(orig, right_norm)
        replacement = trim_left(replacement, left_norm)

        result = find_span_in_raw(modified, ctx)
        if not result:
            print(f"  NOT FOUND: {filerel} | ctx={ctx[:70]}")
            stats['not_found'] += 1
            continue

        raw_start, raw_end, actual = result

        # Borrado puro: hay que llevarse también UN espacio adyacente, o queda
        # "of  Vedanta" (doble) y "salt , (2)" (espacio antes de la coma).
        if replacement == '':
            if raw_start > 0 and modified[raw_start-1] == ' ':
                raw_start -= 1
            elif raw_end < len(modified) and modified[raw_end] == ' ':
                raw_end += 1

        if strip_diacritics(actual).lower() == strip_diacritics(replacement).lower():
            print(f"  SAME:      {filerel} | [{actual}] (no change)")
            stats['skipped_same'] += 1
            continue

        file_edits.append((raw_start, raw_end, actual, replacement))

    # Aplicar de derecha a izquierda para no desplazar offsets
    file_edits.sort(key=lambda x: x[0], reverse=True)
    for raw_start, raw_end, actual, replacement in file_edits:
        print(f"  APPLY:     {filerel}\n"
              f"             [{repr(actual)}] → [{repr(replacement)}]")
        if not DRY_RUN:
            modified = modified[:raw_start] + replacement + modified[raw_end:]
        stats['applied'] += 1

    if not DRY_RUN and modified != raw:
        fp.write_text(modified, encoding='utf-8')
        print(f"  WRITTEN:   {fp}")

print()
print(f"=== {'DRY-RUN' if DRY_RUN else 'APPLIED'} ===")
print(f"  Applied / queued: {stats['applied']}")
print(f"  Skipped same:     {stats['skipped_same']}")
print(f"  Not found:        {stats['not_found']}")
