#!/usr/bin/env python3
"""
build_blog.py  --  turn a Markdown blog source into a site blog article page.

It clones an existing blog page of the SAME language (so the header, footer,
SEO markers, scripts and layout are already correct) and swaps in:
  - <title> and the mobile article title
  - the article header (category + date, H1, subtitle)
  - the hero <figure> (image + illustrative caption)
  - the scrollspy sidebar nav (built from the ## headings)
  - the <div class="article-body"> content (converted from Markdown)

Run seo_inject.py afterwards to refresh canonical/hreflang/meta for new slugs.

Usage (see tools/blog_manifest.py for the per-post config the runner passes in):
    from build_blog import build_page
    build_page(cfg)
"""
import io, os, re, html

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# ---- tiny Markdown -> article-body HTML converter --------------------------

_INLINE = [
    (re.compile(r'\[([^\]]+)\]\((https?://[^)\s]+)\)'),
     r'<a href="\2" target="_blank" rel="noopener">\1</a>'),
    (re.compile(r'\[([^\]]+)\]\((?!https?://)([^)\s]+)\)'), r'<a href="\2">\1</a>'),
    (re.compile(r'\*\*([^*]+)\*\*'), r'<strong>\1</strong>'),
    (re.compile(r'(?<!\*)\*(?!\*)([^*]+)\*(?!\*)'), r'<em>\1</em>'),
]

def _inline(text):
    text = html.escape(text, quote=False)
    # unescape the markdown link/format markers we still need to match
    text = text.replace("&amp;", "&")
    for rx, rep in _INLINE:
        text = rx.sub(rep, text)
    return text

def _slug(text):
    s = re.sub(r'[^a-z0-9]+', '-', text.lower()).strip('-')
    return s or "section"

def preprocess(md):
    """Strip metadata lines, the 'Hero image placeholder' section and rule lines
    so only real article content remains."""
    out, skip_hero = [], False
    for ln in md.replace("\r\n", "\n").split("\n"):
        s = ln.strip()
        if s.startswith("## ") and re.search(r'hero image', s, re.I):
            skip_hero = True
            continue
        if skip_hero:
            if s.startswith("## ") or s == "---":
                skip_hero = False
                if s.startswith("## "):
                    out.append(ln)
            continue
        if re.match(r'^\*\*[^*]+:\*\*\s', s) and len(s) < 90:  # metadata like "**Category:** X"
            continue
        if s == "---":
            continue
        out.append(ln)
    return "\n".join(out)


def parse_markdown(md):
    """Return (title, subtitle, sections) where sections=[(id,heading,html)] and
    the first section id may be '' for intro content before the first ##."""
    md = preprocess(md)
    lines = md.split("\n")
    title = ""
    subtitle = ""
    # strip a leading metadata block ("**Category:** ..." lines) if present
    body_lines, i = [], 0
    # title = first # heading
    for i, ln in enumerate(lines):
        if ln.startswith("# "):
            title = ln[2:].strip()
            body_lines = lines[i+1:]
            break
    else:
        body_lines = lines
    # Any further level-1 heading (e.g. "# Sources") becomes a real section.
    body_lines = [("## " + ln[2:]) if ln.startswith("# ") else ln for ln in body_lines]

    sections = []  # list of dict(id, heading, blocks[])
    cur = {"id": "", "heading": None, "blocks": []}
    def flush():
        if cur["heading"] is not None or cur["blocks"]:
            sections.append(dict(cur))

    para, list_items, list_type, quote = [], [], None, []
    def flush_blocks():
        nonlocal para, list_items, list_type, quote
        if para:
            txt = " ".join(para).strip()
            if txt:
                cur["blocks"].append(("p", txt))
            para = []
        if list_items:
            cur["blocks"].append((list_type, list_items))
            list_items, list_type = [], None
        if quote:
            cur["blocks"].append(("quote", " ".join(quote).strip()))
            quote = []

    for ln in body_lines:
        s = ln.rstrip().strip()
        if s.startswith("## "):
            flush_blocks(); flush()
            h = s[3:].strip()
            cur = {"id": _slug(h), "heading": h, "blocks": []}
            continue
        if s.startswith("### "):
            flush_blocks()
            cur["blocks"].append(("h3", s[4:].strip()))
            continue
        if s.startswith(">"):
            content = s[1:].strip()
            if content:
                quote.append(content)
            continue
        m = re.match(r'^(\d+)\.\s+(.*)', s)
        if m:
            if list_type not in (None, "ol"): flush_blocks()
            list_type = "ol"; list_items.append(m.group(2)); continue
        m = re.match(r'^[-*]\s+(.*)', s)
        if m:
            if list_type not in (None, "ul"): flush_blocks()
            list_type = "ul"; list_items.append(m.group(1)); continue
        if s == "":
            flush_blocks(); continue
        # normal paragraph line
        if list_items or quote:
            flush_blocks()
        para.append(s)
    flush_blocks(); flush()

    # subtitle = first paragraph of the intro section (often bold lede)
    for sec in sections:
        for kind, val in sec["blocks"]:
            if kind == "p":
                subtitle = re.sub(r'\*\*|\*', '', val).strip()
                break
        if subtitle:
            break
    return title, subtitle, sections


