#!/usr/bin/env python3
"""scripts/render_pptx.py — render a structured, editable, S&P-themed PPTX from
the consolidated Marp markdown deck, using python-pptx.

REQ-269 (v1.23): a comparison artifact to the primary MARP-rendered PPTX. The
HTML deck remains the pixel-perfect artifact; this PPTX is the editable,
native-shape version (real text boxes, native tables, embedded PNGs) so a
reviewer can open it in PowerPoint and see a properly S&P-themed deck with
titles, bullets, blockquotes, images, tables, and benefit callouts.

Usage:
    python3 scripts/render_pptx.py [deck-name]

Defaults to `nova-autonomous-cloud-delivery`. Reads
`docs/presentations/{deck}-marp.md`, writes
`docs/presentations/{deck}-python.pptx`.
"""
import os
import re
import sys
from pathlib import Path

# --- Dependency check --------------------------------------------------------
try:
    from pptx import Presentation
    from pptx.util import Inches, Pt, Emu
    from pptx.dml.color import RGBColor
    from pptx.enum.shapes import MSO_SHAPE
    from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
    from pptx.oxml.ns import qn
except ImportError:
    print("ERROR: python-pptx not installed.", file=sys.stderr)
    print("       pip install -e .[slides]", file=sys.stderr)
    sys.exit(1)

# --- S&P theme constants -----------------------------------------------------
RED = RGBColor(0xD6, 0x00, 0x2A)        # S&P red
BLACK = RGBColor(0x1B, 0x1B, 0x1B)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GREY_TEXT = RGBColor(0x2E, 0x2E, 0x2E)
GREY_HEADER = RGBColor(0xF0, 0xF0, 0xF0)
BODY_TEXT = RGBColor(0x1B, 0x1B, 0x1B)

FONT_NAME = "Akkurat Pro"

SLIDE_W = Inches(13.333)
SLIDE_H = Inches(7.5)

# Content area geometry (matches Marp padding ~48/56 px at 96dpi → ~0.5"/0.58")
MARGIN_X = Inches(0.58)
MARGIN_TOP = Inches(0.4)
CONTENT_W = Inches(12.17)
TITLE_H = Inches(0.7)

# Image fit
IMG_MAX_W = Inches(8.0)
IMG_MAX_H = Inches(4.0)


# --- Markdown parsing --------------------------------------------------------
def split_slides(md_text: str):
    """Strip YAML frontmatter, then split the deck into slide source strings."""
    # Strip YAML frontmatter (between first pair of `---` lines).
    if md_text.lstrip().startswith("---"):
        end = md_text.find("\n---", 3)
        if end != -1:
            md_text = md_text[end + 4 :]
    # Normalize slide separators. Marp uses `\n---\n` on its own line.
    parts = re.split(r"\n---\s*\n", md_text)
    slides = []
    for p in parts:
        p = p.strip("\n")
        if p.strip():
            slides.append(p)
    return slides


# --- Cell/table helpers ------------------------------------------------------
def _set_cell_text(cell, text: str, *, bold: bool = False, size: int = 14,
                   color: RGBColor = BODY_TEXT, fill=None):
    cell.text = ""
    tf = cell.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.LEFT
    run = p.add_run()
    run.text = text
    run.font.name = FONT_NAME
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.color.rgb = color
    if fill is not None:
        cell.fill.solid()
        cell.fill.fore_color.rgb = fill
    # tighten cell margins
    cell.margin_left = Inches(0.06)
    cell.margin_right = Inches(0.06)
    cell.margin_top = Inches(0.02)
    cell.margin_bottom = Inches(0.02)


def _add_red_header_bottom_border(table):
    """Add a red 2pt bottom border to the header row (row 0) cells."""
    for col_idx in range(len(table.columns)):
        cell = table.cell(0, col_idx)
        tcPr = cell._tc.get_or_add_tcPr()
        for tag in ("a:lnB",):
            for old in tcPr.findall(qn(tag)):
                tcPr.remove(old)
        ln = tcPr.makeelement(qn("a:lnB"), {
            "w": "12700",       # 1pt = 12700 EMU; ~2pt
            "cap": "flat",
            "cmpd": "sng",
            "algn": "ctr",
        })
        solidFill = ln.makeelement(qn("a:solidFill"), {})
        srgb = solidFill.makeelement(qn("a:srgbClr"), {"val": "D6002A"})
        solidFill.append(srgb)
        ln.append(solidFill)
        tcPr.append(ln)


