#!/usr/bin/env python3
"""WCAG 2.1 AA accessibility audit of Canvas course pages.

Usage:
    python audit_pages.py [--output <report_path>]

Requires environment variables:
    CANVAS_TOKEN, CANVAS_DOMAIN, CANVAS_COURSE_ID

Checks (now unified with wcag_checks.WCAGContentParser — QB4):
    - Images missing alt text (WCAG 1.1.1)
    - Heading hierarchy skips / wrong start level (WCAG 1.3.1)
    - Generic link text ("click here", "here", "link", "read more", "more") (WCAG 2.4.4)
    - Low contrast inline colors when BOTH fg+bg are declared (WCAG 1.4.3)
      Large text (h1–h3): 3:1 threshold; normal text: 4.5:1 threshold
    - Disallowed elements (marquee, blink) (WCAG 2.2.2)
    - Invalid tabindex (WCAG 2.4.3)
    - role=radio without aria-checked (WCAG 4.1.2)
    - Form controls missing accessible name (WCAG 4.1.2)
    - Missing <html lang> / <meta viewport> (suppressed for Canvas body fragments
      per the fragment-mode convention in preflight_checks._wcag_issues)

NOTE — Convergence from fork (audit_pages v1 → v2):
  The original AccessibilityAuditor checked inline color contrast against an
  assumed white background (#FFFFFF) when no background color was declared.
  WCAGContentParser requires BOTH fg and bg to be declared inline, which is more
  correct — a color-only inline style against an unknown background is
  speculative and not a WCAG 1.4.3 finding.  Findings that existed only because
  no background was specified will no longer fire; that is an improvement.
"""

import os
import re
import sys
import argparse
from collections import defaultdict

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from canvas_api import get_config, require_course_id, get_all_pages
from wcag_checks import WCAGContentParser

try:
    import timeline_logger
except Exception:
    timeline_logger = None


def _run_wcag_parser(html, slug):
    """Feed HTML to WCAGContentParser and return the list of finding strings.

    Finding strings preserve the audit_pages v1 output format for callers and
    the report template (severity_distribution, type_counts, etc.).

    Fragment mode: Canvas page bodies never have <html lang> / <meta viewport>
    — suppress those document-level findings exactly as preflight_checks does.

    Canvas-wrapped convention: Canvas strips <h1> from page bodies (Canvas
    itself renders the page title as h1 in its chrome).  The shared parser
    allows h2 as the first heading level when the <!-- canvas-wrapped -->
    comment is present.  We prepend that comment here so Canvas page bodies
    (which always start at h2) are not incorrectly flagged for heading-start.
    """
    # Prepend the canvas-wrapped marker so WCAGContentParser treats h2 as a
    # valid heading start (Canvas injects h1 for the page title in its chrome).
    wrapped_html = "<!-- canvas-wrapped -->\n" + html
    parser = WCAGContentParser(slug)
    try:
        parser.feed(wrapped_html)
        parser.close()
        parser.finalize()
    except Exception:
        pass

    # Document-level criteria suppressed for Canvas body fragments
    # (same set as preflight_checks._WCAG_FRAGMENT_SUPPRESS)
    _SUPPRESS = {"WCAG 3.1.1", "WCAG 1.4.10"}
    # Suppress interactive-gate review notes that are not WCAG SCs
    _SUPPRESS_CODES = {"WARNING"}

    issues = []
    for finding in parser.failures + parser.warnings:
        code = finding.get("code", "")
        if code in _SUPPRESS or code in _SUPPRESS_CODES:
            continue
        msg = finding.get("message", "")
        element = finding.get("element", "")
        issues.append(_format_finding(code, msg, element))
    return issues


def _format_finding(code, message, element=""):
    """Format a WCAGContentParser finding as a human-readable string.

    The string format matches what the existing report/callers expect:
      - "IMG missing alt: <src>"       — for WCAG 1.1.1
      - "Heading skip: h2 -> h4 ..."   — for WCAG 1.3.1 (level jump)
      - "Heading start: h3 ..."        — for WCAG 1.3.1 (bad start level)
      - "Generic link text '...' -> <href>" — for WCAG 2.4.4
      - "Low contrast ..."             — for WCAG 1.4.3
      - raw message for everything else
    """
    if code == "WCAG 1.1.1":
        return f"IMG missing alt: {message}"
    if code == "WCAG 1.3.1":
        if "jumps from" in message:
            # "Heading level jumps from h2 to h4" → "Heading skip: h2 -> h4"
            m = re.search(r"jumps from h(\d+) to h(\d+)", message)
            if m:
                return f"Heading skip: h{m.group(1)} -> h{m.group(2)}"
        if "must start at" in message or "hierarchy" in message.lower():
            return f"Heading start: {element} — {message}"
        return f"Heading issue: {message}"
    if code == "WCAG 2.4.4":
        # "Generic link text 'here' is not descriptive" + element contains href
        # Reconstruct in v1 format: "Generic link text 'here' -> <href>"
        text_m = re.search(r"'([^']+)'", message)
        text = text_m.group(1) if text_m else ""
        href_m = re.search(r"href=(.+)$", element)
        href = href_m.group(1) if href_m else element
        return f"Generic link text '{text}' -> {href}"
    if code == "WCAG 1.4.3":
        return f"Low contrast: {message}"
    # Everything else: prefix with WCAG code
    return f"{code}: {message}"


