#!/usr/bin/env python3
"""ASUCB Push Syllabus — push staged syllabus body to Canvas.

Workflow:
    1. Read staged syllabus_body.html
    2. Backup current Canvas syllabus_body
    3. PUT /api/v1/courses/{id} with course[syllabus_body]
    4. Verify write by re-fetching
    5. Clear staged file only on success
"""

import argparse
import json
import logging
import os
import sys
from datetime import datetime
from pathlib import Path

sys.path.insert(0, os.path.dirname(__file__))
import canvas_api
from canvas_api import validate_course_id
from mockup_manager import lint_content

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger(__name__)

COURSES_DIR = Path(__file__).parent.parent / "courses"


def _backup(course_id, html):
    backup_dir = COURSES_DIR / str(course_id) / "backups" / "syllabus"
    backup_dir.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d-%H%M%S")
    dest = backup_dir / f"syllabus_body_{ts}.html"
    dest.write_text(html, encoding="utf-8")
    log.info("  Backup saved: %s", dest)
    return str(dest)


def _log_audience_violations(violations, level="error"):
    log_fn = log.warning if level == "warning" else log.error
    for violation in violations:
        log_fn("  %s: %s", violation["token"], violation["line_hint"])


def push_syllabus(course_id, dry_run=False, force_shrink=False,
                  allow_audience_violations=False):
    staged = COURSES_DIR / str(course_id) / "staging" / "syllabus" / "syllabus_body.html"
    if not staged.exists():
        log.error("No staged syllabus found at %s", staged)
        return False

    new_html = staged.read_text(encoding="utf-8")
    log.info("[push_syllabus] Course %s — %d bytes staged", course_id, len(new_html))

    audience_violations = lint_content(new_html, "student")
    if audience_violations and not allow_audience_violations:
        log.error("  Audience lint blocked syllabus push.")
        _log_audience_violations(audience_violations)
        return False
    if audience_violations:
        log.warning("  Audience lint found forbidden token(s); proceeding by explicit override.")
        _log_audience_violations(audience_violations, level="warning")

    # Fetch current syllabus for backup
    config = canvas_api.get_config(course_id=str(course_id))
    domain = config["domain"].rstrip("/")
    if not domain.startswith("http"):
        domain = "https://" + domain
    token = config["token"]
    headers = {"Authorization": f"Bearer {token}"}

    import requests
    resp = requests.get(
        f"{domain}/api/v1/courses/{course_id}",
        headers=headers,
        params={"include[]": "syllabus_body"},
        timeout=30,
    )
    resp.raise_for_status()
    current_html = resp.json().get("syllabus_body") or ""
    backup_path = _backup(course_id, current_html)
    log.info("  Backed up %d chars from Canvas", len(current_html))

    # Shrink-guard: block if staged is >30% smaller than current Canvas syllabus
    if current_html and len(new_html) < len(current_html) * 0.7 and not force_shrink:
        pct = 100 * (1 - len(new_html) / len(current_html))
        log.error(
            "  Staged syllabus is %.0f%% smaller than current Canvas syllabus (%d → %d chars).",
            pct, len(current_html), len(new_html),
        )
        log.error("  Refusing to push without --force-shrink. Backup at %s", backup_path)
        return False

    if dry_run:
        log.info("  [DRY RUN] Would PUT %d chars to course %s syllabus", len(new_html), course_id)
        return True

    # Push
    log.info("  Pushing syllabus_body (%d chars)…", len(new_html))
    put_resp = requests.put(
        f"{domain}/api/v1/courses/{course_id}",
        headers=headers,
        data={"course[syllabus_body]": new_html},
        timeout=30,
    )
    if put_resp.status_code not in (200, 201):
        log.error("  PUT failed: %s %s", put_resp.status_code,
                  canvas_api._sanitize_error_text(put_resp.text))
        return False

    # Verify
    ver_resp = requests.get(
        f"{domain}/api/v1/courses/{course_id}",
        headers=headers,
        params={"include[]": "syllabus_body"},
        timeout=30,
    )
    ver_resp.raise_for_status()
    verified = ver_resp.json().get("syllabus_body") or ""
    if not verified.strip():
        log.error("  Verification failed: Canvas returned empty syllabus_body after write")
        return False
    log.info("  Verified: %d chars in Canvas", len(verified))

    # Clear staging only on success
    staged.unlink()
    log.info("  Staging cleared: %s", staged)
    log.info("[push_syllabus] Done. Backup at %s", backup_path)
    return True


def main():
    parser = argparse.ArgumentParser(description="Push staged syllabus body to Canvas")
    parser.add_argument("--course-id", required=True, help="Canvas course ID")
    parser.add_argument("--dry-run", action="store_true", help="Skip actual Canvas write")
    parser.add_argument("--force-shrink", action="store_true",
                        help="Allow push even when staged content is >30%% smaller than Canvas current")
    parser.add_argument("--allow-audience-violations", action="store_true",
                        help="Warn but do not block on audience-lint violations")
    parser.add_argument("--confirm-verb",
                        choices=["push", "approved", "deploy"],
                        help="Explicit push verb (required for non-dry-run). "
                        "Accepts only 'push', 'approved', or 'deploy'.")
    args = parser.parse_args()
    if not args.dry_run and not args.confirm_verb:
        parser.error(
            "non-dry-run push requires --confirm-verb {push,approved,deploy}. "
            "Ambiguous affirmatives ('ok','yes','confirm') are rejected."
        )
    if getattr(args, 'course_id', None) is not None:
        try:
            args.course_id = validate_course_id(args.course_id)
        except ValueError as exc:
            parser.error(str(exc))

    ok = push_syllabus(
        args.course_id,
        dry_run=args.dry_run,
        force_shrink=args.force_shrink,
        allow_audience_violations=args.allow_audience_violations,
    )
    sys.exit(0 if ok else 1)


if __name__ == "__main__":
    main()
