#!/usr/bin/env python3
"""Build the Spanish 5-letter word list (words-es.json / words-es.txt) for Pista de Letras.

Source: RLA-ES Spanish spelling dictionary (hunspell-es) v2.9, es_ES.aff + es_ES.dic ONLY,
taken from the official release package
  https://github.com/sbosio/rla-es/releases/download/v2.9/es_ES.oxt
  sha256 3eea87836b24b6004aa1ee6fd285b6c71774b0996aec70d096d75d5526efb4ac
Credit: the RLA-ES project and contributors. Upstream licence: GPL-3.0+ / LGPL-3.0+ / MPL-1.1+ (pick one).
This derived list and this script are distributed under the Mozilla Public License 2.0
(chosen under the "MPL 1.1 or later" option). https://www.mozilla.org/en-US/MPL/2.0/

Steps (the "Modifications"):
 1. Expand every dictionary entry with its affix rules (prefixes, suffixes, cross products and
    one level of continuation suffixes) using the pure-Python expander below. hunspell's unmunch
    is not used: it misreads this dictionary's multi-byte FLAG UTF-8 flags.
 2. Drop entries containing any uppercase letter (proper nouns, acronyms, abbreviations such as ADN).
 3. Drop entries with characters outside a-z, ñ, á é í ó ú ü (dots, hyphens, digits, apostrophes).
 4. Keep words that are exactly 5 letters once accents are removed (same as the Portuguese list:
    accents are kept in the stored word and ignored when matching; in the site's Spanish mode ñ is
    matched as its own letter, distinct from n).
 5. Drop words with no vowel a/e/i/o/u (interjections such as "pfff", letter strings).
 6. Drop offensive words: the English blocklist used for the site's word-search books (exact matches)
    plus a Spanish slur/profanity blocklist (exact words and stems), both listed below.
 7. De-duplicate, sort, write words-es.json (array) and words-es.txt (one per line).

Usage: python3 build_es_words.py [path/to/es_ES.oxt] [output_dir]
"""
import hashlib, json, re, sys, unicodedata, zipfile, os
OXT_SHA256 = "3eea87836b24b6004aa1ee6fd285b6c71774b0996aec70d096d75d5526efb4ac"
OXT = sys.argv[1] if len(sys.argv) > 1 else "/workspace/legal-review/rla-es-20261019/release-v2.9-es_ES-oxt/es_ES.oxt"
OUT = sys.argv[2] if len(sys.argv) > 2 else os.path.dirname(os.path.abspath(__file__))

# English blocklist: same strings as the word-search book blocklist (exact whole-word matches here).
EN_BLOCK_RAW = set('''
CUNT TWAT PAKI KIKE WANK PORN FAG FUK COON ARSE TURD ANUS
FUCK FUC FCK FUX FUQ PHUK SHIT SHT SHAT PISS COCK DICK DIK TIT TITS TITTY SEX SEXY NAZI HOMO SPIC SPIK RAPE RAPIST POO POOP BUM CRAP DAMN HELL SLUT WHORE HOE HO
ASS ARS AZZ BUTT CUM JIZ JIZZ SPUNK SEMEN PENIS PENI VAGINA VAG CLIT BOOB BOOBS NIPPLE ANAL ANALS ORGY ORAL DILDO HUMP SHAG BONK BANG NUDE NUDES NAKED STRIP XXX PUSSY PUSS PUSY TWAT MILF HORNY LUST KINKY FETISH BDSM ERECT SPERM SCROTUM TESTES BALLS BALLZ KNOB NOB BELL END
NIGGER NIGGA NIGG NIG NEGRO WOG GOOK CHINK JAP DAGO WOP SPAZ SPAS RETARD TARD MONG GYPO GYP PIKEY KAFFIR KAFIR DYKE DIKE FAGG FAGOT FAGGOT HOMO QUEER TRANNY HEEB YID INJUN REDSKIN COLORED KRAUT SAMBO JEW JEWS MUSLIM ISIS
BASTARD BITCH BICH BIATCH BLOODY BOLLOCK BOLLOX BUGGER CHOAD DOUCHE FECK FRIG GODDAMN MINGE MUFF PRICK PUNANI SKANK SLAG SOD TOSS TOSSER WANKER WTF STFU OMFG GTFO PEE WEE WEEWEE TURDS POOS CRAPS FARTS FART FARTED PUKE VOMIT
DRUG DRUGS DOPE WEED POT HEMP COKE CRACK METH HEROIN SMACK LSD MDMA PCP ACID BONG SPLIFF STONED HIGH OPIUM CRANK
KILL KILLS DIE DEAD DEATH MURDER STAB GUN GUNS BOMB HATE SUICIDE HANG NOOSE KKK HITLER
'''.split())
EN_ALLOW = {'HO','END','BELL','POT','WEE','ARS','STRIP','STAB','CRACK','JEW','JEWS','MUSLIM','ISIS','DIE','HIGH','COLORED','ACID','SMACK','HEMP','CRANK','HANG','DIKE'}
# Spanish exceptions (flagged for review): "negro" is the ordinary Spanish word for the colour black; "fagot" is the bassoon.
ES_ALLOW_FROM_EN = {'negro', 'fagot'}   # 'fagot' = bassoon in Spanish
EN_BLOCK = {w.lower() for w in EN_BLOCK_RAW - EN_ALLOW} - ES_ALLOW_FROM_EN

