#!/usr/bin/env python3
"""
IterFact Assembler v2.0
=======================
Fixes from v1: strips :root/body/* overrides, unique element IDs, 
replaces CUSTOMIZABLE placeholders, proper keyframes, theme-controlled fonts.
"""

import json, re, os, sys
from pathlib import Path

DESIGN_TOKENS_PATH = Path(__file__).resolve().parent / "iterfact-design-tokens.css"
DESIGN_TOKENS_CSS = DESIGN_TOKENS_PATH.read_text(encoding="utf-8") if DESIGN_TOKENS_PATH.exists() else ""

ENGINE_DATA_MAP = {
    "01": {"sequences": "sequences", "config": "CONFIG"},
    "02": {"slides": "slides"},
    "03": {"metrics": "KPI_DATA", "charts": "CHARTS"},
    "04": {"objects": "SCENE_OBJECTS", "config": "SCENE_CONFIG"},
    "05": {"events": "EVENTS"},
    "06": {"nodes": "NODES", "connections": "CONNECTIONS"},
    "07": {"items": "PRODUCTS", "features": "FEATURES"},
    "08": {"inputs": "INPUTS", "outputs": "OUTPUTS", "formula": "FORMULA"},
    "09": {"questions": "QUESTIONS", "results": "RESULTS"},
    "10": {"columns": "COLUMNS", "cards": "INITIAL_CARDS"},
    "11": {"plans": "PLANS"},
    "12": {"sections": "SECTIONS"},
    "13": {"cards": "CARDS"},
    "14": {},
    "15": {"nodes": "NODES"},
    "16": {"steps": "STEPS"},
    "17": {"variables": "VARIABLES", "scenarios": "SCENARIOS", "outputs": "OUTPUTS"},
    "18": {"chapters": "CHAPTERS"},
    "19": {"people": "PEOPLE"},
    "20": {"locations": "LOCATIONS"},
    "21": {"steps": "STEPS"},
    "22": {"categories": "CATEGORIES"},
    "23": {"images": "IMAGES"},
    "24": {"event": "EVENT"},
    "25": {"sections": "DOCUMENT"},
    "26": {"columns": "COLUMNS", "rows": "DATA"},
    "27": {"testimonials": "TESTIMONIALS", "metrics": "METRICS"},
    "28": {"sections": "SECTIONS"},
    "29": {"items": "ITEMS"},
    "30": {"examples": "EXAMPLES"},
    "31": {"tasks": "TASKS"},
    "32": {"slices": "SLICES", "center": "CENTER"},
    "33": {"media": "MEDIA"},
    "34": {"counters": "COUNTERS"},
    "35": {"tabs": "TABS"},
    "36": {"cards": "CARDS"},
    "37": {"goal": "GOAL", "milestones": "MILESTONES"},
    "38": {"embeds": "EMBEDS"},
    "39": {"testimonials": "TESTIMONIALS", "locations": "LOCATION_POINTS", "config": "GLOBE_CONFIG"},
    "44": {"items": "ITEMS", "sort_options": "SORT_OPTIONS", "config": "RANKING_CONFIG"},
}


def _text(value, default=""):
    if value is None:
        return default
    text = str(value).strip()
    return text if text else default


def _list(value):
    return value if isinstance(value, list) else []


def _dict(value):
    return value if isinstance(value, dict) else {}


def _is_number(value):
    return isinstance(value, (int, float)) and not isinstance(value, bool)


def _slugify_identifier(value, fallback):
    base = _text(value, fallback).lower()
    slug = re.sub(r"[^a-z0-9]+", "-", base).strip("-")
    return slug or fallback


def _default_flowchart_position(index, total):
    if total <= 1:
        return 600, 140
    cols = 2 if total <= 4 else 3
    col_gap = 320 if cols == 2 else 260
    row_gap = 180
    start_x = int(600 - ((cols - 1) * col_gap) / 2)
    row = index // cols
    col = index % cols
    return start_x + (col * col_gap), 140 + (row * row_gap)


