"""
location.py — resolve the owner's current location, IANA timezone, and local time
from the travel schedule, so the hourly sweep can decide when it is 9am
*wherever the owner currently is* (the host's own clock is always the home timezone).

Primary source: the top-level Travel/ folder's "<YEAR> Travel Schedule.md"
notes, top-level bullets like
  - May 27–June 10: [[San Francisco|San Francisco, CR]]
Only the current-year note is read — it answers "where is the owner now" — except in
January, when last year's note is also read to catch a December→January segment
that wraps past New Year (anchored to last year, e.g. "December 28–January 8").
The rest of the archive is never opened. Each segment is anchored to its file's
declared year (see current_location).
City -> IANA tz is **vault-driven**: the matched `Travel/Cities/<note>.md` note's
`timezone:` frontmatter wins, then the built-in CITY_TZ map, then DEFAULT_TZ
(confident=False only for that last fallback). The city note also supplies
`latitude:`/`longitude:`, which `resolve()` returns for the digest's weather/sun
line. To add or correct a city, edit its Cities note — no code change needed.
"""
import os
import re
import datetime as dt
from pathlib import Path

try:
    from zoneinfo import ZoneInfo
except Exception:  # pragma: no cover
    ZoneInfo = None

# DEFAULT_TZ is the home/fallback timezone. The runner sets it from config
# (owner.timezone_default); the literal here is just a neutral last resort.
DEFAULT_TZ = os.environ.get("DEFAULT_TZ", "UTC")

# A small convenience seed of major cities -> IANA tz. NOT authoritative: the matched
# Areas/Travel/Cities/<note>.md `timezone:` frontmatter always wins, then this map,
# then DEFAULT_TZ. Add or correct any city by editing its Cities note — no code change.
CITY_TZ = {
    "new york": "America/New_York", "boston": "America/New_York",
    "washington": "America/New_York", "miami": "America/New_York",
    "chicago": "America/Chicago", "denver": "America/Denver",
    "los angeles": "America/Los_Angeles", "san francisco": "America/Los_Angeles",
    "seattle": "America/Los_Angeles", "mexico city": "America/Mexico_City",
    "sao paulo": "America/Sao_Paulo", "são paulo": "America/Sao_Paulo",
    "london": "Europe/London", "paris": "Europe/Paris", "berlin": "Europe/Berlin",
    "amsterdam": "Europe/Amsterdam", "zurich": "Europe/Zurich",
    "dubai": "Asia/Dubai", "mumbai": "Asia/Kolkata", "singapore": "Asia/Singapore",
    "hong kong": "Asia/Hong_Kong", "tokyo": "Asia/Tokyo", "sydney": "Australia/Sydney",
}

MONTHS = {m: i for i, m in enumerate(
    ["January", "February", "March", "April", "May", "June", "July",
     "August", "September", "October", "November", "December"], start=1)}

# top-level bullet only (column 0): "- May 27–June 10: [[San Francisco|San Francisco, CR]]"
# Groups: 1 start-month, 2 start-day, 3 end-month, 4 end-day, 5 wikilink target
# (the note name, e.g. "Washington DC"), 6 display label (e.g. "Washington, DC").
BULLET = re.compile(
    r"^- ([A-Z][a-z]+) (\d{1,2})\s*[\u2013-]\s*(?:([A-Z][a-z]+) )?(\d{1,2})\s*:\s*"
    r"(?:\[\[(?:([^\]|]*)\|)?([^\]]+)\]\]|(.+?))\s*$")


def _schedule_files(vault_dir, today=None):
    # Travel is a top-level vault folder holding one "<YEAR> Travel Schedule.md"
    # per year. The hourly check only needs the CURRENT year's note — except in
    # January, when an end-of-December segment from LAST year's note can still be
    # running (it's anchored to last year and wraps past New Year, e.g.
    # "December 28–January 8"). So read this year's note plus, only in January,
    # last year's — never the whole archive, which would just burn context/tokens.
    today = today or dt.date.today()
    travel = Path(vault_dir) / "Areas" / "Travel"
    years = [today.year] + ([today.year - 1] if today.month == 1 else [])
    files = [p for p in (travel / f"{y} Travel Schedule.md" for y in years) if p.exists()]
    if files:
        return files
    # Fallbacks: any schedule in Travel/, then the legacy Areas/ location.
    return (sorted(travel.glob("*Travel Schedule.md"))
            or sorted(Path(vault_dir).glob("Areas/**/*Travel Schedule.md")))


