"""Aduce de la Wiktionary secțiunea românească a fiecărui cuvânt din listă și
extrage din ea faptele structurate: lanțul etimologic, partea de vorbire,
eticheta de uz, sensul în engleză.

Lanțul se citește din șabloanele de etimologie, în ordinea din pagină:
{{inh|ro|la|casa}} = moștenit din latină, {{bor|ro|ota|محله}} = împrumutat din
turca osmanlie, {{lbor|ro|la|...}} = împrumut savant. Șabloanele {{cog}} sunt
înrudiri, nu verigi, și se lasă afară.

Ieșire: raw/entries.json
"""
import gzip, json, os, re, sys, time, urllib.parse, urllib.request

HERE = os.path.dirname(os.path.abspath(__file__))
RAW = os.path.join(HERE, "..", "raw")
API = "https://en.wiktionary.org/w/api.php"
UA = "straturi-unui-cuvant/1.0 (https://straturi.mariuscomper.uk/)"
BATCH = 50

# Șabloane care adaugă o verigă la lanț, cu felul legăturii.
LINK = {
    "inh": "moștenit", "inh+": "moștenit",
    "bor": "împrumutat", "bor+": "împrumutat",
    "lbor": "împrumut savant", "lbor+": "împrumut savant",
    "slbor": "împrumut semisavant",
    "der": "din", "der+": "din",
    "calque": "calc", "cal": "calc", "clq": "calc",
    "sl": "împrumutat",
}
AFFIX = {"af", "affix", "suf", "suffix", "pre", "prefix", "com", "compound", "blend"}
POS_HEADS = [
    ("noun", r"\{\{ro-noun"), ("verb", r"\{\{ro-verb"), ("adj", r"\{\{ro-adj"),
    ("adv", r"\{\{ro-adv"), ("prep", r"\{\{ro-prep"), ("conj", r"\{\{ro-conj"),
    ("pron", r"\{\{ro-pron"), ("interj", r"\{\{ro-interj"), ("num", r"\{\{ro-num"),
]
POS_HEADERS = {
    "Noun": "noun", "Verb": "verb", "Adjective": "adj", "Adverb": "adv",
    "Preposition": "prep", "Conjunction": "conj", "Pronoun": "pron",
    "Interjection": "interj", "Numeral": "num", "Particle": "part",
    "Determiner": "det", "Proper noun": "propn",
}


def get_json(url, tries=6):
    for a in range(tries):
        try:
            req = urllib.request.Request(url, headers={"User-Agent": UA})
            with urllib.request.urlopen(req, timeout=120) as r:
                return json.load(r)
        except Exception as exc:  # noqa: BLE001
            if a == tries - 1:
                raise
            wait = 3 * (a + 1)
            print(f"    aștept {wait}s ({exc})", file=sys.stderr, flush=True)
            time.sleep(wait)
    raise RuntimeError("unreachable")


def split_template(t):
    """Desface {{nume|a|b|k=v}} în (nume, [poziționale], {chei}). Ignoră
    imbricările, care nu apar în șabloanele de etimologie."""
    inner = t[2:-2]
    parts, depth, buf = [], 0, []
    for ch in inner:
        if ch in "[{":
            depth += 1
        elif ch in "]}":
            depth -= 1
        if ch == "|" and depth == 0:
            parts.append("".join(buf)); buf = []
        else:
            buf.append(ch)
    parts.append("".join(buf))
    name = parts[0].strip().lower()
    pos, kw = [], {}
    for p in parts[1:]:
        if "=" in p and re.match(r"^\s*[A-Za-z0-9_-]+\s*=", p):
            k, v = p.split("=", 1)
            kw[k.strip()] = v.strip()
        else:
            pos.append(p.strip())
    return name, pos, kw


def top_templates(s):
    """Șabloanele de nivel superior, în ordinea din text."""
    out, i, n = [], 0, len(s)
    while i < n - 1:
        if s[i] == "{" and s[i + 1] == "{":
            depth, j = 0, i
            while j < n - 1:
                if s[j] == "{" and s[j + 1] == "{":
                    depth += 1; j += 2; continue
                if s[j] == "}" and s[j + 1] == "}":
                    depth -= 1; j += 2
                    if depth == 0:
                        out.append(s[i:j]); i = j; break
                    continue
                j += 1
            else:
                break
            continue
        i += 1
    return out


