# -*- coding: utf-8 -*-
"""Inject a technical-SEO <head> block into every language page.

Idempotent: the block lives between <!-- SEO:START --> and <!-- SEO:END -->,
so re-running replaces it in place. Absolute URLs point at the FINAL domain
(msosorg.com), not the temporary host, so nothing needs rewriting at launch.

Usage:
    python seo_inject.py            # pilot only (prints injected block)
    python seo_inject.py --all      # write every page
    python seo_inject.py --all --sitemap   # also (re)build sitemap + robots
"""
import os, re, sys, html, json
try:
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass

# Repo root = parent of this tools/ directory (portable; no hardcoded path).
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DOMAIN = "https://msosorg.com"
SITE_NAME = {"en": "Macedonian Student Organisation in Slovenia",
             "mk": "Македонска студентска организација во Словенија",
             "si": "Makedonska študentska organizacija v Sloveniji"}
LANGS = ["en", "mk", "si"]
# folder -> ISO-639-1 hreflang code (Slovene is "sl", not "si")
HREF = {"en": "en", "mk": "mk", "si": "sl"}
OG_LOCALE = {"en": "en_GB", "mk": "mk_MK", "si": "sl_SI"}

# Section (first path segment) display names, for Article breadcrumbs.
# Any 2-segment path "section/slug" whose section is here is treated as an article.
SECTION_NAMES = {
    "blog": {"en": "Blog", "mk": "Блог", "si": "Blog"},
    "news": {"en": "News", "mk": "Новости", "si": "Novice"},
    "projects": {"en": "Events & projects", "mk": "Настани и проекти", "si": "Dogodki in projekti"},
    "get-to-know-slovenia": {"en": "Get to know Slovenia", "mk": "Запознај ја Словенија", "si": "Spoznaj Slovenijo"},
    "get-to-know-macedonia": {"en": "Get to know Macedonia", "mk": "Запознај ја Македонија", "si": "Spoznaj Makedonijo"},
    "student-welcome-guide": {"en": "Student Welcome Guide", "mk": "Водич за добредојде", "si": "Vodič za bruce"},
}
HOME_CRUMB = {"en": "Home", "mk": "Дома", "si": "Domov"}

# Article dates (ISO), curated once from the English pages (dates are
# language-independent). Emitted as datePublished/dateModified in Article schema.
ARTICLE_DATES = {
    "blog/how-to-open-slovenian-bank-account": {"modified": "2026-08-01"},
    "blog/where-to-search-for-accommodation-in-slovenia": {"published": "2026-08-03"},
    "blog/learning-slovene-where-to-start": {"published": "2026-07-28"},
    "blog/study-in-north-macedonia-government-scholarship-2026-2027": {"published": "2026-07-30"},
    "blog/ad-futura-scholarship-slovenia-2026": {"published": "2026-08-02"},
    "blog/welcome-days-slovenian-universities-2026-2027": {"published": "2026-08-03"},
    "blog/how-i-coped-with-leaving-home": {"published": "2024-04-24"},
    "news/proof-of-means-updated-2026": {"published": "2026-08-01"},
    "projects/humanitarian-tournament-in-maribor": {"published": "2025-04-05"},
    "projects/in-memory-of-frosina-kulakova": {"published": "2025-02-08"},
    "projects/in-memory-of-the-kochani-victims": {"published": "2025-03-21"},
    "projects/macedonian-student-night-in-ljubljana": {"published": "2022-10-08"},
    "projects/meet-student-slovenia-2023": {"published": "2023-09-18"},
    "projects/meet-student-slovenia-2024": {"published": "2024-09-16"},
    "projects/morning-coffee-in-front-of-ctk": {"published": "2024-06-08"},
    "projects/paint-and-wine-at-sunset": {"published": "2025-03-08"},
    "projects/watching-handball-together-in-slovenia": {"published": "2025-01-25"},
}

START, END = "<!-- SEO:START -->", "<!-- SEO:END -->"
BLOCK_RE = re.compile(re.escape(START) + r".*?" + re.escape(END) + r"\s*", re.DOTALL)
TITLE_RE = re.compile(r"(<title>.*?</title>)", re.DOTALL)