def _file_year(text, path):
    m = re.search(r"^year:\s*(\d{4})", text, re.M)
    if m:
        return int(m.group(1))
    m = re.search(r"(\d{4})", Path(path).name)
    return int(m.group(1)) if m else dt.date.today().year


def _ranges(vault_dir, today=None):
    out = []
    for path in _schedule_files(vault_dir, today):
        text = path.read_text(encoding="utf-8")
        y = _file_year(text, path)
        for raw in text.splitlines():
            if not raw.startswith("- "):
                continue
            m = BULLET.match(raw)
            if not m:
                continue
            sm = MONTHS.get(m.group(1))
            em = MONTHS.get(m.group(3)) if m.group(3) else sm
            if not sm or not em:
                continue
            out.append((y, sm, int(m.group(2)), em, int(m.group(4)),
                        (m.group(5) or "").strip(),
                        (m.group(6) or m.group(7) or "").strip()))
    return out


def _now_in(now, tz):
    """`now` (a tz-aware instant) expressed in IANA `tz`; system-local if tz/zoneinfo
    is unavailable."""
    if ZoneInfo and tz:
        try:
            return now.astimezone(ZoneInfo(tz))
        except Exception:
            pass
    return now.astimezone()


def _local_date(now, tz):
    """The calendar date it currently is in IANA `tz`, given tz-aware instant `now`."""
    return _now_in(now, tz).date()


def _segment_tz(vault_dir, note, label, default_tz=None):
    """A segment's IANA tz, resolved the same way `resolve()` reports it: the matched
    `Travel/Cities/<note>.md` `timezone:` wins, then the built-in CITY_TZ map, then
    DEFAULT_TZ. Returns (tz, source, meta) — `meta` carries the city note's lat/lon so
    resolve() needn't read it twice. `source` is 'city-note' | 'map' | 'default'."""
    default_tz = default_tz or DEFAULT_TZ
    meta = city_meta(vault_dir, note) if note else {}
    if not meta and label:                       # try the display city if the target missed
        meta = city_meta(vault_dir, label.split(",")[0].strip())
    if meta.get("tz"):
        return meta["tz"], "city-note", meta
    city = label.split(",")[0].strip().lower() if label else ""
    if city in CITY_TZ:
        return CITY_TZ[city], "map", meta
    return default_tz, "default", meta


def _best_segment(vault_dir, now=None):
    """The travel segment the owner is in RIGHT NOW, as
    (start_date, label, note, tz, source, meta). `now` is a tz-aware instant (default:
    real UTC now). Each segment is tested against the calendar date in ITS OWN
    timezone, so the active segment flips the instant it becomes a new day *where the owner
    actually is* — not when the home timezone (the host's clock) rolls over. On a big eastward
    move (New York June 10–14 → Manila June 15–…) both segments can cover one instant —
    it's still the 14th in New York while already the 15th in Manila — so the
    latest-starting match wins: the place the owner has moved TO. None if nothing covers now."""
    now = now or dt.datetime.now(dt.timezone.utc)
    ref = _local_date(now, DEFAULT_TZ)           # which year-file(s) to read (Jan wrap)
    best = None  # (start_date, label, note, tz, source, meta)
    for (y, sm, sd, em, ed, note, label) in _ranges(vault_dir, ref):
        tz, source, meta = _segment_tz(vault_dir, note, label)
        local_date = _local_date(now, tz)
        # Anchor each segment to ITS file's declared year. A normal segment lives
        # in year y; a wrap segment (Dec→Jan, em < sm) ends in y+1. Do NOT test
        # against neighbouring years — that let a June segment from the 2025
        # schedule match a June date in 2026 (the Amsterdam-vs-CR bug).
        try:
            start = dt.date(y, sm, sd)
            end = dt.date(y if em >= sm else y + 1, em, ed)
        except ValueError:
            continue
        if start <= local_date <= end and (best is None or start > best[0]):
            best = (start, label, note or label.split(",")[0].strip(), tz, source, meta)
    return best


