#!/usr/bin/env python3
"""Generate interactive HTML activities for Canvas courses.

Supports 5 activity types:
  - dialog_cards: Flip-card flashcards
  - sequencing: Drag-and-order items
  - fill_blanks: Fill-in-the-blank passages
  - branching: Branching scenario with decision points
  - quiz: Multi-question formative quiz

Usage:
    python generator.py <content_data_path> [--course-id <id>] [--output-dir <dir>]

    content_data_path: Python file containing ALL_ACTIVITIES dict
    --course-id: Canvas course ID for staging directory
    --output-dir: Override output root (default: courses/{course-id}/staging/interactives)

Content data format:
    The content data file must define ALL_ACTIVITIES as a dict:
    {
        "m1": [
            {"type": "dialog_cards", "filename": "m1-vocab.html", "title": "...", "cards": [...]},
            ...
        ],
        "m2": [...],
    }

    See the interactive-content skill for full schema per activity type.
"""

import os
import sys
import json
import argparse
import logging
import re
import html as html_module
from pathlib import Path

# Add script dir to path for canvas_api import when imported from tests.
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from canvas_api import get_config


PLUGIN_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT_DIR = "courses/{course-id}/staging/interactives"
BANNER_LINE = "═════════════════════════════════════════════════════════"

DEFAULT_INSTRUCTIONS = {
    "dialog_cards": "Click or tap each card to reveal the definition. Use the Shuffle button to test yourself in a different order.",
    "sequencing": "Click an item on the right, then click a numbered slot on the left to place it. Click a filled slot to remove it.",
    "fill_blanks": "Read each passage and fill in the missing terms. Check your answers when you are ready.",
    "branching": "Read each scenario carefully and choose the best course of action. Your choices affect the outcome.",
    "quiz": "Answer each question to test your understanding. You will receive feedback after each submission.",
}

COMMON_ACTIVITY_KEYS = {
    "type",
    "title",
    "filename",
    "instructions",
    "instruction",
    "instruction_text",
    "download_results",
    "page_slug",
    "page_type",
}

REQUIRED_ACTIVITY_KEYS = {
    "dialog_cards": {"title", "filename", "cards", "instructions"},
    "sequencing": {"title", "filename", "items", "feedback_correct", "feedback_incorrect", "instructions"},
    "fill_blanks": {"title", "filename", "passages", "instructions"},
    "branching": {"title", "filename", "nodes", "endpoints", "instructions"},
    "quiz": {"title", "filename", "questions", "instructions"},
}

KNOWN_ACTIVITY_KEYS = {
    "dialog_cards": COMMON_ACTIVITY_KEYS | {"cards"},
    "sequencing": COMMON_ACTIVITY_KEYS | {"items", "feedback_correct", "feedback_incorrect"},
    "fill_blanks": COMMON_ACTIVITY_KEYS | {"passages"},
    "branching": COMMON_ACTIVITY_KEYS | {"nodes", "endpoints", "steps"},
    "quiz": COMMON_ACTIVITY_KEYS | {"questions"},
}


def _schema_warning(message):
    logging.warning("SCHEMA WARNING: %s", message)


def _safe_json(obj):
    """Serialize for inline script data without creating executable script tags."""
    return (
        json.dumps(obj)
        .replace("</", "<\\/")
        .replace("<!--", "<\\!--")
        .replace("<script", "<\\script")
        .replace("<SCRIPT", "<\\SCRIPT")
    )


def activity_instructions(data, act_type):
    if "instructions" in data:
        return data["instructions"]
    if "instruction" in data:
        logging.warning("SCHEMA WARNING: use instructions (plural)")
        return data["instruction"]
    return DEFAULT_INSTRUCTIONS[act_type]


def normalize_download_results(activity, label):
    """Normalize optional per-activity download_results flag."""
    if "download_results" not in activity:
        return False
    value = activity["download_results"]
    if isinstance(value, bool):
        return value
    if value:
        _schema_warning(
            f"{label} download_results should be bool; coercing truthy value {value!r} to True"
        )
        activity["download_results"] = True
        return True
    activity["download_results"] = False
    return False


def validate_activity_schema(all_activities):
    for module_key, activities in sorted(all_activities.items()):
        if not isinstance(activities, list):
            _schema_warning(f"{module_key} should map to a list of activity dicts")
            continue
        for index, activity in enumerate(activities):
            label = f"{module_key}[{index}]"
            if not isinstance(activity, dict):
                _schema_warning(f"{label} should be an activity dict")
                continue
            act_type = activity.get("type")
            if act_type not in GENERATORS:
                _schema_warning(f"{label} has unknown type {act_type!r}")
                continue
            normalize_download_results(activity, label)
            required = REQUIRED_ACTIVITY_KEYS[act_type]
            for key in sorted(required):
                if key == "instructions" and "instruction" in activity:
                    continue
                if key not in activity:
                    _schema_warning(f"{label} missing required key {key!r}")
            known = KNOWN_ACTIVITY_KEYS[act_type]
            for key in sorted(set(activity) - known):
                _schema_warning(f"{label} has unknown key {key!r}")


def resolve_course_id(course_id):
    """Resolve course ID from CLI first, then Canvas config."""
    if course_id:
        return str(course_id)
    config = get_config()
    return config.get("course_id")


def resolve_output_root(output_dir, course_id):
    """Return the absolute course-scoped staging root."""
    if output_dir:
        return os.path.abspath(os.path.expanduser(output_dir))
    return str((PLUGIN_ROOT / "courses" / str(course_id) / "staging" / "interactives").resolve())


def preview_url(course_id, module_key, filename):
    """Return the staging-server preview URL for a generated HTML file."""
    activity = Path(filename).stem
    return f"http://127.0.0.1:8111/courses/{course_id}/interactives/{module_key}/{activity}"


def print_review_banner(total, output_root, preview_urls, content_data_path):
    print(BANNER_LINE)
    print("REVIEW BEFORE DEPLOY")
    print(BANNER_LINE)
    print(f"Generated {total} interactive HTML files at:")
    print(output_root)
    print()
    print("Preview files via staging server:")
    for url in preview_urls:
        print(f"- {url}")
    print()
    print("After review, deploy to Canvas:")
    print(f"python scripts/deploy_interactives.py {content_data_path} --from-staging")
    print(BANNER_LINE)


