#!/usr/bin/env python3
"""Génère la version en ligne de la vitrine CARD.PM : une page HTML statique par langue,
données structurées, robots.txt, sitemap, llms.txt et configuration de sécurité.

Usage : python3 build.py <dossier_de_sortie>
"""
import html, json, os, re, shutil, sys

SRC = os.path.dirname(os.path.abspath(__file__))
OUT = sys.argv[1] if len(sys.argv) > 1 else os.path.join(SRC, "dist")
BASE = "https://card.pm"
ASSETS = "/_site"
LANGS = ["fr", "en", "es", "it", "de", "pt"]
PATH = {"fr": "/", "en": "/en/", "es": "/es/", "it": "/it/", "de": "/de/", "pt": "/pt/"}
LOCALE = {"fr": "fr_FR", "en": "en_GB", "es": "es_ES", "it": "it_IT", "de": "de_DE", "pt": "pt_PT"}

page = open(os.path.join(SRC, "index.html"), encoding="utf-8").read()
raw = open(os.path.join(SRC, "i18n.js"), encoding="utf-8").read()
raw = raw[raw.index("{"):raw.rindex("}") + 1]
raw = re.sub(r"\n(fr|en|es|it|de|pt):\{", r'\n"\1":{', raw)
I18N = json.loads(raw)

style_end = page.index("</style>") + len("</style>")
head_src, body_src = page[:style_end], page[style_end:]
style = head_src[head_src.index("<style>"):]

# le français est lu dans la page source
FR = dict(I18N["fr"])
for m in re.finditer(r'<(\w+)[^>]*\sdata-i18n="([^"]+)"[^>]*>(.*?)</\1>', body_src, re.S):
    FR.setdefault(m.group(2), m.group(3))
for m in re.finditer(r'alt="([^"]*)"\s+data-i18n-alt="([^"]+)"', body_src):
    FR.setdefault(m.group(2), html.unescape(m.group(1)))
for m in re.finditer(r'aria-label="([^"]*)"\s+data-i18n-aria="([^"]+)"', body_src):
    FR.setdefault(m.group(2), html.unescape(m.group(1)))
I18N["fr"] = FR


def t(lang, key):
    return I18N[lang].get(key, FR.get(key, ""))


def text(s):
    return html.unescape(re.sub(r"<[^>]+>", "", s.replace("<br>", " "))).strip()


def translate(body, lang):
    if lang != "fr":
        body = re.sub(r'(<(\w+)[^>]*\sdata-i18n="([^"]+)"[^>]*>)(.*?)(</\2>)',
                      lambda m: m.group(1) + t(lang, m.group(3)) + m.group(5), body, flags=re.S)
        body = re.sub(r'alt="[^"]*"(\s+data-i18n-alt="([^"]+)")',
                      lambda m: 'alt="%s"%s' % (html.escape(t(lang, m.group(2))), m.group(1)), body)
        body = re.sub(r'aria-label="[^"]*"(\s+data-i18n-aria="([^"]+)")',
                      lambda m: 'aria-label="%s"%s' % (html.escape(t(lang, m.group(2))), m.group(1)), body)
    body = body.replace('<p class="mat-desc" id="mat-desc" aria-live="polite"></p>',
                        '<p class="mat-desc" id="mat-desc" aria-live="polite">%s</p>' % t(lang, "mat.pvc.d"))
    body = body.replace("img/wallet/google-fr.svg", "img/wallet/google-%s.svg" % lang)
    body = body.replace('<option value="%s">' % lang, '<option value="%s" selected>' % lang)
    body = body.replace('data-lang="%s"' % lang, 'data-lang="%s" aria-current="true"' % lang)
    for a, b in [('src="img/', 'src="%s/img/' % ASSETS), ('src="js/', 'src="%s/js/' % ASSETS),
                 ('src="i18n.js"', 'src="%s/i18n.js"' % ASSETS), ('src="app.js"', 'src="%s/app.js"' % ASSETS)]:
        body = body.replace(a, b)
    # mesure d'audience existante de card.pm
    body = body.replace('<script src="%s/app.js" defer></script>' % ASSETS,
                        '<script src="%s/app.js" defer></script>\n<script defer src="https://track.pm/tracker.js" '
                        'data-ackee-server="https://track.pm" data-ackee-domain-id="514f6f09-2165-4e01-89d5-22865fd08647" '
                        'data-ackee-opts=\'{"detailed":true}\'></script>' % ASSETS)
    return body


