#!/usr/bin/env python3
"""Build an audit-to-remediation baseline from local course timelines."""

from __future__ import annotations

import argparse
import html
import json
import os
import statistics
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

sys.path.insert(0, os.path.dirname(__file__))
try:
    from canvas_api import get_plugin_root
except Exception:
    def get_plugin_root():
        return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


AUDIT_EVENT_TYPES = {"audit_completed", "accessibility_audit_completed"}
REMEDIATION_EVENT_TYPES = {"push_verified"}
WINDOW_DAYS = 60


class BaselineError(Exception):
    pass


def _parse_timestamp(value):
    if not value:
        return None
    raw = str(value).strip()
    if raw.endswith("Z"):
        raw = raw[:-1] + "+00:00"
    try:
        parsed = datetime.fromisoformat(raw)
    except ValueError:
        return None
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _read_json(path):
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError as exc:
        raise BaselineError(f"courses/registry.json not found at {path}") from exc
    except json.JSONDecodeError as exc:
        raise BaselineError(f"courses/registry.json is not valid JSON: {exc}") from exc


def load_registry(root):
    registry_path = Path(root) / "courses" / "registry.json"
    data = _read_json(registry_path)
    courses = data if isinstance(data, list) else data.get("courses", [])
    if not isinstance(courses, list):
        raise BaselineError("courses/registry.json must contain a courses list")
    normalized = []
    for course in courses:
        if not isinstance(course, dict) or course.get("id") is None:
            continue
        normalized.append({
            "id": str(course.get("id")),
            "name": str(course.get("name") or f"Course {course.get('id')}"),
            "code": str(course.get("code") or ""),
        })
    return normalized, registry_path


def read_timeline(path):
    records = []
    try:
        with path.open("r", encoding="utf-8") as handle:
            for line in handle:
                line = line.strip()
                if not line:
                    continue
                try:
                    records.append(json.loads(line))
                except json.JSONDecodeError:
                    continue
    except FileNotFoundError:
        return None
    return records


def _payload(event):
    payload = event.get("payload", {})
    return payload if isinstance(payload, dict) else {}


def _metadata_value(payload, key):
    value = payload.get(key)
    if value is None:
        return None
    return str(value)


def _content_type(payload):
    return _metadata_value(payload, "content_type") or _metadata_value(payload, "object_type")


def _find_remediation(audit_time, audit_payload, remediation_events):
    object_id = _metadata_value(audit_payload, "object_id")
    content_type = _content_type(audit_payload)
    if not object_id or not content_type:
        return None

    window_end = audit_time + timedelta(days=WINDOW_DAYS)
    for event in remediation_events:
        event_time = event.get("_parsed_timestamp")
        if event_time is None or event_time < audit_time or event_time > window_end:
            continue
        push_payload = _payload(event)
        if _metadata_value(push_payload, "object_id") != object_id:
            continue
        if _content_type(push_payload) != content_type:
            continue
        latency = (event_time - audit_time).total_seconds() / 86400
        return {
            "timestamp": event.get("timestamp"),
            "latency_days": round(latency, 6),
            "event_type": event.get("event_type"),
        }
    return None


def _rate(remediated, total):
    if total == 0:
        return None
    return round(remediated / total * 100, 1)


def _median(values):
    if not values:
        return None
    return round(float(statistics.median(values)), 6)


def analyze_course(root, course):
    course_id = str(course["id"])
    timeline_path = Path(root) / "courses" / course_id / "timeline.jsonl"
    events = read_timeline(timeline_path)
    if events is None:
        return {
            "course_id": course_id,
            "course_name": course.get("name", f"Course {course_id}"),
            "course_code": course.get("code", ""),
            "timeline_path": str(timeline_path),
            "skipped": True,
            "note": "timeline.jsonl not found",
            "audits": [],
            "audit_count": 0,
            "with_remediation_count": 0,
            "remediation_rate": None,
            "median_latency_days": None,
        }

    parsed_events = []
    for event in events:
        if not isinstance(event, dict):
            continue
        parsed_time = _parse_timestamp(event.get("timestamp"))
        if parsed_time is None:
            continue
        event = dict(event)
        event["_parsed_timestamp"] = parsed_time
        parsed_events.append(event)

    parsed_events.sort(key=lambda item: item["_parsed_timestamp"])
    remediation_events = [
        event for event in parsed_events
        if event.get("event_type") in REMEDIATION_EVENT_TYPES
    ]

    audits = []
    for event in parsed_events:
        if event.get("event_type") not in AUDIT_EVENT_TYPES:
            continue
        audit_payload = _payload(event)
        remediation = _find_remediation(
            event["_parsed_timestamp"],
            audit_payload,
            remediation_events,
        )
        audits.append({
            "timestamp": event.get("timestamp"),
            "event_type": event.get("event_type"),
            "audit_type": audit_payload.get("audit_type"),
            "object_id": _metadata_value(audit_payload, "object_id"),
            "content_type": _content_type(audit_payload),
            "finding_count": audit_payload.get("finding_count"),
            "severity_distribution": audit_payload.get("severity_distribution", {}),
            "remediation": remediation,
            "latency_days": remediation["latency_days"] if remediation else None,
        })

    latencies = [audit["latency_days"] for audit in audits if audit["latency_days"] is not None]
    remediated = len(latencies)
    total = len(audits)
    return {
        "course_id": course_id,
        "course_name": course.get("name", f"Course {course_id}"),
        "course_code": course.get("code", ""),
        "timeline_path": str(timeline_path),
        "skipped": False,
        "note": None,
        "audits": audits,
        "audit_count": total,
        "with_remediation_count": remediated,
        "remediation_rate": _rate(remediated, total),
        "median_latency_days": _median(latencies),
    }