# ============================================================
# SHARED CSS (ASU-branded, WCAG 2.1 AA compliant)
# ============================================================
SHARED_CSS = """
/* Scrollable: when activity content exceeds viewport, scroll inside the iframe so users see all cards/questions/steps without losing the wrapper's back button + checklist */
html, body { overflow-y: auto; }
html { min-height: 100%; }
:root {
    --asu-maroon: #8C1D40;
    --asu-gold: #FFC627;
    --white: #FFFFFF;
    --bg-light: #f8f4f0;
    --bg-gray: #f5f5f5;
    --text-dark: #333333;
    --text-muted: #595959;
    --success: #78BE20;
    --error: #dc3545;
    --border-radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
    font-family: Arial, Helvetica, 'Nimbus Sans L', 'Liberation Sans', FreeSans, sans-serif;
    background: var(--white);
    color: var(--text-dark);
    font-size: 16px;
    line-height: 1.6;
    padding: 16px 20px;
    max-width: 800px;
    margin: 0 auto;
    height: 100%;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}
h1, h2 { color: var(--asu-maroon); margin-bottom: 12px; font-size: 1.4em; word-wrap: break-word; overflow-wrap: break-word; }
h3 { color: var(--asu-maroon); margin-bottom: 8px; font-size: 1.15em; word-wrap: break-word; overflow-wrap: break-word; }
p { margin-bottom: 12px; word-wrap: break-word; overflow-wrap: break-word; }
button {
    cursor: pointer;
    font-size: 16px;
    border: none;
    border-radius: var(--border-radius);
    padding: 12px 28px;
    font-weight: 600;
    min-height: 44px;
    min-width: 44px;
    transition: background 0.2s, transform 0.1s;
}
/* High-contrast focus indicators */
:focus-visible { outline: 3px solid var(--asu-gold); outline-offset: 2px; box-shadow: 0 0 0 5px rgba(140, 29, 64, 0.3); }
button:focus-visible { outline: 3px solid var(--asu-gold); outline-offset: 2px; box-shadow: 0 0 0 5px rgba(140, 29, 64, 0.3); }
button:active { transform: scale(0.97); }
.btn-primary { background: var(--asu-maroon); color: var(--white); }
.btn-primary:hover { background: #6d1633; }
.btn-secondary { background: var(--bg-gray); color: var(--text-dark); border: 1px solid #ccc; }
.btn-secondary:hover { background: #e8e8e8; }
.btn-success { background: var(--success); color: var(--white); }
.btn-disabled { color: #6b7280; background: #e5e7eb; cursor: not-allowed; }
.download-results-panel {
    margin: 18px 0 0; padding: 16px; border: 1px solid #d1d5db;
    border-radius: var(--border-radius); background: var(--bg-light);
}
.download-results-panel[hidden] { display: none; }
.download-notes-label {
    display: block; margin-bottom: 6px; color: var(--text-dark);
    font-size: 14px; font-weight: 600;
}
.download-results-notes {
    display: block; width: 100%; min-height: 88px; margin-bottom: 12px;
    padding: 10px 12px; border: 1px solid #b8b8b8;
    border-radius: var(--border-radius); font: inherit; color: var(--text-dark);
    resize: vertical;
}
.download-results-notes:focus-visible {
    outline: 3px solid var(--asu-gold); outline-offset: 2px;
    box-shadow: 0 0 0 5px rgba(140, 29, 64, 0.3);
}
.download-results-button { background: var(--asu-maroon); color: var(--white); }
.download-results-button:hover { background: #6d1633; }
.feedback-box {
    padding: 15px 18px; border-radius: var(--border-radius); margin: 15px 0;
    font-size: 15px; line-height: 1.5; display: none;
}
.feedback-correct { background: #d4edda; border-left: 4px solid var(--success); color: #155724; }
.feedback-correct::before { content: "\\2713 "; font-weight: 700; }
.feedback-incorrect { background: #f8d7da; border-left: 4px solid var(--error); color: #721c24; }
.feedback-incorrect::before { content: "\\2717 "; font-weight: 700; }
.progress-bar {
    background: var(--bg-gray); border-radius: 20px; height: 8px; margin-bottom: 20px; overflow: hidden; flex-shrink: 0;
}
.progress-fill {
    background: var(--asu-maroon); height: 100%; border-radius: 20px; transition: width 0.4s ease;
}
.sr-only {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0,0,0,0); border: 0;
}
/* Skip-to-content link for keyboard users */
.skip-link {
    position: absolute; left: -9999px; top: 0;
    background: var(--asu-maroon); color: var(--white);
    padding: 8px 16px; z-index: 1000;
    font-size: 14px; text-decoration: none; border-radius: 0 0 4px 0;
}
.skip-link:focus { left: 0; }
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
@media (max-width: 600px) {
    body { padding: 10px 12px; font-size: 14px; }
    button { padding: 10px 20px; font-size: 14px; }
}
"""


DOWNLOAD_BUTTON_JS = """
function padDownloadNumber(value) {
    return String(value).padStart(2, '0');
}
function getDownloadDateParts() {
    const now = new Date();
    const yyyy = now.getFullYear();
    const mm = padDownloadNumber(now.getMonth() + 1);
    const dd = padDownloadNumber(now.getDate());
    const hh = padDownloadNumber(now.getHours());
    const min = padDownloadNumber(now.getMinutes());
    return {
        fileDate: yyyy + '-' + mm + '-' + dd,
        displayDate: yyyy + '-' + mm + '-' + dd + ' ' + hh + ':' + min
    };
}
function showDownloadResultsControls() {
    const panel = document.getElementById('download-results-panel');
    if (panel) panel.hidden = false;
}
function hideDownloadResultsControls() {
    const panel = document.getElementById('download-results-panel');
    if (panel) panel.hidden = true;
}
function downloadActivityResults() {
    const parts = getDownloadDateParts();
    const notesEl = document.getElementById('download-results-notes');
    const notes = notesEl ? notesEl.value : '';
    const body = (typeof buildResultsBody === 'function') ? buildResultsBody() : '';
    const summary = [
        'ASU Self-Assessment Results',
        'Activity: ' + RESULTS_ACTIVITY_TITLE,
        'Date: ' + parts.displayDate,
        '-----',
        body,
        '-----',
        'Personal notes:',
        notes
    ].join('\\n');
    const blob = new Blob([summary], { type: 'text/plain;charset=utf-8' });
    const link = document.getElementById('download-results-link');
    const url = URL.createObjectURL(blob);
    link.href = url;
    link.download = 'results-' + RESULTS_ACTIVITY_SLUG + '-' + parts.fileDate + '.txt';
    link.click();
    setTimeout(function() { URL.revokeObjectURL(url); }, 0);
}
"""


def download_results_enabled(data):
    return bool(data.get("download_results", False))


def download_results_slug(data):
    raw = Path(data.get("filename", "")).stem or data.get("title", "activity")
    slug = re.sub(r"[^a-z0-9]+", "-", raw.lower()).strip("-")
    return slug or "activity"


def download_results_markup(data):
    if not download_results_enabled(data):
        return ""
    slug = html_module.escape(download_results_slug(data), quote=True)
    return f"""
<div class="download-results-panel" id="download-results-panel" hidden>
  <label class="download-notes-label" for="download-results-notes">Personal notes (optional)</label>
  <textarea id="download-results-notes" class="download-results-notes" aria-label="Personal notes (optional)" rows="4"></textarea>
  <button type="button" class="download-results-button" onclick="downloadActivityResults()">📄 Download my results</button>
  <a id="download-results-link" download="results-{slug}.txt" hidden></a>
</div>"""


def download_results_context_js(data):
    if not download_results_enabled(data):
        return ""
    title = _safe_json(data["title"])
    slug = _safe_json(download_results_slug(data))
    return f"""
const RESULTS_ACTIVITY_TITLE = {title};
const RESULTS_ACTIVITY_SLUG = {slug};
{DOWNLOAD_BUTTON_JS}
"""


