"""Alimenta il sito da un post di slide.py: Turso, Vercel Blob (blob.py), permalink.

    python3 codice/sito.py demo
    python3 codice/sito.py pubblica post4 --data 2026-09-11T19:00 --ig ID --th ID [--gia-uscito] [--didascalia file] [--senza-deploy]
    python3 codice/sito.py verifica cryptid
    python3 codice/sito.py permalink cryptid --ig URL --th URL
    python3 codice/sito.py traduci cryptid en traduzione_en.json

I testi vengono dal markup delle slide (`slide.POST`), i numeri dal portale
(`portale.dati_gioco`, sempre riletti in rete: i numeri invecchiano), la
didascalia da Buffer. Le chiavi stanno in `sito/.env` e non si stampano mai.
"""
import argparse, csv, html, json, os, re, sqlite3, subprocess, sys, unicodedata, urllib.request
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import slide, portale, blob

QUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
IMM = os.path.join(QUI, "bgp-immagini", "sito")
ENV = {}
for _riga in open(os.path.join(QUI, "sito", ".env"), encoding="utf-8"):
    if "=" in _riga and not _riga.startswith("#"):
        _k, _v = _riga.strip().split("=", 1); ENV[_k] = _v

# --- testo -------------------------------------------------------------------

_NON_SCOMPONIBILI = str.maketrans({"×": "x", "ø": "o", "Ø": "O", "æ": "ae", "Æ": "AE", "ß": "ss", "ł": "l", "Ł": "L", "đ": "d", "Đ": "D"})

def slug(s):
    s = unicodedata.normalize("NFKD", s.translate(_NON_SCOMPONIBILI)).lower()
    s = "".join(c for c in s if not unicodedata.combining(c))
    return re.sub(r"[^a-z0-9]+", "-", s).strip("-")


def _t(h):  # testo pulito da un frammento html
    return html.unescape(re.sub(r"<[^>]+>", " ", h)).replace("\xa0", " ").strip()
def _t1(h): return re.sub(r"\s+", " ", _t(h))
def _tra(s, a, b=None):
    i = s.find(a); assert i >= 0, a
    j = s.find(b, i) if b else len(s); return s[i + len(a):j]


PICCOLE = {"di", "del", "della", "dei", "delle", "degli", "dello", "da", "dal", "la", "le", "il", "lo", "i", "gli", "agli", "alle", "al", "nel",
           "e", "a", "in", "con", "su", "per", "et", "of", "the", "and"}

def titolo_sito(h):
    """CRYPTID -> Cryptid, L’EREDITÀ<br>DI YU -> L’eredità di Yu: le parole piccole
    restano minuscole (tranne la prima), il <br> diventa uno spazio."""
    out = []
    for i, p in enumerate(_t1(h).lower().split(" ")):
        if i == 0 or p not in PICCOLE:
            p = re.sub(r"(^|-)(\w)", lambda m: m.group(1) + m.group(2).upper(), p)
        if re.fullmatch(r"[ivx]+", p, re.I): p = p.upper()   # numeri romani: Glen More II, non Ii
        out.append(p)
    return " ".join(out)


def estrai_post(chiave):
    v = slide.POST[chiave]; cop = v[0]
    per = lambda cls: [s for s in v if f'class="post {cls}"' in s]
    fs, ts, sc, ch = per("fs")[0], per("ts"), per("sc")[0], per("ch")[0]
    titolo = titolo_sito(_tra(cop, 'class="tit', "</div>").split(">", 1)[1])
    cred = _t1(_tra(cop, 'class="cred">', "</div>"))
    dati = [_t1(x) for x in re.findall(r'class="v">(.*?)</span></span>', cop)]
    g, d = ([int(x) for x in re.findall(r"\d+", dati[i])] for i in (0, 1))
    tag = lambda cls: [_t1(x) for x in re.findall(rf'<span class="{cls}">(.*?)</span>', sc)]
    ed = [_t1(x) for x in re.findall(r'<div class="ed">(.*?)</div>', sc)]
    foto = [re.search(r'<img src="([^"]+)"', s).group(1) for s in (cop, fs, ts[0], ch)]
    tre_d = re.search(r'class="box"><img src="([^"]+)"', sc)
    return dict(post=chiave, titolo=titolo, autori_testo=cred.split(" · ")[0], anno=int(re.search(r"\b\d{4}\b", cred).group()),
        editore=_t1(_tra(cop, "<b>", "</b>")), editore_it=ed[0], testo_nel_gioco=ed[1] if len(ed) > 1 else None,
        giocatori=(g[0], g[-1]), durata=(d[0], d[-1]), peso_etichetta=dati[2] if len(dati) > 2 else None,
        cos_e=_t1(_tra(fs, 'class="corpo">', "</div>")),
        come_gira="\n\n".join(_t1(_tra(s, 'class="corpo">', "</div>")) for s in ts),
        domanda=_t1(_tra(ch, 'class="dom">', "</div>")),
        domanda_sotto=_t1(_tra(ch, 'class="grigia">', "</div>")) if 'class="grigia"' in ch else None,
        meccaniche=tag("tag"), categorie=tag("tag tagA"), foto=foto, scatola3d=tre_d.group(1) if tre_d else None)


