#!/usr/bin/env python3
"""
Genera il sito di Spuncin, una pagina HTML vera per ogni lingua.

Il testo finisce dentro al file, non montato dal JavaScript dopo il caricamento,
perché è l'unico modo perché i motori di ricerca e gli assistenti leggano la
pagina nella lingua giusta. L'inglese sta nella radice ed è anche il ritorno
quando la lingua del browser non è fra quelle tradotte.

    python3 build.py

Il risultato sta in dist/, pronto da caricare su Cloudflare Pages.
"""

import json
import re
import shutil
from pathlib import Path

HERE = Path(__file__).parent
SRC = HERE / "src"
DIST = HERE / "dist"
SITE = "https://spuncin.com"

# L'ordine è quello del menu a tendina. L'inglese resta primo perché è la lingua
# di partenza e il ritorno quando non ne abbiamo altre.
LANGS = ["en", "it", "es", "fr", "de", "pt-BR", "nl", "pl", "ru", "ja", "id", "tr"]

# Aggiunte man mano che le traduzioni arrivano. Il resto del build non cambia.
PLANNED = []

RTL = {"ar", "he", "fa", "ur"}

# I nomi che scorrono nei due nastri. Sono crawler veri, presi dal catalogo del
# plugin, scelti fra quelli che un blogger ha qualche possibilità di riconoscere.
TICKER_STOP = [
    "GPTBot", "ClaudeBot", "Bytespider", "CCBot", "Google-Extended",
    "Applebot-Extended", "Meta-ExternalAgent", "DeepSeekBot", "Diffbot",
    "FacebookBot", "PanguBot", "TikTokSpider", "FirecrawlAgent", "Crawl4AI",
    "img2dataset", "omgilibot", "YandexAdditional", "Webzio-Extended",
    "cohere-training-data-crawler", "AI2Bot", "Brightbot", "QuillBot",
    "ICC-Crawler", "GoogleOther", "SBIntuitionsBot", "Amazonbot",
]

TICKER_PASS = [
    "Googlebot", "Bingbot", "DuckDuckBot", "Baiduspider", "YandexBot",
    "Applebot", "PetalBot", "Twitterbot", "Slackbot", "WhatsApp",
    "LinkedInBot", "Discordbot", "TelegramBot", "Pinterestbot",
    "facebookexternalhit", "ChatGPT-User", "Perplexity-User", "Claude-User",
    "feedly", "Feedbin", "Google-InspectionTool", "Bingbot-Preview",
]

# Il livello dal quale ciascuno viene fermato. "never" sono quelli che nessun
# livello tocca, e restano accesi qualunque cosa scelga chi guarda.
LEVEL_CHIPS = [
    ("GPTBot", "1"), ("ClaudeBot", "1"), ("Bytespider", "1"), ("CCBot", "1"),
    ("Google-Extended", "1"), ("Applebot-Extended", "1"), ("Meta-ExternalAgent", "1"),
    ("DeepSeekBot", "1"), ("Diffbot", "1"), ("FacebookBot", "1"), ("PanguBot", "1"),
    ("TikTokSpider", "1"), ("FirecrawlAgent", "1"), ("img2dataset", "1"),
    ("OAI-SearchBot", "2"), ("PerplexityBot", "2"), ("Claude-SearchBot", "2"),
    ("Amazonbot", "2"), ("Applebot", "2"), ("PetalBot", "2"), ("YouBot", "2"),
    ("DuckAssistBot", "2"), ("Bravebot", "2"), ("TavilyBot", "2"), ("TongyiBot", "2"),
    ("ChatGPT-User", "3"), ("Perplexity-User", "3"), ("Claude-User", "3"),
    ("MistralAI-User", "3"), ("Kimi-User", "3"), ("NotebookLM", "3"),
    ("kagi-fetcher", "3"), ("facebookexternalhit", "3"), ("Gemini-Deep-Research", "3"),
    ("Googlebot", "never"), ("Bingbot", "never"), ("DuckDuckBot", "never"),
    ("Baiduspider", "never"), ("YandexBot", "never"), ("Twitterbot", "never"),
    ("Slackbot", "never"), ("LinkedInBot", "never"),
]

# Riconoscimento della lingua del browser. Gira solo sulla pagina inglese, che è
# la radice: da una pagina già tradotta non si rimanda da nessuna parte, o si
# finirebbe per contraddire la scelta di chi ci è arrivato apposta.
REDIRECT = """<script>
(function(){
	try {
		var saved = localStorage.getItem('spuncin-lang');
		if (saved === 'en') { return; }
		var known = %s;
		var want = saved || (navigator.language || 'en');
		var hit = known.indexOf(want) > -1 ? want : null;
		if (!hit) {
			var base = want.split('-')[0];
			for (var i = 0; i < known.length; i++) {
				if (known[i].split('-')[0] === base) { hit = known[i]; break; }
			}
		}
		if (hit && hit !== 'en') { location.replace('/' + hit + '/'); }
	} catch (e) {}
})();
</script>"""


def chips(names, kind):
    """Un nastro deve scorrere all'infinito, quindi la lista sta due volte."""
    one = "".join('<span class="chip chip--%s">%s</span>' % (kind, n) for n in names)
    return one + one


def level_chips():
    """L'indice serve al CSS per far cambiare i nomi a onda invece che tutti
    insieme quando si passa da un livello all'altro."""
    return "".join(
        '<span class="chip" data-from="%s" style="--i:%d">%s</span>' % (level, i, name)
        for i, (name, level) in enumerate(LEVEL_CHIPS)
    )


