#!/usr/bin/env python3
"""Build a faculty-facing staged quiz mockup from per-question decisions."""

from __future__ import annotations

import argparse
import html
import json
import os
import re
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, os.path.dirname(__file__))

import canvas_api
import backup_manager
import staging_manager

try:
    import yaml
except ImportError:  # pragma: no cover - dependency is pinned for the project.
    yaml = None


# Canonical ASU-brand pill badge HTML — must match preflight_checks.CANONICAL_*_BADGE.
# Color contract: KEEP=#78BE20 (green), REPLACE=#8C1D40 (maroon), ADD=#FFC627 (gold),
# OPTIONAL=outlined maroon. Do not change these without updating preflight_checks.py too.
KEEP_BADGE = (
    '<span style="display:inline-block; background:#78BE20; color:#191919; '
    'padding:3px 9px; border-radius:4px; font-size:12px;">KEEP</span>'
)
REPLACE_BADGE = (
    '<span style="display:inline-block; background:#8C1D40; color:#FFFFFF; '
    'padding:3px 9px; border-radius:4px; font-size:12px;">REPLACE</span>'
)
ADD_BADGE = (
    '<span style="display:inline-block; background:#FFC627; color:#191919; '
    'padding:3px 9px; border-radius:4px; font-size:12px;">ADD</span>'
)
OPTIONAL_CHIP = (
    '<span style="display:inline-block; border:1px solid #8C1D40; color:#8C1D40; '
    'padding:2px 8px; border-radius:4px; font-size:12px;">OPTIONAL</span>'
)

# CLO-only pathway badges — Feature D.
# MOVE: ASU purple (5C2D91). DROP: neutral dark gray (4E4E4E).
MOVE_BADGE = (
    '<span style="display:inline-block; background:#5C2D91; color:#FFFFFF; '
    'padding:3px 9px; border-radius:4px; font-size:12px;">MOVE</span>'
)
DROP_BADGE = (
    '<span style="display:inline-block; background:#4E4E4E; color:#FFFFFF; '
    'padding:3px 9px; border-radius:4px; font-size:12px;">DROP</span>'
)
# CLO_ONLY_CHIP: yellow outlined chip that follows the primary badge.
CLO_ONLY_CHIP = (
    '<span style="display:inline-block; border:1px solid #FFC627; background:#FFF8E1; '
    'color:#5C4100; padding:2px 8px; border-radius:4px; font-size:12px;">CLO ONLY</span>'
)

_BADGE_MAP = {
    "KEEP": KEEP_BADGE,
    "REPLACE": REPLACE_BADGE,
    "ADD": ADD_BADGE,
    "OPTIONAL": OPTIONAL_CHIP,
    "MOVE": MOVE_BADGE,
    "DROP": DROP_BADGE,
}


def _badge(label: str) -> str:
    if label in _BADGE_MAP:
        return _BADGE_MAP[label]
    # Fallback for unknown labels — plain span, no color
    return f'<span style="display:inline-block;border-radius:4px;padding:2px 8px;font-size:12px;">{html.escape(label)}</span>'


# ── Bloom jargon regression guard ────────────────────────────────────────────
# These are the static template strings this module injects. They must never
# contain Bloom taxonomy jargon. If a future edit adds jargon, the import will
# fail loudly (detected by test_build_mockup_quiz_no_bloom_jargon).
_STATIC_TEMPLATE_STRINGS = [
    "Aligns with",
    "because",
    "Current item misaligns with",
    "replacement targets",
    "Closes a gap on",
    "CLO-only flag: this item is not mapped to a module learning objective.",
    "Current Question:",
    "Example question:",
    "faculty-facing quiz mockup.",
    # CLO-only pathway — Feature D
    "Flag for faculty:",
    "this item does not align with any Module",
    "Faculty decision: keep this item as foundational support for MLO",
    "move it to a module where this concept is taught directly, or drop it from Module",
    "Faculty decision: move this item to Module",
    "where the supporting MLOs are taught directly, keep it here as foundational support, or drop it.",
    "Faculty decision: drop this item from Module",
    "(recommended), move it to another module, or keep it here as foundational support.",
]
_BLOOM_JARGON_RE = re.compile(
    r"\bRemember level\b|\bApply level\b|\bUnderstand level\b|\bAnalyze level\b"
    r"|\bEvaluate level\b|\bCreate level\b|\bBloom\b|\bcognitive level\b"
    r"|\bcognitive demand\b|\bcognitive load level\b",
    re.I,
)
for _s in _STATIC_TEMPLATE_STRINGS:
    assert not _BLOOM_JARGON_RE.search(_s), (
        f"build_mockup_quiz.py static template contains Bloom jargon: {_s!r}"
    )
