"""Générateur du site retraite-maurice.fr.

Lit donnees/*.json (site, chiffres, tarifs, régions, budget, rubriques, actualités) et contenu/ (accueil, articles),
écrit le site complet dans public/ : chaque page, le plan du site, robots.txt.
Puis contrôle chaque page (règles SEO type Yoast, liens internes) et affiche le rapport.

Usage : python build.py            → génère public/
        python build.py --serve    → génère puis sert public/ sur http://localhost:8040
"""
import html.parser, json, math, re, shutil, sys, time
from datetime import date
from pathlib import Path

import markdown
from jinja2 import Environment, FileSystemLoader

sys.stdout.reconfigure(encoding="utf-8")
ICI = Path(__file__).resolve().parent
SORTIE = ICI / "public"
MOIS = ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"]


def lire(nom):
    return json.loads((ICI / "donnees" / f"{nom}.json").read_text(encoding="utf8"))


def date_fr(iso):
    d = date.fromisoformat(iso)
    return f"{d.day} {MOIS[d.month - 1]} {d.year}"


def eur(n):
    return f"{round(n):,}".replace(",", " ") + " €"


def texte_brut(h):
    return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", h)).strip()


# ---------------------------------------------------------------- données
site = lire("site")
f = {k: v["valeur"] for k, v in lire("faits").items() if not k.startswith("_")}
tarifs, budget, actualites = lire("tarifs"), lire("budget"), lire("actualites")
regions = lire("regions")["regions"]
rubriques = lire("rubriques")["rubriques"]
liens = lire("liens")

env = Environment(loader=FileSystemLoader(ICI / "gabarits"), autoescape=True,
                  trim_blocks=True, lstrip_blocks=True)
env.globals["v"] = str(int(time.time()))  # version des CSS et JS : le navigateur recharge après chaque build
env.globals.update(site=site, f=f, tarifs=tarifs, budget=budget, regions=regions, rubriques=rubriques,
                   actualites=actualites, eur=eur)
env.filters["eur"] = eur


def rendre_texte(v):
    """Remplace {{ f.xxx }} dans toutes les chaînes d'un contenu JSON."""
    if isinstance(v, str):
        return env.from_string(v).render() if "{{" in v else v
    if isinstance(v, list):
        return [rendre_texte(x) for x in v]
    if isinstance(v, dict):
        return {k: rendre_texte(x) for k, x in v.items()}
    return v


# ---------------------------------------------------------------- articles
def lire_article(chemin):
    """En-tête « clé: valeur » entre deux lignes ---, puis le texte en Markdown.
    Les clés répétées (faq) s'empilent ; « faq: Question ? | Réponse »."""
    brut = chemin.read_text(encoding="utf8")
    _, tete, corps = brut.split("---", 2)
    meta = {"faq": []}
    for ligne in tete.strip().splitlines():
        if not ligne.strip() or ligne.lstrip().startswith("#"):
            continue
        cle, val = ligne.split(":", 1)
        cle, val = cle.strip(), val.strip()
        if cle == "faq":
            q, r = val.split("|", 1)
            meta["faq"].append([q.strip(), r.strip()])
        else:
            meta[cle] = val
    meta["slug"] = chemin.stem
    meta["source"] = corps
    return meta


rub_par_cle = {r["cle"]: r for r in rubriques}
for r in rubriques:
    r["articles"] = []
    for t in r["themes"]:
        t["articles"] = []

articles = []
for chemin in sorted((ICI / "contenu" / "articles").glob("*.md")):
    a = lire_article(chemin)
    if a.get("brouillon") == "oui":
        continue
    r = rub_par_cle[a["rubrique"]]
    t = next(t for t in r["themes"] if t["cle"] == a["theme"])
    md_src = '{% import "blocs/article.html" as b with context %}' + a["source"]
    corps = env.from_string(md_src).render()
    a["html"] = markdown.markdown(corps, extensions=["tables", "attr_list", "md_in_html"])
    a["faq"] = [rendre_texte(q) for q in a["faq"]]
    a.update(
        type="article", rubrique=r, theme_titre=t["titre"], url=f"{r['lien']}{a['slug']}/", section=r["lien"],
        titre=rendre_texte(a["titre"]), titre_html=rendre_texte(a.get("titre_html", a["titre"])),
        description=rendre_texte(a["description"]), titre_seo=rendre_texte(a["titre_seo"]),
        image=f"/assets/img/{a['image']}.jpg", date_txt=date_fr(a["date"]),
        maj=a.get("maj", a["date"]), lecture=max(1, math.ceil(len(texte_brut(a["html"]).split()) / 220)),
        service_lie=liens["services"][a["service"]], outil_lie=liens["outils"].get(a.get("outil", "")),
    )
    a["maj_txt"] = date_fr(a["maj"])
    articles.append(a)
    r["articles"].append(a)
    t["articles"].append(a)