# Spanish slurs and profanity (all forms matched on the stored, accented, lowercase word).
ES_BLOCK = set('''
puta puto putas putos putón putona putero putera puteros puteras putear putañero hijoputa hijaputa
joder jodes jodido jodida jodidos jodidas jodió jodía
coño coños cojón cojones cojonudo polla pollas pollón verga vergas chocho chochos
culo culos culero culera ojete ano anos
mierda mierdas cagón cagona comemierda
follar folla follas follan follen follé folló follada follado follador
mamada mamadas mamón mamona pajero pajera pajillero
zorra zorras guarra guarras furcia furcias ramera rameras golfa golfas fulana fulanas
cabrón cabrona cabrones cabronas gilipollas gilipolla capullo capulla
maricón maricona maricones marica maricas mariposón julandrón bollera bolleras tortillera tortilleras travelo travelos
sudaca sudacas panchito panchitos moro moros mora_slur negrata negratas gitanada sidoso sidosa sidosos sidosas
subnormal subnormales mongólico mongólica mongolo mongola retrasado_slur
pendejo pendeja pendejos pendejas carajo carajos chingar chinga chingas chingada chingado malparido malparida
pene penes vagina vaginas clítoris semen esperma anal orgía orgías porno porna pornos sexo sexos coito coitos
violar viola_x violador violadora violación nazi nazis
'''.split())
ES_BLOCK = {w for w in ES_BLOCK if "_" not in w}   # entries with _ are notes, not blocked ("mora" = blackberry, "retrasado" = late)
ES_STEMS = re.compile(r"(mierd|jod[ie]d|joder|^coños?$|^coñe|cojon|cojón|follad|follar|maric|gilipoll|cabr[oó]n|polla|putañ|puter|sudac|bollera|pendej|chingad|negrat|hij[oa]put|malparid|travel[oa]s?$|sidos[oa]|^put[oa]s?$|^zorras?$|^guarras?$|^pute|^cag|^tet(as|ón|ona|onas)$|^mong[oa]s?$|^pedos?$|^mea(ba|da|das|do|dos|os|ra|rá|re|ré|se|n|s)$)")

def norm(w):
    return "".join(c for c in unicodedata.normalize("NFD", w) if not unicodedata.combining(c)).lower()

def flags_of(s):
    # FLAG UTF-8: one flag per Unicode code point; ignore variation selectors (U+FE0F etc.)
    return [c for c in s if not ("\ufe00" <= c <= "\ufe0f")]