DEFAULT_OG_IMAGE = DOMAIN + "/images/og-default.jpg"

# Favicons are ACTUAL resources the browser loads for the current host, so they
# stay root-relative (resolve on the temp host, local server, and final domain).
FAVICON_LINES = [
    '    <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">',
    '    <link rel="icon" type="image/svg+xml" href="/favicon.svg">',
    '    <link rel="shortcut icon" href="/favicon.ico">',
    '    <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">',
    '    <link rel="manifest" href="/site.webmanifest">',
]

# Google Analytics 4 (gtag.js) with Consent Mode v2 defaulting to DENIED
# (EEA-compliant: tag loads but sets no tracking cookies until consent is
# granted). A consent banner should call gtag('consent','update',{...}).
GA_ID = "G-WVYV807VSS"
GA_LINES = [
    '    <!-- Google tag (gtag.js) with Consent Mode v2 (default: denied) -->',
    f'    <script async src="https://www.googletagmanager.com/gtag/js?id={GA_ID}"></script>',
    '    <script>',
    '      window.dataLayer = window.dataLayer || [];',
    '      function gtag(){dataLayer.push(arguments);}',
    "      gtag('consent', 'default', {",
    "        ad_storage: 'denied', ad_user_data: 'denied', ad_personalization: 'denied',",
    "        analytics_storage: 'denied', wait_for_update: 500",
    "      });",
    "      // Restore a returning visitor's prior choice before the first hit.",
    "      try { if (localStorage.getItem('msos-consent') === 'granted') {",
    "        gtag('consent', 'update', { ad_storage:'granted', ad_user_data:'granted',",
    "          ad_personalization:'granted', analytics_storage:'granted' }); } } catch(e){}",
    "      gtag('js', new Date());",
    f"      gtag('config', '{GA_ID}');",
    '    </script>',
    '    <script src="/consent.js" defer></script>',
]

# ---- page trees (for hreflang parity) ----
def build_trees():
    trees = {}
    for L in LANGS:
        s = set()
        base = os.path.join(ROOT, L)
        for root, _, files in os.walk(base):
            if "index.html" in files:
                rel = os.path.relpath(root, base).replace("\\", "/")
                s.add("" if rel == "." else rel)
        trees[L] = s
    return trees

TREES = build_trees()

def clean_text(s):
    s = re.sub(r"<[^>]+>", "", s)          # strip tags
    s = html.unescape(s)
    s = re.sub(r"\s+", " ", s).strip()
    return s

def truncate(s, keep=175, n=160):
    # Keep concise leads whole (subtitles are naturally ~150-170 chars);
    # only truncate genuinely long text, at a word boundary.
    if len(s) <= keep:
        return s
    cut = s[:n].rsplit(" ", 1)[0].rstrip(" ,.;:—-")
    return cut + "…"

DESC_PATTERNS = [
    r'<p class="article-subtitle">(.*?)</p>',
    r'<p class="guide-lead">(.*?)</p>',
    r'<p class="page-description">(.*?)</p>',
    r'<p class="blog-lead">(.*?)</p>',
    r'<div class="news-article-body">\s*<p>(.*?)</p>',
]