for r in rubriques:
    r["themes_publies"] = [t for t in r["themes"] if t["articles"]]
env.globals["rubriques_par_cle"] = rub_par_cle
# pages qui existent : le menu et le pied marquent les autres « bientôt »
env.globals["existe"] = {"/", *(a["url"] for a in articles), *(r["lien"] for r in rubriques if r["articles"]),
                         *(json.loads(p.read_text(encoding="utf8"))["url"] for p in (ICI / "contenu" / "pages").glob("*.json"))}


def entree(l):  # un lien du menu : tant que sa page n'existe pas, son lien « en_attendant », sinon l'étiquette « bientôt »
    manque = l["lien"].startswith("/") and l["lien"].split("#")[0] not in env.globals["existe"]
    if manque and l.get("en_attendant"):
        return {**l, "lien": l["en_attendant"], "bientot": False}
    return {**l, "bientot": manque}


menu = []  # site.json « menu » prêt à afficher ; « zones » : débuts d'adresse qui allument l'entrée
for m in site["menu"]:
    cols = []
    for c in m.get("colonnes", []):
        if "rubrique" in c:  # les thèmes de la rubrique, avec leur nombre d'articles
            r = rub_par_cle[c["rubrique"]]
            cols.append({"titre": "Les thèmes", "liens": [
                {"titre": t["titre"], "icone": t["icone"], "bientot": not t["articles"],
                 "lien": f"{r['lien']}#{t['cle']}" if t["articles"] else r["lien"],  # thème vide : pas encore d'ancre sur la page de rubrique
                 "texte": f"{len(t['articles'])} article{'s' if len(t['articles']) > 1 else ''}" if t["articles"] else ""}
                for t in r["themes"]]})
        else:
            cols.append({"titre": c.get("titre"), "liens": [entree(l) for l in c["liens"]]})
    # adresses prévues (pas celles « en_attendant ») : la page des tarifs ne doit pas allumer « Le permis »
    adresses = {m["lien"]} | {l["lien"].split("#")[0] for c in m.get("colonnes", []) for l in c.get("liens", [])}
    carte = m.get("carte") and (entree(m["carte"]) if m["carte"].get("lien") else m["carte"])
    menu.append({**entree(m), "colonnes": cols, "plus": [entree(l) for l in m.get("plus", [])], "carte": carte,
                 "tout": any("rubrique" in c for c in m.get("colonnes", [])),  # lien « tout voir » vers la page de rubrique
                 "zones": sorted(adresses - {"/"})})
env.globals["menu"] = menu
env.globals["prix_depart"] = min(x["base"] for x in tarifs["formules"] if "base" in x)
articles.sort(key=lambda a: a["maj"], reverse=True)
for a in articles:  # à lire ensuite : même thème d'abord, puis même rubrique, puis le reste
    autres = [x for x in articles if x is not a]
    autres.sort(key=lambda x: (x["theme"] != a["theme"], x["rubrique"] is not a["rubrique"]))
    a["liees"] = autres[:3]


# ---------------------------------------------------------------- données structurées (schema.org)
ORGA = {"@type": "ProfessionalService", "@id": site["domaine"] + "/#agence", "name": site["nom"],
        "url": site["domaine"] + "/", "logo": site["domaine"] + "/assets/logo.svg",
        "image": site["domaine"] + site["image_partage"], "description": site["description"],
        "telephone": site["contact"]["tel_maurice"], "email": site["contact"]["email"],
        "address": {"@type": "PostalAddress", "addressLocality": "Grand Baie", "addressCountry": "MU"},
        "areaServed": ["MU", "FR", "BE", "CH"]}


def ld_faq(faq):
    return {"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [
        {"@type": "Question", "name": q, "acceptedAnswer": {"@type": "Answer", "text": r}} for q, r in faq]}


def ld_fil(etapes):
    return {"@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [
        {"@type": "ListItem", "position": i + 1, "name": n, "item": site["domaine"] + u} for i, (n, u) in enumerate(etapes)]}


# ---------------------------------------------------------------- écriture
pages = {}  # url → html


def ecrire(url, gabarit, page):
    pages[url] = env.get_template(gabarit).render(page=page)


# public/ est synchronisé par Dropbox : on écrase les fichiers au lieu de supprimer le dossier (verrouillé pendant la synchro)
shutil.copytree(ICI / "assets", SORTIE / "assets", dirs_exist_ok=True)

