#!/usr/bin/env python3
"""ASU session calendar utilities — pure, deterministic, no network.

Encodes ASU Fall/Spring A/B/C session structure and build-complete window math
so any build or conversion workflow can compute "how long is this session" and
"when must the build be done."

Summer sessions differ from Fall/Spring A/B/C and are out of scope for v1.
Year-specific session dates live only in standards/asu-academic-calendar.json —
they are never hardcoded in this file.

CLI:
    python scripts/session_calendar.py --session A
    python scripts/session_calendar.py --term "Fall 2026" --session A
    python scripts/session_calendar.py --term "Fall 2026" --session A --start 2026-08-20
"""

from __future__ import annotations

import argparse
import json
import sys
from datetime import date, timedelta
from pathlib import Path
from typing import Any

PLUGIN_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = Path(__file__).resolve().parent
if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPTS_DIR))

# ---------------------------------------------------------------------------
# Constants — session-type lengths (Fall/Spring only; Summer is out of scope).
# These lengths are structural (ASU calendar architecture) and do not change
# year-to-year; year-specific start/end dates live in asu-academic-calendar.json.
# ---------------------------------------------------------------------------

SESSION_LENGTHS_WEEKS: dict[str, float] = {
    "A": 7.5,
    "B": 7.5,
    "C": 15.0,
    "full": 15.0,
}

_VALID_TYPES = tuple(SESSION_LENGTHS_WEEKS.keys())

# Build-window constants (in days relative to session start).
BUILD_TARGET_DAYS: int = 28  # 4 weeks before session start — comfortable target
BUILD_FLOOR_DAYS: int = 14   # 2 weeks before session start — hard floor

# Path to the calendar data file — dates live here, never in .py code.
_CALENDAR_JSON = PLUGIN_ROOT / "standards" / "asu-academic-calendar.json"


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _parse_date(value: date | str) -> date:
    """Accept a ``datetime.date`` object or an ISO-8601 string; return ``date``."""
    if isinstance(value, date):
        return value
    return date.fromisoformat(str(value))


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def session_length_weeks(session_type: str) -> float:
    """Return the number of weeks for *session_type* (case-insensitive).

    Valid types: ``A``, ``B``, ``C``, ``full``.
    Raises ``ValueError`` for unknown types, including Summer sessions (out of
    scope for v1).

    >>> session_length_weeks("A")
    7.5
    >>> session_length_weeks("c")
    15.0
    >>> session_length_weeks("full")
    15.0
    """
    normalized = session_type.strip()
    # Try exact match first, then case-insensitive match against all keys
    if normalized in SESSION_LENGTHS_WEEKS:
        return SESSION_LENGTHS_WEEKS[normalized]
    normalized_lower = normalized.lower()
    for key, value in SESSION_LENGTHS_WEEKS.items():
        if key.lower() == normalized_lower:
            return value
    valid = ", ".join(sorted(SESSION_LENGTHS_WEEKS))
    raise ValueError(
        f"Unknown session type {session_type!r}. Valid types: {valid}. "
        "Note: Summer sessions differ from Fall/Spring and are out of scope for v1."
    )


def build_deadlines(session_start: date | str) -> dict[str, date]:
    """Return the build-complete target and floor dates for *session_start*.

    Returns a dict with keys:
        ``target_date`` — session_start - 28 days (4-week comfortable target)
        ``floor_date``  — session_start - 14 days (2-week hard floor)

    >>> from datetime import date
    >>> build_deadlines(date(2026, 8, 20))
    {'target_date': datetime.date(2026, 7, 23), 'floor_date': datetime.date(2026, 8, 6)}
    """
    start = _parse_date(session_start)
    return {
        "target_date": start - timedelta(days=BUILD_TARGET_DAYS),
        "floor_date": start - timedelta(days=BUILD_FLOOR_DAYS),
    }


def runway_status(
    session_start: date | str,
    today: date | None = None,
) -> dict[str, Any]:
    """Return build-runway status relative to *today*.

    Args:
        session_start: The session start date (``date`` object or ISO string).
        today:         Reference date (defaults to ``date.today()``; inject in tests).

    Returns a dict with keys:
        ``days_to_target`` — int, days between today and the target date (negative = past)
        ``days_to_floor``  — int, days between today and the floor date (negative = past)
        ``status``         — one of:
            ``"comfortable"``  — today <= target_date (build on track)
            ``"tight"``        — target_date < today <= floor_date (urgent)
            ``"past_floor"``   — today > floor_date (missed hard floor)
    """
    if today is None:
        today = date.today()
    today = _parse_date(today)
    deadlines = build_deadlines(session_start)
    target = deadlines["target_date"]
    floor = deadlines["floor_date"]

    days_to_target = (target - today).days
    days_to_floor = (floor - today).days

    if today <= target:
        status = "comfortable"
    elif today <= floor:
        status = "tight"
    else:
        status = "past_floor"

    return {
        "days_to_target": days_to_target,
        "days_to_floor": days_to_floor,
        "status": status,
    }