def video_it(lista):
    """Solo i tutorial in italiano: galleria "instructional" o "tutorial" nel titolo. Uno per id YouTube."""
    out, visti = [], set()
    for v in lista or []:
        m = re.search(r"(?:v=|youtu\.be/|/embed/)([A-Za-z0-9_-]{11})", v.get("url") or "")
        if not m or v.get("lingua") != "Italian": continue
        if v.get("galleria") != "instructional" and "tutorial" not in (v.get("titolo") or "").lower(): continue
        if m.group(1) in visti: continue
        visti.add(m.group(1)); out.append({"url": v["url"], "id": m.group(1), "titolo": v.get("titolo"), "autore": v.get("autore")})
    return out


def arricchisci(d, bgg_id, usa_cache=False):
    p = portale.dati_gioco(bgg_id, usa_cache=usa_cache)
    if p.get("errore"): raise SystemExit(f"portale {bgg_id}: {p['errore']}")
    st = p.get("partiteStats") or {}
    d.update(bgg_id=int(bgg_id), peso=float(p["peso"]) if p.get("peso") else None,
        partite=p["partiteTavoli"] if p.get("partiteTavoli") is not None else p.get("partite"),
        durate=[{"giocatori": x["giocatori"], "partite": x["partite"], "mediana": x["durataMediana"]}
                for x in st.get("perGiocatori", []) if x.get("durataMediana")],
        autori=[{"nome": a["nome"], "slug": slug(a["nome"]), "handle": handle_autore(a["nome"])} for a in p.get("autori", [])],
        # Sul sito vanno tutte le categorie e le meccaniche del portale (nomi già in italiano,
        # corretti alla fonte il 2026-09-14), non le tre scelte per la slide: lì il limite era lo spazio.
        categorie=p["categorieNomi"], meccaniche=p["meccanicheNomi"],
        video=video_it(p.get("video")),
        collegati=[{"bgg_id": c["gioco"]["id"], "nome": c["gioco"]["nome"], "tipo": c["relazione"]} for c in p.get("collegati") or []],
        immagine_bgg=p.get("image"))
    return d


def handle_autore(nome):
    with open(os.path.join(QUI, "dati", "handle_autori.csv"), encoding="utf-8") as f:
        for r in csv.DictReader(f):
            if r["Autore"].strip().lower() == nome.lower(): return r["Instagram"].strip() or None


def handle_editore(nome):
    """Senza spazi e minuscolo: sulle slide c'e' "GateOnGames", in tabella "Gate on Games"."""
    db = sqlite3.connect(os.path.join(QUI, "dati", "bgp.sqlite"))
    r = db.execute("select instagram from editori where lower(replace(nome,' ','')) = ?",
                   [nome.lower().replace(" ", "")]).fetchone()
    return "@" + r[0] if r and r[0] else None


def bgg_id_di(titolo):
    """Dall'indice dell'archivio, confrontando gli slug; se manca, si chiede. Mai a memoria."""
    with open(os.path.join(QUI, "dati", "indice_nomi.json"), encoding="utf-8") as f:
        indice = json.load(f)
    s = slug(titolo)
    for nome, gid in indice.items():
        if slug(nome) == s: return int(gid)
    return int(input(f"id BGG di «{titolo}» (non è in indice_nomi.json): "))