del _s  # cleanup loop variable


def _mlo_phrase(mlos: list[Any]) -> str:
    if not mlos:
        return "CLO-only"
    return "MLO " + ", ".join(str(item) for item in mlos)


def _question_heading(q_num: str, mlos: list[Any]) -> str:
    label = f"Question {q_num}"
    if not mlos:
        return (
            '<h3><mark style="background:#fff3bf;color:#191919;'
            f'padding:0 4px;">{html.escape(label)}</mark></h3>'
        )
    return f"<h3>{html.escape(label)}</h3>"


def _example_answers() -> list[dict[str, Any]]:
    return [
        {"answer_text": "a. Example option A", "answer_weight": 0},
        {"answer_text": "b. Example option B", "answer_weight": 0},
        {"answer_text": "c. Example option C", "answer_weight": 100},
        {"answer_text": "d. Example option D", "answer_weight": 0},
    ]


def _example_answers_html() -> str:
    return (
        "<ol type=\"a\">"
        "<li>Example option A</li>"
        "<li>Example option B</li>"
        "<li>Example option C</li>"
        "<li>Example option D</li>"
        "</ol>"
    )


def _normalize_live_question(question: dict[str, Any] | None) -> tuple[str, list[dict[str, Any]]]:
    if not question:
        return "<p>Current question text unavailable for faculty review.</p>", [
            {"answer_text": "a. Current option unavailable A", "answer_weight": 0},
            {"answer_text": "b. Current option unavailable B", "answer_weight": 0},
            {"answer_text": "c. Current option unavailable C", "answer_weight": 100},
            {"answer_text": "d. Current option unavailable D", "answer_weight": 0},
        ]
    text = question.get("question_text") or question.get("text") or "<p>Current question text unavailable.</p>"
    q_type = question.get("question_type") or "multiple_choice_question"
    answers = canvas_api._normalize_quiz_answers_for_read(question.get("answers") or [], question_type=q_type)
    if not answers:
        answers = _example_answers()
    return str(text), answers


def _validate_clo_decision(q_num: str, tag: str, decision: dict[str, Any]) -> None:
    """Raise ValueError when MOVE/DROP is used without clo_only: true."""
    if tag in {"MOVE", "DROP"} and not decision.get("clo_only"):
        raise ValueError(
            f"MOVE and DROP tags require `clo_only: true` (Q{q_num})."
        )


def _render_clo_only_rationale(
    tag: str,
    why: str,
    module_number: int | None,
    clo_supports_mlo: int | None,
    target_module: int | None,
) -> str:
    """Build the yellow <mark> callout block for CLO-only decisions.

    Returns the HTML paragraph containing:
      - "Flag for faculty: this item does not align with any Module N MLO."
      - The user's why text
      - A closing faculty-decision sentence based on tag.
    """
    mod_label = f"Module {module_number}" if module_number is not None else "this module"

    flag_prefix = (
        f'<strong>Flag for faculty:</strong> this item does not align with any {mod_label} MLO. '
    )

    if tag == "KEEP" and clo_supports_mlo is not None:
        closing = (
            f"Faculty decision: keep this item as foundational support for MLO {clo_supports_mlo}, "
            f"move it to a module where this concept is taught directly, or drop it from {mod_label}."
        )
    elif tag == "MOVE" and target_module is not None:
        closing = (
            f"Faculty decision: move this item to Module {target_module} where the supporting MLOs "
            f"are taught directly, keep it here as foundational support, or drop it."
        )
    elif tag == "DROP":
        closing = (
            f"Faculty decision: drop this item from {mod_label} (recommended), "
            f"move it to another module, or keep it here as foundational support."
        )
    else:
        # Generic fallback: covers KEEP without clo_supports_mlo, MOVE without target_module, etc.
        if tag == "KEEP":
            closing = (
                f"Faculty decision: keep this item as foundational support, "
                f"move it to a module where this concept is taught directly, or drop it from {mod_label}."
            )
        elif tag == "MOVE":
            closing = (
                f"Faculty decision: move this item to the module where the supporting MLOs are taught, "
                f"keep it here as foundational support, or drop it."
            )
        else:
            closing = (
                f"Faculty decision: drop this item from {mod_label} (recommended), "
                f"move it to another module, or keep it here as foundational support."
            )

    inner = (
        f'{flag_prefix}{html.escape(why)} {html.escape(closing)}'
    )
    return (
        f'<p><mark style="background-color: #fff2a8; color: inherit;">'
        f'{inner}'
        f'</mark></p>'
    )


