from __future__ import annotations

import importlib.util
import json
import os
import subprocess
import textwrap
from urllib import parse, request
from datetime import datetime, timedelta
from pathlib import Path
from zoneinfo import ZoneInfo


ROOT = Path(__file__).resolve().parent
OUTPUT = ROOT / "morning.md"
SNAPSHOT = ROOT / "coros_snapshot.json"
CALENDAR_SNAPSHOT = ROOT / "calendar_snapshot.json"
TZ = ZoneInfo("America/Los_Angeles")
WEATHER_LOCATION = "92627"
LINE_WIDTH = 45

OURA_SERVER = Path(
    r"C:\Users\valle\.codex\plugins\cache\personal\oura-ring\0.2.0+codex.20260605\scripts\server.py"
)
def load_module(name: str, path: Path, cwd: Path | None = None):
    old_cwd = Path.cwd()
    try:
        if cwd:
            os.chdir(cwd)
        spec = importlib.util.spec_from_file_location(name, path)
        if spec is None or spec.loader is None:
            raise RuntimeError(f"Cannot load {path}")
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return module
    finally:
        os.chdir(old_cwd)


def val(value, fallback: str = "?") -> str:
    return fallback if value is None or value == "" else str(value)


def score(value) -> int | None:
    try:
        return int(round(float(value)))
    except (TypeError, ValueError):
        return None


def load_oura() -> dict:
    try:
        oura = load_module("oura_server", OURA_SERVER)
        summary = oura.summarize_recovery_trends(days=7, live=True)
        days = summary.get("days") or []
        latest = days[-1] if days else {}
        return {
            "ok": True,
            "readiness": latest.get("readiness_score") or summary.get("average_readiness"),
            "sleep": latest.get("sleep_score") or summary.get("average_sleep_score"),
            "hrv": latest.get("average_sleep_hrv") or summary.get("average_sleep_hrv"),
            "rhr": latest.get("resting_heart_rate") or summary.get("average_resting_hr"),
            "sleep_hours": latest.get("sleep_hours") or summary.get("average_sleep_hours"),
            "source": summary.get("source", "oura"),
        }
    except Exception as exc:
        return {"ok": False, "error": str(exc)}


def load_calendar(now: datetime) -> dict:
    try:
        cmd = [
            "gog",
            "calendar",
            "events",
            "primary",
            "--today",
            "--max=5",
            "--json",
            "--results-only",
        ]
        completed = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
        if completed.returncode != 0:
            raise RuntimeError((completed.stderr or completed.stdout).strip() or "gog calendar failed")
        payload = json.loads(completed.stdout or "[]")
        events = payload if isinstance(payload, list) else payload.get("events", [])
        compact = []
        for event in events[:3]:
            title = event.get("summary") or event.get("display_title") or "Event"
            start = event_time(event.get("start"))
            end = event_time(event.get("end"))
            compact.append({"title": title, "start": start, "end": end})
        return {"ok": True, "events": compact}
    except Exception as exc:
        fallback = load_calendar_snapshot()
        if fallback.get("ok"):
            fallback["warning"] = str(exc)
            return fallback
        return {"ok": False, "error": str(exc)}


def load_weather() -> dict:
    try:
        query = parse.quote(WEATHER_LOCATION)
        url = f"https://wttr.in/{query}?format=j1"
        req = request.Request(url, headers={"User-Agent": "eInk-dashboard/1.0"})
        with request.urlopen(req, timeout=15) as response:
            payload = json.loads(response.read().decode("utf-8"))

        current = (payload.get("current_condition") or [{}])[0]
        today = (payload.get("weather") or [{}])[0]
        hourly = today.get("hourly") or []
        rain_chance = max(
            int(hour.get("chanceofrain") or 0)
            for hour in hourly
        ) if hourly else None

        return {
            "ok": True,
            "temp_f": current.get("temp_F"),
            "feels_f": current.get("FeelsLikeF"),
            "condition": (current.get("weatherDesc") or [{}])[0].get("value"),
            "wind_mph": current.get("windspeedMiles"),
            "rain_chance": rain_chance,
        }
    except Exception as exc:
        return {"ok": False, "error": str(exc)}