def chicca_da_didascalia(testo, chiedi=False):
    par = [p.strip() for p in re.split(r"\n\s*\n", testo.strip()) if p.strip()]
    par = [p for p in par if not p.startswith("#")]
    apertura = par[0]
    # il 🍬 apre la chicca per convenzione (testi-e-fonti): se c'e', vince su tutto
    dolce = [p for p in par if p.startswith("🍬")]
    if dolce: return apertura, dolce[0].lstrip("🍬").strip()
    centrali = [p for p in par[1:] if "?" not in p.split("\n")[-1] or len(p) > 200]  # via la CTA
    centrali = [p for p in centrali if not p.startswith("🎲")]  # via il cos'è
    chicca = centrali[-1] if centrali else None
    if chiedi and (chicca is None or len(centrali) > 2):
        for i, p in enumerate(par, 1): print(f"{i}. {p}\n")
        n = input("quale paragrafo è la chicca? (numero, vuoto = nessuna) ").strip()
        chicca = par[int(n) - 1] if n else None
    return apertura, chicca

# --- immagini, Turso, Buffer -------------------------------------------------

def prepara_immagini(d, s, deploy=True):
    from PIL import Image, ImageOps
    out = os.path.join(IMM, s); os.makedirs(out, exist_ok=True); nomi = ["cop.jpg", "cose.jpg", "gira.jpg", "fine.jpg"]
    for src, nome in zip(d["foto"], nomi):
        im = ImageOps.exif_transpose(Image.open(src)).convert("RGB"); im.thumbnail((1600, 1600)); im.save(os.path.join(out, nome), quality=80)
    piatta = portale.scatola_bgg(d["bgg_id"], s)
    im = Image.open(piatta).convert("RGB"); im.thumbnail((600, 600)); im.save(os.path.join(out, "scatola-bgg.jpg"), quality=82)
    if d["scatola3d"]:
        im = Image.open(d["scatola3d"]).convert("RGBA"); im.thumbnail((700, 700)); im.save(os.path.join(out, "scatola.png"))
    file_scatola = "scatola.png" if d["scatola3d"] else None
    if deploy:
        # Un file alla volta su Vercel Blob, non piu' un `vercel deploy` dell'intera
        # cartella: vedi blob.py per il perche'.
        for nome in nomi + ["scatola-bgg.jpg"] + ([file_scatola] if file_scatola else []):
            blob.carica(os.path.join(out, nome), f"sito/{s}/{nome}")
    return nomi, "scatola-bgg.jpg", file_scatola


