#!/usr/bin/env python3
"""Fetch a Canvas object into ASUCB staged-content shape.

Quiz fetches preserve Canvas question IDs. That is a safety requirement because
push_to_canvas.py reconciles quiz questions by ID and deletes Canvas questions
missing from staged IDs.
"""

from __future__ import annotations

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

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

import canvas_api
import staging_manager


def _cast_int_when_possible(value: Any) -> Any:
    if isinstance(value, float) and value.is_integer():
        return int(value)
    if isinstance(value, str):
        try:
            parsed = float(value)
        except ValueError:
            return value
        if parsed.is_integer():
            return int(parsed)
    return value


def _quiz_settings(quiz: dict[str, Any]) -> dict[str, Any]:
    keys = (
        "title",
        "quiz_type",
        "allowed_attempts",
        "scoring_policy",
        "time_limit",
        "shuffle_answers",
        "published",
        "description",
        "show_correct_answers",
        "due_at",
        "unlock_at",
        "lock_at",
        "assignment_group_id",
        "points_possible",
    )
    settings = {key: quiz.get(key) for key in keys if key in quiz}
    settings["canvas_quiz_id"] = quiz.get("id")
    return settings


def _normalize_question(question: dict[str, Any], strip_html: bool = False) -> dict[str, Any]:
    question_type = str(question.get("question_type") or "")
    staged = {
        "id": question.get("id"),
        "question_name": question.get("question_name") or question.get("name") or f"Question {question.get('id', '')}",
        "question_type": question_type,
        "points_possible": _cast_int_when_possible(question.get("points_possible", 1)),
        "question_text": question.get("question_text") or question.get("text") or "",
        "correct_comments": question.get("correct_comments") or "",
        "incorrect_comments": question.get("incorrect_comments") or "",
        "answers": canvas_api._normalize_quiz_answers_for_read(
            question.get("answers") or [],
            question_type=question_type,
            strip_html=strip_html,
        ),
    }
    for key in (
        "neutral_comments",
        "text_after_answers",
        "position",
        "matching_answer_incorrect_matches",
    ):
        if key in question and question[key] not in (None, ""):
            staged[key] = question[key]
    return staged


def fetch_quiz(config: dict[str, Any], quiz_id: str, strip_html: bool = False) -> dict[str, Any]:
    quiz = canvas_api.get_quiz(config, quiz_id)
    if not quiz:
        raise RuntimeError(f"Quiz not found: {quiz_id}")
    questions = canvas_api.get_quiz_questions(config, quiz_id) or []
    return {
        "quiz": _quiz_settings(quiz),
        "questions": [
            _normalize_question(question, strip_html=strip_html)
            for question in questions
            if isinstance(question, dict)
        ],
    }


def _find_page(config: dict[str, Any], page_id_or_slug: str) -> tuple[str, dict[str, Any]]:
    if not str(page_id_or_slug).isdigit():
        page = canvas_api.get_page(config, page_id_or_slug)
        if not page:
            raise RuntimeError(f"Page not found: {page_id_or_slug}")
        return str(page.get("url") or page_id_or_slug), page
    for page_summary in canvas_api.list_pages(config) or []:
        candidates = {
            str(page_summary.get("id") or ""),
            str(page_summary.get("page_id") or ""),
        }
        if str(page_id_or_slug) not in candidates:
            continue
        slug = str(page_summary.get("url") or "")
        page = canvas_api.get_page(config, slug)
        if not page:
            raise RuntimeError(f"Page summary found but body fetch failed: {page_id_or_slug}")
        return slug, page
    raise RuntimeError(f"Page not found by Canvas ID: {page_id_or_slug}")


def fetch_object(config: dict[str, Any], object_type: str, canvas_id: str, strip_html: bool = False) -> tuple[Any, dict[str, Any]]:
    if object_type == "quiz":
        payload = fetch_quiz(config, canvas_id, strip_html=strip_html)
        return payload, {
            "stage_type": "quizzes",
            "default_key": str(canvas_id),
            "title": payload["quiz"].get("title"),
            "source_canvas_id": canvas_id,
            "destructive_reconcile_ok": True,
        }
    if object_type == "page":
        slug, page = _find_page(config, canvas_id)
        return page.get("body") or "", {
            "stage_type": "pages",
            "default_key": slug,
            "title": page.get("title"),
            "source_canvas_id": page.get("page_id") or page.get("id") or canvas_id,
        }
    if object_type == "assignment":
        assignment = canvas_api.get_assignment(config, canvas_id)
        if not assignment:
            raise RuntimeError(f"Assignment not found: {canvas_id}")
        return assignment.get("description") or "", {
            "stage_type": "assignments",
            "default_key": str(canvas_id),
            "title": assignment.get("name"),
            "source_canvas_id": canvas_id,
        }
    if object_type == "discussion":
        discussion = canvas_api.get_discussion(config, canvas_id)
        if not discussion:
            raise RuntimeError(f"Discussion not found: {canvas_id}")
        return discussion.get("message") or "", {
            "stage_type": "discussions",
            "default_key": str(canvas_id),
            "title": discussion.get("title"),
            "source_canvas_id": canvas_id,
        }
    raise ValueError(f"Unsupported type: {object_type}")


def _write_output(path: str, payload: Any, stage_type: str) -> None:
    output = Path(path)
    output.parent.mkdir(parents=True, exist_ok=True)
    if stage_type == "quizzes":
        output.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    else:
        output.write_text(str(payload), encoding="utf-8")


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Fetch Canvas content into ASUCB staging shape.")
    parser.add_argument("--course-id", required=True, help="Canvas course ID.")
    parser.add_argument("--type", required=True, choices=["page", "quiz", "assignment", "discussion"])
    parser.add_argument("--id", required=True, help="Canvas object ID. For pages, a slug is also accepted.")
    parser.add_argument("--stage-key", help="Override staged object id/key.")
    parser.add_argument("--output", help="Optional file path to also write the staged payload.")
    parser.add_argument("--no-stage", action="store_true", help="Only write --output; do not stage under courses/{id}.")
    parser.add_argument("--strip-html", action="store_true", help="Strip HTML tags from quiz answer text. Default preserves Canvas text/html.")
    args = parser.parse_args(argv)

    config = canvas_api.get_config(course_id=args.course_id)
    canvas_api.require_course_id(config)
    payload, meta = fetch_object(config, args.type, args.id, strip_html=args.strip_html)
    stage_key = args.stage_key or meta["default_key"]
    result = {
        "ok": True,
        "course_id": str(args.course_id),
        "type": args.type,
        "stage_type": meta["stage_type"],
        "stage_key": str(stage_key),
    }
    if args.output:
        _write_output(args.output, payload, meta["stage_type"])
        result["output"] = args.output
    if not args.no_stage:
        staged = staging_manager.stage(
            args.course_id,
            meta["stage_type"],
            str(stage_key),
            payload,
            module_number=None,
            title=meta.get("title"),
            source_canvas_id=meta.get("source_canvas_id"),
            destructive_reconcile_ok=meta.get("destructive_reconcile_ok"),
        )
        result["staged"] = staged
        if not staged.get("ok"):
            result["ok"] = False
    print(json.dumps(result, indent=2, ensure_ascii=False))
    return 0 if result.get("ok") else 1


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