DISPUTE_RE = re.compile(
    r"\b(uncertain|unknown|disputed|obscure|debated|contested|possibly|perhaps|"
    r"often considered|other theories|several theories|may alternatively|"
    r"probably|of unclear)\b", re.I)


def sentences(s):
    """Bucățile de text ale unei etimologii, tăiate la punct, la rând nou dublu
    și la început de listă, dar numai în afara șabloanelor: „to show, point out"
    e o glosă dintr-un șablon, nu sfârșit de frază."""
    out, buf, depth, i, n = [], [], 0, 0, len(s)
    while i < n:
        c = s[i]
        if s.startswith("{{", i) or s.startswith("[[", i):
            depth += 1; buf.append(s[i:i + 2]); i += 2; continue
        if s.startswith("}}", i) or s.startswith("]]", i):
            depth = max(0, depth - 1); buf.append(s[i:i + 2]); i += 2; continue
        if depth == 0:
            if c == "." and (i + 1 >= n or s[i + 1] in " \n\t"):
                out.append("".join(buf)); buf = []; i += 1; continue
            if s.startswith("\n\n", i) or s.startswith("\n*", i) or s.startswith("\n#", i):
                out.append("".join(buf)); buf = []; i += 1; continue
        buf.append(c); i += 1
    if buf:
        out.append("".join(buf))
    return [x.strip() for x in out if x.strip()]


def parse_etymology(sec):
    """Lanțul de verigi și, separat, formarea internă.

    Lanțul se citește numai din prima frază care conține o verigă. Etimologiile
    disputate își înșiră ipotezele în fraze următoare, iar dacă le-am citi pe
    toate ar ieși un drum care nu există: „brânză" ar veni din substrat *și* din
    latină *și* din albaneză, când de fapt acestea sunt trei păreri despre
    aceeași necunoscută.
    """
    disputed = bool(DISPUTE_RE.search(sec))
    unknown, doublet, affix = False, None, None
    chosen = []
    for chunk in sentences(sec) or [""]:
        links, aff = [], None
        for t in top_templates(chunk):
            name, pos, kw = split_template(t)
            if name in ("unk", "unknown"):
                unknown = True
            elif name in ("dbt", "doublet"):
                doublet = pos[1] if len(pos) > 1 else None
            elif name in AFFIX and pos and pos[0] == "ro":
                parts = [p for p in pos[1:] if p and p != "-"]
                if parts:
                    aff = parts
            elif name in LINK and len(pos) >= 2:
                # {{inh|ro|la|casa}} → pos = [ro, la, casa]
                lang = pos[1]
                word = pos[2] if len(pos) > 2 else ""
                if not lang or lang == "ro":
                    continue
                w = (word or "").lstrip("*").strip()
                links.append({
                    "kind": LINK[name], "lang": lang,
                    "word": w if w and w != "-" else None,
                    "tr": kw.get("tr"), "gloss": kw.get("t") or kw.get("gloss"),
                })
        if aff and not affix:
            affix = aff
        if links and not chosen:
            chosen = links
        if chosen and affix:
            break
    return chosen, affix, unknown, doublet, disputed


DEF_CLEAN = [
    (re.compile(r"\{\{lb\|ro\|([^}]*)\}\}"), ""),
    (re.compile(r"\{\{l\|[^|}]*\|([^|}]*)(?:\|[^}]*)?\}\}"), r"\1"),
    (re.compile(r"\{\{m\|[^|}]*\|([^|}]*)(?:\|[^}]*)?\}\}"), r"\1"),
    (re.compile(r"\{\{(?:q|qualifier|i)\|([^}]*)\}\}"), r"(\1)"),
    (re.compile(r"\{\{gloss\|([^}]*)\}\}"), r"(\1)"),
    (re.compile(r"\{\{[^}]*\}\}"), ""),
    (re.compile(r"\[\[[^\]|]*\|([^\]]*)\]\]"), r"\1"),
    (re.compile(r"\[\[|\]\]"), ""),
    (re.compile(r"'{2,}"), ""),
    (re.compile(r"<[^>]*>"), ""),
]