def adapt_engine_data(engine_num, data):
    source = _dict(data)
    if engine_num == "03":
        metrics = []
        for index, metric in enumerate(_list(source.get("metrics"))):
            item = _dict(metric)
            change = _text(item.get("change") or item.get("delta"))
            direction = _text(item.get("direction") or item.get("trend")).lower()
            if direction not in {"up", "down"}:
                if change.startswith("+"):
                    direction = "up"
                elif change.startswith("-"):
                    direction = "down"
                else:
                    direction = ""
            sparkline = [value for value in _list(item.get("sparkline")) if _is_number(value)]
            metrics.append(
                {
                    "label": _text(item.get("label") or item.get("name"), f"Metric {index + 1}"),
                    "value": _text(item.get("value"), "N/A"),
                    "change": change,
                    "direction": direction,
                    "sparkline": sparkline,
                }
            )
        raw_charts = _list(source.get("charts"))
        chart = source.get("chart")
        if not raw_charts and isinstance(chart, dict):
            raw_charts = [chart]

        charts = []
        for index, raw_chart in enumerate(raw_charts):
            entry = _dict(raw_chart)
            datasets = []
            for dataset_index, raw_dataset in enumerate(_list(entry.get("datasets"))):
                dataset = _dict(raw_dataset)
                datasets.append(
                    {
                        "label": _text(dataset.get("label"), f"Series {dataset_index + 1}"),
                        "data": [value for value in _list(dataset.get("data")) if _is_number(value)],
                        "tone": _text(dataset.get("tone"), ""),
                        "fill": bool(dataset.get("fill")),
                        "tension": dataset.get("tension") if _is_number(dataset.get("tension")) else None,
                    }
                )
            annotations = []
            for raw_annotation in _list(entry.get("annotations")):
                annotation = _dict(raw_annotation)
                x_value = annotation.get("x")
                if x_value is None:
                    continue
                annotations.append(
                    {
                        "x": x_value,
                        "label": _text(annotation.get("label")),
                        "tone": _text(annotation.get("tone"), ""),
                        "dashed": bool(annotation.get("dashed")),
                    }
                )
            charts.append(
                {
                    "title": _text(entry.get("title"), f"Chart {index + 1}"),
                    "note": _text(entry.get("note")),
                    "meta": _text(entry.get("meta")),
                    "type": _text(entry.get("type"), "bar"),
                    "formatter": _text(entry.get("formatter"), "number"),
                    "tone": _text(entry.get("tone"), ""),
                    "labels": [str(value) for value in _list(entry.get("labels"))],
                    "datasets": datasets,
                    "annotations": annotations,
                    "stacked": bool(entry.get("stacked")),
                    "indexAxis": _text(entry.get("indexAxis")),
                }
            )
        return {"metrics": metrics, "charts": charts}

    if engine_num == "05":
        events = []
        for index, event in enumerate(_list(source.get("events"))):
            item = _dict(event)
            events.append(
                {
                    "date": _text(item.get("date"), f"Phase {index + 1}"),
                    "title": _text(item.get("title") or item.get("name"), f"Event {index + 1}"),
                    "description": _text(item.get("description") or item.get("body"), ""),
                    "icon": _text(item.get("icon"), "•"),
                    "side": _text(item.get("side"), "left" if index % 2 == 0 else "right"),
                    "image": item.get("image"),
                }
            )
        return {"events": events}

    if engine_num == "06":
        nodes = []
        known_ids = []
        raw_nodes = _list(source.get("nodes"))
        total = len(raw_nodes)
        for index, node in enumerate(raw_nodes):
            item = _dict(node)
            node_id = _slugify_identifier(item.get("id") or item.get("label") or item.get("name"), f"node-{index + 1}")
            x, y = _default_flowchart_position(index, total)
            nodes.append(
                {
                    "id": node_id,
                    "label": _text(item.get("label") or item.get("name"), f"Node {index + 1}"),
                    "type": _text(item.get("type"), "process") if _text(item.get("type"), "process") in {"start", "process", "decision", "end"} else "process",
                    "x": item.get("x") if _is_number(item.get("x")) else x,
                    "y": item.get("y") if _is_number(item.get("y")) else y,
                }
            )
            known_ids.append(node_id)
        connections = []
        known = set(known_ids)
        for connection in _list(source.get("connections")):
            item = _dict(connection)
            from_id = _slugify_identifier(item.get("from"), "")
            to_id = _slugify_identifier(item.get("to"), "")
            if not from_id or not to_id or from_id not in known or to_id not in known:
                continue
            payload = {"from": from_id, "to": to_id}
            label = _text(item.get("label"))
            if label:
                payload["label"] = label
            connections.append(payload)
        return {"nodes": nodes, "connections": connections}

    if engine_num == "07":
        products = []
        for index, item in enumerate(_list(source.get("items"))):
            entry = _dict(item)
            products.append(
                {
                    "name": _text(entry.get("name") or entry.get("title"), f"Option {index + 1}"),
                    "price": _text(entry.get("price") or entry.get("subtitle") or entry.get("description"), ""),
                    "recommended": bool(entry.get("recommended")),
                }
            )
        features = []
        for feature in _list(source.get("features")):
            entry = _dict(feature)
            values = list(_list(entry.get("values")))
            if products:
                values = values[: len(products)] + [""] * max(0, len(products) - len(values))
            features.append(
                {
                    "name": _text(entry.get("name"), "Feature"),
                    "values": values,
                }
            )
        return {"items": products, "features": features}

    if engine_num == "28":
        sections = []
        raw_sections = _list(source.get("sections"))
        if raw_sections:
            for index, section in enumerate(raw_sections):
                entry = _dict(section)
                items = []
                for item in _list(entry.get("items")):
                    row = _dict(item)
                    items.append(
                        {
                            "text": _text(row.get("text") or row.get("label"), f"Item {len(items) + 1}"),
                            "required": bool(row.get("required")),
                        }
                    )
                if items:
                    sections.append(
                        {
                            "title": _text(entry.get("title"), f"Section {index + 1}"),
                            "items": items,
                        }
                    )
        elif _list(source.get("items")):
            sections.append(
                {
                    "title": _text(source.get("section_title") or source.get("title"), "Checklist"),
                    "items": [
                        {
                            "text": _text(_dict(item).get("text") or _dict(item).get("label"), f"Item {index + 1}"),
                            "required": bool(_dict(item).get("required")),
                        }
                        for index, item in enumerate(_list(source.get("items")))
                    ],
                }
            )
        return {"sections": sections}

    if engine_num == "29":
        items = []
        running_total = 0
        for index, item in enumerate(_list(source.get("items"))):
            entry = _dict(item)
            label = _text(entry.get("label") or entry.get("name"), f"Step {index + 1}")
            raw_value = entry.get("value")
            value = raw_value if _is_number(raw_value) else 0
            item_type = _text(entry.get("type"), "add").lower()
            if item_type == "subtract" and value > 0:
                value = -abs(value)
            elif item_type == "add" and value < 0:
                value = abs(value)
            elif item_type == "total":
                value = running_total if index > 0 else value
            elif item_type == "start":
                running_total = value
            else:
                running_total += value
            if item_type in {"start", "total"}:
                running_total = value if item_type == "start" else running_total
            items.append({"label": label, "value": value, "type": item_type})
        return {"items": items}

    return source

