#!/usr/bin/env python3
"""Audit Canvas assignment rubric presence and basic quality."""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path

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 config_loader import load_course_config as _load_course_config_shared  # noqa: E402
from html_utils import strip_html as _strip_html_shared  # noqa: E402


def _strip_html(value):
    """Strip HTML and decode entities.  Thin alias — delegates to :func:`html_utils.strip_html`."""
    return _strip_html_shared(value)


def _load_course_config(course_id=None, config_path=None):
    """Load course config.  Thin alias — delegates to :func:`config_loader.load_course_config`."""
    return _load_course_config_shared(course_id=course_id, config_path=config_path)


def _assessment_name(item):
    return (
        item.get("name")
        or item.get("title")
        or item.get("assignment_name")
        or item.get("id")
        or "Untitled assignment"
    )


def _is_gradable(item):
    if item.get("grading_type") == "not_graded":
        return False
    if item.get("published") is False and item.get("workflow_state") == "unpublished":
        return False
    points = item.get("points_possible", item.get("points"))
    try:
        return float(points or 0) > 0
    except (TypeError, ValueError):
        return bool(item.get("rubric") or item.get("rubric_settings"))


def _extract_config_assignments(course_config):
    assignments = []

    for item in course_config.get("assignments", []) or []:
        if isinstance(item, dict):
            assignments.append(dict(item))

    for module in course_config.get("modules", []) or []:
        module_number = module.get("number") or module.get("id")
        assessments = module.get("assessments", [])
        if isinstance(assessments, dict):
            for key, value in assessments.items():
                if key in ("knowledge_check", "quiz"):
                    continue
                if not isinstance(value, dict):
                    continue
                record = dict(value)
                record.setdefault("type", key)
                record.setdefault("module_id", module_number)
                if key == "artifact":
                    record.setdefault("title", value.get("title") or f"Module {module_number} Artifact")
                assignments.append(record)
        elif isinstance(assessments, list):
            for value in assessments:
                if not isinstance(value, dict):
                    continue
                item_type = str(value.get("type", "")).lower()
                if item_type in ("quiz", "knowledge_check"):
                    continue
                record = dict(value)
                record.setdefault("module_id", module_number)
                assignments.append(record)

        for item in module.get("items", []) or []:
            if not isinstance(item, dict):
                continue
            item_type = str(item.get("type", "")).lower()
            if item_type == "assignment":
                record = dict(item)
                record.setdefault("module_id", module_number)
                assignments.append(record)

    return assignments


def _collect_canvas_assignments(course_id):
    """Fetch all assignments for course_id from the live Canvas assignments index.

    The index endpoint (GET /courses/:id/assignments) already returns the
    rubric field on each assignment when one is attached -- the same field
    the individual GET /assignments/:id endpoint returns. Two other call
    sites in this codebase already rely on that: alignment_validator.py's
    extract_assessments() reads rubric/use_rubric_for_grading straight off
    list_assignments(), and criterion_evaluator.py's collect_course_data()
    derives assignments_with_rubric/assignments_no_rubric the same way. A
    per-assignment GET here was pure N+1: it could only confirm what the
    index already stated, since an assignment with no rubric in the index
    has no rubric on the individual endpoint either. This takes an unbuilt
    40-assignment course from 41 calls (1 index + 40 individual) to 1.
    """
    import canvas_api

    config = canvas_api.get_config(course_id=course_id)
    return canvas_api.list_assignments(config) or []


def _rubric_criteria(rubric):
    if not rubric:
        return []
    if isinstance(rubric, dict):
        for key in ("criteria", "data", "rubric"):
            value = rubric.get(key)
            if isinstance(value, list):
                return value
            if isinstance(value, dict):
                nested = _rubric_criteria(value)
                if nested:
                    return nested
        return []
    if isinstance(rubric, list):
        return rubric
    return []


def _criterion_title(criterion):
    return _strip_html(
        criterion.get("description")
        or criterion.get("criterion")
        or criterion.get("name")
        or criterion.get("title")
    )


def _is_pure_point_count(value):
    text = _strip_html(value).lower()
    if not text:
        return False
    return bool(re.fullmatch(r"\d+(?:\.\d+)?\s*(?:points?|pts?)(?:\.)?", text))


def _criterion_point_count_descriptions(criterion):
    offenders = []
    for key in ("description", "long_description", "criterion", "name", "title"):
        value = criterion.get(key)
        if _is_pure_point_count(value):
            offenders.append(_strip_html(value))
    for rating in criterion.get("ratings", []) or []:
        if not isinstance(rating, dict):
            continue
        for key in ("description", "long_description"):
            value = rating.get(key)
            if _is_pure_point_count(value):
                offenders.append(_strip_html(value))
    return offenders


def _finding(assignment, issue_type, detail):
    return {
        "assignment_id": assignment.get("id") or assignment.get("assignment_id"),
        "assignment_name": _assessment_name(assignment),
        "issue_type": issue_type,
        "detail": detail,
    }


def audit_rubrics(course_id=None, course_config=None, assignments=None, config_path=None):
    """Return rubric audit findings for gradable assignments."""
    if assignments is None:
        if course_config is None:
            course_config = _load_course_config(course_id=course_id, config_path=config_path)
        assignments = _extract_config_assignments(course_config)
        if not assignments and course_id:
            assignments = _collect_canvas_assignments(course_id)

    findings = []
    for assignment in assignments or []:
        if not isinstance(assignment, dict) or not _is_gradable(assignment):
            continue

        rubric = assignment.get("rubric") or assignment.get("rubric_criteria")
        criteria = _rubric_criteria(rubric)
        if not criteria:
            findings.append(_finding(
                assignment,
                "missing_rubric",
                "Gradable assignment has no rubric attached.",
            ))
            continue

        if len(criteria) < 3 or len(criteria) > 5:
            findings.append(_finding(
                assignment,
                "rubric_criteria_count",
                f"Rubric has {len(criteria)} criteria; expected 3-5.",
            ))

        for index, criterion in enumerate(criteria, start=1):
            ratings = criterion.get("ratings", []) if isinstance(criterion, dict) else []
            title = _criterion_title(criterion) if isinstance(criterion, dict) else f"Criterion {index}"
            if len(ratings) != 4:
                findings.append(_finding(
                    assignment,
                    "rubric_performance_levels",
                    f"Criterion {index} ({title or 'untitled'}) has {len(ratings)} performance levels; expected 4.",
                ))
            if isinstance(criterion, dict):
                offenders = _criterion_point_count_descriptions(criterion)
                if offenders:
                    shown = ", ".join(sorted(set(offenders)))
                    findings.append(_finding(
                        assignment,
                        "point_count_description",
                        f"Criterion {index} ({title or 'untitled'}) uses pure point-count description(s): {shown}.",
                    ))

    return findings


def main():
    parser = argparse.ArgumentParser(description="Audit assignment rubric presence and quality")
    parser.add_argument("--course-id", required=True, help="Canvas course ID")
    parser.add_argument("--output", choices=["text", "json"], default="text")
    parser.add_argument("--config", help="Optional course-config.json path")
    args = parser.parse_args()

    findings = audit_rubrics(course_id=args.course_id, config_path=args.config)
    if args.output == "json":
        print(json.dumps(findings, indent=2))
        return
    if not findings:
        print("No rubric findings.")
        return
    for finding in findings:
        print(
            f"{finding['assignment_name']}: {finding['issue_type']} - "
            f"{finding['detail']}"
        )


if __name__ == "__main__":
    main()
