#!/usr/bin/env python3
"""Extract a reusable, genericized layout template from a source Canvas course.

Given a source course ID, captures:
  - Module order + name_pattern
  - Per-module SubHeader placement and item-type/role sequence
  - Lightweight page signatures (first-element + heading outline only, no bodies)
  - Optional syllabus section structure (--include-syllabus), tagged as
    boilerplate vs course-specific via syllabus_boilerplate.boilerplate_sections()

Output JSON can feed course-mockup / course-build to build a new course "like
course X."

CLI:
    python scripts/extract_course_layout.py --course-id N
    python scripts/extract_course_layout.py --course-id N --include-syllabus
    python scripts/extract_course_layout.py --course-id N --json
    python scripts/extract_course_layout.py --course-id N --save
    python scripts/extract_course_layout.py --course-id N --include-syllabus --save --json
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from datetime import date
from pathlib import Path
from typing import Any

PLUGIN_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = Path(__file__).resolve().parent
if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPTS_DIR))

from html_utils import strip_html as _strip_html_shared  # noqa: E402


# ---------------------------------------------------------------------------
# HTML helpers (mirrors audit_conversion_readiness.py)
# ---------------------------------------------------------------------------

def _strip_html(value: Any) -> str:
    """Strip HTML tags and normalise whitespace.  Thin alias — delegates to :func:`html_utils.strip_html`."""
    return _strip_html_shared(value)


# ---------------------------------------------------------------------------
# Pure helpers — genericize_title and detect_role
# ---------------------------------------------------------------------------

# Matches any occurrence of "Module N" in a title string.
# Groups: (1) everything before+including "Module ", (2) the number, (3) rest
_MOD_NUM_RE = re.compile(
    r"(.*?\bModule\s+)(\d+)(.*)",
    re.IGNORECASE | re.DOTALL,
)


def genericize_title(title: str) -> str:
    """Replace a module number with ``N`` (or ``N-1`` for previous-module refs).

    Rules (applied in order):
    1. ``"Peer Review of Module K Homework"`` — the K is a previous-module
       reference; replace K with ``N-1``.
    2. ``"Module K: [Specific Title]"`` — title has a module number followed by
       a colon and a topic name → collapse to ``"Module N: [Title]"``.
    3. ``"Module K Word …"`` — module number with no colon →
       replace K with ``N``.
    4. No module number detected → return title unchanged.

    Examples::
        >>> genericize_title("Module 1 Overview")
        'Module N Overview'
        >>> genericize_title("Module 12: Categorical Predictors")
        'Module N: [Title]'
        >>> genericize_title("Peer Review of Module 2 Homework")
        'Peer Review of Module N-1 Homework'
        >>> genericize_title("Course Introduction")
        'Course Introduction'
    """
    # Rule 1: "Peer Review of Module K Homework" style
    # Heuristic: if "peer review … of Module N" → N-1
    peer_review_re = re.compile(
        r"^(.*?(?:peer\s+review|review)\s+of\s+Module\s+)(\d+)(\s+.*)?$",
        re.IGNORECASE | re.DOTALL,
    )
    m_peer = peer_review_re.match(title)
    if m_peer:
        prefix = m_peer.group(1)
        suffix = m_peer.group(3) or ""
        return f"{prefix}N-1{suffix}"

    # Rule 2 + 3: "Module N: Title" or "Module N Word …"
    m = _MOD_NUM_RE.match(title)
    if m:
        pre = m.group(1)       # e.g. "Module " or "SomePrefix Module "
        rest = m.group(3)      # everything after the number
        if rest.startswith(":"):
            # Colon present — collapse topic name to [Title]
            return f"{pre}N: [Title]"
        else:
            # No colon: keep whatever follows the number as-is
            return f"{pre}N{rest}"

    return title


# Role keyword table (deterministic).
# Each entry: (keyword_regex, role_string).
# First match wins.  `type` is the Canvas item type ("Page", "Quiz", etc.).
_ROLE_RULES: list[tuple[re.Pattern[str], str]] = [
    (re.compile(r"\boverview\b", re.IGNORECASE), "overview"),
    (re.compile(r"\btask[\s_-]?list\b", re.IGNORECASE), "task_list"),
    (re.compile(r"\blecture[\s_-]?video|videos?\b", re.IGNORECASE), "videos"),
    (re.compile(r"\breading|textbook|covered\s+section", re.IGNORECASE), "readings"),
    (re.compile(r"\btopics?\b", re.IGNORECASE), "topic"),
    (re.compile(r"\bpeer[\s_-]?review\b", re.IGNORECASE), "peer_review"),
    (re.compile(r"\bquiz\b", re.IGNORECASE), "quiz"),
    (re.compile(r"\bhomework\b", re.IGNORECASE), "homework"),
    (re.compile(r"\bdiscussion\b", re.IGNORECASE), "discussion"),
]


def detect_role(item_type: str, title: str) -> str:
    """Map a module item to a role by title keywords.

    Keyword table (first match wins):
        overview       → "Module N Overview", "Module Overview"
        task_list      → "Task List"
        videos         → "Lecture Video", "Lecture Videos", "Videos"
        readings       → "Readings", "Textbook Sections", "Covered Sections"
        topic          → "Topic", "Module Topic"
        peer_review    → "Peer Review of …"
        quiz           → "Quiz", "Module Quiz"
        homework       → "Homework"
        discussion     → "Discussion"
        other          → fallback when no keyword matches

    ``item_type`` is the Canvas item type string (``"Page"``, ``"Quiz"``,
    ``"Assignment"``, ``"Discussion"``, etc.).  It is accepted as a parameter
    for potential future type-aware disambiguation but is not used in the
    current implementation (title keywords are sufficient and more reliable).

    Parameters
    ----------
    item_type:
        Canvas module item type string (e.g. ``"Page"``, ``"Quiz"``).
    title:
        Module item title string.

    Returns
    -------
    str
        Role string — one of ``overview``, ``task_list``, ``videos``,
        ``readings``, ``topic``, ``peer_review``, ``quiz``, ``homework``,
        ``discussion``, or ``"other"``.
    """
    for pattern, role in _ROLE_RULES:
        if pattern.search(title):
            return role
    return "other"


# ---------------------------------------------------------------------------
# Page-signature extraction (lightweight — first element + headings only)
# ---------------------------------------------------------------------------

_HEADING_RE = re.compile(r"<h[23][^>]*>(.*?)</h[23]>", re.IGNORECASE | re.DOTALL)
_BANNER_IMG_RE = re.compile(r"^\s*<img\b", re.IGNORECASE)
_VIDEO_EMBED_RE = re.compile(r"^\s*<iframe\b|^\s*<video\b", re.IGNORECASE)


def _extract_page_signature(body_html: str) -> dict[str, Any]:
    """Extract a lightweight page signature: first_element + heading outline.

    NEVER stores the full page body.  Only captures structural outline.

    Parameters
    ----------
    body_html:
        Raw HTML body of the page.

    Returns
    -------
    dict with keys:
        ``first_element``: ``"banner_image"`` | ``"video_embed"`` | ``"text"``
        ``headings``: list of heading text strings (h2/h3 level, in order)
    """
    stripped = (body_html or "").strip()

    # Determine first_element
    if _BANNER_IMG_RE.match(stripped):
        first_element = "banner_image"
    elif _VIDEO_EMBED_RE.match(stripped):
        first_element = "video_embed"
    else:
        first_element = "text"

    # Extract headings (text only — no bodies)
    headings = [
        _strip_html(m.group(1))
        for m in _HEADING_RE.finditer(body_html or "")
    ]

    return {"first_element": first_element, "headings": headings}


# ---------------------------------------------------------------------------
# Syllabus section parsing
# ---------------------------------------------------------------------------

_SYLLABUS_H2_RE = re.compile(r"<h2[^>]*>(.*?)</h2>", re.IGNORECASE | re.DOTALL)


def _parse_syllabus_sections(html_body: str) -> list[str]:
    """Return an ordered list of section titles from a syllabus HTML body.

    Sections are delimited by <h2> headings.
    """
    return [_strip_html(m.group(1)) for m in _SYLLABUS_H2_RE.finditer(html_body or "")]


# ---------------------------------------------------------------------------
# Main orchestrator (the only function that touches the network)
# ---------------------------------------------------------------------------

def extract_layout(course_id: str, include_syllabus: bool = False) -> dict[str, Any]:
    """Extract a genericized layout template from a source Canvas course.

    This is the only function that calls the Canvas API (via ``canvas_api``).
    All helper functions (``genericize_title``, ``detect_role``,
    ``_extract_page_signature``) are pure and network-free.

    Parameters
    ----------
    course_id:
        Canvas course ID (numeric string).
    include_syllabus:
        When True, fetch and parse the syllabus to produce a ``syllabus``
        section in the output, tagging sections as boilerplate vs
        course-specific (via ``syllabus_boilerplate.boilerplate_sections()``).
        Section *structure* only — course-specific body text is never stored.

    Returns
    -------
    dict
        Layout template JSON matching the schema in
        ``docs/plan-extract-course-layout.md``.
    """
    import canvas_api  # noqa: PLC0415

    cfg = canvas_api.get_config(course_id=course_id)

    # ── Fetch modules ──────────────────────────────────────────────────────
    modules_raw = canvas_api.get_modules(cfg) or []

    # ── Fetch pages (summary list for slug lookup) ─────────────────────────
    pages_summary = canvas_api.list_pages(cfg) or []
    page_slug_map: dict[str, dict] = {}
    for p in pages_summary:
        slug = p.get("url") or p.get("page_url") or ""
        if slug:
            page_slug_map[slug] = p

    # ── Build module templates ─────────────────────────────────────────────
    modules_out: list[dict[str, Any]] = []
    page_signatures: dict[str, dict[str, Any]] = {}

    # Track which roles we have already fetched a representative page for
    sig_roles_seen: set[str] = set()

    for mod in modules_raw:
        mod_id = mod.get("id")
        mod_name = mod.get("name") or ""
        mod_position = mod.get("position", 0)

        items_raw = canvas_api.get_module_items(cfg, mod_id) or []

        items_out: list[dict[str, Any]] = []

        for item in items_raw:
            item_type = item.get("type") or "Page"

            if item_type == "SubHeader":
                items_out.append({
                    "type": "SubHeader",
                    "text": item.get("title") or "",
                })
                continue

            title = item.get("title") or ""
            role = detect_role(item_type, title)
            title_pattern = genericize_title(title)

            items_out.append({
                "type": item_type,
                "role": role,
                "title_pattern": title_pattern,
            })

            # ── Capture one page signature per role (pages only) ──────────
            if item_type == "Page" and role not in sig_roles_seen:
                slug = item.get("page_url") or ""
                if slug:
                    page_full = canvas_api.get_page(cfg, slug)
                    if page_full:
                        body = page_full.get("body") or ""
                        sig = _extract_page_signature(body)
                        page_signatures[role] = sig
                        sig_roles_seen.add(role)

        modules_out.append({
            "name_pattern": genericize_title(mod_name),
            "position": mod_position,
            "items": items_out,
        })

    # ── Assemble output ────────────────────────────────────────────────────
    output: dict[str, Any] = {
        "source_course_id": str(course_id),
        "extracted": str(date.today()),
        "modules": modules_out,
        "page_signatures": page_signatures,
    }

    # ── Optional syllabus extension ────────────────────────────────────────
    if include_syllabus:
        import syllabus_boilerplate  # noqa: PLC0415

        syllabus_data = canvas_api.get_syllabus(cfg) or {"html": ""}
        syllabus_html = syllabus_data.get("html") or ""

        boilerplate_keys = {k.lower() for k in syllabus_boilerplate.boilerplate_sections()}
        section_order = _parse_syllabus_sections(syllabus_html)

        boilerplate_sections_list: list[str] = []
        course_specific_sections_list: list[str] = []

        for sec_title in section_order:
            if sec_title.lower() in boilerplate_keys:
                boilerplate_sections_list.append(sec_title)
            else:
                course_specific_sections_list.append(sec_title)

        output["syllabus"] = {
            "section_order": section_order,
            "boilerplate_sections": boilerplate_sections_list,
            "course_specific_sections": course_specific_sections_list,
        }

    return output


# ---------------------------------------------------------------------------
# Human-readable outline printer
# ---------------------------------------------------------------------------

def print_outline(layout: dict[str, Any]) -> None:
    """Print a human-readable outline of the extracted layout."""
    print(f"\n=== Course Layout Template ===")
    print(f"Source course: {layout.get('source_course_id')}")
    print(f"Extracted: {layout.get('extracted')}")

    modules = layout.get("modules") or []
    print(f"\nModules ({len(modules)}):\n")

    for mod in modules:
        print(f"  [{mod.get('position', '?')}] {mod.get('name_pattern')}")
        for item in mod.get("items") or []:
            if item.get("type") == "SubHeader":
                print(f"        ── {item.get('text')} ──")
            else:
                print(
                    f"        • {item.get('type'):12s}  "
                    f"{item.get('role', ''):12s}  {item.get('title_pattern', '')}"
                )
        print()

    sigs = layout.get("page_signatures") or {}
    if sigs:
        print("Page signatures:")
        for role, sig in sigs.items():
            headings = ", ".join(sig.get("headings") or [])
            print(
                f"  {role:14s}  first_element={sig.get('first_element')}  "
                f"headings=[{headings}]"
            )
        print()

    syl = layout.get("syllabus")
    if syl:
        print(f"Syllabus sections ({len(syl.get('section_order', []))}):")
        for sec in syl.get("section_order") or []:
            tag = (
                "[boilerplate]"
                if sec in syl.get("boilerplate_sections", [])
                else "[course-specific]"
            )
            print(f"  {tag:18s}  {sec}")
        print()


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Extract a reusable layout template from a Canvas course."
    )
    parser.add_argument("--course-id", required=True, help="Canvas course ID (numeric)")
    parser.add_argument(
        "--include-syllabus",
        action="store_true",
        help="Also extract syllabus section structure (boilerplate vs course-specific)",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Print the template JSON to stdout",
    )
    parser.add_argument(
        "--save",
        action="store_true",
        help=(
            "Write template to "
            "templates/canvas-templates/extracted/{course_id}-layout.json"
        ),
    )
    args = parser.parse_args()

    course_id = args.course_id.strip()
    if not re.match(r"^\d+$", course_id):
        parser.error(f"--course-id must be numeric, got: {course_id!r}")

    layout = extract_layout(course_id, include_syllabus=args.include_syllabus)

    if args.json:
        print(json.dumps(layout, indent=2, default=str))
    else:
        print_outline(layout)

    if args.save:
        out_dir = PLUGIN_ROOT / "templates" / "canvas-templates" / "extracted"
        out_dir.mkdir(parents=True, exist_ok=True)
        out_path = out_dir / f"{course_id}-layout.json"
        out_path.write_text(json.dumps(layout, indent=2, default=str), encoding="utf-8")
        print(f"\nSaved to: {out_path}")


if __name__ == "__main__":
    main()