THEMES = {
    "midnight-mint": {
        "mode": "dark",
        "--bg": "#06060b", "--bg2": "#0c0c14", "--surface": "#12121c",
        "--border": "rgba(255,255,255,0.06)", "--text": "#e8e8f0",
        "--muted": "#8080a0", "--accent": "#3dffa2", "--accent2": "#00d4ff",
        "--green": "#22c55e", "--red": "#ef4444", "--yellow": "#f59e0b",
        "--blue": "#6366f1", "--orange": "#fb923c", "--purple": "#a78bfa",
        "--card": "#12121c", "--line": "rgba(255,255,255,0.08)",
        "--font-display": "'Fraunces', serif",
        "--font-body": "'DM Sans', sans-serif",
        "--font-mono": "'JetBrains Mono', monospace",
        "--radius": "10px",
        "fonts_url": "Fraunces:ital,wght@0,300;0,500;0,700;1,300&family=DM+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;600;700",
    },
    "obsidian-cyan": {
        "mode": "dark",
        "--bg": "#0a0f1a", "--bg2": "#111827", "--surface": "#1e293b",
        "--border": "rgba(255,255,255,0.08)", "--text": "#f1f5f9",
        "--muted": "#94a3b8", "--accent": "#22d3ee", "--accent2": "#6366f1",
        "--green": "#22c55e", "--red": "#ef4444", "--yellow": "#f59e0b",
        "--blue": "#6366f1", "--orange": "#fb923c", "--purple": "#a78bfa",
        "--card": "#1e293b", "--line": "rgba(255,255,255,0.06)",
        "--font-display": "'Playfair Display', serif",
        "--font-body": "'DM Sans', sans-serif",
        "--font-mono": "'JetBrains Mono', monospace",
        "--radius": "8px",
        "fonts_url": "Playfair+Display:wght@400;700&family=DM+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600",
    },
    "warm-ivory": {
        "mode": "light",
        "--bg": "#faf8f5", "--bg2": "#f5f0eb", "--surface": "#ffffff",
        "--border": "rgba(0,0,0,0.08)", "--text": "#1c1917",
        "--muted": "#78716c", "--accent": "#b45309", "--accent2": "#92400e",
        "--green": "#16a34a", "--red": "#dc2626", "--yellow": "#ca8a04",
        "--blue": "#6366f1", "--orange": "#fb923c", "--purple": "#a78bfa",
        "--card": "#ffffff", "--line": "rgba(0,0,0,0.06)",
        "--font-display": "'Crimson Pro', serif",
        "--font-body": "'DM Sans', sans-serif",
        "--font-mono": "'JetBrains Mono', monospace",
        "--radius": "8px",
        "fonts_url": "Crimson+Pro:wght@300;400;600;700&family=DM+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600",
    },
    "clean-slate": {
        "mode": "light",
        "--bg": "#f8fafc", "--bg2": "#f1f5f9", "--surface": "#ffffff",
        "--border": "#e2e8f0", "--text": "#0f172a",
        "--muted": "#64748b", "--accent": "#6366f1", "--accent2": "#8b5cf6",
        "--green": "#22c55e", "--red": "#ef4444", "--yellow": "#f59e0b",
        "--blue": "#6366f1", "--orange": "#fb923c", "--purple": "#a78bfa",
        "--card": "#ffffff", "--line": "#e2e8f0",
        "--font-display": "'Plus Jakarta Sans', sans-serif",
        "--font-body": "'Plus Jakarta Sans', sans-serif",
        "--font-mono": "'JetBrains Mono', monospace",
        "--radius": "12px",
        "fonts_url": "Plus+Jakarta+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600",
    },
    "deep-violet": {
        "mode": "dark",
        "--bg": "#0d0a1a", "--bg2": "#140f26", "--surface": "#1c1533",
        "--border": "rgba(167,139,250,0.15)", "--text": "#ede9fe",
        "--muted": "rgba(167,139,250,0.5)", "--accent": "#a78bfa", "--accent2": "#c084fc",
        "--green": "#4ade80", "--red": "#fb7185", "--yellow": "#fbbf24",
        "--blue": "#6366f1", "--orange": "#fb923c", "--purple": "#a78bfa",
        "--card": "#1c1533", "--line": "rgba(167,139,250,0.1)",
        "--font-display": "'Cormorant Garamond', serif",
        "--font-body": "'DM Sans', sans-serif",
        "--font-mono": "'JetBrains Mono', monospace",
        "--radius": "12px",
        "fonts_url": "Cormorant+Garamond:wght@300;400;600;700&family=DM+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600",
    },
}