# ============================================================
# TEMPLATE: DIALOG CARDS
# ============================================================
def generate_dialog_cards(data):
    cards_json = _safe_json(data["cards"])
    title = html_module.escape(data["title"])
    instructions = html_module.escape(activity_instructions(data, "dialog_cards"))
    download_markup = download_results_markup(data)
    download_context_js = download_results_context_js(data)
    download_progress_js = ""
    download_body_js = ""
    if download_results_enabled(data):
        download_progress_js = """
    if (total > 0 && flippedSet.size === total) {
        showDownloadResultsControls();
    } else {
        hideDownloadResultsControls();
    }"""
        download_body_js = """
function buildResultsBody() {
    return flippedSet.size + ' of ' + CARDS.length + ' cards reviewed';
}
"""
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
{SHARED_CSS}
.cards-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin: 20px 0; }}
.card-container {{ perspective: 1000px; height: 200px; cursor: pointer; }}
.card-inner {{
    position: relative; width: 100%; height: 100%;
    transition: transform 0.5s; transform-style: preserve-3d;
}}
.card-container.flipped .card-inner {{ transform: rotateY(180deg); }}
.card-front, .card-back {{
    position: absolute; width: 100%; height: 100%; backface-visibility: hidden;
    border-radius: var(--border-radius); display: flex; align-items: center;
    justify-content: center; padding: 20px; text-align: center;
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
}}
.card-front {{
    background: var(--asu-maroon); color: var(--white);
    font-size: 1.2em; font-weight: 600;
    word-wrap: break-word; overflow-wrap: break-word;
}}
.card-back {{
    background: var(--white); color: var(--text-dark); border: 2px solid var(--asu-maroon);
    transform: rotateY(180deg); font-size: 0.9em; line-height: 1.4;
    overflow: hidden; align-items: flex-start;
    word-wrap: break-word; overflow-wrap: break-word;
}}
.card-hint {{ font-size: 12px; color: var(--text-muted); margin-top: 8px; }}
.controls {{ display: flex; gap: 12px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }}
.counter {{ text-align: center; color: var(--text-muted); font-size: 14px; margin-bottom: 8px; }}
@media (prefers-reduced-motion: reduce) {{
    .card-inner {{ transition: none; }}
}}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to activity</a>
<main id="main-content">
<h1>{title}</h1>
<p id="instructions">{instructions}</p>
<div class="counter" aria-live="polite"><span id="flipped-count">0</span> of <span id="total-count">0</span> cards reviewed</div>
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Cards reviewed" id="progress-bar"><div class="progress-fill" id="progress" style="width:0%"></div></div>
<div class="cards-grid" id="cards-grid" role="group" aria-label="Flashcards" aria-describedby="instructions"></div>
<div class="controls">
	  <button class="btn-secondary" onclick="shuffleCards()" aria-label="Shuffle cards">Shuffle</button>
	  <button class="btn-secondary" onclick="resetCards()" aria-label="Reset all cards">Reset</button>
	</div>
	{download_markup}
	</main>
	<script>
	const CARDS = {cards_json};
	let flippedSet = new Set();
function render(cards) {{
    const grid = document.getElementById('cards-grid');
    grid.replaceChildren();
    document.getElementById('total-count').textContent = cards.length;
    cards.forEach((c, i) => {{
        const container = document.createElement('div');
        container.className = 'card-container';
        container.setAttribute('tabindex', '0');
        container.setAttribute('role', 'button');
        container.setAttribute('aria-label', 'Flashcard ' + (i+1) + ' of ' + cards.length + ': ' + c.front + '. Press Enter to flip.');
        const inner = document.createElement('div');
        inner.className = 'card-inner';
        const front = document.createElement('div');
        front.className = 'card-front';
        front.textContent = c.front;
        const back = document.createElement('div');
        back.className = 'card-back';
        back.textContent = c.back;
        inner.append(front, back);
        container.appendChild(inner);
        container.addEventListener('click', () => flipCard(container, i));
        container.addEventListener('keydown', e => {{ if(e.key==='Enter'||e.key===' ') {{ e.preventDefault(); flipCard(container, i); }} }});
        grid.appendChild(container);
    }});
    updateProgress();
}}
function flipCard(el, idx) {{
    el.classList.toggle('flipped');
    if(el.classList.contains('flipped')) flippedSet.add(idx); else flippedSet.delete(idx);
    updateProgress();
}}
function updateProgress() {{
    const total = CARDS.length;
    const pct = Math.round(flippedSet.size/total*100);
	    document.getElementById('flipped-count').textContent = flippedSet.size;
	    document.getElementById('progress').style.width = pct+'%';
	    document.getElementById('progress-bar').setAttribute('aria-valuenow', pct);
	{download_progress_js}
	}}
function shuffleCards() {{
    const arr = [...CARDS];
    for(let i=arr.length-1;i>0;i--){{ const j=Math.floor(Math.random()*(i+1));[arr[i],arr[j]]=[arr[j],arr[i]]; }}
    flippedSet.clear();
    render(arr);
	}}
	function resetCards() {{ flippedSet.clear(); render(CARDS); }}
	{download_body_js}
	{download_context_js}
	render(CARDS);
	</script>
	</body></html>"""


# ============================================================
# TEMPLATE: SEQUENCING
# ============================================================
def generate_sequencing(data):
    raw_items = data["items"]
    normalized_items = []
    for idx, item in enumerate(raw_items):
        if isinstance(item, str):
            normalized_items.append({"text": item, "position": idx + 1})
        elif isinstance(item, dict):
            normalized_items.append({
                "text": item.get("text", ""),
                "position": item.get("position", idx + 1),
            })
        else:
            normalized_items.append({"text": str(item), "position": idx + 1})
    items_json = _safe_json(normalized_items)
    title = html_module.escape(data["title"])
    instructions = html_module.escape(activity_instructions(data, "sequencing"))
    fb_correct = _safe_json(data["feedback_correct"])
    fb_incorrect = _safe_json(data["feedback_incorrect"])
    download_markup = download_results_markup(data)
    download_context_js = download_results_context_js(data)
    download_hide_js = "hideDownloadResultsControls();" if download_results_enabled(data) else ""
    download_show_js = "showDownloadResultsControls();" if download_results_enabled(data) else ""
    download_body_js = ""
    if download_results_enabled(data):
        download_body_js = """