def current_location(vault_dir, now=None):
    seg = _best_segment(vault_dir, now)
    return seg[1] if seg else None


def tz_for(label, default=None):
    default = default or DEFAULT_TZ
    if not label:
        return default
    city = label.split(",")[0].strip().lower()
    return CITY_TZ.get(city, default)


def _frontmatter(text):
    m = re.match(r"^---\n(.*?)\n---", text, re.S)
    return m.group(1) if m else ""


def city_meta(vault_dir, note_or_city):
    """Read `timezone` / `latitude` / `longitude` from a `Travel/Cities/<note>.md`
    note's frontmatter. `note_or_city` is the wikilink target (exact note stem) or a
    plain city name; matched case-insensitively. Returns {} when the note or fields
    are absent. Dependency-free line parse — the vault has no guaranteed YAML lib."""
    if not note_or_city:
        return {}
    cities = Path(vault_dir) / "Areas" / "Travel" / "Cities"
    p = cities / f"{note_or_city}.md"
    if not p.exists():
        want = note_or_city.strip().lower()
        p = next((q for q in cities.glob("*.md") if q.stem.lower() == want), None)
        if not p:
            return {}
    try:
        fm = _frontmatter(p.read_text(encoding="utf-8"))
    except Exception:
        return {}
    out = {}
    tz = re.search(r"^timezone:\s*(.+?)\s*$", fm, re.M)
    if tz:
        out["tz"] = tz.group(1).strip().strip("\"'")
    for field, key in (("latitude", "lat"), ("longitude", "lon")):
        m = re.search(rf"^{field}:\s*(-?\d+(?:\.\d+)?)\s*$", fm, re.M)
        if m:
            out[key] = float(m.group(1))
    return out


def now_for(tz):
    """Current tz-aware 'now' in an IANA tz. Lets the sweep recompute local time
    each run from a once-per-day cached tz, without re-parsing the schedule."""
    if ZoneInfo and tz:
        try:
            return dt.datetime.now(ZoneInfo(tz))
        except Exception:
            pass
    return dt.datetime.now()


def resolve(vault_dir, now=None, default_tz=None):
    """Return {location, tz, now, confident, tz_source, lat, lon} for wherever the owner is
    at instant `now` (tz-aware; default real UTC now). Location/tz come from the travel
    schedule via `_best_segment`, which matches each segment against the date in its OWN
    timezone — so this tracks the owner's local day, NOT the host's the home timezone clock. Called
    every sweep run (re-resolving one schedule note hourly is cheap), so the zone flips
    the instant it's a new day where the owner is.

    Timezone precedence (vault-driven first): the matched `Travel/Cities/<note>.md`
    note's `timezone:` frontmatter → the built-in `CITY_TZ` map → `DEFAULT_TZ`. The
    city note also supplies `lat`/`lon` for weather/sun tasks. `tz_source` records
    which layer answered ('city-note' | 'map' | 'default'); `confident` is True for
    the first two (a real match) and False when we fell back to the default. The `now`
    in the result is that instant in the owner's local tz — its `.hour`/`.date()` drive the
    9am-local digest gate and the digest's "today"."""
    default_tz = default_tz or DEFAULT_TZ
    now = now or dt.datetime.now(dt.timezone.utc)
    seg = _best_segment(vault_dir, now)
    if not seg:
        return {"location": None, "tz": default_tz, "now": _now_in(now, default_tz),
                "confident": False, "tz_source": "default", "lat": None, "lon": None}
    _, label, note, tz, source, meta = seg
    return {"location": label, "tz": tz, "now": _now_in(now, tz),
            "confident": source != "default", "tz_source": source,
            "lat": meta.get("lat"), "lon": meta.get("lon")}