def render_body(sections, category, date_label, last_checked_label):
    out = []
    for idx, sec in enumerate(sections):
        if sec["heading"]:
            out.append(f'                    <h2 id="{sec["id"]}">{_inline(sec["heading"])}</h2>')
        for kind, val in sec["blocks"]:
            if kind == "p":
                out.append(f'                    <p>{_inline(val)}</p>')
            elif kind == "h3":
                out.append(f'                    <h3>{_inline(val)}</h3>')
            elif kind == "quote":
                out.append('                    <aside class="guide-box guide-box--warning">')
                out.append(f'                        <p>{_inline(val)}</p>')
                out.append('                    </aside>')
            elif kind in ("ul", "ol"):
                out.append(f'                    <{kind}>')
                for it in val:
                    out.append(f'                        <li>{_inline(it)}</li>')
                out.append(f'                    </{kind}>')
    if last_checked_label:
        out.append(f'                    <p class="guide-last-checked">{last_checked_label}</p>')
    return "\n".join(out)


def build_scrollspy(sections):
    links = []
    first = True
    for sec in sections:
        if not sec["heading"]:
            continue
        cls = ' class="active"' if first else ""
        links.append(f'                        <li><a href="#{sec["id"]}"{cls}>{_inline(sec["heading"])}</a></li>')
        first = False
    return "\n".join(links)


def build_page(cfg):
    """cfg keys: base (path), out (path), title, category, date_label,
    last_checked, hero_img, hero_alt, hero_caption, md (markdown text)."""
    title, subtitle, sections = parse_markdown(cfg["md"])
    title = cfg.get("title") or title
    subtitle = cfg.get("subtitle") or subtitle

    base = io.open(cfg["base"], encoding="utf-8").read()

    # <title>
    base = re.sub(r'<title>.*?</title>',
                  f'<title>{html.escape(title)} - MSOS</title>', base, count=1, flags=re.DOTALL)
    # mobile article title
    base = re.sub(r'(<span id="mobile-article-title">).*?(</span>)',
                  lambda m: m.group(1) + html.escape(title) + m.group(2), base, count=1, flags=re.DOTALL)
    # article header block
    header = (
        f'                    <p class="article-publish-date">{_inline(cfg["date_label"])}</p>\n'
        f'                    <h1>{_inline(title)}</h1>\n'
        f'                    <p class="article-subtitle">{_inline(subtitle)}</p>'
    )
    base = re.sub(r'<p class="article-publish-date">.*?</p>\s*<h1>.*?</h1>\s*<p class="article-subtitle">.*?</p>',
                  header, base, count=1, flags=re.DOTALL)
    # hero figure
    fig = (
        f'                <figure class="article-main-image">\n'
        f'                    <img width="1600" height="1067" fetchpriority="high" src="{cfg["hero_img"]}" alt="{html.escape(cfg["hero_alt"])}">\n'
        f'                    <figcaption><span class="fig-illustrative"><i class="fas fa-circle-info" aria-hidden="true"></i> {cfg["illus_label"]}</span> &middot; {cfg["hero_caption"]}</figcaption>\n'
        f'                </figure>'
    )
    base = re.sub(r'<figure class="article-main-image">.*?</figure>', fig, base, count=1, flags=re.DOTALL)
    # scrollspy nav
    nav = '                    <ul>\n' + build_scrollspy(sections) + '\n                    </ul>'
    base = re.sub(r'(<nav class="scrollspy-nav">\s*)<ul>.*?</ul>',
                  lambda m: m.group(1) + nav, base, count=1, flags=re.DOTALL)
    # article body (up to the article-footer)
    body = ('                <div class="article-body">\n'
            + render_body(sections, cfg["category"], cfg["date_label"], cfg["last_checked"])
            + '\n                </div>')
    base = re.sub(r'<div class="article-body">.*</div>\s*(<footer class="article-footer">)',
                  lambda m: body + '\n\n                ' + m.group(1), base, count=1, flags=re.DOTALL)

    os.makedirs(os.path.dirname(cfg["out"]), exist_ok=True)
    io.open(cfg["out"], "w", encoding="utf-8").write(base)
    return len([s for s in sections if s["heading"]])