def weather_lines(weather: dict) -> list[str]:
    if not weather.get("ok"):
        return ["Weather: unavailable"]
    rain = weather.get("rain_chance")
    rain_text = f" | rain {rain}%" if rain is not None else ""
    return [
        f"Weather: {val(weather.get('temp_f'))}F"
        f" feels {val(weather.get('feels_f'))}F"
        f" | {val(weather.get('condition'))}",
        f"Wind: {val(weather.get('wind_mph'))} mph{rain_text}",
    ]


def load_calendar_snapshot() -> dict:
    if not CALENDAR_SNAPSHOT.exists():
        return {"ok": False, "error": "no calendar_snapshot.json"}
    try:
        data = json.loads(CALENDAR_SNAPSHOT.read_text(encoding="utf-8"))
        events = data.get("events") or []
        return {"ok": True, "events": events, "source": "snapshot"}
    except Exception as exc:
        return {"ok": False, "error": str(exc)}


def event_time(value) -> str | None:
    if isinstance(value, dict):
        return value.get("dateTime") or value.get("date")
    return value


def load_coros() -> dict:
    if not SNAPSHOT.exists():
        return {"ok": False, "error": "no coros_snapshot.json"}
    try:
        data = json.loads(SNAPSHOT.read_text(encoding="utf-8"))
        data["ok"] = True
        return data
    except Exception as exc:
        return {"ok": False, "error": str(exc)}


def parse_date(value: str | None):
    if not value:
        return None
    try:
        return datetime.fromisoformat(value).date()
    except ValueError:
        return None


def coros_workouts(coros: dict) -> list[dict]:
    workouts = coros.get("workouts")
    if isinstance(workouts, list):
        return [item for item in workouts if isinstance(item, dict) and item.get("date")]

    # Backward compatibility for the older undated snapshot.
    legacy = []
    if coros.get("today_workout"):
        legacy.append({"date": datetime.now(TZ).date().isoformat(), "title": coros["today_workout"]})
    if coros.get("next_workout"):
        legacy.append({"date": (datetime.now(TZ).date() + timedelta(days=1)).isoformat(), "title": coros["next_workout"]})
    return legacy


def workout_for_date(coros: dict, target_date) -> dict | None:
    for workout in coros_workouts(coros):
        if parse_date(workout.get("date")) == target_date:
            return workout
    return None


def next_workout_after(coros: dict, target_date) -> dict | None:
    future = [
        workout
        for workout in coros_workouts(coros)
        if (parsed := parse_date(workout.get("date"))) and parsed > target_date
    ]
    return min(future, key=lambda item: parse_date(item["date"]), default=None)


def workout_title(workout: dict | None, fallback: str = "planned run") -> str:
    if not workout:
        return fallback
    title = workout.get("title") or workout.get("name") or fallback
    distance = workout.get("distance_mi")
    if distance:
        return f"{title} ({distance} mi)"
    return title


def short_time(iso_text: str | None) -> str:
    if not iso_text:
        return "?"
    try:
        return datetime.fromisoformat(iso_text.replace("Z", "+00:00")).astimezone(TZ).strftime("%I:%M %p").lstrip("0")
    except Exception:
        return "?"


def local_time(dt: datetime) -> str:
    return dt.strftime("%I:%M %p").lstrip("0")


def local_day(dt: datetime) -> str:
    return f"{dt.strftime('%a %b')} {dt.day}"


def first_calendar_line(calendar: dict) -> str:
    if not calendar.get("ok"):
        return "Calendar: unavailable"
    events = calendar.get("events") or []
    if not events:
        return "Calendar: open day"
    event = events[0]
    return f"Calendar: {event['title']} {short_time(event.get('start'))}-{short_time(event.get('end'))}"


def run_decision(oura: dict, coros: dict, target_date) -> tuple[str, str]:
    readiness = score(oura.get("readiness"))
    recovery = score(coros.get("recovery"))
    load_ratio = coros.get("load_ratio")
    today_workout = workout_for_date(coros, target_date)
    next_workout = next_workout_after(coros, target_date)
    planned = workout_title(today_workout)

    if not today_workout:
        if next_workout:
            next_date = parse_date(next_workout.get("date"))
            label = next_date.strftime("%a") if next_date else "later"
            return "NO COROS PLAN", f"Next: {label} {workout_title(next_workout)}"
        return "NO COROS PLAN", "No scheduled COROS workout"

    if readiness is not None and readiness < 65:
        return "REDUCE", f"{planned}; keep it easy"
    if recovery is not None and recovery < 60:
        return "REDUCE", f"{planned}; cap effort"
    if isinstance(load_ratio, (int, float)) and load_ratio > 1.3:
        return "REDUCE", f"{planned}; no hero pace"
    if readiness is None and recovery is None:
        return "CHECK", "missing recovery inputs"
    return "KEEP", str(planned)