def clean_def(s):
    for pat, rep in DEF_CLEAN:
        s = pat.sub(rep, s)
    return re.sub(r"\s+", " ", s).strip(" .;,")


def parse_page(title, content):
    m = re.search(r"\n==\s*Romanian\s*==\s*\n(.*?)(?=\n==[^=]|\Z)", "\n" + content, re.S)
    if not m:
        return None
    sec = m.group(1)
    # prima secțiune de etimologie
    em = re.search(r"===+\s*Etymology[^=\n]*===+\s*\n(.*?)(?=\n===|\Z)", sec, re.S)
    chain, affix, unknown, doublet, disputed = parse_etymology(em.group(1) if em else "")

    pos, gloss, labels = None, None, []
    for header, code in POS_HEADERS.items():
        hm = re.search(r"===+\s*" + header + r"\s*===+\s*\n(.*?)(?=\n===|\Z)", sec, re.S)
        if not hm:
            continue
        block = hm.group(1)
        if pos is None:
            pos = code
        for line in block.split("\n"):
            if not line.startswith("#") or line.startswith("#*") or line.startswith("#:"):
                continue
            lb = re.search(r"\{\{lb\|ro\|([^}]*)\}\}", line)
            if lb:
                labels = [x.strip() for x in lb.group(1).split("|")
                          if x.strip() and "=" not in x]
            g = clean_def(line.lstrip("# "))
            if g and gloss is None:
                gloss = g[:180]
            break
        if gloss:
            break
    return {
        "w": title, "pos": pos, "gloss": gloss, "labels": labels,
        "chain": chain, "affix": affix, "unknown": unknown, "doublet": doublet,
        "disputed": disputed,
    }


def main():
    words = json.load(open(os.path.join(RAW, "shortlist.json"), encoding="utf8"))
    cache_path = os.path.join(RAW, "wikt_sections.json.gz")
    cache = {}
    if os.path.exists(cache_path):
        with gzip.open(cache_path, "rt", encoding="utf8") as f:
            cache = json.load(f)
        print(f"cache: {len(cache)} secțiuni")
    todo = [w for w in words if w not in cache]
    print(f"de adus: {len(todo)} / {len(words)}")
    for i in range(0, len(todo), BATCH):
        batch = todo[i:i + BATCH]
        url = API + "?" + urllib.parse.urlencode({
            "action": "query", "prop": "revisions", "rvslots": "main",
            "rvprop": "content", "format": "json", "formatversion": "2",
            "titles": "|".join(batch), "maxlag": "5",
        })
        d = get_json(url)
        for p in d.get("query", {}).get("pages", []):
            t = p.get("title")
            if p.get("missing") or "revisions" not in p:
                cache[t] = ""
                continue
            c = p["revisions"][0]["slots"]["main"].get("content", "")
            m = re.search(r"\n==\s*Romanian\s*==\s*\n(.*?)(?=\n==[^=]|\Z)", "\n" + c, re.S)
            cache[t] = ("\n== Romanian ==\n" + m.group(1)) if m else ""
        for w in batch:
            cache.setdefault(w, "")
        if (i // BATCH) % 20 == 0:
            print(f"    {i + len(batch)}/{len(todo)}", flush=True)
            with gzip.open(cache_path, "wt", encoding="utf8") as f:
                json.dump(cache, f, ensure_ascii=False)
        time.sleep(0.15)
    with gzip.open(cache_path, "wt", encoding="utf8") as f:
        json.dump(cache, f, ensure_ascii=False)

    entries, no_section, no_ety = {}, 0, 0
    for w in words:
        c = cache.get(w) or ""
        if not c:
            no_section += 1
            continue
        e = parse_page(w, c)
        if not e:
            no_section += 1
            continue
        if not e["chain"] and not e["affix"] and not e["unknown"]:
            no_ety += 1
        entries[w] = e
    with open(os.path.join(RAW, "entries.json"), "w", encoding="utf8") as f:
        json.dump(entries, f, ensure_ascii=False)
    print(f"\nintrări: {len(entries)}   fără secțiune română: {no_section}   "
          f"fără etimologie citibilă: {no_ety}")
    print("scris raw/entries.json")


if __name__ == "__main__":
    main()