# --- Slide builders ----------------------------------------------------------
def _set_bg(slide, rgb: RGBColor):
    """Solid-fill a slide background with `rgb`."""
    bg = slide.background
    fill = bg.fill
    fill.solid()
    fill.fore_color.rgb = rgb


def _add_title_bar(slide):
    """Red rectangle across the top of a content slide (subtle accent)."""
    bar = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, Inches(0.08)
    )
    bar.fill.solid()
    bar.fill.fore_color.rgb = RED
    bar.line.fill.background()
    bar.shadow.inherit = False
    return bar


def _add_title_text(slide, title: str, *, color: RGBColor = RED,
                    size: int = 28, top: float = 0.25, bold: bool = True,
                    height: float = 0.7, white_bg: bool = False):
    box = slide.shapes.add_textbox(MARGIN_X, Inches(top), CONTENT_W, Inches(height))
    tf = box.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.LEFT
    run = p.add_run()
    run.text = title
    run.font.name = FONT_NAME
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.color.rgb = color
    return box


def _add_text_block(slide, text: str, *, top: Inches, left: Inches = None,
                    width: Inches = None, size: int = 18, color: RGBColor = BODY_TEXT,
                    bold: bool = False, italic: bool = False,
                    align=PP_ALIGN.LEFT, height: Inches = None):
    if left is None:
        left = MARGIN_X
    if width is None:
        width = CONTENT_W
    if height is None:
        height = Inches(0.4)
    tb = slide.shapes.add_textbox(left, top, width, height)
    tf = tb.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.alignment = align
    run = p.add_run()
    run.text = text
    run.font.name = FONT_NAME
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.italic = italic
    run.font.color.rgb = color
    return tb


def _add_bullets(slide, bullets, *, top: Inches, size: int = 18,
                 color: RGBColor = BODY_TEXT, width: Inches = None,
                 height: Inches = None):
    if width is None:
        width = CONTENT_W
    if height is None:
        height = Inches(0.35) * len(bullets) + Inches(0.2)
    tb = slide.shapes.add_textbox(MARGIN_X, top, width, height)
    tf = tb.text_frame
    tf.word_wrap = True
    for i, (lvl, text) in enumerate(bullets):
        p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
        p.alignment = PP_ALIGN.LEFT
        p.level = lvl
        run = p.add_run()
        prefix = "• " if lvl == 0 else ("– " if lvl == 1 else "· ")
        run.text = prefix + text
        run.font.name = FONT_NAME
        run.font.size = Pt(size if lvl == 0 else max(12, size - 2))
        run.font.color.rgb = color
    return tb


def _strip_inline_emphasis(text: str) -> str:
    """Strip `**bold**` and `*italic*` and `` `code` `` markers for plain runs.
    We render bold via separate runs only for the **lead** paragraph; here we
    collapse emphasis to plain text (the python PPTX is a comparison artifact).
    """
    # `code` → plain
    text = re.sub(r"`([^`]+)`", r"\1", text)
    # **bold** → text
    text = re.sub(r"\*\*([^*]+)\*\*", r"\1", text)
    # *italic* → text
    text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"\1", text)
    return text