def render_question_html(q_num: str, decision: dict[str, Any], live_question: dict[str, Any] | None,
                         module_number: int | None = None) -> str:
    tag = str(decision.get("tag") or "").upper()
    if tag not in {"KEEP", "REPLACE", "ADD", "MOVE", "DROP"}:
        raise ValueError(f"Unsupported decision tag for Q{q_num}: {tag}")

    # Schema rule: MOVE and DROP require clo_only: true
    _validate_clo_decision(q_num, tag, decision)

    mlos = decision.get("mlos") or []
    why = str(decision.get("why") or "faculty rationale pending")
    clo_only = bool(decision.get("clo_only"))
    clo_supports_mlo = decision.get("clo_supports_mlo")  # int or None
    target_module = decision.get("target_module")         # int or None

    # Build badge row
    badges = [_badge(tag)]
    if decision.get("optional"):
        badges.append(_badge("OPTIONAL"))
    if clo_only:
        badges.append(CLO_ONLY_CHIP)

    current_text, _ = _normalize_live_question(live_question)
    chunks = [_question_heading(q_num, mlos), f'<p>{" ".join(badges)}</p>']

    if clo_only:
        # CLO-only pathway: yellow mark callout replaces the plain rationale paragraph.
        chunks.append(
            _render_clo_only_rationale(tag, why, module_number, clo_supports_mlo, target_module)
        )
        if tag in {"KEEP"}:
            chunks.append(f"<div>{current_text}</div>")
        # MOVE and DROP do not include the current question body in the mockup.
    else:
        # Legacy pathway (no clo_only) — original CLO-only flag for empty mlos list
        if not mlos:
            chunks.append(
                '<p><mark style="background:#fff3bf;color:#191919;padding:2px 4px;">'
                "CLO-only flag: this item is not mapped to a module learning objective."
                "</mark></p>"
            )

        if tag == "KEEP":
            chunks.append(f"<p>Aligns with {html.escape(_mlo_phrase(mlos))} because {html.escape(why)}</p>")
            chunks.append(f"<div>{current_text}</div>")
        elif tag == "REPLACE":
            bloom_target = html.escape(str(decision.get("bloom_target") or "target Bloom level pending"))
            chunks.append(
                f"<p>Current item misaligns with {html.escape(_mlo_phrase(mlos))} "
                f"because {html.escape(why)}; replacement targets {bloom_target}</p>"
            )
            chunks.append(f"<p><strong>Current Question:</strong></p><div>{current_text}</div>")
            chunks.append("<p><strong>Example question:</strong> Example question paragraph for faculty review.</p>")
            chunks.append(_example_answers_html())
        elif tag == "ADD":
            chunks.append(f"<p>Closes a gap on {html.escape(_mlo_phrase(mlos))} because {html.escape(why)}</p>")
            chunks.append("<p><strong>Example question:</strong> Example question placeholder for faculty review.</p>")
            chunks.append(_example_answers_html())

    return "\n".join(chunks)