def turso(sql, args=()):
    url = ENV["TURSO_DATABASE_URL"].replace("libsql://", "https://") + "/v2/pipeline"
    conv = lambda a: ({"type": "null"} if a is None else {"type": "integer", "value": str(a)} if isinstance(a, int)
                      else {"type": "float", "value": a} if isinstance(a, float) else {"type": "text", "value": a})
    corpo = json.dumps({"requests": [{"type": "execute", "stmt": {"sql": sql, "args": [conv(a) for a in args]}}, {"type": "close"}]}).encode()
    req = urllib.request.Request(url, data=corpo, headers={"Authorization": f"Bearer {ENV['TURSO_AUTH_TOKEN']}", "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=30) as r: j = json.load(r)
    res = j["results"][0]
    if res["type"] != "ok": raise SystemExit(f"Turso: {res}")
    return res["response"]["result"]


def buffer(post_id):
    """text, externalLink, status di un post Buffer."""
    if not ENV.get("BUFFER_API_KEY"): raise SystemExit("manca BUFFER_API_KEY in sito/.env (serve per didascalia e link)")
    q = "query ($id: PostId!) { post(input:{id:$id}) { text externalLink status } }"
    req = urllib.request.Request("https://api.buffer.com/graphql", data=json.dumps({"query": q, "variables": {"id": post_id}}).encode(),
        headers={"Authorization": f"Bearer {ENV['BUFFER_API_KEY']}", "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=30) as r: j = json.load(r)
    if j.get("errors") or not (j.get("data") or {}).get("post"): raise SystemExit(f"Buffer {post_id}: {j.get('errors')}")
    return j["data"]["post"]


def data_roma(s):
    """2026-10-02T19:00 -> 2026-10-02T19:00:00+02:00; un ISO con offset o Z viene portato a Roma."""
    dt = datetime.fromisoformat(s); roma = ZoneInfo("Europe/Rome")
    dt = dt.replace(tzinfo=roma) if dt.tzinfo is None else dt.astimezone(roma)
    return dt.isoformat(timespec="seconds")

# --- comandi -----------------------------------------------------------------

def pubblica(chiave, data, ig, th, gia_uscito=False, didascalia=None, deploy=True):
    d = estrai_post(chiave)
    arricchisci(d, bgg_id_di(d["titolo"])); s = slug(d["titolo"]); t = d["titolo"]
    post_ig = buffer(ig) if gia_uscito or not didascalia else None
    testo = open(didascalia, encoding="utf-8").read() if didascalia else post_ig["text"]
    apertura, chicca = chicca_da_didascalia(testo, chiedi=True)
    foto, piatta, tre_d = prepara_immagini(d, s, deploy)
    link = lambda p: p["externalLink"] if p and p.get("status") == "sent" else None
    ig_url, th_url = (link(post_ig), link(buffer(th))) if gia_uscito else (None, None)
    # L'editore del sito e' quello dei crediti (il tag della slide 1). Il testo della slide
    # scatola ("Ghenos, annunciata", "Non esiste") resta com'e' in edizione_it, solo per la sezione La scatola.
    ed = d["editore"]
    j = lambda x: json.dumps(x, ensure_ascii=False)
    cols = dict(slug=s, post=chiave, bgg_id=d["bgg_id"], titolo=t, autori=j(d["autori"]), anno=d["anno"],
        editore_it=ed, editore_slug=slug(ed), editore_handle=handle_editore(ed), edizione_it=d["editore_it"],
        giocatori_min=d["giocatori"][0], giocatori_max=d["giocatori"][1], durata_min=d["durata"][0], durata_max=d["durata"][1],
        peso=d["peso"], peso_etichetta=d["peso_etichetta"], categorie=j(d["categorie"]), meccaniche=j(d["meccaniche"]),
        testo_nel_gioco=d["testo_nel_gioco"], cos_e=d["cos_e"], come_gira=d["come_gira"], apertura=apertura, chicca=chicca,
        domanda=d["domanda"], domanda_sotto=d["domanda_sotto"], partite=d["partite"], durate=j(d["durate"]),
        video=j(d["video"]), collegati=j(d["collegati"]),
        foto=j(foto), alt=j([f"{t} sul tavolo", f"{t}, cos’è", f"{t}, come gira", f"{t}, fine partita"]),
        scatola_piatta=piatta, scatola_3d=tre_d, buffer_ig=ig, buffer_th=th, instagram_url=ig_url, threads_url=th_url,
        pubblicato_il=data_roma(data), aggiornato_il=datetime.now(timezone.utc).isoformat(timespec="seconds"))
    k = list(cols)
    sql = (f"insert into giochi ({','.join(k)}) values ({','.join('?' * len(k))}) on conflict(slug) do update set "
           + ",".join(f"{c}=excluded.{c}" for c in k if c != "slug"))
    turso(sql, [cols[c] for c in k]); print("ok", s, cols["pubblicato_il"])


def verifica(s):
    r = turso("select * from giochi where slug=?", [s])
    if not r["rows"]: print("nessuna riga", s); return
    for c, v in zip(r["cols"], r["rows"][0]): print(f"{c['name']}: {v.get('value')}")


def permalink(s, ig, th):
    r = turso("update giochi set instagram_url=?, threads_url=?, aggiornato_il=? where slug=?",
              [ig, th, datetime.now(timezone.utc).isoformat(timespec="seconds"), s])
    print("righe aggiornate:", r["affected_row_count"])


CAMPI_TRADOTTI = ["edizione_it", "testo_nel_gioco", "categorie", "meccaniche", "peso_etichetta",
                  "cos_e", "come_gira", "apertura", "chicca", "domanda", "domanda_sotto", "alt"]


def traduci(slug_gioco, lingua, file):
    """Carica in 'traduzioni' un file JSON con i campi tradotti di un gioco (vedi CAMPI_TRADOTTI).
    Fatto per ogni gioco appena messo in coda con 'pubblica', nelle quattro lingue diverse
    dall'italiano: i testi li traduco io, questo comando li scrive nel database del sito."""
    with open(file, encoding="utf-8") as f:
        g = json.load(f)
    cols = {"slug": slug_gioco, "lingua": lingua, "aggiornato_il": datetime.now(timezone.utc).isoformat(timespec="seconds")}
    for c in CAMPI_TRADOTTI:
        v = g.get(c)
        cols[c] = json.dumps(v, ensure_ascii=False) if isinstance(v, list) else v
    k = list(cols)
    sql = (f"insert into traduzioni ({','.join(k)}) values ({','.join('?' * len(k))}) on conflict(slug, lingua) do update set "
           + ",".join(f"{c}=excluded.{c}" for c in k if c not in ("slug", "lingua")))
    turso(sql, [cols[c] for c in k])
    print("ok", slug_gioco, lingua)


def main(argv):
    p = argparse.ArgumentParser(prog="sito.py"); sub = p.add_subparsers(dest="cmd", required=True)
    a = sub.add_parser("pubblica"); a.add_argument("chiave"); a.add_argument("--data", required=True)
    a.add_argument("--ig", required=True); a.add_argument("--th", required=True)
    a.add_argument("--gia-uscito", action="store_true"); a.add_argument("--didascalia"); a.add_argument("--senza-deploy", action="store_true")
    b = sub.add_parser("verifica"); b.add_argument("slug")
    c = sub.add_parser("permalink"); c.add_argument("slug"); c.add_argument("--ig", required=True); c.add_argument("--th", required=True)
    d = sub.add_parser("traduci"); d.add_argument("slug"); d.add_argument("lingua"); d.add_argument("file")
    o = p.parse_args(argv)
    if o.cmd == "pubblica": pubblica(o.chiave, o.data, o.ig, o.th, o.gia_uscito, o.didascalia, deploy=not o.senza_deploy)
    elif o.cmd == "verifica": verifica(o.slug)
    elif o.cmd == "permalink": permalink(o.slug, o.ig, o.th)
    else: traduci(o.slug, o.lingua, o.file)


def demo():
    d = estrai_post("post4")
    assert d["titolo"] == "Cryptid" and d["giocatori"] == (3, 5) and d["durata"] == (30, 50)
    assert d["cos_e"].startswith("Da qualche parte") and d["domanda"] == "Qual è il deduttivo che non reggi?"
    assert d["meccaniche"] == ["Deduzione", "Griglia esagonale", "Plancia modulare"] and d["categorie"] == ["Deduttivo"]
    arricchisci(d, 246784, usa_cache=True)   # dalla cache: la demo non va in rete
    assert d["durate"] == [{"giocatori": 3, "partite": 9, "mediana": 11}, {"giocatori": 4, "partite": 5, "mediana": 30}, {"giocatori": 5, "partite": 5, "mediana": 25}]
    assert "Induzione" in d["meccaniche"] and d["categorie"] == ["Deduttivo"]   # dal portale, non dalla slide: la slide ne aveva tre
    assert d["video"] == [{"url": "https://www.youtube.com/watch?v=Em0hLjA_iMw", "id": "Em0hLjA_iMw",
                           "titolo": "Tutorial in italiano di Cryptid", "autore": "Cercatore di Perle"}]   # la Recensione Minuta resta fuori
    assert d["collegati"] == []
    w = arricchisci({}, 473508, usa_cache=True)   # Wingspan Pocket: video solo in inglese, un solo parente
    assert w["video"] == [] and w["collegati"] == [{"bgg_id": 266192, "nome": "Wingspan", "tipo": "precedente"}]
    assert video_it([{"url": "https://youtu.be/abcdefghijk", "titolo": "Tutorial", "lingua": "Italian", "galleria": "review"},
                     {"url": "https://www.youtube.com/watch?v=abcdefghijk", "titolo": "Tutorial", "lingua": "Italian", "galleria": "instructional"},
                     {"url": "https://www.youtube.com/watch?v=zzzzzzzzzzz", "titolo": "How to play", "lingua": "English", "galleria": "instructional"}]) \
        == [{"url": "https://youtu.be/abcdefghijk", "id": "abcdefghijk", "titolo": "Tutorial", "autore": None}]
    ap, ch = chicca_da_didascalia("Trentaquattro partite in casa nostra.\n\n🎲 Da qualche parte...\n\nCome in ogni deduttivo...\n\nGli indizi sono frasi corte, quindi non lo consiglio.\n\nQual è il deduttivo che non reggi? Scrivi il titolo qui sotto.\n\n#cryptid #gdt")
    assert ap == "Trentaquattro partite in casa nostra." and ch.startswith("Gli indizi")
    assert slug("L’EREDITÀ DI YU") == "l-eredita-di-yu"
    print("demo ok")


if __name__ == "__main__":
    if len(sys.argv) == 1 or sys.argv[1] == "demo": demo()
    else: main(sys.argv[1:])