def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT):
    """Add inline runs to paragraph `p`, rendering **bold** as red strong,
    `code` as monospace, *italic* as italic. Other text is plain."""
    # Tokenize on `**...**`, `*...*`, `` `...` ``
    tokens = re.split(r"(\*\*[^*]+\*\*|`[^`]+`|\*[^*]+\*)", text)
    for tok in tokens:
        if not tok:
            continue
        if tok.startswith("**") and tok.endswith("**"):
            r = p.add_run()
            r.text = tok[2:-2]
            r.font.name = FONT_NAME
            r.font.size = Pt(size)
            r.font.bold = True
            r.font.color.rgb = RED
        elif tok.startswith("`") and tok.endswith("`"):
            r = p.add_run()
            r.text = tok[1:-1]
            r.font.name = "Courier New"
            r.font.size = Pt(size)
            r.font.color.rgb = BODY_TEXT
        elif tok.startswith("*") and tok.endswith("*") and len(tok) >= 2:
            r = p.add_run()
            r.text = tok[1:-1]
            r.font.name = FONT_NAME
            r.font.size = Pt(size)
            r.font.italic = True
            r.font.color.rgb = base_color
        else:
            r = p.add_run()
            r.text = tok
            r.font.name = FONT_NAME
            r.font.size = Pt(size)
            r.font.color.rgb = base_color


def _add_picture(slide, image_path: Path, *, top: Inches, max_w: Inches = IMG_MAX_W,
                 max_h: Inches = IMG_MAX_H):
    """Add an image, centered horizontally, scaled to fit max_w x max_h."""
    if not image_path.is_file():
        # Placeholder text box if image missing
        tb = slide.shapes.add_textbox(MARGIN_X, top, CONTENT_W, Inches(0.4))
        tf = tb.text_frame
        p = tf.paragraphs[0]
        r = p.add_run()
        r.text = f"[image not found: {image_path}]"
        r.font.name = FONT_NAME
        r.font.size = Pt(14)
        r.font.color.rgb = GREY_TEXT
        return tb
    # native size of the picture
    pic = slide.shapes.add_picture(str(image_path), MARGIN_X, top)
    # scale
    w = pic.width
    h = pic.height
    ratio = min(max_w / w, max_h / h, 1.0)
    w = Emu(int(w * ratio))
    h = Emu(int(h * ratio))
    pic.width = w
    pic.height = h
    # center horizontally
    pic.left = Emu(int((SLIDE_W - w) / 2))
    return pic


def _add_table(slide, rows, *, top: Inches, width: Inches = None):
    """rows: list of list[str]. First row is header."""
    if width is None:
        width = CONTENT_W
    n_rows = len(rows)
    n_cols = max(len(r) for r in rows)
    # pad ragged rows
    rows = [r + [""] * (n_cols - len(r)) for r in rows]
    # estimate height
    height = Inches(0.3) * n_rows
    tbl_shape = slide.shapes.add_table(n_rows, n_cols, MARGIN_X, top, width, height)
    table = tbl_shape.table
    # remove default banding style for a cleaner look
    try:
        table.first_row = False
        table.horz_banding = False
    except Exception:
        pass
    for r_idx, row in enumerate(rows):
        for c_idx, val in enumerate(row):
            is_header = r_idx == 0
            _set_cell_text(
                table.cell(r_idx, c_idx),
                _strip_inline_emphasis(val),
                bold=is_header,
                size=13 if is_header else 12,
                color=BODY_TEXT,
                fill=GREY_HEADER if is_header else WHITE,
            )
    _add_red_header_bottom_border(table)
    return tbl_shape


def _add_benefit(slide, text: str, *, top: Inches):
    """Benefit callout: a thin red top-rule rectangle, then italic text."""
    rule = slide.shapes.add_shape(
        MSO_SHAPE.RECTANGLE, MARGIN_X, top, Inches(6.0), Inches(0.03)
    )
    rule.fill.solid()
    rule.fill.fore_color.rgb = RED
    rule.line.fill.background()
    rule.shadow.inherit = False
    tb = slide.shapes.add_textbox(
        MARGIN_X, top + Inches(0.08), CONTENT_W, Inches(0.6)
    )
    tf = tb.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.LEFT
    r = p.add_run()
    r.text = text
    r.font.name = FONT_NAME
    r.font.size = Pt(16)
    r.font.italic = True
    r.font.color.rgb = BODY_TEXT
    return tb