def jsonld(lang):
    url = BASE + PATH[lang]
    org = {
        "@type": "Organization", "@id": BASE + "/#org", "name": "CARD.PM", "legalName": "DATAMIX.IO SAS",
        "url": BASE + "/", "logo": BASE + ASSETS + "/img/og-card-pm.jpg", "email": "contact@card.pm",
        "address": {"@type": "PostalAddress", "streetAddress": "6 avenue Neil Armstrong", "postalCode": "33700",
                    "addressLocality": "Mérignac", "addressCountry": "FR"},
        "identifier": {"@type": "PropertyValue", "propertyID": "SIRET", "value": "80929075200025"},
        "contactPoint": [{"@type": "ContactPoint", "contactType": "customer support", "email": "support@card.pm",
                          "url": BASE + "/support", "availableLanguage": ["fr", "en"]}],
        "sameAs": ["https://www.linkedin.com/company/cardpm/", "https://www.facebook.com/card.pm",
                   "https://vimeo.com/cardpm", "https://consent.pm"],
    }
    site = {"@type": "WebSite", "@id": BASE + "/#site", "url": BASE + "/", "name": "CARD.PM",
            "publisher": {"@id": BASE + "/#org"}, "inLanguage": LANGS}
    webpage = {"@type": "WebPage", "@id": url + "#page", "url": url, "name": t(lang, "meta.title"),
               "description": t(lang, "meta.desc"), "inLanguage": lang, "isPartOf": {"@id": BASE + "/#site"},
               "about": {"@id": BASE + "/#org"}, "primaryImageOfPage": BASE + ASSETS + "/img/og-card-pm.jpg"}
    faq = {"@type": "FAQPage", "@id": url + "#faq", "inLanguage": lang, "mainEntity": [
        {"@type": "Question", "name": text(t(lang, "faq.q%d" % i)),
         "acceptedAnswer": {"@type": "Answer", "text": text(t(lang, "faq.a%d" % i))}} for i in range(1, 7)]}
    products = {"@type": "ItemList", "@id": url + "#products", "itemListElement": [
        {"@type": "ListItem", "position": n + 1, "item": {
            "@type": "Product", "name": name, "brand": {"@id": BASE + "/#org"},
            "description": text(t(lang, desc)), "url": url + anchor}}
        for n, (name, desc, anchor) in enumerate([
            ("CARD.PM — " + text(t(lang, "nav.nfc")), "nfc.lead", "#nfc"),
            ("FEED", "feed.lead", "#feed"), ("SPOT", "spot.lead", "#spot"),
            ("consent.pm", "cs.lead", "#consent")])]}
    data = {"@context": "https://schema.org", "@graph": [org, site, webpage, faq, products]}
    return '<script type="application/ld+json">%s</script>' % json.dumps(data, ensure_ascii=False).replace("</", "<\\/")


def spec(lang):
    others = [PATH[l] for l in LANGS if l != lang]
    rules = {"prerender": [{"urls": others, "eagerness": "moderate"}],
             "prefetch": [{"urls": ["/start", "/demo"], "eagerness": "moderate"}]}
    return '<script type="speculationrules">%s</script>' % json.dumps(rules)


def head(lang):
    url = BASE + PATH[lang]
    title, desc = html.escape(t(lang, "meta.title")), html.escape(t(lang, "meta.desc"))
    img = BASE + ASSETS + "/img/og-card-pm.jpg"
    alts = "\n".join('<link rel="alternate" hreflang="%s" href="%s%s">' % (l, BASE, PATH[l]) for l in LANGS)
    alts += '\n<link rel="alternate" hreflang="x-default" href="%s/">' % BASE
    og_alt = "\n".join('<meta property="og:locale:alternate" content="%s">' % LOCALE[l] for l in LANGS if l != lang)
    fonts = re.search(r'<link rel="stylesheet" href="https://fonts.googleapis.com[^"]+">', head_src).group(0)
    return f'''<!doctype html>
<html lang="{lang}" data-static="1">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>{title}</title>
<meta name="description" content="{desc}">
<meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1">
<link rel="canonical" href="{url}">
{alts}
<meta name="theme-color" content="#151713">
<meta property="og:type" content="website">
<meta property="og:site_name" content="CARD.PM">
<meta property="og:url" content="{url}">
<meta property="og:title" content="{title}">
<meta property="og:description" content="{desc}">
<meta property="og:image" content="{img}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="CARD.PM">
<meta property="og:locale" content="{LOCALE[lang]}">
{og_alt}
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{title}">
<meta name="twitter:description" content="{desc}">
<meta name="twitter:image" content="{img}">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
{fonts}
<link rel="preload" as="image" href="{ASSETS}/img/demo-long.jpg" fetchpriority="high">
{jsonld(lang)}
{spec(lang)}
{style}
</head>
<body>'''