def build_one(lang, texts, template):
    is_root = lang == "en"
    out_dir = DIST if is_root else DIST / lang
    out_dir.mkdir(parents=True, exist_ok=True)

    root = "" if is_root else "../"
    canonical = SITE + "/" if is_root else "%s/%s/" % (SITE, lang)

    alternates = []
    for other in LANGS:
        href = SITE + "/" if other == "en" else "%s/%s/" % (SITE, other)
        alternates.append('<link rel="alternate" hreflang="%s" href="%s">' % (other, href))
    alternates.append('<link rel="alternate" hreflang="x-default" href="%s/">' % SITE)

    names = {
        code: json.loads((SRC / "i18n" / (code + ".json")).read_text(encoding="utf-8"))["_name"]
        for code in LANGS
    }

    def href(code):
        return "/" if code == "en" else "/%s/" % code

    items = "".join(
        '<li role="none"><a role="menuitemradio" aria-checked="%s" href="%s" lang="%s" hreflang="%s">'
        '<svg class="flag" aria-hidden="true"><use href="#f-%s"/></svg>%s</a></li>'
        % ("true" if code == lang else "false", href(code), code, code, code, names[code])
        for code in LANGS
    )

    menu = (
        '<button type="button" class="lang__btn" aria-expanded="false" aria-haspopup="true">'
        '<svg class="flag" aria-hidden="true"><use href="#f-%s"/></svg>'
        '<span class="lang__now">%s</span>'
        '<svg class="lang__caret" viewBox="0 0 10 6" aria-hidden="true">'
        '<path d="M1 1 5 5 9 1" fill="none" stroke="currentColor" stroke-width="1.6" '
        'stroke-linecap="round" stroke-linejoin="round"/></svg></button>'
        '<ul class="lang__list" role="menu" hidden>%s</ul>'
    ) % (lang, names[lang], items)

    faq = {
        "@context": "https://schema.org",
        "@type": "FAQPage",
        "inLanguage": lang,
        "mainEntity": [
            {
                "@type": "Question",
                "name": texts["deep_%d_q" % n],
                "acceptedAnswer": {"@type": "Answer", "text": texts["deep_%d_a" % n]},
            }
            for n in range(1, 6)
        ],
    }

    values = dict(texts)
    values.update({
        "lang": lang,
        "dir": ' dir="rtl"' if lang.split("-")[0] in RTL else "",
        "root": root,
        "canonical": canonical,
        "alternates": "\n".join(alternates),
        "og_locale": texts["_locale"],
        "redirect": REDIRECT % json.dumps(LANGS) if is_root else "",
        "ticker_stop_chips": chips(TICKER_STOP, "stop"),
        "ticker_pass_chips": chips(TICKER_PASS, "pass"),
        "level_chips": level_chips(),
        "lang_menu": menu,
        "faq_jsonld": '<script type="application/ld+json">%s</script>'
        % json.dumps(faq, ensure_ascii=False),
        "flags": (SRC / "flags.svg").read_text(encoding="utf-8"),
    })

    page = re.sub(r"\{\{(\w+)\}\}", lambda m: str(values.get(m.group(1), "")), template)

    missing = re.findall(r"\{\{(\w+)\}\}", template)
    unknown = sorted(set(k for k in missing if k not in values))
    if unknown:
        raise SystemExit("manca la traduzione %s per la lingua %s" % (unknown, lang))

    (out_dir / "index.html").write_text(page, encoding="utf-8")
    return len(page.encode("utf-8"))


def main():
    if DIST.exists():
        shutil.rmtree(DIST)
    DIST.mkdir()

    template = (SRC / "template.html").read_text(encoding="utf-8")

    shutil.copytree(HERE / "assets", DIST / "assets")
    shutil.copy(SRC / "style.css", DIST / "assets" / "style.css")
    shutil.copy(SRC / "app.js", DIST / "assets" / "app.js")

    for lang in LANGS:
        texts = json.loads((SRC / "i18n" / (lang + ".json")).read_text(encoding="utf-8"))
        size = build_one(lang, texts, template)
        print("  %-6s %6.1f KB" % (lang, size / 1024))

    # Il sito di un plugin che blocca i crawler AI vuole essere trovato da tutti,
    # compresi gli assistenti: qui non c'è niente da proteggere, c'è da spiegare.
    (DIST / "robots.txt").write_text(
        "User-agent: *\nAllow: /\n\nSitemap: %s/sitemap.xml\n" % SITE, encoding="utf-8"
    )

    def loc(code):
        return SITE + "/" if code == "en" else "%s/%s/" % (SITE, code)

    # Ogni voce elenca tutte le sue traduzioni. Google chiede che il rimando sia
    # reciproco, e ripeterlo nella sitemap oltre che nella pagina è il modo che
    # consigliano per i siti tradotti.
    links = "".join(
        '<xhtml:link rel="alternate" hreflang="%s" href="%s"/>' % (code, loc(code))
        for code in LANGS
    ) + '<xhtml:link rel="alternate" hreflang="x-default" href="%s/"/>' % SITE

    urls = "".join(
        "<url><loc>%s</loc>%s<changefreq>monthly</changefreq></url>" % (loc(lang), links)
        for lang in LANGS
    )
    (DIST / "sitemap.xml").write_text(
        '<?xml version="1.0" encoding="UTF-8"?>'
        '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" '
        'xmlns:xhtml="http://www.w3.org/1999/xhtml">%s</urlset>' % urls,
        encoding="utf-8",
    )

    total = sum(f.stat().st_size for f in DIST.rglob("*") if f.is_file())
    print("\n%d lingue, %.1f KB in tutto" % (len(LANGS), total / 1024))
    if PLANNED:
        print("da tradurre ancora: %s" % ", ".join(PLANNED))


if __name__ == "__main__":
    main()