# ═══════════════════════════════════════════════════════════════
# ENGINE PARSER
# ═══════════════════════════════════════════════════════════════

class EngineParts:
    def __init__(self, engine_id, html_source):
        self.engine_id = engine_id
        self.raw = html_source
        self.css = ""
        self.js = ""
        self.body = ""
        self.cdn_scripts = []
        self._parse()

    def _parse(self):
        src = self.raw
        for m in re.finditer(r'<script\s+src="([^"]+)"[^>]*>\s*</script>', src):
            self.cdn_scripts.append(m.group(1))
        css_blocks = re.findall(r'<style[^>]*>(.*?)</style>', src, re.DOTALL)
        self.css = "\n".join(css_blocks)
        self.css = re.sub(r"@import\s+url\([^)]+\);\s*", "", self.css)
        js_blocks = []
        for m in re.finditer(r'<script(?!\s+src=)[^>]*>(.*?)</script>', src, re.DOTALL):
            js_blocks.append(m.group(1))
        self.js = "\n".join(js_blocks)
        m = re.search(r'<body[^>]*>(.*?)</body>', src, re.DOTALL)
        if m:
            body = m.group(1)
            body = re.sub(r'<script[^>]*>.*?</script>', '', body, flags=re.DOTALL)
            self.body = body.strip()


def load_engine(engine_num, vault_path):
    vault = Path(vault_path)
    matches = list(vault.glob(f"{engine_num}-*/template.html"))
    if not matches:
        raise FileNotFoundError(f"Engine {engine_num} not found in {vault_path}")
    return EngineParts(engine_num, matches[0].read_text(encoding='utf-8'))


# ═══════════════════════════════════════════════════════════════
# CSS CLEANING (v2 — THE BIG FIX)
# ═══════════════════════════════════════════════════════════════

def clean_and_namespace_css(raw_css, namespace):
    """
    1. REMOVE :root{}, body{}, html{}, *{} completely
    2. Namespace @keyframes
    3. Update animation references
    4. Namespace all remaining selectors
    """
    css = raw_css
    safe_ns = namespace.replace("-", "_")

    # ── Step 1: Strip global rules that would override theme ──
    # Remove :root { ... } blocks (handles nested braces poorly but :root is always flat)
    css = re.sub(r':root\s*\{[^}]*\}', '', css)
    # Remove body { ... } 
    css = re.sub(r'(?<![.\w#-])body\s*\{[^}]*\}', '', css)
    # Remove html { ... }
    css = re.sub(r'(?<![.\w#-])html\s*\{[^}]*\}', '', css)
    # Remove standalone * { ... } but not .foo * { }
    css = re.sub(r'(?<![.\w#-])\*\s*\{[^}]*\}', '', css)

    # ── Step 2: Collect and rename @keyframes ──
    kf_names = re.findall(r'@keyframes\s+([a-zA-Z_][\w-]*)', css)
    kf_map = {}
    for kf in set(kf_names):
        new_name = f"{safe_ns}__{kf}"
        kf_map[kf] = new_name

    # Rename declarations
    for old, new in kf_map.items():
        css = re.sub(rf'@keyframes\s+{re.escape(old)}\b', f'@keyframes {new}', css)

    # ── Step 3: Update animation references ──
    for old, new in kf_map.items():
        # animation shorthand
        css = re.sub(rf'(animation\s*:[^;]*)\b{re.escape(old)}\b',
                     lambda m, o=old, n=new: m.group(0).replace(o, n), css)
        # animation-name
        css = re.sub(rf'animation-name\s*:\s*{re.escape(old)}\b',
                     f'animation-name: {new}', css)

    # ── Step 4: Namespace selectors ──
    # Strategy: find each rule block (selector { ... }) and prefix the selector
    output = []
    pos = 0

    while pos < len(css):
        # Skip whitespace
        while pos < len(css) and css[pos] in ' \t\n\r':
            output.append(css[pos])
            pos += 1
        if pos >= len(css):
            break

        # Find opening brace
        brace = css.find('{', pos)
        if brace == -1:
            output.append(css[pos:])
            break

        selector = css[pos:brace].strip()

        # Find matching close brace (handle nesting for @media, @keyframes)
        depth = 0
        end = brace
        while end < len(css):
            if css[end] == '{': depth += 1
            elif css[end] == '}': 
                depth -= 1
                if depth == 0: break
            end += 1
        
        block = css[brace:end+1]
        pos = end + 1

        if not selector:
            output.append(block)
            continue

        # @keyframes — already renamed, keep as-is
        if selector.startswith('@keyframes'):
            output.append(f"\n{selector} {block}")
            continue

        # @media — namespace the rules INSIDE
        if selector.startswith('@media'):
            inner = block[1:-1]  # strip outer { }
            inner_namespaced = _namespace_inner_block(inner, namespace, kf_map)
            output.append(f"\n{selector} {{{inner_namespaced}}}")
            continue

        # Other @-rules
        if selector.startswith('@'):
            output.append(f"\n{selector} {block}")
            continue

        # Regular selector — namespace it
        ns_selector = _ns_selector(selector, namespace)
        if ns_selector:
            output.append(f"\n{ns_selector} {block}")

    return ''.join(output)