# --- Slide parse + render ----------------------------------------------------
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
IMAGE_RE = re.compile(r"^!\[[^\]]*\]\(([^)\s]+)(?:\s+\"([^\"]*)\")?\)")
TABLE_SEP_RE = re.compile(r"^\|?[\s:|-]+\|?$")


def parse_slide(slide_src: str):
    """Parse a single slide's markdown into a structured dict."""
    lines = slide_src.splitlines()
    title = None
    title_is_h1 = False
    is_title_class = False
    body = []  # list of ("lead", text) | ("bullet", lvl, text) | ("quote", text)
               #      | ("code", text) | ("image", path) | ("table", rows)
               #      | ("benefit", text) | ("plain", text) | ("ordered", n, text)
    i = 0
    while i < len(lines):
        line = lines[i]
        stripped = line.strip()

        # HTML comments — skip, but detect Marp directives
        if stripped.startswith("<!--") and stripped.endswith("-->"):
            inner = stripped[4:-3].strip()
            if "_class: title" in inner:
                is_title_class = True
            # _paginate: false / _class etc. — just skip
            i += 1
            continue
        # multi-line HTML comments (rare in this deck)
        if stripped.startswith("<!--") and "-->" not in stripped:
            while i < len(lines) and "-->" not in lines[i]:
                i += 1
            i += 1
            continue

        # Heading
        m = HEADING_RE.match(stripped)
        if m and title is None:
            level = len(m.group(1))
            title = m.group(2).strip()
            if level == 1:
                title_is_h1 = True
            i += 1
            continue
        if m and title is not None:
            # Sub-heading inside a slide — treat as plain bold lead text.
            body.append(("lead", m.group(2).strip()))
            i += 1
            continue

        # Fenced code block
        if stripped.startswith("```"):
            i += 1
            code_lines = []
            while i < len(lines) and not lines[i].strip().startswith("```"):
                code_lines.append(lines[i])
                i += 1
            if i < len(lines):
                i += 1  # skip closing fence
            body.append(("code", "\n".join(code_lines)))
            continue

        # Image
        m = IMAGE_RE.match(stripped)
        if m:
            body.append(("image", m.group(1)))
            i += 1
            continue

        # Blockquote
        if stripped.startswith(">"):
            quote_text = stripped[1:].strip()
            # join consecutive blockquote lines
            i += 1
            while i < len(lines) and lines[i].strip().startswith(">"):
                quote_text += " " + lines[i].strip().lstrip(">").strip()
                i += 1
            body.append(("quote", quote_text))
            continue

        # Benefit callout
        bm = re.match(r"<div\s+class=\"benefit\">(.*)</div>", stripped)
        if bm:
            body.append(("benefit", bm.group(1).strip()))
            i += 1
            continue

        # Table — starts with `| ... |` and the next line is a separator
        if stripped.startswith("|") and i + 1 < len(lines) and TABLE_SEP_RE.match(lines[i + 1].strip()):
            table_rows = []
            # header
            header = [c.strip() for c in stripped.strip("|").split("|")]
            table_rows.append(header)
            i += 2  # header + separator
            while i < len(lines) and lines[i].strip().startswith("|"):
                row = [c.strip() for c in lines[i].strip().strip("|").split("|")]
                table_rows.append(row)
                i += 1
            body.append(("table", table_rows))
            continue

        # Ordered list item: `1. ` or `1. `
        om = re.match(r"^(\d+)\.\s+(.*)", stripped)
        if om:
            body.append(("ordered", int(om.group(1)), om.group(2).strip()))
            i += 1
            continue

        # Unordered list item
        um = re.match(r"^(\s*)([-*+])\s+(.*)", line)
        if um:
            indent = len(um.group(1))
            lvl = 0 if indent < 2 else (1 if indent < 4 else 2)
            body.append(("bullet", lvl, um.group(3).strip()))
            i += 1
            continue

        # Blank line
        if not stripped:
            i += 1
            continue

        # Bold lead paragraph (entire paragraph wrapped in **...**)
        if stripped.startswith("**") and stripped.endswith("**") and stripped.count("**") == 2:
            body.append(("lead", stripped[2:-2].strip()))
            i += 1
            continue

        # Plain text
        # collect contiguous non-empty, non-special lines into one paragraph
        para_lines = [line]
        i += 1
        while i < len(lines):
            nxt = lines[i].strip()
            if (not nxt or nxt.startswith("#") or nxt.startswith("-")
                    or nxt.startswith("*") or nxt.startswith(">")
                    or nxt.startswith("|") or nxt.startswith("<")
                    or nxt.startswith("```") or nxt.startswith("!")
                    or re.match(r"^\d+\.\s", nxt)):
                break
            para_lines.append(lines[i])
            i += 1
        para_text = " ".join(l.strip() for l in para_lines).strip()
        if para_text:
            body.append(("plain", para_text))
        continue

    return {
        "title": title or "(untitled)",
        "title_is_h1": title_is_h1,
        "is_title_class": is_title_class,
        "body": body,
    }