function buildResultsBody() {
    const lines = ['Final order vs canonical:'];
    placements.forEach((itemIdx, slotIdx) => {
        const selected = itemIdx === null ? '[empty]' : shuffled[itemIdx].text;
        const canonical = ITEMS.find(item => item.position === slotIdx + 1);
        lines.push((slotIdx + 1) + '. Selected: ' + selected + ' | Correct: ' + (canonical ? canonical.text : '[not found]'));
    });
    return lines.join('\\n');
}
"""
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
{SHARED_CSS}
.seq-container {{ display: flex; gap: 20px; margin: 20px 0; flex-wrap: wrap; }}
.slots-col, .items-col {{ flex: 1; min-width: 250px; }}
.slot {{
    min-height: 52px; border: 2px dashed #ccc; border-radius: var(--border-radius);
    padding: 12px 16px; margin-bottom: 10px; display: flex; align-items: center;
    gap: 10px; transition: border-color 0.2s;
}}
.slot-num {{
    background: var(--asu-maroon); color: var(--white); width: 28px; height: 28px;
    border-radius: 50%; display: flex; align-items: center; justify-content: center;
    font-weight: 700; font-size: 14px; flex-shrink: 0;
}}
.slot.filled {{ border-color: var(--asu-maroon); border-style: solid; background: #fdf6f8; cursor: pointer; }}
.slot.correct {{ border-color: var(--success); background: #d4edda; }}
.slot.incorrect {{ border-color: var(--error); background: #f8d7da; }}
.item-card {{
    background: var(--white); border: 2px solid var(--asu-maroon); border-radius: var(--border-radius);
    padding: 12px 16px; margin-bottom: 10px; cursor: pointer;
    transition: background 0.2s, transform 0.1s; min-height: 44px;
    word-wrap: break-word; overflow-wrap: break-word;
}}
.item-card:hover {{ background: #fdf6f8; }}
.item-card:focus-visible {{ outline: 3px solid var(--asu-gold); outline-offset: 2px; }}
.item-card.selected {{ background: var(--asu-gold); border-color: var(--asu-gold); color: var(--text-dark); }}
.item-card.used {{ opacity: 0.4; pointer-events: none; }}
.controls {{ display: flex; gap: 12px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }}
.slot-text {{ font-size: 15px; overflow: hidden; text-overflow: ellipsis; flex: 1; word-wrap: break-word; overflow-wrap: break-word; }}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to activity</a>
<main id="main-content">
<h1>{title}</h1>
<p id="instructions">{instructions}</p>
<p style="color:var(--text-muted);font-size:14px;">Click an item on the right, then click a numbered slot on the left to place it. Click a filled slot to remove it.</p>
<div class="seq-container" aria-describedby="instructions">
  <div class="slots-col" id="slots" role="list" aria-label="Answer slots"></div>
  <div class="items-col" id="items" role="listbox" aria-label="Available items"></div>
</div>
<div class="controls">
  <button class="btn-primary" id="check-btn" onclick="checkAnswer()">Check Answer</button>
  <button class="btn-secondary" id="retry-btn" onclick="retry()" style="display:none">Try Again</button>
  <button class="btn-secondary" id="show-btn" onclick="showAnswer()" style="display:none">Show Answer</button>
  <button class="btn-secondary" onclick="resetAll()">Reset</button>
	</div>
	<div class="feedback-box" id="feedback" role="alert"></div>
	{download_markup}
	</main>
	<script>
const ITEMS = {items_json};
const FB_CORRECT = {fb_correct};
const FB_INCORRECT = {fb_incorrect};
let selectedItem = null;
let placements = new Array(ITEMS.length).fill(null);
let attempts = 0;
let shuffled = [];
function setFeedback(fb, className, label, message) {{
    fb.style.display = 'block';
    fb.className = className;
    fb.replaceChildren();
    if (label) {{
        const strong = document.createElement('strong');
        strong.textContent = label;
        fb.appendChild(strong);
        fb.appendChild(document.createTextNode(' '));
    }}
    fb.appendChild(document.createTextNode(message));
}}
function init() {{
    shuffled = [...ITEMS].sort(() => Math.random() - 0.5);
    const slotsEl = document.getElementById('slots');
    const itemsEl = document.getElementById('items');
    slotsEl.replaceChildren();
    itemsEl.replaceChildren();
    placements = new Array(ITEMS.length).fill(null);
    selectedItem = null;
    attempts = 0;
    document.getElementById('feedback').style.display = 'none';
	    document.getElementById('check-btn').style.display = '';
	    document.getElementById('retry-btn').style.display = 'none';
	    document.getElementById('show-btn').style.display = 'none';
	    {download_hide_js}
	    for (let i = 0; i < ITEMS.length; i++) {{
        const slot = document.createElement('div');
        slot.className = 'slot';
        slot.setAttribute('role', 'listitem');
        slot.setAttribute('tabindex', '0');
        slot.setAttribute('aria-label', 'Position ' + (i+1) + ', empty');
        const slotNum = document.createElement('span');
        slotNum.className = 'slot-num';
        slotNum.textContent = i + 1;
        const slotText = document.createElement('span');
        slotText.className = 'slot-text';
        slotText.id = 'slot-text-' + i;
        slot.append(slotNum, slotText);
        slot.addEventListener('click', () => slotClick(i));
        slot.addEventListener('keydown', e => {{ if(e.key==='Enter'||e.key===' ') {{ e.preventDefault(); slotClick(i); }} }});
        slotsEl.appendChild(slot);
    }}
    shuffled.forEach((item, idx) => {{
        const card = document.createElement('div');
        card.className = 'item-card';
        card.setAttribute('tabindex', '0');
        card.setAttribute('role', 'option');
        card.setAttribute('data-idx', idx);
        card.textContent = item.text;
        card.addEventListener('click', () => selectItem(idx, card));
        card.addEventListener('keydown', e => {{ if(e.key==='Enter'||e.key===' ') {{ e.preventDefault(); selectItem(idx, card); }} }});
        itemsEl.appendChild(card);
    }});
}}
function selectItem(idx, el) {{
    document.querySelectorAll('.item-card').forEach(c => c.classList.remove('selected'));
    if (selectedItem === idx) {{ selectedItem = null; return; }}
    selectedItem = idx;
    el.classList.add('selected');
}}
function slotClick(slotIdx) {{
    if (placements[slotIdx] !== null) {{
        const removedIdx = placements[slotIdx];
        placements[slotIdx] = null;
        document.getElementById('slot-text-'+slotIdx).textContent = '';
        const slot = document.querySelectorAll('.slot')[slotIdx];
        slot.classList.remove('filled','correct','incorrect');
        slot.setAttribute('aria-label', 'Position '+(slotIdx+1)+', empty');
        document.querySelectorAll('.item-card')[removedIdx].classList.remove('used');
        return;
    }}
    if (selectedItem === null) return;
    placements[slotIdx] = selectedItem;
    document.getElementById('slot-text-'+slotIdx).textContent = shuffled[selectedItem].text;
    const slot = document.querySelectorAll('.slot')[slotIdx];
    slot.classList.add('filled');
    slot.setAttribute('aria-label', 'Position '+(slotIdx+1)+': '+shuffled[selectedItem].text);
    document.querySelectorAll('.item-card')[selectedItem].classList.add('used');
    document.querySelectorAll('.item-card').forEach(c => c.classList.remove('selected'));
    selectedItem = null;
}}
function checkAnswer() {{
    if (placements.some(p => p === null)) {{
        const fb = document.getElementById('feedback');
        fb.style.display = 'block';
        fb.className = 'feedback-box feedback-incorrect';
        fb.textContent = 'Please place all items in the slots before checking your answer.';
        return;
    }}
    attempts++;
    let allCorrect = true;
    placements.forEach((itemIdx, slotIdx) => {{
        const slot = document.querySelectorAll('.slot')[slotIdx];
        if (shuffled[itemIdx].position === slotIdx + 1) {{
            slot.classList.add('correct');
            slot.classList.remove('incorrect');
        }} else {{
            slot.classList.add('incorrect');
            slot.classList.remove('correct');
            allCorrect = false;
        }}
    }});
    const fb = document.getElementById('feedback');
    fb.style.display = 'block';
    if (allCorrect) {{
	        setFeedback(fb, 'feedback-box feedback-correct', 'Correct!', FB_CORRECT);
	        document.getElementById('check-btn').style.display = 'none';
	        {download_show_js}
	    }} else {{
        setFeedback(fb, 'feedback-box feedback-incorrect', 'Not quite.', FB_INCORRECT);
        document.getElementById('retry-btn').style.display = '';
        document.getElementById('check-btn').style.display = 'none';
        if (attempts >= 2) document.getElementById('show-btn').style.display = '';
    }}
}}
function retry() {{
    placements.forEach((_, i) => {{
        const slot = document.querySelectorAll('.slot')[i];
        if (slot.classList.contains('incorrect')) {{
            const itemIdx = placements[i];
            placements[i] = null;
            document.getElementById('slot-text-'+i).textContent = '';
            slot.classList.remove('filled','incorrect');
            slot.setAttribute('aria-label', 'Position '+(i+1)+', empty');
            document.querySelectorAll('.item-card')[itemIdx].classList.remove('used');
        }}
    }});
	    document.getElementById('feedback').style.display = 'none';
	    document.getElementById('retry-btn').style.display = 'none';
	    document.getElementById('check-btn').style.display = '';
	    {download_hide_js}
	}}
function showAnswer() {{
    placements = new Array(ITEMS.length).fill(null);
    document.querySelectorAll('.item-card').forEach(c => c.classList.add('used'));
    ITEMS.forEach(item => {{
        const slotIdx = item.position - 1;
        const shuffIdx = shuffled.findIndex(s => s.text === item.text);
        placements[slotIdx] = shuffIdx;
        document.getElementById('slot-text-'+slotIdx).textContent = item.text;
        const slot = document.querySelectorAll('.slot')[slotIdx];
        slot.classList.add('filled','correct');
        slot.classList.remove('incorrect');
        slot.setAttribute('aria-label', 'Position '+(slotIdx+1)+': '+item.text);
    }});
    const fb = document.getElementById('feedback');
    setFeedback(fb, 'feedback-box feedback-correct', 'Answer revealed.', FB_CORRECT);
	    document.getElementById('check-btn').style.display = 'none';
	    document.getElementById('retry-btn').style.display = 'none';
	    document.getElementById('show-btn').style.display = 'none';
	    {download_show_js}
	}}
	function resetAll() {{ init(); }}
	{download_body_js}
	{download_context_js}
	init();
	</script>
	</body></html>"""