def parse_aff(text):
    aff = {}
    lines = text.splitlines(); i = 0
    while i < len(lines):
        p = lines[i].split()
        if len(p) >= 4 and p[0] in ("PFX", "SFX") and p[2] in ("Y", "N") and p[3].isdigit() and len(p) == 4:
            kind, flag, cross, n = p[0], flags_of(p[1])[0], p[2] == "Y", int(p[3]); rules = []
            for j in range(1, n + 1):
                q = lines[i + j].split()
                strip = "" if q[2] == "0" else q[2]
                add, _, cont = q[3].partition("/")
                add = "" if add == "0" else add
                cond = q[4] if len(q) > 4 else "."
                rx = re.compile(("^" + cond) if kind == "PFX" else (cond + "$")) if cond != "." else None
                rules.append((strip, add, set(flags_of(cont)), rx))
            aff[flag] = (kind, cross, rules); i += n + 1
        else:
            i += 1
    return aff

def apply(word, rule, kind):
    strip, add, cont, rx = rule
    if rx is not None and not rx.search(word): return None
    if len(word) <= len(strip): return None          # hunspell: stripping may not consume the whole word (no FULLSTRIP)
    if kind == "SFX":
        if strip and not word.endswith(strip): return None
        base = word[:len(word) - len(strip)] if strip else word
        return base + add
    if strip and not word.startswith(strip): return None
    return add + word[len(strip):]

def expand(word, flags, aff):
    out = {word}
    sfx = [(f, aff[f]) for f in flags if f in aff and aff[f][0] == "SFX"]
    pfx = [(f, aff[f]) for f in flags if f in aff and aff[f][0] == "PFX"]
    suffixed = []  # (form, cross, cont_flags)
    for f, (k, cross, rules) in sfx:
        for r in rules:
            w = apply(word, r, "SFX")
            if w is None: continue
            out.add(w); suffixed.append((w, cross, r[2]))
            for cf in r[2]:                       # one level of continuation suffixes
                if cf in aff and aff[cf][0] == "SFX":
                    for r2 in aff[cf][2]:
                        w2 = apply(w, r2, "SFX")
                        if w2: out.add(w2); suffixed.append((w2, cross and aff[cf][1], set()))
    for f, (k, cross, rules) in pfx:
        for r in rules:
            w = apply(word, r, "PFX")
            if w: out.add(w)
            if cross:
                for s, scross, _ in suffixed:
                    if scross:
                        w3 = apply(s, r, "PFX")
                        if w3: out.add(w3)
    return out

def main():
    raw = open(OXT, "rb").read()
    got = hashlib.sha256(raw).hexdigest()
    if got != OXT_SHA256: sys.exit(f"sha256 mismatch: {got}")
    z = zipfile.ZipFile(OXT)
    aff = parse_aff(z.read("es_ES.aff").decode("utf-8"))
    dic = z.read("es_ES.dic").decode("utf-8").splitlines()[1:]
    allforms = set()
    for line in dic:
        line = line.split("\t")[0].strip()
        if not line: continue
        w, _, fl = line.partition("/")
        allforms |= expand(w, flags_of(fl.split()[0] if fl else ""), aff)
    st = {"dictionary_entries": len(dic), "expanded_forms_unique": len(allforms)}
    a = {w for w in allforms if not any(c.isupper() for c in w)}; st["after_drop_uppercase"] = len(a)
    a = {w for w in a if re.fullmatch(r"[a-zñáéíóúü]+", w)}; st["after_drop_nonletters"] = len(a)
    a = {w for w in a if len(norm(w)) == 5}; st["five_letter"] = len(a)
    a = {w for w in a if re.search(r"[aeiou]", norm(w))}; st["after_drop_no_vowel"] = len(a)
    blocked = sorted(w for w in a if w in ES_BLOCK or norm(w) in EN_BLOCK or ES_STEMS.search(w))
    a -= set(blocked); st["blocked"] = len(blocked); st["final"] = len(a)
    words = sorted(a, key=lambda w: (norm(w), w))
    json.dump(words, open(os.path.join(OUT, "words-es.json"), "w"), ensure_ascii=False, separators=(",", ":"))
    open(os.path.join(OUT, "words-es.txt"), "w").write("\n".join(words) + "\n")
    st["blocklist_sizes"] = {"english_exact": len(EN_BLOCK), "spanish_exact": len(ES_BLOCK), "spanish_stem_patterns": ES_STEMS.pattern.count("|") + 1}
    print(json.dumps(st, ensure_ascii=False, indent=1)); print("blocked:", " ".join(blocked))

if __name__ == "__main__":
    main()
