#!/usr/bin/env python3
"""refresh_asu_calendar.py — Scrape ASU registrar academic calendar.

Usage:
    python3 scripts/refresh_asu_calendar.py [--year 2026]

Writes data/asu_calendar.json. Tolerant parser: on any parse failure writes
a stub file with empty terms array so downstream scripts never crash.
"""

import argparse
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path

# ---------------------------------------------------------------------------
# Dependency check
# ---------------------------------------------------------------------------
try:
    import requests
    from bs4 import BeautifulSoup
except ImportError as e:
    print(f"[refresh_asu_calendar] Missing dependency: {e}")
    print("  Install with: pip install requests beautifulsoup4")
    sys.exit(1)

SOURCE_URL = "https://registrar.asu.edu/academic-calendar"
TIMEOUT = 30

# Month abbreviations → zero-padded month number
_MONTHS = {
    "jan": "01", "feb": "02", "mar": "03", "apr": "04",
    "may": "05", "jun": "06", "jul": "07", "aug": "08",
    "sep": "09", "oct": "10", "nov": "11", "dec": "12",
}

# Term label patterns we care about
_SESSION_RE = re.compile(
    r"(fall|spring|summer)\s*([ab]|i{1,3}|iv)?[\s\-]*(\d{4})",
    re.IGNORECASE,
)

# Date patterns: "Aug 19" / "August 19, 2026" / "08/19/2026" / "2026-08-19"
_DATE_RE = re.compile(
    r"""
    (?:
        (\d{4})-(\d{2})-(\d{2})                          # ISO
      | (\d{1,2})/(\d{1,2})/(\d{4})                      # MM/DD/YYYY
      | ([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:,\s*(\d{4}))?  # Month DD, YYYY
    )
    """,
    re.VERBOSE,
)


def _normalize_date(text, default_year=None):
    """Return YYYY-MM-DD string or None."""
    text = text.strip()
    m = _DATE_RE.search(text)
    if not m:
        return None
    iso_y, iso_m, iso_d, sl_m, sl_d, sl_y, mn, dd, yr = m.groups()
    if iso_y:
        return f"{iso_y}-{iso_m}-{iso_d}"
    if sl_m:
        return f"{sl_y}-{int(sl_m):02d}-{int(sl_d):02d}"
    if mn:
        month_key = mn[:3].lower()
        month_num = _MONTHS.get(month_key)
        if not month_num:
            return None
        year = yr or (str(default_year) if default_year else None)
        if not year:
            return None
        return f"{year}-{month_num}-{int(dd):02d}"
    return None


def _normalize_session_label(raw):
    """Return a canonical session label like 'Fall B 2026'."""
    m = _SESSION_RE.search(raw)
    if not m:
        return raw.strip()
    season, part, year = m.groups()
    season = season.capitalize()
    part = (part or "").strip().upper()
    # Normalize roman numerals / A/B
    part_map = {"A": "A", "B": "B", "I": "A", "II": "B", "III": "C", "IV": "D"}
    part_label = part_map.get(part, part)
    if part_label:
        return f"{season} {part_label} {year}"
    return f"{season} {year}"


# ---------------------------------------------------------------------------
# Scraper
# ---------------------------------------------------------------------------

def _scrape(year, verbose=True):
    """Return list of term dicts. Returns [] on any parse failure."""
    url = SOURCE_URL
    try:
        resp = requests.get(url, timeout=TIMEOUT, headers={"User-Agent": "ASUCB-Calendar/1.0"})
        resp.raise_for_status()
    except requests.RequestException as exc:
        if verbose:
            print(f"[refresh_asu_calendar] Network error: {exc}")
            print("  Wrote stub file. Edit data/asu_calendar.json manually.")
        return []

    soup = BeautifulSoup(resp.text, "html.parser")
    terms = []

    # Strategy 1: find <table> elements with date-like content
    tables = soup.find_all("table")
    for table in tables:
        rows = table.find_all("tr")
        for row in rows:
            cells = [c.get_text(" ", strip=True) for c in row.find_all(["td", "th"])]
            if len(cells) < 2:
                continue
            label_cell = cells[0]
            # Only rows whose first cell looks like a term/session label
            if not _SESSION_RE.search(label_cell):
                continue
            session = _normalize_session_label(label_cell)
            # Collect dates from remaining cells
            dates = []
            for cell in cells[1:]:
                d = _normalize_date(cell, default_year=year)
                if d:
                    dates.append(d)
            if not dates:
                continue
            dates.sort()
            start = dates[0]
            end = dates[-1] if len(dates) > 1 else None
            terms.append({
                "session": session,
                "start": start,
                "end": end,
                "drop_deadline": None,
                "withdraw_deadline": None,
            })

    # Strategy 2: definition lists / heading+paragraph pairs
    if not terms:
        headings = soup.find_all(re.compile(r"^h[2-4]$"))
        for h in headings:
            if not _SESSION_RE.search(h.get_text()):
                continue
            session = _normalize_session_label(h.get_text())
            # Grab sibling text for dates
            sib = h.find_next_sibling()
            if not sib:
                continue
            sib_text = sib.get_text(" ", strip=True)
            dates = []
            for m in _DATE_RE.finditer(sib_text):
                full_str = m.group(0)
                d = _normalize_date(full_str, default_year=year)
                if d:
                    dates.append(d)
            dates.sort()
            if not dates:
                continue
            terms.append({
                "session": session,
                "start": dates[0],
                "end": dates[-1] if len(dates) > 1 else None,
                "drop_deadline": None,
                "withdraw_deadline": None,
            })

    # Deduplicate by session label
    seen = set()
    unique = []
    for t in terms:
        key = t["session"]
        if key not in seen:
            seen.add(key)
            unique.append(t)

    # Filter to requested year if found in label
    if year:
        filtered = [t for t in unique if str(year) in t["session"]]
        if filtered:
            unique = filtered

    if verbose and not unique:
        print("[refresh_asu_calendar] Could not parse term rows from page.")
        print("  The registrar page layout may have changed.")
        print("  Edit data/asu_calendar.json manually with correct session dates.")

    return unique


# ---------------------------------------------------------------------------
# Output
# ---------------------------------------------------------------------------

def write_calendar(terms, out_path, verbose=True):
    payload = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "source_url": SOURCE_URL,
        "timezone": "America/Phoenix",
        "terms": terms,
        "holidays": [],
    }
    out_path.parent.mkdir(parents=True, exist_ok=True)
    with open(out_path, "w", encoding="utf-8") as f:
        json.dump(payload, f, indent=2)
        f.write("\n")
    if verbose:
        print(f"[refresh_asu_calendar] Wrote {out_path}  ({len(terms)} term(s))")


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Scrape ASU registrar academic calendar to data/asu_calendar.json"
    )
    parser.add_argument("--year", type=int, default=datetime.now().year,
                        help="Academic year to filter (default: current year)")
    args = parser.parse_args()

    root = Path(__file__).parent.parent
    out_path = root / "data" / "asu_calendar.json"

    terms = _scrape(args.year, verbose=True)
    write_calendar(terms, out_path, verbose=True)


if __name__ == "__main__":
    main()