accueil = rendre_texte(json.loads((ICI / "contenu" / "accueil.json").read_text(encoding="utf8")))
env.globals["derniers"] = articles[:3]
# ce que les outils de l'accueil lisent dans la page (éligibilité, budget, formules, quiz)
env.globals["donnees_js"] = {"faits": f, "regions": regions, "budget": budget, "tarifs": tarifs, "quiz": accueil["quiz"]["questions"]}
ecrire("/", "accueil.html", {
    "type": "accueil", "url": "/", "section": "/", "image": None, "contenu": accueil,
    "titre_seo": accueil["titre_seo"], "description": accueil["description"], "motcle": accueil["motcle"],
    "jsonld": [{"@context": "https://schema.org", **ORGA},
               {"@context": "https://schema.org", "@type": "WebSite", "name": site["nom"], "url": site["domaine"] + "/",
                "inLanguage": "fr-FR", "publisher": {"@id": ORGA["@id"]}},
               ld_faq(accueil["faq"])],
})

# pages de vente : contenu/pages/<page>.json, gabarit vente.html (blocs de l'accueil)
ventes = []
for chemin in sorted((ICI / "contenu" / "pages").glob("*.json")):
    v = rendre_texte(json.loads(chemin.read_text(encoding="utf8")))
    ventes.append(v)
    ecrire(v["url"], "vente.html", {
        "type": "vente", "url": v["url"], "section": v["url"], "image": f"/assets/img/{v['image']}.jpg", "contenu": v,
        "titre_seo": v["titre_seo"], "description": v["description"], "motcle": v["motcle"],
        "jsonld": [{"@context": "https://schema.org", "@type": "WebPage", "name": v["titre_seo"], "description": v["description"],
                    "url": site["domaine"] + v["url"], "inLanguage": "fr-FR", "publisher": {"@id": ORGA["@id"]}},
                   ld_fil([("Accueil", "/"), (v["fil"], v["url"])])] + ([ld_faq(v["faq"])] if v.get("faq") else []),
    })

for a in articles:
    a["jsonld"] = [
        {"@context": "https://schema.org", "@type": "Article", "headline": a["titre"], "description": a["description"],
         "image": site["domaine"] + a["image"], "datePublished": a["date"], "dateModified": a["maj"], "inLanguage": "fr-FR",
         "author": {"@id": ORGA["@id"], "@type": "Organization", "name": site["nom"]}, "publisher": ORGA,
         "mainEntityOfPage": site["domaine"] + a["url"]},
        ld_fil([("Accueil", "/"), (a["rubrique"]["court"], a["rubrique"]["lien"]), (a["titre"], a["url"])]),
    ] + ([ld_faq(a["faq"])] if a["faq"] else [])
    ecrire(a["url"], "article.html", a)

for r in rubriques:
    if not r["articles"]:
        continue
    ecrire(r["lien"], "rubrique.html", {
        "type": "rubrique", "url": r["lien"], "section": r["lien"], "rubrique": r, "image": f"/assets/img/{r['img']}.jpg",
        "titre_seo": r["titre_seo"], "description": r["description"], "motcle": r.get("motcle", ""),
        "jsonld": [{"@context": "https://schema.org", "@type": "CollectionPage", "name": r["titre"], "description": r["description"],
                    "url": site["domaine"] + r["lien"]}, ld_fil([("Accueil", "/"), (r["court"], r["lien"])])],
    })

ecrites = set()
for url, h in pages.items():
    dest = SORTIE / url.strip("/") / "index.html"
    dest.parent.mkdir(parents=True, exist_ok=True)
    dest.write_text(h, encoding="utf8")
    ecrites.add(dest)
for vieux in SORTIE.rglob("index.html"):  # page retirée (article supprimé ou repassé en brouillon)
    if vieux not in ecrites and "assets" not in vieux.parts:
        vieux.unlink()

auj = date.today().isoformat()
maj_par_url = {a["url"]: a["maj"] for a in articles}
(SORTIE / "sitemap.xml").write_text(
    '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' +
    "".join(f"  <url><loc>{site['domaine']}{u}</loc><lastmod>{maj_par_url.get(u, auj)}</lastmod></url>\n" for u in pages) +
    "</urlset>\n", encoding="utf8")
(SORTIE / "robots.txt").write_text(f"User-agent: *\nAllow: /\n\nSitemap: {site['domaine']}/sitemap.xml\n", encoding="utf8")