def _load_decisions(path: Path) -> dict[str, Any]:
    if yaml is None:
        raise RuntimeError("PyYAML is required to read quiz decision YAML.")
    data = yaml.safe_load(path.read_text(encoding="utf-8"))
    if not isinstance(data, dict):
        raise ValueError("Decision file must be a YAML mapping.")
    if not isinstance(data.get("decisions"), dict):
        raise ValueError("Decision file requires a decisions mapping.")
    return data


def _optional_alignment_cache(course_id: str) -> dict[str, Any] | None:
    path = (
        Path(canvas_api.get_plugin_root())
        / "courses"
        / str(course_id)
        / "reports"
        / "alignment_mockup_decisions.json"
    )
    if not path.exists():
        return None
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return None
    return data if isinstance(data, dict) else None


def build_staged_quiz(course_id: str, decisions_file: Path, output_staged_key: str) -> dict[str, Any]:
    decisions_doc = _load_decisions(decisions_file)
    quiz_id = decisions_doc.get("quiz_canvas_id")
    if quiz_id is None:
        raise ValueError("Decision file requires quiz_canvas_id.")
    config = canvas_api.get_config(course_id=course_id)
    canvas_api.require_course_id(config)
    live_questions = canvas_api.get_quiz_questions(config, quiz_id) or []

    staged_questions = []
    for raw_q_num, decision in sorted(
        decisions_doc["decisions"].items(),
        key=lambda item: int(item[0]) if str(item[0]).isdigit() else str(item[0]),
    ):
        q_num = str(raw_q_num)
        if not isinstance(decision, dict):
            raise ValueError(f"Decision for Q{q_num} must be a mapping.")
        live_question = live_questions[int(q_num) - 1] if q_num.isdigit() and int(q_num) - 1 < len(live_questions) else None
        tag = str(decision.get("tag") or "").upper()
        mod_num = decisions_doc.get("module_number")
        question_html = render_question_html(q_num, decision, live_question, module_number=mod_num)
        _, live_answers = _normalize_live_question(live_question)
        # MOVE and DROP use example answers (no live answers in the mockup); REPLACE and ADD also use examples.
        answers = _example_answers() if tag in {"REPLACE", "ADD", "MOVE", "DROP"} else live_answers
        staged_questions.append({
            "id": live_question.get("id") if isinstance(live_question, dict) else None,
            "question_name": f"Q{q_num} {tag}",
            "question_text": question_html,
            "question_type": "multiple_choice_question",
            "points_possible": live_question.get("points_possible", 1) if isinstance(live_question, dict) else 1,
            "answers": answers,
        })

    cache = _optional_alignment_cache(course_id)
    payload = {
        "quiz": {
            "title": decisions_doc.get("title") or f"Mockup Quiz {quiz_id}",
            "canvas_quiz_id": quiz_id,
            "description": "<p>Faculty-facing quiz mockup.</p>",
        },
        "questions": staged_questions,
    }
    result = staging_manager.stage(
        course_id,
        "quizzes",
        output_staged_key,
        payload,
        validate=False,
        title=payload["quiz"]["title"],
        source_canvas_id=quiz_id,
        destructive_reconcile_ok=True,
    )
    if not result.get("ok"):
        return result
    meta_path = Path(result["path"] + ".meta.json")
    meta = json.loads(meta_path.read_text(encoding="utf-8")) if meta_path.exists() else {}
    meta["canvas_id"] = quiz_id
    if cache is not None:
        meta["alignment_mockup_decisions_cache"] = "courses/{}/reports/alignment_mockup_decisions.json".format(course_id)
    meta_path.write_text(json.dumps(meta, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    result["questions"] = len(staged_questions)
    return result


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description="Build a staged quiz mockup from per-question decisions.")
    parser.add_argument("--course-id", required=True)
    parser.add_argument("--decisions-file", required=True)
    parser.add_argument("--output-staged-key", required=True)
    return parser


def main(argv: list[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    result = build_staged_quiz(args.course_id, Path(args.decisions_file), args.output_staged_key)
    print(json.dumps(result, indent=2, ensure_ascii=False))
    return 0 if result.get("ok") else 1


if __name__ == "__main__":
    raise SystemExit(main())