def _issue_type(issue_str):
    """Map a finding string to a type label for the summary report."""
    if issue_str.startswith("IMG missing alt"):
        return "Missing alt text"
    if issue_str.startswith("Heading skip") or issue_str.startswith("Heading start"):
        return "Heading hierarchy issue"
    if issue_str.startswith("Generic link text"):
        return "Generic link text"
    if issue_str.startswith("Low contrast"):
        return "Low contrast color"
    return "Other"


def _issue_severity(issue_str):
    """Map a finding string to a severity bucket for timeline events."""
    if issue_str.startswith("IMG missing alt") or "Heading skip" in issue_str or "Heading start" in issue_str:
        return "critical"
    if issue_str.startswith("Generic link text") or issue_str.startswith("Low contrast"):
        return "warning"
    return "info"


def audit_course(config, output_path=None):
    """Run WCAG audit on all pages in a Canvas course."""
    require_course_id(config)

    print("Fetching all pages (with bodies)...")
    pages = get_all_pages(config, include_body=True)
    print(f"Found {len(pages)} pages\n")

    all_issues = defaultdict(list)
    summary = {"pages": 0, "issues": 0, "pages_with_issues": 0}

    for page_info in sorted(pages, key=lambda p: p.get("url", "")):
        slug = page_info.get("url", "")
        if not slug:
            continue

        body = page_info.get("body", "") or ""
        if not body.strip():
            continue

        summary["pages"] += 1
        try:
            page_issues = _run_wcag_parser(body, slug)
        except Exception as e:
            print(f"  Parse error on {slug}: {e}")
            continue

        if page_issues:
            all_issues[slug] = page_issues
            summary["pages_with_issues"] += 1
            summary["issues"] += len(page_issues)
            print(f"  {slug}: {len(page_issues)} issue(s)")
            for issue in page_issues:
                print(f"    - {issue}")

    # Print report
    print(f"\n{'=' * 60}")
    print(f"WCAG 2.1 AA AUDIT REPORT")
    print(f"{'=' * 60}")
    print(f"Pages scanned: {summary['pages']}")
    print(f"Pages with issues: {summary['pages_with_issues']}")
    print(f"Total issues: {summary['issues']}")

    if all_issues:
        print(f"\n--- Issues by type ---")
        type_counts = defaultdict(int)
        for slug, issues in all_issues.items():
            for issue in issues:
                type_counts[_issue_type(issue)] += 1
        for t, c in sorted(type_counts.items(), key=lambda x: -x[1]):
            print(f"  {t}: {c}")
    else:
        print("\nNo issues found!")

    # Save report
    if output_path:
        with open(output_path, "w") as f:
            f.write(f"WCAG 2.1 AA Audit Report - Canvas Course {config['course_id']}\n")
            f.write(f"Pages: {summary['pages']} | Issues: {summary['issues']}\n\n")
            for slug, issues in sorted(all_issues.items()):
                f.write(f"{slug}:\n")
                for issue in issues:
                    f.write(f"  - {issue}\n")
                f.write("\n")
        print(f"\nReport saved to: {output_path}")

    return summary, all_issues


def _accessibility_severity_distribution(all_issues):
    distribution = {"critical": 0, "warning": 0, "info": 0}
    for issues in all_issues.values():
        for issue in issues:
            distribution[_issue_severity(issue)] += 1
    return distribution


def _emit_accessibility_audit_completed(config, summary, all_issues):
    course_id = config.get("course_id")
    if not course_id:
        return
    distribution = _accessibility_severity_distribution(all_issues)
    try:
        if timeline_logger is None:
            raise RuntimeError("timeline_logger unavailable")
        timeline_logger.append_event(course_id, "accessibility_audit_completed", {
            "audit_type": "accessibility_audit",
            "finding_count": int(summary.get("issues", 0) or 0),
            "severity_distribution": distribution,
            "pages_scanned": int(summary.get("pages", 0) or 0),
            "pages_with_issues": int(summary.get("pages_with_issues", 0) or 0),
        })
    except Exception as exc:
        import logging
        logging.warning("Failed to emit %s event for course %s: %s",
                        "accessibility_audit_completed", course_id, exc)


def main():
    parser = argparse.ArgumentParser(
        description="WCAG 2.1 AA accessibility audit of Canvas course pages."
    )
    parser.add_argument(
        "--course-id",
        help="Canvas course ID (overrides CANVAS_COURSE_ID env var)",
    )
    parser.add_argument(
        "--output",
        default="audit_report.txt",
        help="Path to save audit report (default: audit_report.txt)",
    )
    args = parser.parse_args()

    config = get_config(course_id=args.course_id)
    summary, all_issues = audit_course(config, args.output)
    _emit_accessibility_audit_completed(config, summary, all_issues)


if __name__ == "__main__":
    main()