# Curated descriptions for pages with no extractable lead paragraph.
# Keyed by page path (""=home) then folder lang code (en/mk/si).
# MK/SI copy is AI-drafted — flag for native-speaker review.
DESC_OVERRIDES = {
    "": {
        "en": "The first official Macedonian student organisation in Slovenia — support, events, community and a voice for Macedonian students from application to graduation.",
        "mk": "Првата официјална македонска студентска организација во Словенија — поддршка, настани, заедница и глас за македонските студенти од пријава до дипломирање.",
        "si": "Prva uradna makedonska študentska organizacija v Sloveniji — podpora, dogodki, skupnost in glas makedonskih študentov od prijave do diplome.",
    },
    "become-a-member": {
        "en": "Join MSOS and find your people in Slovenia. Meet students, attend events, get support and help create projects across the Macedonian student community.",
        "mk": "Придружете се на МСОС и најдете ги вашите луѓе во Словенија. Запознајте студенти, посетувајте настани, добијте поддршка и создавајте проекти во македонската студентска заедница.",
        "si": "Pridružite se MSOS in najdite svoje ljudi v Sloveniji. Spoznajte študente, obiskujte dogodke, prejmite podporo in soustvarjajte projekte makedonske študentske skupnosti.",
    },
    "faq": {
        "en": "Short answers to the questions Macedonian students ask most about studying and living in Slovenia — applications, residence, work, housing and more.",
        "mk": "Кратки одговори на прашањата што најчесто ги поставуваат македонските студенти за студирање и живеење во Словенија — пријави, престој, работа, сместување и повеќе.",
        "si": "Kratki odgovori na vprašanja, ki jih makedonski študenti najpogosteje postavljajo o študiju in življenju v Sloveniji — prijave, prebivanje, delo, nastanitev in več.",
    },
    "gallery": {
        "en": "A look back at the events, gatherings and moments that make the MSOS community — browse photo albums from Macedonian student life in Slovenia.",
        "mk": "Поглед наназад на настаните, дружењата и моментите што ја градат заедницата на МСОС — прелистајте фото-албуми од македонскиот студентски живот во Словенија.",
        "si": "Pogled nazaj na dogodke, druženja in trenutke, ki gradijo skupnost MSOS — prebrskajte foto-albume iz makedonskega študentskega življenja v Sloveniji.",
    },
    "news": {
        "en": "Everything new at MSOS in one place — events and projects, blog stories, announcements and official updates for Macedonian students in Slovenia.",
        "mk": "Сè ново кај МСОС на едно место — настани и проекти, блог приказни, соопштенија и официјални новости за македонските студенти во Словенија.",
        "si": "Vse novo pri MSOS na enem mestu — dogodki in projekti, blog zgodbe, obvestila in uradne novice za makedonske študente v Sloveniji.",
    },
    "student-welcome-guide/my-route": {
        "en": "Your personal study route to Slovenia — follow the Student Welcome Guide steps for Macedonian students, from choosing a programme to enrolment and residence.",
        "mk": "Вашиот личен студиски пат до Словенија — следете ги чекорите од Водичот за добредојде за македонските студенти, од избор на програма до упис и престој.",
        "si": "Vaša osebna študijska pot v Slovenijo — sledite korakom Vodnika za bruce za makedonske študente, od izbire programa do vpisa in prebivanja.",
    },
}

def extract_description(content, lang=None, path=None):
    if path is not None and path in DESC_OVERRIDES:
        ov = DESC_OVERRIDES[path].get(lang)
        if ov:
            return ov
    for pat in DESC_PATTERNS:
        m = re.search(pat, content, re.DOTALL)
        if m:
            txt = clean_text(m.group(1))
            if len(txt) >= 40:
                return truncate(txt)
    return None

HERO_PATTERNS = [
    r'<figure class="article-main-image">.*?<img[^>]*?src="(.*?)"',
    r'<figure class="news-article-hero">\s*<img[^>]*?src="(.*?)"',
]

def extract_og_image(content):
    for pat in HERO_PATTERNS:
        m = re.search(pat, content, re.DOTALL)
        if m:
            src = m.group(1)
            # normalise ../../../images/x -> /images/x
            idx = src.find("images/")
            if idx != -1:
                return DOMAIN + "/" + src[idx:]
    return DEFAULT_OG_IMAGE

def title_text(content):
    m = TITLE_RE.search(content)
    if not m:
        return SITE_NAME["en"]
    return clean_text(m.group(1))

H1_RE = re.compile(r"<h1[^>]*>(.*?)</h1>", re.DOTALL)
def extract_h1(content):
    m = H1_RE.search(content)
    return clean_text(m.group(1)) if m else None

def canonical_for(lang, path):
    return f"{DOMAIN}/{lang}/" + (f"{path}/" if path else "")

