"""
wivexx_calendar.py — Google Calendar read + scheduling  (v1.16)
═══════════════════════════════════════════════════════════════════════
Lets WIVEXX answer "what's my first meeting tomorrow?", read the day's agenda,
say what's next, and book time blocks ("schedule a client call tomorrow at 3pm
for an hour").

Reuses the SAME Google OAuth client as the photo delivery feature
(`credentials.json` in this folder — see DRIVE_SETUP.md) but keeps its own token
with the calendar scope, so you grant calendar access once.  You DO need to
enable the "Google Calendar API" in the same Cloud project.

Public API (used by wivexx_api.py):
    available() / auth_status() / setup_help()
    agenda(day_offset=0) / first_meeting(day_offset=0) / next_event()
    create_event(title, start_dt, duration_min=60)
    parse_query(text) -> dict        # {kind, day_offset, label}
    parse_event_request(text) -> dict# {title, start, duration_min, label}
    describe_events(events, label)   # speakable summary
"""

import datetime as _dt
import os
import re
from typing import Optional

try:
    import wivexx_config as _cfg
except Exception:                                    # pragma: no cover
    _cfg = None


def _c(name, default):
    return getattr(_cfg, name, default) if _cfg else default


_HERE = os.path.dirname(os.path.abspath(__file__))
_SCOPES = ["https://www.googleapis.com/auth/calendar.events",
           "https://www.googleapis.com/auth/calendar.readonly"]
_logger = None


def set_logger(logger) -> None:
    global _logger
    _logger = logger


# ══════════════════════════════════════════════════════════════════════════════
#  Auth (mirrors wivexx_deliver — same credentials.json, separate token)
# ══════════════════════════════════════════════════════════════════════════════
def _credentials_path() -> str:
    p = _c("DRIVE_CREDENTIALS_FILE", "credentials.json")
    return p if os.path.isabs(p) else os.path.join(_HERE, p)


def _token_path() -> str:
    p = _c("CALENDAR_TOKEN_FILE", "wivexx_calendar_token.json")
    return p if os.path.isabs(p) else os.path.join(_HERE, p)


def available() -> bool:
    try:
        import googleapiclient.discovery     # noqa: F401
        import google_auth_oauthlib.flow     # noqa: F401
        import google.oauth2.credentials      # noqa: F401
        return True
    except Exception:
        return False


def auth_status() -> str:
    """'ready' | 'need_consent' | 'need_credentials'."""
    if not available() or not os.path.exists(_credentials_path()):
        return "need_credentials"
    if not os.path.exists(_token_path()):
        return "need_consent"
    try:
        from google.oauth2.credentials import Credentials
        creds = Credentials.from_authorized_user_file(_token_path(), _SCOPES)
        if creds and (creds.valid or (creds.expired and creds.refresh_token)):
            return "ready"
        return "need_consent"
    except Exception:
        return "need_consent"


def setup_help() -> str:
    return (
        "Calendar needs Google access set up (same project as Drive). In short:\n"
        "  1. console.cloud.google.com → your project → enable the "
        "“Google Calendar API”.\n"
        "  2. You already have credentials.json from the Drive setup "
        "(DRIVE_SETUP.md); reuse it.\n"
        "  3. Ask me about your calendar once — a browser opens so you can "
        "grant calendar access, then it's automatic.")


def _service():
    from google.oauth2.credentials import Credentials
    from google.auth.transport.requests import Request
    from google_auth_oauthlib.flow import InstalledAppFlow
    from googleapiclient.discovery import build

    creds = None
    tok = _token_path()
    if os.path.exists(tok):
        try:
            creds = Credentials.from_authorized_user_file(tok, _SCOPES)
        except Exception:
            creds = None
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            cred_file = _credentials_path()
            if not os.path.exists(cred_file):
                raise FileNotFoundError("credentials.json not found — see DRIVE_SETUP.md.")
            flow = InstalledAppFlow.from_client_secrets_file(cred_file, _SCOPES)
            creds = flow.run_local_server(port=0)
        try:
            with open(tok, "w", encoding="utf-8") as fh:
                fh.write(creds.to_json())
        except Exception as e:
            if _logger:
                _logger.warning("calendar: could not cache token: %s", e)
    return build("calendar", "v3", credentials=creds, cache_discovery=False)


# ══════════════════════════════════════════════════════════════════════════════
#  Date / time parsing (self-contained — no dateutil dependency)
# ══════════════════════════════════════════════════════════════════════════════
_WEEKDAYS = {"monday": 0, "tuesday": 1, "wednesday": 2, "thursday": 3,
             "friday": 4, "saturday": 5, "sunday": 6,
             "mon": 0, "tue": 1, "tues": 1, "wed": 2, "thu": 3, "thur": 3,
             "thurs": 3, "fri": 4, "sat": 5, "sun": 6}