# ============================================================
# TEMPLATE: FILL IN THE BLANKS
# ============================================================
def generate_fill_blanks(data):
    title = html_module.escape(data["title"])
    instructions = html_module.escape(activity_instructions(data, "fill_blanks"))
    passages_json = _safe_json(data["passages"])
    download_markup = download_results_markup(data)
    download_context_js = download_results_context_js(data)
    download_hide_js = "hideDownloadResultsControls();" if download_results_enabled(data) else ""
    download_show_js = "showDownloadResultsControls();" if download_results_enabled(data) else ""
    download_body_js = ""
    if download_results_enabled(data):
        download_body_js = """
function buildResultsBody() {
    const lines = ['Blank answers:'];
    let blankNumber = 1;
    PASSAGES.forEach((p, pi) => {
        p.segments.forEach((seg, si) => {
            if (seg.type !== 'blank') return;
            const input = document.getElementById('blank-' + pi + '-' + si);
            const answer = input && input.value ? input.value : '[blank]';
            const correct = input ? seg.answers.some(a => normalize(a) === normalize(input.value)) : false;
            lines.push('Blank ' + blankNumber + ': ' + answer + ' | ' + (correct ? 'Correct' : 'Incorrect') + ' | Correct answer: ' + seg.answers[0]);
            blankNumber++;
        });
    });
    return lines.join('\\n');
}
"""
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
{SHARED_CSS}
.passage {{
    background: var(--bg-light); border-left: 4px solid var(--asu-maroon);
    padding: 20px; border-radius: 0 var(--border-radius) var(--border-radius) 0;
    margin: 20px 0; line-height: 1.8; font-size: 16px;
    word-wrap: break-word; overflow-wrap: break-word;
}}
.blank-input {{
    border: none; border-bottom: 2px solid var(--asu-maroon); background: transparent;
    font-size: 16px; padding: 2px 8px; min-width: 120px; max-width: 200px;
    font-family: inherit; color: var(--text-dark);
}}
.blank-input:focus {{ outline: none; border-bottom-color: var(--asu-gold); border-bottom-width: 3px; }}
.blank-input.correct {{ border-bottom-color: var(--success); background: #d4edda; }}
.blank-input.incorrect {{ border-bottom-color: var(--error); background: #f8d7da; }}
.hint {{ font-size: 13px; color: var(--error); margin-top: 4px; display: none; }}
.passage-feedback {{
    margin-top: 12px; padding: 12px; border-radius: var(--border-radius);
    background: #d4edda; border-left: 4px solid var(--success); display: none;
    font-size: 15px; line-height: 1.5;
}}
.controls {{ display: flex; gap: 12px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to activity</a>
<main id="main-content">
<h1>{title}</h1>
<p id="instructions">{instructions}</p>
<div id="passages-container" aria-describedby="instructions"></div>
<div class="controls">
  <button class="btn-primary" id="check-btn" onclick="checkAll()">Check Answers</button>
	  <button class="btn-secondary" id="retry-btn" onclick="retryIncorrect()" style="display:none">Try Again</button>
	  <button class="btn-secondary" id="show-btn" onclick="showAll()" style="display:none">Show Answers</button>
	</div>
	{download_markup}
	</main>
	<script>
const PASSAGES = {passages_json};
let attempts = 0;
function init() {{
    const container = document.getElementById('passages-container');
    container.replaceChildren();
    attempts = 0;
	    document.getElementById('check-btn').style.display = '';
	    document.getElementById('retry-btn').style.display = 'none';
	    document.getElementById('show-btn').style.display = 'none';
	    {download_hide_js}
	    PASSAGES.forEach((p, pi) => {{
        const div = document.createElement('div');
        div.className = 'passage';
        p.segments.forEach((seg, si) => {{
            if (seg.type === 'text') {{
                div.appendChild(document.createTextNode(seg.value));
            }} else {{
                const id = 'blank-'+pi+'-'+si;
                const input = document.createElement('input');
                input.type = 'text';
                input.className = 'blank-input';
                input.id = id;
                input.dataset.passage = pi;
                input.dataset.seg = si;
                input.setAttribute('aria-label', seg.hint);
                input.autocomplete = 'off';
                input.spellcheck = false;
                const hint = document.createElement('span');
                hint.className = 'hint';
                hint.setAttribute('role', 'status');
                hint.id = 'hint-'+pi+'-'+si;
                hint.textContent = 'Hint: '+seg.hint;
                div.append(input, hint);
            }}
        }});
        const feedback = document.createElement('div');
        feedback.className = 'passage-feedback';
        feedback.setAttribute('aria-live', 'polite');
        feedback.id = 'pfb-'+pi;
        feedback.textContent = p.feedback;
        div.appendChild(feedback);
        container.appendChild(div);
    }});
}}
function normalize(s) {{ return s.trim().toLowerCase().replace(/[^a-z0-9\u03b1-\u03c9\u0391-\u03a9\u207a\u207b\u208a\u208b+\\-\\/]/g, ''); }}
function checkAll() {{
    attempts++;
    let allCorrect = true;
    PASSAGES.forEach((p, pi) => {{
        let passageCorrect = true;
        p.segments.forEach((seg, si) => {{
            if (seg.type !== 'blank') return;
            const input = document.getElementById('blank-'+pi+'-'+si);
            const val = normalize(input.value);
            const correct = seg.answers.some(a => normalize(a) === val);
            if (correct) {{
                input.classList.add('correct');
                input.classList.remove('incorrect');
                input.readOnly = true;
                document.getElementById('hint-'+pi+'-'+si).style.display = 'none';
            }} else {{
                input.classList.add('incorrect');
                input.classList.remove('correct');
                document.getElementById('hint-'+pi+'-'+si).style.display = 'block';
                allCorrect = false;
                passageCorrect = false;
            }}
        }});
        if (passageCorrect) document.getElementById('pfb-'+pi).style.display = 'block';
    }});
	    if (allCorrect) {{
	        document.getElementById('check-btn').style.display = 'none';
	        {download_show_js}
	    }} else {{
        document.getElementById('retry-btn').style.display = '';
        document.getElementById('check-btn').style.display = 'none';
        if (attempts >= 2) document.getElementById('show-btn').style.display = '';
    }}
}}
function retryIncorrect() {{
    document.querySelectorAll('.blank-input.incorrect').forEach(el => {{
        el.value = '';
        el.classList.remove('incorrect');
    }});
	    document.querySelectorAll('.hint').forEach(h => h.style.display = 'none');
	    document.getElementById('retry-btn').style.display = 'none';
	    document.getElementById('check-btn').style.display = '';
	    {download_hide_js}
	}}
function showAll() {{
    PASSAGES.forEach((p, pi) => {{
        p.segments.forEach((seg, si) => {{
            if (seg.type !== 'blank') return;
            const input = document.getElementById('blank-'+pi+'-'+si);
            input.value = seg.answers[0];
            input.classList.add('correct');
            input.classList.remove('incorrect');
            input.readOnly = true;
            document.getElementById('hint-'+pi+'-'+si).style.display = 'none';
        }});
        document.getElementById('pfb-'+pi).style.display = 'block';
    }});
	    document.getElementById('check-btn').style.display = 'none';
	    document.getElementById('retry-btn').style.display = 'none';
	    document.getElementById('show-btn').style.display = 'none';
	    {download_show_js}
	}}
	{download_body_js}
	{download_context_js}
	init();
	</script>
	</body></html>"""


# ============================================================
# TEMPLATE: BRANCHING SCENARIO
# ============================================================
def generate_branching(data):
    title = html_module.escape(data["title"])
    instructions = html_module.escape(activity_instructions(data, "branching"))
    nodes_json = _safe_json(data["nodes"])
    endpoints_json = _safe_json(data["endpoints"])
    download_markup = download_results_markup(data)
    download_context_js = download_results_context_js(data)
    download_hide_js = "hideDownloadResultsControls();" if download_results_enabled(data) else ""
    download_show_js = "showDownloadResultsControls();" if download_results_enabled(data) else ""
    download_body_js = ""
    if download_results_enabled(data):
        download_body_js = """
function buildResultsBody() {
    if (!history.length) return 'Path taken: No choices recorded.';
    const lines = ['Path taken:'];
    history.forEach((entry, i) => {
        const node = NODES[entry.nodeIdx];
        const opt = node.options[entry.optIdx];
        lines.push('Decision ' + (i + 1) + ': ' + node.question);
        lines.push('Choice: ' + opt.text + ' | ' + (entry.correct ? 'Correct' : 'Incorrect'));
    });
    lines.push('Score: ' + score + '/' + NODES.length);
    return lines.join('\\n');
}
"""
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
{SHARED_CSS}
.scenario-box {{
    background: var(--bg-light); border-left: 4px solid var(--asu-maroon);
    padding: 20px; border-radius: 0 var(--border-radius) var(--border-radius) 0;
    margin: 15px 0; font-size: 16px; line-height: 1.6;
    word-wrap: break-word; overflow-wrap: break-word;
}}
.option-card {{
    border: 2px solid #ddd; border-radius: var(--border-radius);
    padding: 14px 18px; margin: 10px 0; cursor: pointer;
    transition: border-color 0.2s, background 0.2s; min-height: 44px;
    word-wrap: break-word; overflow-wrap: break-word;
}}
.option-card:hover {{ border-color: var(--asu-maroon); background: #fdf6f8; }}
.option-card:focus-visible {{ outline: 3px solid var(--asu-gold); outline-offset: 2px; }}
.option-card.selected {{ border-color: var(--asu-maroon); background: #fdf6f8; }}
.option-card.correct-choice {{ border-color: var(--success); background: #d4edda; }}
.option-card.incorrect-choice {{ border-color: var(--error); background: #f8d7da; }}
.option-card.disabled {{ pointer-events: none; opacity: 0.7; }}
.option-label {{ font-weight: 600; color: var(--asu-maroon); margin-right: 8px; }}
.breadcrumb {{
    display: flex; gap: 8px; align-items: center; margin: 15px 0;
    font-size: 13px; color: var(--text-muted); flex-wrap: wrap;
}}
.breadcrumb span {{ background: var(--bg-gray); padding: 4px 10px; border-radius: 12px; }}
.breadcrumb span.active {{ background: var(--asu-maroon); color: var(--white); }}
.breadcrumb .arrow {{ background: none; padding: 0; }}
.endpoint {{
    text-align: center; padding: 30px; border: 2px solid var(--asu-maroon);
    border-radius: var(--border-radius); margin: 20px 0;
}}
.endpoint h3 {{ margin-bottom: 10px; }}
.score {{ font-size: 2em; color: var(--asu-maroon); font-weight: 700; margin: 10px 0; }}
.controls {{ display: flex; gap: 12px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to activity</a>
<main id="main-content">
<h1>{title}</h1>
<p id="instructions" style="color:var(--text-muted);font-size:14px;margin-bottom:8px;">{instructions}</p>
	<div class="breadcrumb" id="breadcrumb" aria-label="Progress"></div>
	<div id="content-area" aria-describedby="instructions"></div>
	<div class="controls" id="controls"></div>
	{download_markup}
	</main>
	<script>
const NODES = {nodes_json};
const ENDPOINTS = {endpoints_json};
let currentNode = 0;
let score = 0;
let history = [];
	function renderNode(idx) {{
	    currentNode = idx;
	    {download_hide_js}
	    const node = NODES[idx];
	    const area = document.getElementById('content-area');
    const controls = document.getElementById('controls');
    const bc = document.getElementById('breadcrumb');
    bc.replaceChildren();
    for (let i = 0; i <= idx; i++) {{
        if (i > 0) {{ const arrow = document.createElement('span'); arrow.className='arrow'; arrow.textContent='\u2192'; bc.appendChild(arrow); }}
        const s = document.createElement('span');
        s.textContent = 'Decision ' + (i+1);
        if (i === idx) s.className = 'active';
        bc.appendChild(s);
    }}
    area.replaceChildren();
    const scenario = document.createElement('div');
    scenario.className = 'scenario-box';
    scenario.textContent = node.scenario;
    const question = document.createElement('h3');
    question.textContent = node.question;
    const group = document.createElement('div');
    group.setAttribute('role', 'radiogroup');
    group.setAttribute('aria-label', 'Choose your answer');
    const labels = ['A','B','C','D'];
    node.options.forEach((opt, oi) => {{
        const option = document.createElement('div');
        option.className = 'option-card';
        option.tabIndex = 0;
        option.setAttribute('role', 'radio');
        option.setAttribute('aria-checked', 'false');
        option.dataset.idx = oi;
        const label = document.createElement('span');
        label.className = 'option-label';
        label.textContent = labels[oi] + '.';
        option.append(label, document.createTextNode(' ' + opt.text));
        option.addEventListener('click', () => selectOption(idx, oi));
        group.appendChild(option);
    }});
    const feedback = document.createElement('div');
    feedback.className = 'feedback-box';
    feedback.id = 'option-feedback';
    feedback.setAttribute('role', 'alert');
    area.append(scenario, question, group, feedback);
    controls.replaceChildren();
    const continueBtn = document.createElement('button');
    continueBtn.className = 'btn-primary';
    continueBtn.id = 'continue-btn';
    continueBtn.style.display = 'none';
    continueBtn.textContent = 'Continue';
    continueBtn.addEventListener('click', advance);
    controls.appendChild(continueBtn);
}}
function selectOption(nodeIdx, optIdx) {{
    const node = NODES[nodeIdx];
    const opt = node.options[optIdx];
    document.querySelectorAll('.option-card').forEach(c => {{
        c.classList.add('disabled');
        c.setAttribute('aria-checked', 'false');
    }});
    const selected = document.querySelectorAll('.option-card')[optIdx];
    selected.setAttribute('aria-checked', 'true');
    if (opt.correct) {{
        selected.classList.add('correct-choice');
        score++;
    }} else {{
        selected.classList.add('incorrect-choice');
        node.options.forEach((o, i) => {{
            if (o.correct) document.querySelectorAll('.option-card')[i].classList.add('correct-choice');
        }});
    }}
    const fb = document.getElementById('option-feedback');
    fb.style.display = 'block';
    fb.className = 'feedback-box ' + (opt.correct ? 'feedback-correct' : 'feedback-incorrect');
    fb.textContent = opt.feedback;
    history.push({{ nodeIdx, optIdx, correct: opt.correct }});
    document.getElementById('continue-btn').style.display = '';
    document.getElementById('continue-btn').focus();
}}
function advance() {{
    if (currentNode + 1 < NODES.length) {{
        renderNode(currentNode + 1);
    }} else {{
        renderEndpoint();
    }}
}}
function renderEndpoint() {{
    const total = NODES.length;
    const ep = ENDPOINTS.find(e => score >= e.minScore) || ENDPOINTS[ENDPOINTS.length-1];
    const area = document.getElementById('content-area');
    const controls = document.getElementById('controls');
    const bc = document.getElementById('breadcrumb');
    bc.replaceChildren();
    area.replaceChildren();
    const endpoint = document.createElement('div');
    endpoint.className = 'endpoint';
    const heading = document.createElement('h3');
    heading.textContent = ep.title;
    const scoreBox = document.createElement('div');
    scoreBox.className = 'score';
    scoreBox.textContent = score + '/' + total;
    const message = document.createElement('p');
    message.textContent = ep.message;
    endpoint.append(heading, scoreBox, message);
    area.appendChild(endpoint);
    controls.replaceChildren();
    const restartBtn = document.createElement('button');
    restartBtn.className = 'btn-primary';
    restartBtn.textContent = 'Try Again';
    restartBtn.addEventListener('click', restart);
    controls.appendChild(restartBtn);
	    {download_show_js}
	}}
	function restart() {{ score = 0; history = []; renderNode(0); }}
document.addEventListener('keydown', function(e) {{
    if ((e.key === 'Enter' || e.key === ' ') && e.target.getAttribute('role') === 'radio') {{
        e.preventDefault();
        e.target.click();
	    }}
	}});
	{download_body_js}
	{download_context_js}
	renderNode(0);
	</script>
	</body></html>"""


# ============================================================
# TEMPLATE: QUIZ (QUESTION SET)
# ============================================================
def generate_quiz(data):
    title = html_module.escape(data["title"])
    instructions = html_module.escape(activity_instructions(data, "quiz"))
    questions_json = _safe_json(data["questions"])
    download_markup = download_results_markup(data)
    download_context_js = download_results_context_js(data)
    download_hide_js = "hideDownloadResultsControls();" if download_results_enabled(data) else ""
    download_show_js = "showDownloadResultsControls();" if download_results_enabled(data) else ""
    download_body_js = ""
    if download_results_enabled(data):
        download_body_js = """
function buildResultsBody() {
    const lines = ['Score: ' + score + '/' + QS.length];
    QS.forEach((q, i) => {
        let selected = '[unanswered]';
        let correctAnswer = '[not found]';
        let isCorrect = false;
        if (q.type === 'mc') {
            if (answers[i] !== null && q.options[answers[i]]) {
                selected = q.options[answers[i]].text;
                isCorrect = !!q.options[answers[i]].correct;
            }
            const correctOption = q.options.find(opt => opt.correct);
            if (correctOption) correctAnswer = correctOption.text;
        } else {
            if (answers[i] !== null) {
                selected = answers[i] ? 'True' : 'False';
                isCorrect = answers[i] === q.correct;
            }
            correctAnswer = q.correct ? 'True' : 'False';
        }
        lines.push('Q' + (i + 1) + ': ' + q.stem);
        lines.push('Selected: ' + selected);
        lines.push('Correct: ' + correctAnswer);
        lines.push('Result: ' + (isCorrect ? 'Correct' : 'Incorrect'));
    });
    return lines.join('\\n');
}
"""
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<style>
{SHARED_CSS}
.question-container {{ margin: 20px 0; }}
.stem {{ font-size: 16px; margin-bottom: 15px; line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word; }}
.option {{
    border: 2px solid #ddd; border-radius: var(--border-radius);
    padding: 12px 16px; margin: 8px 0; cursor: pointer;
    transition: border-color 0.2s, background 0.2s; min-height: 44px;
    display: flex; align-items: center; gap: 10px;
    word-wrap: break-word; overflow-wrap: break-word;
}}
.option:hover {{ border-color: var(--asu-maroon); background: #fdf6f8; }}
.option:focus-visible {{ outline: 3px solid var(--asu-gold); outline-offset: 2px; }}
.option.selected {{ border-color: var(--asu-maroon); background: #fdf6f8; }}
.option.correct-ans {{ border-color: var(--success); background: #d4edda; }}
.option.incorrect-ans {{ border-color: var(--error); background: #f8d7da; }}
.option.disabled {{ pointer-events: none; opacity: 0.8; }}
.option-marker {{
    width: 24px; height: 24px; border-radius: 50%; border: 2px solid #ccc;
    display: flex; align-items: center; justify-content: center; flex-shrink: 0;
    font-size: 13px; font-weight: 700; color: var(--text-muted);
}}
.option.selected .option-marker {{ border-color: var(--asu-maroon); color: var(--asu-maroon); }}
.tf-group {{ display: flex; gap: 12px; margin: 10px 0; }}
.tf-btn {{
    flex: 1; padding: 14px; border: 2px solid #ddd; border-radius: var(--border-radius);
    background: var(--white); font-size: 16px; font-weight: 600; cursor: pointer;
    min-height: 44px; transition: border-color 0.2s;
}}
.tf-btn:hover {{ border-color: var(--asu-maroon); }}
.tf-btn:focus-visible {{ outline: 3px solid var(--asu-gold); outline-offset: 2px; }}
.tf-btn.selected {{ border-color: var(--asu-maroon); background: #fdf6f8; }}
.tf-btn.correct-ans {{ border-color: var(--success); background: #d4edda; }}
.tf-btn.incorrect-ans {{ border-color: var(--error); background: #f8d7da; }}
.result-box {{
    text-align: center; padding: 30px; border: 2px solid var(--asu-maroon);
    border-radius: var(--border-radius); margin: 20px 0;
}}
.result-score {{ font-size: 2.5em; color: var(--asu-maroon); font-weight: 700; }}
.nav-dots {{
    display: flex; justify-content: center; gap: 8px; margin: 15px 0;
}}
.nav-dot {{
    width: 24px; height: 24px; min-width: 24px; min-height: 24px; border-radius: 50%; background: #ddd;
    cursor: pointer; transition: background 0.2s;
}}
.nav-dot.active {{ background: var(--asu-maroon); }}
.nav-dot.answered {{ background: var(--asu-gold); }}
.controls {{ display: flex; gap: 12px; justify-content: center; margin: 20px 0; flex-wrap: wrap; }}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to activity</a>
<main id="main-content">
<h1>{title}</h1>
<p id="instructions" class="sr-only">{instructions}</p>
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Quiz progress" id="progress-bar"><div class="progress-fill" id="progress" style="width:0%"></div></div>
<div class="nav-dots" id="nav-dots" role="tablist" aria-label="Question navigation"></div>
<div class="question-container" id="question-area" aria-describedby="instructions"></div>
	<div class="feedback-box" id="feedback" role="alert"></div>
	<div class="controls" id="controls"></div>
	<div class="result-box" id="result" style="display:none"></div>
	{download_markup}
	</main>
	<script>
const QS = {questions_json};
let currentQ = 0;
let answers = new Array(QS.length).fill(null);
let checked = new Array(QS.length).fill(false);
let score = 0;
function setFeedback(fb, correct, message) {{
    fb.style.display = 'block';
    fb.className = 'feedback-box ' + (correct ? 'feedback-correct' : 'feedback-incorrect');
    fb.replaceChildren();
    const strong = document.createElement('strong');
    strong.textContent = correct ? 'Correct!' : 'Not quite.';
    fb.append(strong, document.createTextNode(' ' + message));
}}
function initDots() {{
    const dots = document.getElementById('nav-dots');
    dots.replaceChildren();
    QS.forEach((_, i) => {{
        const d = document.createElement('div');
        d.className = 'nav-dot' + (i === 0 ? ' active' : '');
        d.setAttribute('role', 'tab');
        d.setAttribute('tabindex', '0');
        d.setAttribute('aria-label', 'Question ' + (i+1));
        d.addEventListener('click', () => goTo(i));
        dots.appendChild(d);
    }});
}}
function renderQ(idx) {{
    currentQ = idx;
    const q = QS[idx];
    const area = document.getElementById('question-area');
    const fb = document.getElementById('feedback');
	    const controls = document.getElementById('controls');
	    fb.style.display = 'none';
	    document.getElementById('result').style.display = 'none';
	    {download_hide_js}
	    const pct = Math.round((idx+1)/QS.length*100);
    document.getElementById('progress').style.width = pct+'%';
    document.getElementById('progress-bar').setAttribute('aria-valuenow', pct);
    document.querySelectorAll('.nav-dot').forEach((d,i) => {{
        d.classList.toggle('active', i === idx);
        d.classList.toggle('answered', answers[i] !== null);
    }});
    area.replaceChildren();
    const stem = document.createElement('p');
    stem.className = 'stem';
    const stemLabel = document.createElement('strong');
    stemLabel.textContent = 'Question '+(idx+1)+' of '+QS.length+':';
    stem.append(stemLabel, document.createTextNode(' '+q.stem));
    area.appendChild(stem);
    if (q.type === 'mc') {{
        const labels = ['A','B','C','D','E'];
        const group = document.createElement('div');
        group.setAttribute('role', 'radiogroup');
        group.setAttribute('aria-label', 'Answer options');
        q.options.forEach((opt, oi) => {{
            let cls = 'option';
            if (checked[idx]) {{
                cls += ' disabled';
                if (opt.correct) cls += ' correct-ans';
                else if (answers[idx] === oi) cls += ' incorrect-ans';
            }} else if (answers[idx] === oi) cls += ' selected';
            const option = document.createElement('div');
            option.className = cls;
            option.tabIndex = 0;
            option.setAttribute('role', 'radio');
            option.setAttribute('aria-checked', answers[idx] === oi);
            if (!checked[idx]) option.addEventListener('click', () => pickMC(oi));
            const marker = document.createElement('span');
            marker.className = 'option-marker';
            marker.textContent = labels[oi];
            const optionText = document.createElement('span');
            optionText.textContent = opt.text;
            option.append(marker, optionText);
            group.appendChild(option);
        }});
        area.appendChild(group);
    }} else {{
        const group = document.createElement('div');
        group.className = 'tf-group';
        group.setAttribute('role', 'radiogroup');
        group.setAttribute('aria-label', 'True or False');
        ['True','False'].forEach((lbl, ti) => {{
            let cls = 'tf-btn';
            const val = ti === 0;
            const isChecked = answers[idx] === val;
            if (checked[idx]) {{
                if (val === q.correct) cls += ' correct-ans';
                else if (answers[idx] === val) cls += ' incorrect-ans';
            }} else if (isChecked) cls += ' selected';
            const button = document.createElement('button');
            button.className = cls;
            button.setAttribute('role', 'radio');
            button.setAttribute('aria-checked', isChecked);
            button.textContent = lbl;
            if (!checked[idx]) button.addEventListener('click', () => pickTF(val));
            group.appendChild(button);
        }});
        area.appendChild(group);
    }}
    controls.replaceChildren();
    const button = document.createElement('button');
    button.className = 'btn-primary';
    if (checked[idx]) {{
        showFeedback(idx);
        if (idx < QS.length-1) {{
            button.textContent = 'Next Question';
            button.addEventListener('click', () => goTo(idx+1));
        }} else {{
            button.textContent = 'See Results';
            button.addEventListener('click', showResults);
        }}
    }} else {{
        button.textContent = 'Submit';
        button.addEventListener('click', submitQ);
    }}
    controls.appendChild(button);
}}
function pickMC(oi) {{ answers[currentQ] = oi; renderQ(currentQ); }}
function pickTF(val) {{ answers[currentQ] = val; renderQ(currentQ); }}
function submitQ() {{
    if (answers[currentQ] === null) {{
        const fb = document.getElementById('feedback');
        fb.style.display = 'block';
        fb.className = 'feedback-box feedback-incorrect';
        fb.textContent = 'Please select an answer before submitting.';
        return;
    }}
    checked[currentQ] = true;
    const q = QS[currentQ];
    let correct;
    if (q.type === 'mc') correct = q.options[answers[currentQ]].correct;
    else correct = answers[currentQ] === q.correct;
    if (correct) score++;
    renderQ(currentQ);
}}
function showFeedback(idx) {{
    const q = QS[idx];
    const fb = document.getElementById('feedback');
    let correct, fbText;
    if (q.type === 'mc') {{
        correct = q.options[answers[idx]].correct;
        fbText = q.options[answers[idx]].feedback;
    }} else {{
        correct = answers[idx] === q.correct;
        fbText = correct ? q.feedback_true : q.feedback_false;
    }}
    setFeedback(fb, correct, fbText);
}}
function goTo(idx) {{ renderQ(idx); }}
function showResults() {{
    document.getElementById('question-area').replaceChildren();
    document.getElementById('feedback').style.display = 'none';
    const r = document.getElementById('result');
    r.style.display = 'block';
	    r.replaceChildren();
	    const heading = document.createElement('h3');
	    heading.textContent = 'Quiz Complete';
	    const scoreBox = document.createElement('div');
	    scoreBox.className = 'result-score';
	    scoreBox.textContent = score+'/'+QS.length;
	    const message = document.createElement('p');
	    message.style.color = 'var(--text-muted)';
	    message.textContent = 'This is a formative review \u2014 use it to identify areas for further study.';
	    r.append(heading, scoreBox, message);
	    const controls = document.getElementById('controls');
	    controls.replaceChildren();
	    const restartBtn = document.createElement('button');
	    restartBtn.className = 'btn-primary';
	    restartBtn.textContent = 'Try Again';
	    restartBtn.addEventListener('click', restart);
	    controls.appendChild(restartBtn);
	    {download_show_js}
	}}
	function restart() {{
	    score = 0; answers = new Array(QS.length).fill(null); checked = new Array(QS.length).fill(false);
	    {download_hide_js}
	    initDots(); renderQ(0);
	}}
document.addEventListener('keydown', function(e) {{
    if ((e.key === 'Enter' || e.key === ' ') && e.target.getAttribute('role') === 'radio') {{
        e.preventDefault();
        e.target.click();
	    }}
	}});
	{download_body_js}
	{download_context_js}
	initDots(); renderQ(0);
	</script>
	</body></html>"""


# ============================================================
# TEMPLATE DISPATCH
# ============================================================
GENERATORS = {
    "dialog_cards": generate_dialog_cards,
    "sequencing": generate_sequencing,
    "fill_blanks": generate_fill_blanks,
    "branching": generate_branching,
    "quiz": generate_quiz,
}


# ============================================================
# MAIN
# ============================================================
def main():
    parser = argparse.ArgumentParser(
        description="Generate interactive HTML activities for Canvas courses."
    )
    parser.add_argument(
        "content_data",
        help="Path to Python file containing ALL_ACTIVITIES dict",
    )
    parser.add_argument(
        "--course-id",
        help="Canvas course ID for staging directory",
    )
    parser.add_argument(
        "--output-dir",
        default=None,
        help=f"Override output root (default: {DEFAULT_OUTPUT_DIR})",
    )
    args = parser.parse_args()

    course_id = resolve_course_id(args.course_id)
    if not course_id:
        parser.error("--course-id is required when CANVAS_COURSE_ID is not configured")

    # Import content data from the provided path
    import importlib.util
    spec = importlib.util.spec_from_file_location("content_data", args.content_data)
    content_module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(content_module)
    ALL_ACTIVITIES = content_module.ALL_ACTIVITIES
    validate_activity_schema(ALL_ACTIVITIES)

    output_root = resolve_output_root(args.output_dir, course_id)
    total = 0
    errors = 0
    preview_urls = []

    for module_key, activities in sorted(ALL_ACTIVITIES.items()):
        module_dir = os.path.join(output_root, module_key)
        os.makedirs(module_dir, exist_ok=True)

        for activity in activities:
            act_type = activity["type"]
            filename = activity["filename"]

            gen_func = GENERATORS.get(act_type)
            if not gen_func:
                print(f"  ERROR: Unknown type '{act_type}' for {filename}")
                errors += 1
                continue

            try:
                html_content = gen_func(activity)
                filepath = os.path.join(module_dir, filename)
                with open(filepath, "w", encoding="utf-8") as f:
                    f.write(html_content)
                print(f"  OK: {module_key}/{filename} ({act_type})")
                preview_urls.append(preview_url(course_id, module_key, filename))
                total += 1
            except Exception as e:
                print(f"  ERROR: {module_key}/{filename} — {e}")
                errors += 1

    print(f"\n=== GENERATION COMPLETE ===")
    print(f"  Generated: {total}")
    print(f"  Errors: {errors}")
    print(f"  Output: {output_root}")
    print()
    print_review_banner(total, output_root, preview_urls, args.content_data)


if __name__ == "__main__":
    main()