def article_schema(lang, path, content, canon, desc, ogimg):
    """Article + BreadcrumbList JSON-LD for 2-segment 'section/slug' pages."""
    parts = path.split("/")
    if len(parts) != 2 or parts[0] not in SECTION_NAMES:
        return []
    section, _ = parts
    headline = extract_h1(content) or title_text(content).split(" - ")[0]
    org = {"@type": "Organization", "name": SITE_NAME[lang], "url": f"{DOMAIN}/{lang}/"}
    article = {
        "@context": "https://schema.org", "@type": "Article",
        "headline": headline,
        "description": desc or "",
        "image": ogimg,
        "inLanguage": HREF[lang],
        "author": org,
        "publisher": {"@type": "Organization", "name": SITE_NAME[lang],
                      "logo": {"@type": "ImageObject", "url": f"{DOMAIN}/images/1-logo.png"}},
        "mainEntityOfPage": canon,
    }
    if not article["description"]:
        del article["description"]
    dates = ARTICLE_DATES.get(path)
    if dates:
        pub = dates.get("published") or dates.get("modified")
        mod = dates.get("modified") or dates.get("published")
        article["datePublished"] = pub
        article["dateModified"] = mod
    crumbs = {
        "@context": "https://schema.org", "@type": "BreadcrumbList",
        "itemListElement": [
            {"@type": "ListItem", "position": 1, "name": HOME_CRUMB[lang], "item": f"{DOMAIN}/{lang}/"},
            {"@type": "ListItem", "position": 2, "name": SECTION_NAMES[section][lang], "item": f"{DOMAIN}/{lang}/{section}/"},
            {"@type": "ListItem", "position": 3, "name": headline, "item": canon},
        ],
    }
    dump = lambda o: '    <script type="application/ld+json">' + json.dumps(o, ensure_ascii=False, separators=(",", ":")) + "</script>"
    return [dump(article), dump(crumbs)]

def build_block(lang, path, content):
    canon = canonical_for(lang, path)
    title = title_text(content)
    desc = extract_description(content, lang, path)
    ogimg = extract_og_image(content)

    lines = [START]
    lines.extend(GA_LINES)
    if desc:
        lines.append(f'    <meta name="description" content="{html.escape(desc, quote=True)}">')
    lines.append(f'    <link rel="canonical" href="{canon}">')

    # hreflang alternates (only langs that actually have this path)
    have = [L for L in LANGS if path in TREES[L]]
    for L in have:
        lines.append(f'    <link rel="alternate" hreflang="{HREF[L]}" href="{canonical_for(L, path)}">')
    xdef = "en" if "en" in have else have[0]
    lines.append(f'    <link rel="alternate" hreflang="x-default" href="{canonical_for(xdef, path)}">')

    # Open Graph
    lines.append(f'    <meta property="og:type" content="website">')
    lines.append(f'    <meta property="og:site_name" content="{html.escape(SITE_NAME[lang], quote=True)}">')
    lines.append(f'    <meta property="og:title" content="{html.escape(title, quote=True)}">')
    if desc:
        lines.append(f'    <meta property="og:description" content="{html.escape(desc, quote=True)}">')
    lines.append(f'    <meta property="og:url" content="{canon}">')
    lines.append(f'    <meta property="og:image" content="{ogimg}">')
    lines.append(f'    <meta property="og:locale" content="{OG_LOCALE[lang]}">')
    for L in have:
        if L != lang:
            lines.append(f'    <meta property="og:locale:alternate" content="{OG_LOCALE[L]}">')

    # Twitter
    lines.append(f'    <meta name="twitter:card" content="summary_large_image">')
    lines.append(f'    <meta name="twitter:title" content="{html.escape(title, quote=True)}">')
    if desc:
        lines.append(f'    <meta name="twitter:description" content="{html.escape(desc, quote=True)}">')
    lines.append(f'    <meta name="twitter:image" content="{ogimg}">')

    # Organization JSON-LD (site-wide)
    org = (
        '    <script type="application/ld+json">'
        '{"@context":"https://schema.org","@type":"Organization",'
        f'"name":"{SITE_NAME[lang]}","alternateName":"MSOS",'
        f'"url":"{DOMAIN}/{lang}/",'
        f'"logo":"{DOMAIN}/images/1-logo.png",'
        '"email":"info@msosorg.com",'
        '"address":{"@type":"PostalAddress","streetAddress":"Masarykova cesta 24","postalCode":"1000","addressLocality":"Ljubljana","addressCountry":"SI"},'
        '"sameAs":["https://www.facebook.com/msos.org","https://www.instagram.com/msosorg_com","https://www.linkedin.com/company/msos-org","https://www.youtube.com/@msosorg"]}'
        '</script>'
    )
    lines.append(org)
    lines.extend(article_schema(lang, path, content, canon, desc, ogimg))
    lines.extend(FAVICON_LINES)
    lines.append("    " + END)
    return "\n".join(lines)