# ---------------------------------------------------------------- aperçu en ligne
# Sync_ftp envoie le dossier sur expatriation-maurice.com/minisites/retraitemaurice/ : public/ y est cassé (chemins « /… »
# pris à la racine du domaine). apercu/ en est une copie aux chemins préfixés, fermée aux moteurs de recherche.
APERCU, PREFIXE = ICI / "apercu", "/minisites/retraitemaurice/site/apercu/"
racine = re.compile(r"""(?<=["'`])/(?![/>])""")  # chemin entre guillemets ; pas « // » (adresse externe), « /> » (fin de balise) ni « (/ » (expression régulière d'un script)
for src in SORTIE.rglob("*"):
    if not src.is_file():
        continue
    dest = APERCU / src.relative_to(SORTIE)
    dest.parent.mkdir(parents=True, exist_ok=True)
    if src.suffix in (".html", ".css", ".js"):
        txt = racine.sub(PREFIXE, src.read_text(encoding="utf8"))
        if src.suffix == ".html":
            txt = txt.replace("<head>", '<head>\n<meta name="robots" content="noindex">', 1)
        dest.write_text(txt, encoding="utf8")
    elif not dest.exists() or dest.stat().st_mtime < src.stat().st_mtime:  # images : copiées seulement si nouvelles
        shutil.copy2(src, dest)
(APERCU / "robots.txt").write_text("User-agent: *\nDisallow: /\n", encoding="utf8")


# ---------------------------------------------------------------- contrôle des pages
class Lecteur(html.parser.HTMLParser):
    def __init__(self):
        super().__init__()
        self.h1, self.titre, self.desc, self.ids, self.liens, self.sans_alt = [], "", "", set(), [], 0
        self._dans, self.p1, self._p = None, "", False

    def handle_starttag(self, tag, attrs):
        a = dict(attrs)
        if "id" in a:
            self.ids.add(a["id"])
        if tag == "h1":
            self._dans = "h1"
            self.h1.append("")
        elif tag == "title":
            self._dans = "title"
        elif tag == "meta" and a.get("name") == "description":
            self.desc = a.get("content", "")
        elif tag == "img" and "alt" not in a:
            self.sans_alt += 1
        elif tag == "a" and a.get("href"):
            self.liens.append(a["href"])
        elif tag == "p" and not self.p1 and self.h1:
            self._p = True

    def handle_endtag(self, tag):
        if tag in ("h1", "title"):
            self._dans = None
        if tag == "p" and self._p:
            self._p = False

    def handle_data(self, d):
        if self._dans == "h1":
            self.h1[-1] += d
        elif self._dans == "title":
            self.titre += d
        if self._p:
            self.p1 += d


lus = {}
for url, h in pages.items():
    L = Lecteur()
    L.feed(h)
    lus[url] = L

fichiers = {"/" + p.relative_to(SORTIE).as_posix() for p in SORTIE.rglob("*") if p.is_file()}
manquantes, rapport = {}, []
for url, L in lus.items():
    page = next((a for a in articles + ventes if a["url"] == url), None)
    motcle = (page or {}).get("motcle") or (accueil["motcle"] if url == "/" else "")
    soucis = []
    if not 30 <= len(L.titre) <= 65:
        soucis.append(f"title de {len(L.titre)} caractères (30 à 65)")
    if not 110 <= len(L.desc) <= 160:
        soucis.append(f"description de {len(L.desc)} caractères (110 à 160)")
    if len(L.h1) != 1:
        soucis.append(f"{len(L.h1)} titres h1 (il en faut 1)")
    if L.sans_alt:
        soucis.append(f"{L.sans_alt} image(s) sans texte alternatif")
    if motcle:
        m = motcle.lower()
        for ou, txt in (("title", L.titre), ("description", L.desc), ("h1", " ".join(L.h1))):
            if m not in txt.lower():
                soucis.append(f"expression clé « {motcle} » absente du {ou}")
        if page and m not in L.p1.lower():
            soucis.append(f"expression clé « {motcle} » absente du premier paragraphe")
    for lien in L.liens:
        if not lien.startswith(("/", "#")):
            continue
        cible, _, ancre = lien.partition("#")
        cible = cible or url
        if cible in pages:
            if ancre and ancre not in lus[cible].ids:
                soucis.append(f"ancre introuvable : {lien}")
        elif cible not in fichiers:
            manquantes.setdefault(cible, set()).add(url)
    rapport.append((url, soucis))

print(f"\n{len(pages)} pages générées dans {SORTIE.relative_to(ICI.parent)}  ·  {len(articles)} article(s)\n")
for url, soucis in rapport:
    print(("  OK   " if not soucis else "  À VOIR ") + url)
    for s in soucis:
        print("         - " + s)
if manquantes:
    print(f"\nPages pas encore écrites, mais déjà liées ({len(manquantes)}) :")
    for cible, depuis in sorted(manquantes.items()):
        print(f"  {cible:<40} liée depuis {len(depuis)} page(s)")

if "--serve" in sys.argv:
    import functools, http.server
    print("\nAperçu : http://localhost:8040  (Ctrl+C pour arrêter)")
    http.server.ThreadingHTTPServer(("", 8040), functools.partial(http.server.SimpleHTTPRequestHandler, directory=SORTIE)).serve_forever()
