#!/usr/bin/env python3
"""Find CLOs with no downstream MLO or assessment coverage."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

SCRIPT_DIR = Path(__file__).resolve().parent
PLUGIN_ROOT = SCRIPT_DIR.parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from config_loader import load_course_config  # noqa: E402


def _clo_id(clo, index):
    if isinstance(clo, dict):
        return clo.get("id") or f"CLO-{index}"
    return f"CLO-{index}"


def _clo_text(clo):
    if isinstance(clo, dict):
        return clo.get("text") or clo.get("description") or ""
    return str(clo)


def _as_list(value):
    if value is None:
        return []
    if isinstance(value, list):
        return value
    return [value]


def _module_id(module):
    return module.get("id") or module.get("number") or module.get("module_id")


def _module_name(module):
    return module.get("title") or module.get("name") or f"Module {_module_id(module)}"


def _extract_clos(course_config):
    return [
        {"id": _clo_id(clo, index), "text": _clo_text(clo)}
        for index, clo in enumerate(course_config.get("clos", []) or [], start=1)
    ]


def _extract_mlo_mappings(course_config):
    mappings = []
    for module in course_config.get("modules", []) or []:
        if not isinstance(module, dict):
            continue
        module_id = _module_id(module)
        for index, objective in enumerate(module.get("objectives", []) or [], start=1):
            if not isinstance(objective, dict):
                continue
            mlo_id = objective.get("id") or f"M{module_id}.{index}"
            clo_refs = (
                objective.get("clo_ids")
                or objective.get("clos")
                or objective.get("clo")
                or objective.get("course_learning_objectives")
            )
            for clo_id in _as_list(clo_refs):
                if clo_id:
                    mappings.append({
                        "clo_id": str(clo_id),
                        "mlo_id": mlo_id,
                        "module_id": module_id,
                        "module_name": _module_name(module),
                    })
    return mappings


def _has_points(value):
    if not isinstance(value, dict):
        return bool(value)
    try:
        return float(value.get("points") or value.get("points_possible") or 0) > 0
    except (TypeError, ValueError):
        return False


def _module_has_assessment(module):
    assessments = module.get("assessments", [])
    if isinstance(assessments, dict):
        for key in ("knowledge_check", "quiz", "artifact", "assignment", "discussion"):
            value = assessments.get(key)
            if isinstance(value, dict) and value:
                return True
            if value and not isinstance(value, dict):
                return True
        guided = assessments.get("guided_practice")
        if _has_points(guided):
            return True
    elif isinstance(assessments, list):
        for item in assessments:
            if not isinstance(item, dict):
                continue
            item_type = str(item.get("type") or "").lower()
            if item_type in ("quiz", "knowledge_check", "assignment", "artifact", "discussion"):
                return True
            if item_type == "guided_practice" and _has_points(item):
                return True

    for item in module.get("items", []) or []:
        if not isinstance(item, dict):
            continue
        if str(item.get("type") or "").lower() in ("quiz", "assignment", "discussion"):
            return True
    return False


def _assessed_modules(course_config):
    return {
        _module_id(module)
        for module in course_config.get("modules", []) or []
        if isinstance(module, dict) and _module_has_assessment(module)
    }


def audit_clo_orphans(course_id=None, course_config=None, config_path=None):
    """Return CLO orphan/hollow findings from course-config.json."""
    if course_config is None:
        course_config = load_course_config(course_id=course_id, config_path=config_path)

    findings = []
    clos = _extract_clos(course_config)
    mappings = _extract_mlo_mappings(course_config)
    mappings_by_clo = {}
    for mapping in mappings:
        mappings_by_clo.setdefault(mapping["clo_id"], []).append(mapping)
    assessed = _assessed_modules(course_config)

    for clo in clos:
        clo_mappings = mappings_by_clo.get(clo["id"], [])
        if not clo_mappings:
            findings.append({
                "clo_id": clo["id"],
                "clo_text": clo["text"],
                "issue_type": "orphan_clo",
                "detail": "CLO has no mapped module learning objectives.",
            })
            continue

        assessed_mappings = [
            mapping for mapping in clo_mappings
            if mapping["module_id"] in assessed
        ]
        if not assessed_mappings:
            module_names = sorted({mapping["module_name"] for mapping in clo_mappings})
            findings.append({
                "clo_id": clo["id"],
                "clo_text": clo["text"],
                "issue_type": "hollow_clo",
                "detail": (
                    "CLO maps to MLOs but none of the mapped modules has a quiz, "
                    f"assignment, or discussion assessment: {', '.join(module_names)}."
                ),
            })

    return findings


def main():
    parser = argparse.ArgumentParser(description="Audit CLO downstream coverage")
    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_clo_orphans(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 CLO orphan findings.")
        return
    for finding in findings:
        print(f"{finding['clo_id']}: {finding['issue_type']} - {finding['detail']}")


if __name__ == "__main__":
    main()