def _ns_selector(selector, namespace):
    """Namespace a selector (handles comma-separated)."""
    parts = [s.strip() for s in selector.split(',')]
    result = []
    for s in parts:
        if not s or s in ('body', 'html', '*', ':root'):
            continue
        result.append(f".{namespace} {s}")
    return ', '.join(result)


def _namespace_inner_block(css_inner, namespace, kf_map):
    """Namespace selectors inside @media blocks."""
    output = []
    pos = 0
    while pos < len(css_inner):
        brace = css_inner.find('{', pos)
        if brace == -1:
            output.append(css_inner[pos:])
            break
        selector = css_inner[pos:brace].strip()
        close = css_inner.find('}', brace)
        if close == -1: close = len(css_inner) - 1
        block = css_inner[brace:close+1]

        ns_sel = _ns_selector(selector, namespace) if selector else ""
        if ns_sel:
            output.append(f"\n  {ns_sel} {block}")
        pos = close + 1
    return ''.join(output)


# ═══════════════════════════════════════════════════════════════
# HTML + JS PROCESSING
# ═══════════════════════════════════════════════════════════════

def process_body_html(body, namespace):
    """Unique IDs + strip CUSTOMIZABLE placeholders."""
    # Unique IDs
    body = re.sub(r'id="([^"]+)"', lambda m: f'id="{namespace}__{m.group(1)}"', body)
    # Strip CUSTOMIZABLE wrappers and their default text
    body = re.sub(r'<!--\s*CUSTOMIZABLE:\s*\w+\s*-->.*?<!--\s*/CUSTOMIZABLE\s*-->', '', body, flags=re.DOTALL)
    body = re.sub(r'<!--\s*(CUSTOMIZABLE|AI_HINT|AI_USAGE|ENGINE_META)[^>]*-->', '', body)
    # Strip empty block elements left by placeholder removal, but keep runtime
    # slots that are intentionally empty and addressed later by id/data hooks.
    def _strip_empty_placeholder_tags(match: re.Match[str]) -> str:
        attrs = match.group(2) or ""
        if 'id="' in attrs or "data-ifx-keep-empty" in attrs:
            return match.group(0)
        return ""

    body = re.sub(r'<(h[1-6]|p|span)([^>]*)>\s*<\/\1>', _strip_empty_placeholder_tags, body)
    return body


def process_js(js, namespace, engine_num, data):
    """Inject data + namespace IDs + IIFE wrap."""
    data = adapt_engine_data(engine_num, data)
    # Inject plan data
    field_map = ENGINE_DATA_MAP.get(engine_num, {})
    for plan_field, engine_var in field_map.items():
        if plan_field in data:
            plan_value = json.dumps(data[plan_field], indent=2)
            pattern = rf'const\s+{re.escape(engine_var)}\s*=\s*(\[[\s\S]*?\];|\{{[\s\S]*?\}};|[^;]+;)'
            def repl(match, var=engine_var, val=plan_value):
                return f'const {var} = {val};'
            new_js, count = re.subn(pattern, repl, js, count=1)
            if count > 0:
                js = new_js

    # Namespace getElementById
    js = re.sub(
        r"document\.getElementById\(['\"]([^'\"]+)['\"]\)",
        lambda m: f"document.getElementById('{namespace}__{m.group(1)}')",
        js
    )
    # Namespace querySelector('#id')
    js = re.sub(
        r"(querySelector(?:All)?)\(['\"]#([^'\"]+)['\"]\)",
        lambda m: f"{m.group(1)}('#{namespace}__{m.group(2)}')",
        js
    )

    return f"\n// ═══ {namespace} ═══\n(function() {{\n{js}\n}})();\n"


# ═══════════════════════════════════════════════════════════════
# WRAPPER BUILDERS
# ═══════════════════════════════════════════════════════════════

def build_wrapper(wtype, plan, section_htmls, footer):
    fl = footer.get("left", "")
    fr = footer.get("right", "Built on IterFact")
    fhtml = f'<footer class="ifx-footer"><span>{fl}</span><span>{fr}</span></footer>'

    if wtype == "02": return _slide_deck(section_htmls, fhtml)
    if wtype == "18": return _parallax(section_htmls, fhtml)
    if wtype == "12": return _infographic(section_htmls, fhtml)
    return _scroll(section_htmls, fhtml)