def lookup_term(term: str, session_type: str | None = None) -> dict[str, str] | None:
    """Look up a term (and optional session type) in the academic calendar JSON.

    Returns ``{"start": "YYYY-MM-DD", "end": "YYYY-MM-DD"}`` if found.
    Returns ``None`` on any miss (unknown term, unknown session, file missing) —
    never raises.

    Args:
        term:         e.g. ``"Fall 2026"``
        session_type: e.g. ``"A"``, ``"B"``, ``"C"`` (case-insensitive).
                      If ``None``, returns the whole term dict or ``None``.
    """
    try:
        data = json.loads(_CALENDAR_JSON.read_text(encoding="utf-8"))
    except Exception:  # noqa: BLE001 — file missing, malformed JSON, etc.
        return None

    terms: dict = data.get("terms") or {}
    term_data = terms.get(term)
    if term_data is None:
        return None

    if session_type is None:
        return term_data  # return the whole term dict

    # Try exact match, then case-insensitive match against session keys in the data
    normalized = session_type.strip()
    session_data = term_data.get(normalized)
    if session_data is None:
        normalized_upper = normalized.upper()
        session_data = term_data.get(normalized_upper)
    if session_data is None:
        # Full case-insensitive scan
        lower = normalized.lower()
        for k, v in term_data.items():
            if str(k).lower() == lower:
                session_data = v
                break
    if session_data is None:
        return None
    return session_data  # {"start": ..., "end": ...}


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

def _print_section(title: str, data: dict) -> None:
    print(f"\n{title}")
    for k, v in data.items():
        print(f"  {k}: {v}")


def main() -> None:
    parser = argparse.ArgumentParser(
        description="ASU session calendar utilities — build windows and term lookup."
    )
    parser.add_argument(
        "--session",
        metavar="TYPE",
        help="Session type: A, B, C, or full",
    )
    parser.add_argument(
        "--term",
        metavar="TERM",
        help='Term label, e.g. "Fall 2026"',
    )
    parser.add_argument(
        "--start",
        metavar="YYYY-MM-DD",
        help="Session start date (ISO 8601). If omitted, use looked-up term start.",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Machine-readable JSON output",
    )
    args = parser.parse_args()

    output: dict[str, Any] = {}

    # Session length
    if args.session:
        try:
            weeks = session_length_weeks(args.session)
            output["session_type"] = args.session.upper()
            output["session_length_weeks"] = weeks
        except ValueError as exc:
            print(f"ERROR: {exc}", file=sys.stderr)
            sys.exit(1)

    # Term lookup
    if args.term:
        session_type = args.session if args.session else None
        term_info = lookup_term(args.term, session_type=session_type)
        if term_info is None:
            print(
                f"Term {args.term!r}"
                + (f" session {args.session!r}" if args.session else "")
                + " not found in asu-academic-calendar.json",
                file=sys.stderr,
            )
        else:
            output["term"] = args.term
            output["term_info"] = term_info
            # Prefer the looked-up start if --start not supplied
            if not args.start and isinstance(term_info, dict) and "start" in term_info:
                args.start = term_info["start"]

    # Build deadlines + runway
    if args.start:
        try:
            start_date = _parse_date(args.start)
        except ValueError as exc:
            print(f"ERROR: invalid --start date: {exc}", file=sys.stderr)
            sys.exit(1)
        deadlines = build_deadlines(start_date)
        runway = runway_status(start_date)
        output["session_start"] = str(start_date)
        output["build_deadlines"] = {k: str(v) for k, v in deadlines.items()}
        output["runway_status"] = runway

    if not output:
        parser.print_help()
        sys.exit(0)

    if args.json:
        print(json.dumps(output, indent=2, default=str))
    else:
        if "session_length_weeks" in output:
            print(
                f"Session {output['session_type']}: "
                f"{output['session_length_weeks']} weeks"
            )
        if "term_info" in output:
            print(f"Term: {output['term']}")
            ti = output["term_info"]
            if isinstance(ti, dict) and "start" in ti:
                print(f"  Start: {ti.get('start')}  End: {ti.get('end')}")
        if "build_deadlines" in output:
            bd = output["build_deadlines"]
            rv = output["runway_status"]
            print(f"Session start:  {output['session_start']}")
            print(f"Build target:   {bd['target_date']} ({rv['days_to_target']:+d} days)")
            print(f"Build floor:    {bd['floor_date']} ({rv['days_to_floor']:+d} days)")
            print(f"Runway status:  {rv['status']}")


if __name__ == "__main__":
    main()