def trim_line(text: str, limit: int = 45) -> str:
    return text if len(text) <= limit else text[: limit - 1].rstrip() + "."


def wrap_dashboard_lines(lines: list[str]) -> list[str]:
    wrapped: list[str] = []
    for line in lines:
        if not line or len(line) <= LINE_WIDTH:
            wrapped.append(line)
            continue

        if line.startswith("- "):
            wrapped.extend(
                textwrap.wrap(
                    line,
                    width=LINE_WIDTH,
                    subsequent_indent="  ",
                    break_long_words=False,
                    break_on_hyphens=False,
                )
            )
            continue

        wrapped.extend(
            textwrap.wrap(
                line,
                width=LINE_WIDTH,
                break_long_words=False,
                break_on_hyphens=False,
            )
        )
    return wrapped


def build_lines() -> list[str]:
    now = datetime.now(TZ)
    today = now.date()
    next_update = now + timedelta(hours=1)
    oura = load_oura()
    weather = load_weather()
    calendar = load_calendar(now)
    coros = load_coros()
    call, plan = run_decision(oura, coros, today)

    lines = [
        f"# {local_day(now)}",
        f"Updated {local_time(now)} | next {local_time(next_update)}",
        *weather_lines(weather),
        "",
        "## Status",
        f"Oura: R {val(score(oura.get('readiness')))} | Sleep {val(score(oura.get('sleep')))} | HRV {val(score(oura.get('hrv')))} | RHR {val(score(oura.get('rhr')))}",
        f"Sleep: {val(oura.get('sleep_hours'))} hr | Oura source {val(oura.get('source'))}",
        f"COROS: Rec {val(coros.get('recovery'))}% | Load {val(coros.get('short_load'))}/{val(coros.get('long_load'))} ({val(coros.get('load_ratio'))})",
        f"COROS state: {val(coros.get('recovery_level'))}",
        first_calendar_line(calendar),
        "",
        "## Run",
        f"Run: {call}",
        f"Plan: {trim_line(plan)}",
        "Route: Back Bay easy; Crystal Cove only if trails",
        "Fuel: carbs before; water/electrolytes after work",
        "",
        "## Today",
    ]

    if calendar.get("ok") and calendar.get("events"):
        for event in calendar["events"][:3]:
            lines.append(f"- {short_time(event.get('start'))} {trim_line(event['title'], 32)}")
    else:
        lines.append("- Calendar unavailable or empty")

    next_workout = next_workout_after(coros, today)
    if next_workout:
        next_date = parse_date(next_workout.get("date"))
        label = next_date.strftime("%a %b %d") if next_date else "Next"
        lines.extend(["", "## Upcoming", f"{label}: {trim_line(workout_title(next_workout), 60)}"])

    source_bits = []
    for name, data in [("Weather", weather), ("Oura", oura), ("Cal", calendar), ("COROS", coros)]:
        source_bits.append(name if data.get("ok") else f"{name}!")
    lines.extend(["", "## Sources", "Sources: " + " ".join(source_bits)])
    if not weather.get("ok"):
        lines.append(f"Weather issue: {trim_line(weather.get('error', 'unknown'), 80)}")
    if not oura.get("ok"):
        lines.append(f"Oura issue: {trim_line(oura.get('error', 'unknown'), 80)}")
    if not calendar.get("ok"):
        lines.append(f"Calendar issue: {trim_line(calendar.get('error', 'unknown'), 80)}")
    elif calendar.get("warning"):
        lines.append(f"Calendar fallback: {trim_line(calendar.get('warning'), 80)}")
    if not coros.get("ok"):
        lines.append(f"COROS issue: {trim_line(coros.get('error', 'unknown'), 80)}")

    return wrap_dashboard_lines([line.rstrip() for line in lines])


def main() -> None:
    lines = build_lines()
    OUTPUT.write_text("\n".join(lines) + "\n", encoding="utf-8")
    print(f"Wrote {OUTPUT} ({len(lines)} lines)")


if __name__ == "__main__":
    main()