def _slide_deck(shs, fhtml):
    html = ""
    for i, sh in enumerate(shs):
        act = "ifx-active" if i == 0 else ""
        html += f'<div class="ifx-slide {act}" data-index="{i}"><div class="ifx-slide-inner">{sh["html"]}</div></div>\n'

    body = f'''<div class="ifx-deck-shell">
  <div class="ifx-deck-toolbar">
    <div class="ifx-deck-meta">
      <div class="ifx-deck-label">Slide deck</div>
      <div id="ifx-counter" class="ifx-deck-counter"></div>
    </div>
    <div class="ifx-deck-actions">
      <button type="button" id="ifx-prev">Previous</button>
      <button type="button" id="ifx-next" class="ifx-primary">Next</button>
    </div>
  </div>
  <div class="ifx-progress-track"><div id="ifx-progress" class="ifx-progress-bar"></div></div>
  <div class="ifx-deck">{html}</div>
</div>
{fhtml}'''

    css = """.ifx-deck-shell{max-width:1200px;margin:0 auto;padding:clamp(1.25rem,3vw,2rem) clamp(1rem,4vw,2rem) 2rem;container-type:inline-size}
.ifx-deck-toolbar{position:sticky;top:0;z-index:5;display:flex;justify-content:space-between;align-items:flex-end;gap:1rem;padding:0 0 0.875rem;margin-bottom:0.875rem;background:var(--color-background-tertiary)}
.ifx-deck-meta{display:grid;gap:0.2rem}
.ifx-deck-label{font-size:12px;line-height:1.4;color:var(--color-text-tertiary)}
.ifx-deck-counter{font-size:13px;line-height:1.4;color:var(--color-text-secondary);font-family:var(--font-mono);font-variant-numeric:tabular-nums}
.ifx-deck-actions{display:flex;gap:0.5rem;flex-wrap:wrap}
.ifx-deck-actions button{appearance:none;border:var(--border-width-hairline) solid var(--color-border-secondary);background:var(--color-background-primary);color:var(--color-text-primary);padding:0.6rem 1rem;border-radius:var(--border-radius-md);font:500 13px var(--font-sans);cursor:pointer}
.ifx-deck-actions button.ifx-primary{background:var(--ramp-purple-600);border-color:var(--ramp-purple-600);color:var(--ramp-gray-50)}
.ifx-deck-actions button:disabled{cursor:not-allowed;opacity:0.45}
.ifx-progress-track{height:var(--border-width-hairline);background:var(--color-border-tertiary);border-radius:999px;overflow:hidden;margin-bottom:1rem}
.ifx-progress-bar{height:100%;width:0%;background:var(--ramp-purple-600);transition:width 0.24s ease}
.ifx-deck{position:relative}
.ifx-slide{display:none;background:var(--color-background-primary);border:var(--border-width-hairline) solid var(--color-border-tertiary);border-radius:var(--border-radius-xl);padding:clamp(1.5rem,4vw,3rem);min-height:38rem}
.ifx-slide.ifx-active{display:flex;flex-direction:column;justify-content:center;animation:ifx-slide-fade 0.24s ease}
.ifx-slide-inner{width:100%;max-width:1100px;margin:0 auto}
@keyframes ifx-slide-fade{from{opacity:0}to{opacity:1}}
@container (max-width: 720px){
  .ifx-deck-toolbar{align-items:flex-start;flex-direction:column}
  .ifx-deck-actions{width:100%}
  .ifx-deck-actions button{flex:1 1 10rem}
  .ifx-slide{min-height:auto;padding:1.25rem}
}"""

    js = """var ifxCur=0,ifxSl=document.querySelectorAll('.ifx-slide'),ifxN=ifxSl.length,ifxPrevBtn=document.getElementById('ifx-prev'),ifxNextBtn=document.getElementById('ifx-next');
function ifxUpdateControls(){var p=document.getElementById('ifx-progress');if(p)p.style.width=((ifxCur+1)/Math.max(ifxN,1)*100)+'%';var c=document.getElementById('ifx-counter');if(c)c.textContent=(ifxCur+1)+' / '+ifxN;if(ifxPrevBtn)ifxPrevBtn.disabled=ifxCur===0;if(ifxNextBtn)ifxNextBtn.disabled=ifxCur>=ifxN-1;}
function ifxGoTo(n){if(!ifxN)return;ifxCur=Math.max(0,Math.min(n,ifxN-1));ifxSl.forEach(function(e,i){e.classList.toggle('ifx-active',i===ifxCur)});ifxUpdateControls();}
function ifxNext(){if(ifxCur<ifxN-1)ifxGoTo(ifxCur+1)}function ifxPrev(){if(ifxCur>0)ifxGoTo(ifxCur-1)}
if(ifxPrevBtn)ifxPrevBtn.addEventListener('click',ifxPrev);if(ifxNextBtn)ifxNextBtn.addEventListener('click',ifxNext);
document.addEventListener('keydown',function(e){if(e.key==='ArrowRight'||e.key===' '){e.preventDefault();ifxNext()}if(e.key==='ArrowLeft'){e.preventDefault();ifxPrev()}});
var ifxTX=0;document.addEventListener('touchstart',function(e){ifxTX=e.touches[0].clientX},{passive:true});document.addEventListener('touchend',function(e){var d=ifxTX-e.changedTouches[0].clientX;if(Math.abs(d)>50)d>0?ifxNext():ifxPrev()},{passive:true});
ifxGoTo(0);"""
    return body, css, js