def render_title_slide(prs, slide_data):
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # blank
    _set_bg(slide, BLACK)
    # red top bar
    bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, Inches(0.4))
    bar.fill.solid()
    bar.fill.fore_color.rgb = RED
    bar.line.fill.background()
    bar.shadow.inherit = False
    # title
    title = slide_data["title"]
    tb = slide.shapes.add_textbox(MARGIN_X, Inches(2.5), CONTENT_W, Inches(2.0))
    tf = tb.text_frame
    tf.word_wrap = True
    p = tf.paragraphs[0]
    p.alignment = PP_ALIGN.LEFT
    r = p.add_run()
    r.text = title
    r.font.name = FONT_NAME
    r.font.size = Pt(40)
    r.font.bold = True
    r.font.color.rgb = WHITE
    # body content (subtitle/lead/benefit) on black bg
    cur_top = Inches(4.6)
    for item in slide_data["body"]:
        kind = item[0]
        if kind == "lead":
            _add_text_block(slide, _strip_inline_emphasis(item[1]),
                            top=cur_top, size=20, color=WHITE, bold=True,
                            height=Inches(0.5))
            cur_top += Inches(0.55)
        elif kind == "plain":
            _add_text_block(slide, _strip_inline_emphasis(item[1]),
                            top=cur_top, size=16, color=WHITE,
                            height=Inches(0.4))
            cur_top += Inches(0.45)
        elif kind == "benefit":
            # benefit on title slide: italic white
            tb_b = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.8))
            tf_b = tb_b.text_frame
            tf_b.word_wrap = True
            p_b = tf_b.paragraphs[0]
            r_b = p_b.add_run()
            r_b.text = item[1]
            r_b.font.name = FONT_NAME
            r_b.font.size = Pt(16)
            r_b.font.italic = True
            r_b.font.color.rgb = WHITE
            cur_top += Inches(0.85)