def write(path, content):
    full = os.path.join(OUT, path.lstrip("/"))
    os.makedirs(os.path.dirname(full), exist_ok=True)
    with open(full, "w", encoding="utf-8") as f:
        f.write(content)


# pages
for lang in LANGS:
    write(PATH[lang] + "index.html", head(lang) + translate(body_src, lang) + "\n</body>\n</html>\n")

# ressources
dst = os.path.join(OUT, ASSETS.strip("/"))
if os.path.isdir(dst):
    shutil.rmtree(dst)
shutil.copytree(os.path.join(SRC, "img"), os.path.join(dst, "img"))
shutil.copytree(os.path.join(SRC, "js"), os.path.join(dst, "js"))
shutil.copy(os.path.join(SRC, "i18n.js"), dst)
app = open(os.path.join(SRC, "app.js"), encoding="utf-8").read().replace("'img/logos/", "'%s/img/logos/" % ASSETS).replace('="img/logos/', '="%s/img/logos/' % ASSETS)
write(ASSETS + "/app.js", app)

# sitemap avec alternatives de langue
urls = []
for lang in LANGS:
    alts = "".join('<xhtml:link rel="alternate" hreflang="%s" href="%s%s"/>' % (l, BASE, PATH[l]) for l in LANGS)
    alts += '<xhtml:link rel="alternate" hreflang="x-default" href="%s/"/>' % BASE
    urls.append('<url><loc>%s%s</loc>%s<changefreq>weekly</changefreq><priority>%s</priority></url>'
                % (BASE, PATH[lang], alts, "1.0" if lang == "fr" else "0.8"))
write("sitemap-vitrine.xml", '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" '
      'xmlns:xhtml="http://www.w3.org/1999/xhtml">\n' + "\n".join(urls) + "\n</urlset>\n")

# robots.txt : moteurs et assistants IA explicitement autorisés sur la vitrine
bots = ["Googlebot", "Bingbot", "DuckDuckBot", "Applebot", "Applebot-Extended", "Google-Extended", "GPTBot",
        "OAI-SearchBot", "ChatGPT-User", "ClaudeBot", "Claude-SearchBot", "Claude-User", "PerplexityBot",
        "Perplexity-User", "Meta-ExternalAgent", "MistralAI-User", "YandexBot", "Qwantify"]
robots = "# Vitrine CARD.PM — moteurs de recherche et assistants IA bienvenus\n\n"
robots += "".join("User-agent: %s\nAllow: /\n\n" % b for b in bots)
robots += "User-agent: *\nAllow: /\n\nSitemap: %s/sitemap-vitrine.xml\n" % BASE
write("robots.txt", robots)