def _parallax(shs, fhtml):
    tones = ["ifx-tone-primary","ifx-tone-secondary","ifx-tone-muted","ifx-tone-primary","ifx-tone-secondary","ifx-tone-muted"]
    aligns = ["left","center","right","center","left","right"]
    html = ""
    for i, sh in enumerate(shs):
        html += f'<div class="ifx-chapter ifx-align-{aligns[i%6]} {tones[i%6]}"><div class="ifx-chapter-content">{sh["html"]}</div></div>\n'

    body = f'''<div class="ifx-parallax-layout">
  <div class="ifx-rail-wrap"><div id="ifx-dots" class="ifx-rail"></div></div>
  <div class="ifx-parallax-shell">{html}</div>
</div>
{fhtml}'''

    css = """.ifx-parallax-layout{display:grid;grid-template-columns:minmax(0,1fr);gap:1rem;max-width:1240px;margin:0 auto;padding:clamp(1.25rem,3vw,2rem) clamp(1rem,4vw,2rem) 2rem;container-type:inline-size}
.ifx-rail-wrap{position:sticky;top:1rem;z-index:4;display:flex;justify-content:flex-end}
.ifx-rail{display:flex;gap:0.5rem;flex-wrap:wrap}
.ifx-dot{appearance:none;width:10px;height:10px;border-radius:999px;border:var(--border-width-hairline) solid var(--color-border-secondary);background:var(--color-background-primary);cursor:pointer}
.ifx-dot.ifx-active{background:var(--ramp-purple-600);border-color:var(--ramp-purple-600)}
.ifx-parallax-shell{display:grid;gap:1rem}
.ifx-chapter{min-height:30rem;display:flex;flex-direction:column;justify-content:center;padding:clamp(1.5rem,4vw,3rem);border:var(--border-width-hairline) solid var(--color-border-tertiary);border-radius:var(--border-radius-xl)}
.ifx-tone-primary{background:var(--color-background-primary)}
.ifx-tone-secondary{background:var(--color-background-secondary)}
.ifx-tone-muted{background:var(--color-background-tertiary)}
.ifx-chapter-content{max-width:900px;opacity:0;transform:translateY(18px);transition:opacity 0.45s ease,transform 0.45s ease}
.ifx-chapter-content.ifx-visible{opacity:1;transform:translateY(0)}
.ifx-align-center .ifx-chapter-content{margin:0 auto;text-align:center}
.ifx-align-right .ifx-chapter-content{margin-left:auto;text-align:right}
.ifx-align-left .ifx-chapter-content{text-align:left}
@container (max-width: 720px){
  .ifx-chapter{min-height:auto;padding:1.25rem}
}"""

    js = """(function(){var dots=document.getElementById('ifx-dots'),chs=document.querySelectorAll('.ifx-chapter');
if(dots)chs.forEach(function(ch,idx){var d=document.createElement('button');d.type='button';d.className='ifx-dot';d.setAttribute('aria-label','Go to chapter '+(idx+1));d.addEventListener('click',function(){ch.scrollIntoView({behavior:"smooth",block:"start"})});dots.appendChild(d)});
var obs=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){var c=e.target.querySelector('.ifx-chapter-content');if(c)c.classList.add('ifx-visible');var idx=Array.from(chs).indexOf(e.target);document.querySelectorAll('.ifx-dot').forEach(function(d,i){d.classList.toggle('ifx-active',i===idx)})}})},{threshold:0.35});
chs.forEach(function(ch){obs.observe(ch)})})();"""
    return body, css, js


def _infographic(shs, fhtml):
    html = ""
    for sh in shs:
        html += f'<div class="ifx-infosec">{sh["html"]}</div>\n'
    css = ".ifx-infosec{padding:clamp(1.75rem,4vw,3rem) clamp(1rem,4vw,2rem);max-width:1100px;margin:0 auto;background:var(--color-background-primary);border:var(--border-width-hairline) solid var(--color-border-tertiary);border-radius:var(--border-radius-xl);opacity:0;transform:translateY(18px);transition:opacity 0.4s ease,transform 0.4s ease}.ifx-infosec.ifx-visible{opacity:1;transform:translateY(0)}.ifx-infosec+.ifx-infosec{margin-top:1rem}.ifx-infosec:nth-child(even){background:var(--color-background-secondary)}"
    js = "(function(){var o=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting)e.target.classList.add('ifx-visible')})},{threshold:0.15});document.querySelectorAll('.ifx-infosec').forEach(function(s){o.observe(s)})})();"
    return html + fhtml, css, js


def _scroll(shs, fhtml):
    html = ""
    for sh in shs:
        html += f'<section class="ifx-sec">{sh["html"]}</section>\n'
    css = ".ifx-scroll-shell{display:grid;gap:1rem;padding:clamp(1.25rem,3vw,2rem) clamp(1rem,4vw,2rem) 2rem}.ifx-sec{padding:clamp(1.5rem,4vw,3rem);max-width:1100px;width:100%;margin:0 auto;background:var(--color-background-primary);border:var(--border-width-hairline) solid var(--color-border-tertiary);border-radius:var(--border-radius-xl)}"
    return f'<div class="ifx-scroll-shell">{html}</div>' + fhtml, css, ""


# ═══════════════════════════════════════════════════════════════
# MAIN ASSEMBLER
# ═══════════════════════════════════════════════════════════════