def render_content_slide(prs, slide_data, deck_dir: Path):
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # blank
    _set_bg(slide, WHITE)
    _add_title_bar(slide)
    _add_title_text(slide, slide_data["title"], color=RED, size=28, top=0.25,
                    bold=True, height=0.7)
    cur_top = Inches(1.05)
    for item in slide_data["body"]:
        kind = item[0]
        if kind == "lead":
            tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.5))
            tf = tb.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.alignment = PP_ALIGN.LEFT
            _inline_runs(p, item[1], size=18, base_color=RED)
            # make the whole lead bold-strong-red
            for r in p.runs:
                r.font.bold = True
                r.font.color.rgb = RED
            cur_top += Inches(0.5)
        elif kind == "plain":
            tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.4))
            tf = tb.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.alignment = PP_ALIGN.LEFT
            _inline_runs(p, item[1], size=18, base_color=BODY_TEXT)
            cur_top += Inches(0.4)
        elif kind == "quote":
            tb = slide.shapes.add_textbox(
                MARGIN_X + Inches(0.3), cur_top, CONTENT_W - Inches(0.3), Inches(0.6)
            )
            tf = tb.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.alignment = PP_ALIGN.LEFT
            _inline_runs(p, item[1], size=18, base_color=GREY_TEXT)
            # italicize the whole blockquote
            for r in p.runs:
                r.font.italic = True
                r.font.color.rgb = GREY_TEXT
            cur_top += Inches(0.6)
        elif kind == "bullet":
            # accumulate consecutive bullets into one text frame
            # (handled below in a second pass; we render single here as fallback)
            tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35))
            tf = tb.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.alignment = PP_ALIGN.LEFT
            p.level = item[1]
            r = p.add_run()
            prefix = "• " if item[1] == 0 else ("– " if item[1] == 1 else "· ")
            r.text = prefix + _strip_inline_emphasis(item[2])
            r.font.name = FONT_NAME
            r.font.size = Pt(18 if item[1] == 0 else 16)
            r.font.color.rgb = BODY_TEXT
            cur_top += Inches(0.35)
        elif kind == "ordered":
            tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.35))
            tf = tb.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.alignment = PP_ALIGN.LEFT
            r = p.add_run()
            r.text = f"{item[1]}. " + _strip_inline_emphasis(item[2])
            r.font.name = FONT_NAME
            r.font.size = Pt(18)
            r.font.color.rgb = BODY_TEXT
            cur_top += Inches(0.35)
        elif kind == "image":
            img_path = deck_dir / item[1]
            _add_picture(slide, img_path, top=cur_top)
            cur_top += Inches(4.1)
        elif kind == "table":
            rows = item[1]
            _add_table(slide, rows, top=cur_top)
            cur_top += Inches(0.32) * len(rows) + Inches(0.1)
        elif kind == "code":
            tb = slide.shapes.add_textbox(MARGIN_X, cur_top, CONTENT_W, Inches(0.6))
            tf = tb.text_frame
            tf.word_wrap = True
            p = tf.paragraphs[0]
            p.alignment = PP_ALIGN.LEFT
            r = p.add_run()
            r.text = item[1]
            r.font.name = "Courier New"
            r.font.size = Pt(14)
            r.font.color.rgb = BLACK
            cur_top += Inches(0.5)
        elif kind == "benefit":
            _add_benefit(slide, item[1], top=cur_top)
            cur_top += Inches(0.75)


def render_deck(md_path: Path, pptx_path: Path):
    md_text = md_path.read_text(encoding="utf-8")
    slide_sources = split_slides(md_text)
    prs = Presentation()
    prs.slide_width = SLIDE_W
    prs.slide_height = SLIDE_H

    deck_dir = md_path.parent
    print(f"Parsing {len(slide_sources)} slides from {md_path}")
    for idx, src in enumerate(slide_sources):
        data = parse_slide(src)
        is_title = (idx == 0) or data["is_title_class"] or data["title_is_h1"]
        # The appendix is a content slide (rendered normally)
        if idx == 0 and (data["title_is_h1"] or data["is_title_class"]):
            render_title_slide(prs, data)
        elif data["is_title_class"] and not data["title_is_h1"] and idx != 0:
            # Marp _class: title on a non-H1 slide (e.g., appendix) — render as
            # content but with a title-style bar. Keep it simple: content slide.
            render_content_slide(prs, data, deck_dir)
        else:
            render_content_slide(prs, data, deck_dir)
        print(f"  [{idx + 1:02d}] {data['title']}  (body: {len(data['body'])} blocks)")

    pptx_path.parent.mkdir(parents=True, exist_ok=True)
    prs.save(str(pptx_path))
    print(f"Saved: {pptx_path}  ({len(prs.slides)} slides)")


def main():
    deck = sys.argv[1] if len(sys.argv) > 1 else "nova-autonomous-cloud-delivery"
    repo_root = Path(__file__).resolve().parent.parent
    md_path = repo_root / "docs" / "presentations" / f"{deck}-marp.md"
    pptx_path = repo_root / "docs" / "presentations" / f"{deck}-python.pptx"
    if not md_path.is_file():
        print(f"ERROR: source deck not found: {md_path}", file=sys.stderr)
        sys.exit(1)
    render_deck(md_path, pptx_path)


if __name__ == "__main__":
    main()