def _now() -> _dt.datetime:
    return _dt.datetime.now().astimezone()          # local, tz-aware


def resolve_day(text: str):
    """(day_offset, label) for a date phrase in *text*, or (None, '')."""
    t = text.lower()
    if "day after tomorrow" in t:
        return 2, "the day after tomorrow"
    if "tomorrow" in t:
        return 1, "tomorrow"
    if "today" in t or "tonight" in t:
        return 0, "today"
    m = re.search(r"\b(next\s+)?(" + "|".join(_WEEKDAYS) + r")\b", t)
    if m:
        nxt = bool(m.group(1))
        target = _WEEKDAYS[m.group(2)]
        today_wd = _now().weekday()
        off = (target - today_wd) % 7
        if nxt and off == 0:
            off = 7
        if nxt and off < 7:
            off += 0
        label = ("next " if nxt else "") + m.group(2).capitalize()
        return off, label
    return None, ""


def parse_time(text: str):
    """(hour, minute) from a time phrase, or None."""
    t = text.lower()
    if "noon" in t:
        return 12, 0
    if "midnight" in t:
        return 0, 0
    m = re.search(r"\b(\d{1,2})(?::(\d{2}))?\s*(a\.?m\.?|p\.?m\.?)", t)
    if m:
        h = int(m.group(1)) % 12
        mins = int(m.group(2) or 0)
        if m.group(3).startswith("p"):
            h += 12
        return h, mins
    m = re.search(r"\b(\d{1,2}):(\d{2})\b", t)        # 24h "15:30"
    if m:
        return int(m.group(1)) % 24, int(m.group(2))
    m = re.search(r"\bat\s+(\d{1,2})\b", t)            # "at 3" → assume daytime
    if m:
        h = int(m.group(1)) % 24
        if 1 <= h <= 6:                               # 3 → 3pm, business hours
            h += 12
        return h, 0
    return None


def parse_duration(text: str) -> int:
    """Minutes; defaults to 60."""
    t = text.lower()
    if re.search(r"\bhalf\s+(?:an\s+)?hour\b", t):
        return 30
    total = 0
    for m in re.finditer(r"(\d+(?:\.\d+)?)\s*(hours?|hrs?|h|minutes?|mins?|m)\b", t):
        val = float(m.group(1)); unit = m.group(2)
        total += int(val * 60) if unit.startswith(("h", "hr")) else int(val)
    return total or 60


def parse_query(text: str) -> dict:
    """{kind: 'first'|'next'|'agenda', day_offset, label}."""
    t = text.lower()
    off, label = resolve_day(t)
    if "next" in t and off is None and not any(d in t for d in _WEEKDAYS):
        return {"kind": "next", "day_offset": 0, "label": "coming up"}
    if "first" in t:
        if off is None:
            off, label = 0, "today"
        return {"kind": "first", "day_offset": off, "label": label}
    if off is None:
        off, label = 0, "today"
    return {"kind": "agenda", "day_offset": off, "label": label}


def parse_event_request(text: str) -> dict:
    """{title, start (aware datetime|None), duration_min, label}."""
    off, label = resolve_day(text)
    if off is None:
        off, label = 0, "today"
    hm = parse_time(text)
    dur = parse_duration(text)
    start = None
    if hm is not None:
        base = (_now() + _dt.timedelta(days=off)).replace(
            hour=hm[0], minute=hm[1], second=0, microsecond=0)
        start = base

    # title: strip the command verb, day/time/duration words, and filler
    title = text
    title = re.sub(r"^.*?\b(?:schedule|book|add|create|put|set\s+up|block"
                   r"(?:\s+out)?|make|new)\b", "", title, count=1,
                   flags=re.IGNORECASE)
    title = re.sub(r"^\s*(?:a|an|the|new)\b", " ", title, flags=re.IGNORECASE)
    # only strip the generic container nouns — keep meaningful words like
    # "call", "block", "focus", "standup" in the title
    title = re.sub(r"\b(?:meeting|event|appointment|appt)\b", " ", title,
                   flags=re.IGNORECASE)
    title = re.sub(r"\b(?:today|tonight|tomorrow|day after tomorrow|next\s+\w+|"
                   + "|".join(_WEEKDAYS) + r")\b", " ", title, flags=re.IGNORECASE)
    title = re.sub(r"\b(?:at|on|for|from|to)\b.*$", " ",
                   title, flags=re.IGNORECASE) if re.search(
                       r"\b(?:at|for)\b", title, re.IGNORECASE) else title
    title = re.sub(r"\b(?:called|titled|named|about)\b", " ", title,
                   flags=re.IGNORECASE)
    title = re.sub(r"\s+", " ", title).strip(" .,-:")
    if not title or len(title) < 2:
        title = "Meeting"
    else:
        title = title[:80].strip().capitalize()
    return {"title": title, "start": start, "duration_min": dur, "label": label}