def aggregate_program(courses):
    total_audits = sum(course["audit_count"] for course in courses)
    remediated = sum(course["with_remediation_count"] for course in courses)
    latencies = [
        audit["latency_days"]
        for course in courses
        for audit in course["audits"]
        if audit["latency_days"] is not None
    ]
    return {
        "course_count": len(courses),
        "skipped_course_count": sum(1 for course in courses if course.get("skipped")),
        "total_audits": total_audits,
        "with_remediation_count": remediated,
        "weighted_remediation_rate": _rate(remediated, total_audits),
        "median_latency_days": _median(latencies),
    }


def build_summary(root, course_id=None):
    courses, registry_path = load_registry(root)
    if course_id:
        courses = [course for course in courses if course["id"] == str(course_id)]
        if not courses:
            courses = [{"id": str(course_id), "name": f"Course {course_id}", "code": ""}]
    analyzed = [analyze_course(root, course) for course in courses]
    return {
        "registry_path": str(registry_path),
        "window_days": WINDOW_DAYS,
        "audit_event_types": sorted(AUDIT_EVENT_TYPES),
        "remediation_event_types": sorted(REMEDIATION_EVENT_TYPES),
        "program": aggregate_program(analyzed),
        "courses": analyzed,
    }


def _display_rate(value):
    return "N/A" if value is None else f"{value:.1f}%"


def _display_number(value):
    return "N/A" if value is None else str(value)


def render_html(summary, generated_at, root=None):
    rows = []
    for course in summary["courses"]:
        note = course.get("note") or ""
        rows.append(
            "<tr>"
            f"<td>{html.escape(course['course_id'])}</td>"
            f"<td>{html.escape(course['course_name'])}</td>"
            f"<td>{course['audit_count']}</td>"
            f"<td>{course['with_remediation_count']}</td>"
            f"<td>{_display_rate(course['remediation_rate'])}</td>"
            f"<td>{_display_number(course['median_latency_days'])}</td>"
            f"<td>{html.escape(note)}</td>"
            "</tr>"
        )

    program = summary["program"]
    body = f"""
  <div class="report-section">
    <div class="report-section-header">
      <span class="report-section-title">Program Baseline</span>
      <span class="report-section-badge badge-pass">{program['total_audits']} audits</span>
    </div>
    <div class="report-section-body">
      <p style="font-size:13px;color:#555;margin-bottom:12px;">
        Audit events are joined to subsequent same-object <code>push_verified</code>
        events within {summary['window_days']} days.
      </p>
      <table style="width:100%;border-collapse:collapse;font-size:13px;">
        <thead>
          <tr style="text-align:left;border-bottom:1px solid #e4e4e7;">
            <th style="padding:8px;">Course ID</th>
            <th style="padding:8px;">Course</th>
            <th style="padding:8px;">Audits</th>
            <th style="padding:8px;">Remediated</th>
            <th style="padding:8px;">Rate</th>
            <th style="padding:8px;">Median latency days</th>
            <th style="padding:8px;">Note</th>
          </tr>
        </thead>
        <tbody>
          {''.join(rows)}
        </tbody>
      </table>
    </div>
  </div>
"""
    template_root = Path(root or get_plugin_root())
    template_path = template_root / "templates" / "report-html" / "report-base.html"
    template = template_path.read_text(encoding="utf-8")
    replacements = {
        "{{REPORT_TITLE}}": "Audit Remediation Baseline",
        "{{COURSE_NAME}}": "Program-wide",
        "{{REPORT_TYPE}}": "Phase 0 Baseline",
        "{{REPORT_DATE}}": generated_at,
        "{{PAGES_SCANNED}}": str(program["course_count"]),
        "{{PASS_COUNT}}": str(program["with_remediation_count"]),
        "{{WARN_COUNT}}": _display_rate(program["weighted_remediation_rate"]),
        "{{FAIL_COUNT}}": str(program["total_audits"] - program["with_remediation_count"]),
        "{{REPORT_BODY}}": body,
    }
    for old, new in replacements.items():
        template = template.replace(old, new)
    return template


def write_outputs(root, summary):
    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    generated_at = datetime.now().astimezone().isoformat(timespec="seconds")
    output_summary = dict(summary)
    output_summary["generated_at"] = generated_at

    reports_dir = Path(root) / "courses" / "reports"
    reports_dir.mkdir(parents=True, exist_ok=True)
    stem = f"{timestamp}-remediation-baseline"
    json_path = reports_dir / f"{stem}.json"
    html_path = reports_dir / f"{stem}.html"

    json_path.write_text(json.dumps(output_summary, indent=2), encoding="utf-8")
    html_path.write_text(render_html(output_summary, generated_at, root=root), encoding="utf-8")
    return html_path, json_path, output_summary


def main(argv=None, root=None):
    parser = argparse.ArgumentParser(
        description="Build audit-to-remediation baseline from local timeline events."
    )
    parser.add_argument("--course-id", help="Analyze a single course ID")
    args = parser.parse_args(argv)

    root = Path(root or get_plugin_root())
    try:
        summary = build_summary(root, course_id=args.course_id)
    except BaselineError as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        return 1

    html_path, json_path, output_summary = write_outputs(root, summary)
    print(json.dumps({
        "ok": True,
        "html_path": str(html_path),
        "json_path": str(json_path),
        "program": output_summary["program"],
    }, indent=2))
    return 0


if __name__ == "__main__":
    sys.exit(main())