def assemble(plan, vault_path="./engines"):
    theme_name = plan.get("theme", "midnight-mint")
    theme = THEMES.get(theme_name, THEMES["midnight-mint"])
    wrapper_engine = plan.get("wrapper", {}).get("engine", "02")
    sections = plan.get("sections", [])
    meta = plan.get("meta", {})
    footer = plan.get("footer", {})

    all_css, all_js = [], []
    all_cdn = set()
    section_htmls = []

    for si, section in enumerate(sections):
        sid = section.get("id", f"s{si}")
        components = section.get("components", [])
        content = section.get("content", {})
        comp_htmls = []

        for comp in components:
            enum = comp.get("engine", "")
            iid = comp.get("instance_id", f"c{si}-{enum}")
            ns = f"ifx-{enum}-{iid}".replace(" ", "-")
            data = comp.get("data", {})

            try:
                parts = load_engine(enum, vault_path)
            except FileNotFoundError as e:
                print(f"  [WARN] {e}")
                continue

            all_cdn.update(parts.cdn_scripts)
            all_css.append(f"/* {ns} */\n{clean_and_namespace_css(parts.css, ns)}")
            all_js.append(process_js(parts.js, ns, enum, data))

            comp_body = process_body_html(parts.body, ns)
            comp_htmls.append(f'<div class="{ns} ifx-component">\n{comp_body}\n</div>')

        # Section HTML
        headline = (content or {}).get("headline", "")
        body_text = (content or {}).get("body", "")
        inner = ""
        if headline or body_text:
            inner += '<div class="ifx-content">\n'
            if headline: inner += f'  <h2 class="ifx-headline">{headline}</h2>\n'
            if body_text:
                for p in body_text.split('\n'):
                    if p.strip(): inner += f'  <p class="ifx-body">{p.strip()}</p>\n'
            inner += '</div>\n'
        for ch in comp_htmls:
            inner += ch + "\n"
        section_htmls.append({"id": sid, "title": section.get("title", ""), "html": inner})

    # Wrapper
    body_content, wcss, wjs = build_wrapper(wrapper_engine, plan, section_htmls, footer)
    all_css.insert(0, wcss)
    all_js.insert(0, wjs)

    # CDN
    cdn_html = ""
    for url in sorted(all_cdn):
        cdn_html += f'<script src="{url}"></script>\n'
    for url in plan.get("cdn_libs", []):
        if url not in all_cdn:
            cdn_html += f'<script src="{url}"></script>\n'

    # Theme
    tvars = "\n".join(f"    {k}: {v};" for k, v in theme.items() if k.startswith("--"))
    furl = theme.get("fonts_url", "")

    gcss = f"""{DESIGN_TOKENS_CSS}
/* ITERFACT_THEME_COMPAT_START */
:root {{
{tvars}
}}
/* ITERFACT_THEME_COMPAT_END */
*{{margin:0;padding:0;box-sizing:border-box}}
body{{background:var(--color-background-tertiary);color:var(--color-text-primary);font-family:var(--font-sans);min-height:100%;overflow-x:hidden}}
.ifx-content{{margin-bottom:1.5rem}}
.ifx-headline{{font-family:var(--font-sans);font-size:clamp(1.375rem,3vw,2rem);font-weight:500;line-height:1.2;margin-bottom:0.75rem;letter-spacing:-0.02em;color:var(--color-text-primary)}}
.ifx-body{{font-size:1rem;line-height:1.7;color:var(--color-text-secondary);max-width:700px;margin-bottom:0.5rem}}
.ifx-component{{margin:1.5rem 0;font-family:var(--font-sans);color:var(--color-text-primary)}}
.ifx-footer{{padding:2rem clamp(1.5rem,6vw,8%);border-top:var(--border-width-hairline) solid var(--color-border-tertiary);display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--color-text-tertiary);font-family:var(--font-sans);gap:1rem;flex-wrap:wrap}}
.ifx-component *{{max-width:100%;overflow-wrap:break-word;word-wrap:break-word}}
.ifx-component table{{table-layout:auto;max-width:100%}}
.ifx-component td,.ifx-component th,.ifx-component [class*="value"],.ifx-component [class*="metric"],.ifx-component [class*="num"]{{font-variant-numeric:tabular-nums}}
.ifx-component pre,.ifx-component code{{white-space:pre-wrap;word-break:break-all}}
.ifx-component img,.ifx-component svg,.ifx-component canvas,.ifx-component video,.ifx-component iframe{{max-width:100%;height:auto}}
@media (prefers-reduced-motion: reduce){{*,*::before,*::after{{animation:none !important;transition:none !important;scroll-behavior:auto !important}}}}
"""

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>{meta.get("title", "IterFact Document")}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family={furl}&display=swap" rel="stylesheet">
{cdn_html}<style>
{gcss}
{chr(10).join(all_css)}
</style>
</head>
<body>
{body_content}
<script>
{chr(10).join(all_js)}
</script>
</body>
</html>"""


# CLI
if __name__ == "__main__":
    import argparse
    p = argparse.ArgumentParser(description="IterFact Assembler v2")
    p.add_argument("plan")
    p.add_argument("--vault", default="./engines")
    p.add_argument("--output", "-o", default=None)
    p.add_argument("--theme", default=None)
    args = p.parse_args()
    with open(args.plan, encoding='utf-8') as f: plan = json.load(f)
    if args.theme: plan["theme"] = args.theme
    t = plan.get("meta",{}).get("title","Untitled")
    print(f"[IterFact v2] Building: {t}")
    print(f"  Theme: {plan.get('theme','midnight-mint')} | Wrapper: {plan.get('wrapper',{}).get('engine','??')} | Sections: {len(plan.get('sections',[]))}")
    html = assemble(plan, args.vault)
    out = args.output or re.sub(r'[^a-z0-9]+','-',t.lower()).strip('-')+'.html'
    with open(out, 'w', encoding='utf-8') as f: f.write(html)
    print(f"  Output: {out} ({os.path.getsize(out)/1024:.1f} KB) [ok]")