# ══════════════════════════════════════════════════════════════════════════════
#  Calendar operations
# ══════════════════════════════════════════════════════════════════════════════
def _day_bounds(day_offset: int):
    start = (_now() + _dt.timedelta(days=day_offset)).replace(
        hour=0, minute=0, second=0, microsecond=0)
    return start, start + _dt.timedelta(days=1)


def _list(time_min: _dt.datetime, time_max: _dt.datetime, max_n: int = 15) -> list:
    svc = _service()
    res = svc.events().list(
        calendarId="primary", timeMin=time_min.isoformat(),
        timeMax=time_max.isoformat(), singleEvents=True,
        orderBy="startTime", maxResults=max_n).execute()
    return res.get("items", [])


def agenda(day_offset: int = 0, max_n: int = 15) -> list:
    lo, hi = _day_bounds(day_offset)
    return _list(lo, hi, max_n)


def first_meeting(day_offset: int = 0):
    for ev in agenda(day_offset):
        if ev.get("start", {}).get("dateTime"):       # skip all-day items
            return ev
    return None


def next_event():
    now = _now()
    items = _list(now, now + _dt.timedelta(days=14), max_n=1)
    return items[0] if items else None


def create_event(title: str, start_dt: _dt.datetime, duration_min: int = 60):
    svc = _service()
    end_dt = start_dt + _dt.timedelta(minutes=duration_min)
    # dateTime carries the offset when start_dt is tz-aware; for a naive datetime Google
    # applies the calendar's default timezone (the user's local tz) — which is what we want.
    # (Don't pass a `timeZone` from tzname(): that returns abbreviations like "EST", not the
    # IANA names Google requires.)
    body = {
        "summary": title,
        "start": {"dateTime": start_dt.isoformat()},
        "end": {"dateTime": end_dt.isoformat()},
    }
    ev = svc.events().insert(calendarId="primary", body=body).execute()
    return {"ok": True, "link": ev.get("htmlLink", ""),
            "id": ev.get("id", "")}


# ══════════════════════════════════════════════════════════════════════════════
#  Formatting
# ══════════════════════════════════════════════════════════════════════════════
def _ev_start(ev) -> Optional[_dt.datetime]:
    s = ev.get("start", {})
    raw = s.get("dateTime") or s.get("date")
    if not raw:
        return None
    try:
        if "T" in raw:
            return _dt.datetime.fromisoformat(raw)
        return _dt.datetime.fromisoformat(raw + "T00:00:00").astimezone()
    except Exception:
        return None


def fmt_time(dtobj: _dt.datetime) -> str:
    h = dtobj.hour % 12 or 12
    ampm = "am" if dtobj.hour < 12 else "pm"
    return f"{h}:{dtobj.minute:02d}{ampm}" if dtobj.minute else f"{h}{ampm}"


def _ev_line(ev) -> str:
    title = ev.get("summary", "(no title)")
    st = ev.get("start", {})
    if st.get("date") and not st.get("dateTime"):
        return f"{title} (all day)"
    dtobj = _ev_start(ev)
    return f"{fmt_time(dtobj)} — {title}" if dtobj else title


def describe_events(events: list, label: str) -> str:
    if not events:
        return f"You have nothing scheduled {label}, sir."
    n = len(events)
    head = (f"You have {n} thing{'s' if n != 1 else ''} {label}, sir: ")
    return head + "; ".join(_ev_line(e) for e in events[:6]) + "."


def describe_first(ev, label: str) -> str:
    if not ev:
        return f"You have no meetings {label}, sir."
    dtobj = _ev_start(ev)
    when = fmt_time(dtobj) if dtobj else "sometime"
    return (f"Your first meeting {label} is {ev.get('summary', 'a meeting')} "
            f"at {when}, sir.")


def describe_next(ev) -> str:
    if not ev:
        return "Nothing on your calendar coming up, sir."
    dtobj = _ev_start(ev)
    now = _now()
    when = fmt_time(dtobj) if dtobj else "soon"
    day = ""
    if dtobj:
        d = (dtobj.date() - now.date()).days
        day = (" today" if d == 0 else " tomorrow" if d == 1
               else dtobj.strftime(" on %A"))
    return (f"Next up{day} at {when}, sir: {ev.get('summary', 'an event')}.")