def inject(content, lang, path):
    block = build_block(lang, path, content)
    # remove any existing block first (idempotent)
    content = BLOCK_RE.sub("", content)
    nl = "\r\n" if "\r\n" in content else "\n"
    block = block.replace("\n", nl)
    def repl(m):
        return m.group(1) + nl + "    " + block
    return TITLE_RE.sub(repl, content, count=1)

def page_path_of(fp, lang):
    base = os.path.join(ROOT, lang)
    rel = os.path.relpath(os.path.dirname(fp), base).replace("\\", "/")
    return "" if rel == "." else rel

def all_pages():
    for L in LANGS:
        base = os.path.join(ROOT, L)
        for root, _, files in os.walk(base):
            if "index.html" in files:
                yield L, os.path.join(root, "index.html")

PILOT = [
    ("en", os.path.join(ROOT, "en", "index.html")),
    ("en", os.path.join(ROOT, "en", "blog", "how-to-open-slovenian-bank-account", "index.html")),
    ("mk", os.path.join(ROOT, "mk", "blog", "how-to-open-slovenian-bank-account", "index.html")),
    ("si", os.path.join(ROOT, "si", "blog", "how-to-open-slovenian-bank-account", "index.html")),
]

def write_sitemap():
    urls = []
    for L, fp in all_pages():
        path = page_path_of(fp, L)
        urls.append((L, path))
    # group by path so we can emit xhtml:link alternates
    from collections import defaultdict
    bypath = defaultdict(list)
    for L, path in urls:
        bypath[path].append(L)
    lines = ['<?xml version="1.0" encoding="UTF-8"?>',
             '<urlset xmlns="http://www.sitemap.org/schemas/sitemap/0.9"',
             '        xmlns:xhtml="http://www.w3.org/1999/xhtml">']
    lines[1] = lines[1].replace("www.sitemap.org", "www.sitemaps.org")
    for path in sorted(bypath):
        have = [L for L in LANGS if L in bypath[path]]
        for L in have:
            lines.append("  <url>")
            lines.append(f"    <loc>{canonical_for(L, path)}</loc>")
            for A in have:
                lines.append(f'    <xhtml:link rel="alternate" hreflang="{HREF[A]}" href="{canonical_for(A, path)}"/>')
            xdef = "en" if "en" in have else have[0]
            lines.append(f'    <xhtml:link rel="alternate" hreflang="x-default" href="{canonical_for(xdef, path)}"/>')
            lines.append("  </url>")
    lines.append("</urlset>")
    with open(os.path.join(ROOT, "sitemap.xml"), "w", encoding="utf-8", newline="\n") as f:
        f.write("\n".join(lines) + "\n")
    robots = f"User-agent: *\nAllow: /\n\nSitemap: {DOMAIN}/sitemap.xml\n"
    with open(os.path.join(ROOT, "robots.txt"), "w", encoding="utf-8", newline="\n") as f:
        f.write(robots)
    print(f"sitemap.xml: {sum(len(v) for v in bypath.values())} urls; robots.txt written")

def main():
    args = sys.argv[1:]
    if "--all" in args:
        n = 0
        for L, fp in all_pages():
            with open(fp, "r", encoding="utf-8", newline="") as f:
                content = f.read()
            path = page_path_of(fp, L)
            out = inject(content, L, path)
            with open(fp, "w", encoding="utf-8", newline="") as f:
                f.write(out)
            n += 1
        print(f"injected SEO block into {n} pages")
        if "--sitemap" in args:
            write_sitemap()
    else:
        # pilot: print blocks only
        for L, fp in PILOT:
            with open(fp, "r", encoding="utf-8", newline="") as f:
                content = f.read()
            path = page_path_of(fp, L)
            print(f"\n===== {L} /{path or '(home)'} =====")
            print(build_block(L, path, content))

if __name__ == "__main__":
    main()