# llms.txt : résumé lisible par les assistants IA
faq_fr = "\n".join("- **%s** %s" % (text(t("fr", "faq.q%d" % i)), text(t("fr", "faq.a%d" % i))) for i in range(1, 7))
faq_en = "\n".join("- **%s** %s" % (text(t("en", "faq.q%d" % i)), text(t("en", "faq.a%d" % i))) for i in range(1, 7))
write("llms.txt", f"""# CARD.PM

> {text(t("fr", "meta.desc"))}
> {text(t("en", "meta.desc"))}

CARD.PM est un éditeur français (DATAMIX.IO SAS, Mérignac) de cartes de visite digitales, actif depuis 2016 auprès des réseaux (immobilier, banque, assurance) et des indépendants. Note Google : 4,9/5.

## Produits
- [Carte de visite digitale]({BASE}/#demo) : {text(t("fr", "demo.lead"))}
- [Cartes NFC]({BASE}/#nfc) : {text(t("fr", "nfc.lead"))} PVC, bois ou métal, format 85,6 × 54 mm.
- [FEED]({BASE}/#feed) : {text(t("fr", "feed.lead"))} Plaques plexiglas 9×9, 12×12, 14×14 cm (adhésif 3M) ou chevalet 14,8 × 10,5 cm.
- [SPOT]({BASE}/#spot) : {text(t("fr", "spot.lead"))} {text(t("fr", "spot.l5"))}
- [consent.pm](https://consent.pm) : {text(t("fr", "cs.lead"))}

## FAQ (FR)
{faq_fr}

## FAQ (EN)
{faq_en}

## Liens
- Démo : {BASE}/demo
- Commencer : {BASE}/start
- Support : {BASE}/support · support@card.pm
- Contact : {BASE}/contact · contact@card.pm
- Versions : {", ".join(BASE + PATH[l] for l in LANGS)}
""")

# security.txt
write(".well-known/security.txt", f"""Contact: mailto:support@card.pm
Expires: 2027-10-04T00:00:00.000Z
Preferred-Languages: fr, en
Canonical: {BASE}/.well-known/security.txt
""")

CSP = ("default-src 'self'; script-src 'self' 'inline-speculation-rules' https://track.pm; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
       "font-src https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://track.pm; "
       "frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'; upgrade-insecure-requests")
HEADERS = [("Content-Security-Policy", CSP),
           ("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload"),
           ("X-Content-Type-Options", "nosniff"), ("X-Frame-Options", "DENY"),
           ("Referrer-Policy", "strict-origin-when-cross-origin"),
           ("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()"),
           ("Cross-Origin-Opener-Policy", "same-origin")]
pages = ["/"] + [PATH[l] + "*" for l in LANGS if l != "fr"]

# Cloudflare Pages / Netlify
h = "# En-têtes de sécurité, limités aux pages de la vitrine\n"
for pth in pages:
    h += pth + "\n" + "".join("  %s: %s\n" % kv for kv in HEADERS) + "  Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400\n"
h += "/_site/img/*\n  Cache-Control: public, max-age=2592000\n  X-Content-Type-Options: nosniff\n"
h += "/_site/*\n  Cache-Control: public, max-age=86400\n  X-Content-Type-Options: nosniff\n"
write("_headers", h)

# nginx
ng = "# Vitrine CARD.PM — extrait nginx (à inclure dans le bloc server de card.pm)\n"
ng += "# Dans le bloc http : limitation de débit par IP\n"
ng += "#   limit_req_zone  $binary_remote_addr zone=vitrine:10m rate=20r/s;\n#   limit_conn_zone $binary_remote_addr zone=perip:10m;\n\n"
ng += "server_tokens off;\nclient_max_body_size 1m;\nclient_body_timeout 10s;\nclient_header_timeout 10s;\nkeepalive_timeout 15s;\nsend_timeout 10s;\n\n"
ng += "location ~ /\\.(?!well-known) { deny all; }\n\n"
ng += "location ~ ^/(|en/|es/|it/|de/|pt/)$ {\n  limit_req zone=vitrine burst=40 nodelay;\n  limit_conn perip 30;\n"
ng += "".join('  add_header %s "%s" always;\n' % kv for kv in HEADERS)
ng += '  add_header Cache-Control "public, max-age=0, s-maxage=3600" always;\n  try_files $uri $uri/index.html =404;\n}\n\n'
ng += "location /_site/ {\n  limit_req zone=vitrine burst=80 nodelay;\n  add_header X-Content-Type-Options nosniff always;\n"
ng += '  add_header Cache-Control "public, max-age=86400" always;\n  try_files $uri =404;\n}\n'
write("nginx-vitrine.conf", ng)

# Apache
ap = "# Vitrine CARD.PM — .htaccess (Apache, mod_headers requis)\n<IfModule mod_headers.c>\n"
ap += "".join('  Header always set %s "%s"\n' % kv for kv in HEADERS)
ap += "</IfModule>\n<FilesMatch \"^\\.(?!well-known)\">\n  Require all denied\n</FilesMatch>\nOptions -Indexes\nServerSignature Off\n"
write(".htaccess", ap)

print("Site généré dans", OUT)
