# tools/weather_tools.py — get_weather tool using the free Open-Meteo API.
# No API key required. Geocodes via Open-Meteo's free geocoding endpoint.
# Reads a default location from [weather] default_location in mugi_config.toml if no location is given.
# On API/SSL failure, returns a fallback instruction so the agent can use search_web instead.
from __future__ import annotations

import json
import ssl
import time
from urllib.parse import quote
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError

from langchain_core.tools import tool

from tools._logging import _debug_log, _emit_input_status

# Wrapper kept at module scope so tests can patch tools.weather_tools.get_config.
def get_config():
    from core.config import get_config as _get_config
    return _get_config()

# SSL context: use certifi's CA bundle when available to avoid "unable to get local issuer certificate" on some macOS/env setups.
def _weather_ssl_context() -> ssl.SSLContext:
    try:
        import certifi
        ctx = ssl.create_default_context(cafile=certifi.where())
        return ctx
    except Exception:
        return ssl.create_default_context()


_SSL_CTX = _weather_ssl_context()


def _fallback_message(location: str) -> str:
    """Instruction for the agent to use search_web when the weather API fails."""
    return f" Use search_web to look up current weather for {location!r}."

def _weather_units() -> tuple[str, str]:
    """Return (temperature_unit, wind_speed_unit) from config. Defaults to fahrenheit/mph."""
    try:
        weather_cfg = (get_config().get("weather") or {})
        return (
            weather_cfg.get("temperature_unit", "fahrenheit"),
            weather_cfg.get("wind_speed_unit", "mph"),
        )
    except Exception:
        return "fahrenheit", "mph"


_GEO_BASE = "https://geocoding-api.open-meteo.com/v1/search?name={name}&count=5&language=en&format=json"


def _infer_country_code(location: str) -> str | None:
    """Infer ISO-3166-1 alpha2 country code from location string so geocoding prefers the right country (e.g. San Rafael, CA -> US not Argentina)."""
    if not location:
        return None
    loc = location.upper().strip()
    # US state abbreviations or "US"/"USA" -> US
    if ", US" in loc or ", USA" in loc or loc.endswith(" US") or loc.endswith(" USA"):
        return "US"
    for abbr in (", CA", ", OR", ", NY", ", TX", ", FL", ", WA", ", CO", ", AZ", ", NV", ", MA", ", IL", ", PA", ", OH", ", GA", ", NC", ", MI", ", NJ", ", VA", ", TN", ", IN", ", MO", ", MD", ", WI", ", MN", ", AL", ", SC", ", LA", ", KY", ", CT", ", UT", ", IA", ", OK", ", AR", ", MS", ", KS", ", NM", ", NE", ", ID", ", WV", ", HI", ", NH", ", ME", ", RI", ", MT", ", DE", ", SD", ", ND", ", AK", ", VT", ", WY", ", DC"):
        if abbr in loc:
            return "US"
    return None
_WEATHER_URL_TEMPLATE = (
    "https://api.open-meteo.com/v1/forecast"
    "?latitude={lat}&longitude={lon}"
    "&current=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m,dew_point_2m"
    "&temperature_unit={temp_unit}"
    "&wind_speed_unit={wind_unit}"
    "&timezone=auto"
)

# Single request: current (greeting / micro) + 7-day daily (dashboard tile). Same [weather] default_location.
DASHBOARD_WEATHER_AND_FORECAST_URL_TEMPLATE = (
    "https://api.open-meteo.com/v1/forecast"
    "?latitude={lat}&longitude={lon}"
    "&current=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,relative_humidity_2m,dew_point_2m"
    "&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max"
    "&forecast_days=7"
    "&temperature_unit={temp_unit}"
    "&wind_speed_unit={wind_unit}"
    "&timezone=auto"
)

# WMO weather interpretation codes → readable description
_WMO_CODES: dict[int, str] = {
    0: "clear sky",
    1: "mainly clear", 2: "partly cloudy", 3: "overcast",
    45: "foggy", 48: "icy fog",
    51: "light drizzle", 53: "moderate drizzle", 55: "dense drizzle",
    61: "slight rain", 63: "moderate rain", 65: "heavy rain",
    71: "slight snow", 73: "moderate snow", 75: "heavy snow", 77: "snow grains",
    80: "slight showers", 81: "moderate showers", 82: "violent showers",
    85: "slight snow showers", 86: "heavy snow showers",
    95: "thunderstorm", 96: "thunderstorm with slight hail", 99: "thunderstorm with heavy hail",
}


def _default_location() -> str:
    """Read default_location from [weather] section of mugi_config.toml, if set."""
    try:
        cfg = get_config()
        return (cfg.get("weather") or {}).get("default_location", "")
    except Exception:
        return ""


def _fetch_json(url: str) -> tuple[bool, dict, str]:
    """GET JSON from url; retry a few times on transient Open-Meteo 5xx / 429."""
    last_err = ""
    for attempt in range(4):
        try:
            req = Request(url, headers={"User-Agent": "Mugi/1.0"})
            resp = urlopen(req, timeout=14, context=_SSL_CTX)
            try:
                raw = resp.read()
            finally:
                try:
                    resp.close()
                except Exception:
                    pass
            return True, json.loads(raw.decode("utf-8")), ""
        except HTTPError as e:
            last_err = str(e)
            code = getattr(e, "code", None)
            if code in (502, 503, 504, 429) and attempt < 3:
                time.sleep(0.35 * (2**attempt))
                continue
            return False, {}, last_err
        except URLError as e:
            return False, {}, str(e)
        except json.JSONDecodeError as e:
            return False, {}, f"Invalid JSON: {e}"
        except Exception as e:
            return False, {}, str(e)
    return False, {}, last_err


def _default_country_code() -> str:
    """Default country for ambiguous queries (e.g. 'San Rafael' with no state). [weather] default_country in config; if unset, use US."""
    try:
        c = (get_config().get("weather") or {}).get("default_country")
        return (c or "US").strip() or "US"
    except Exception:
        return "US"


import re

_US_STATE_PATTERN = re.compile(r',\s*([A-Z]{2})\s*$')


def _strip_region_suffix(location: str) -> tuple[str, str | None]:
    """Strip trailing state/region abbreviation (e.g. ', CA') from a location string, returning (city_name, country_code_override).
    Open-Meteo geocoding searches by city name; the country/state filter goes in countryCode param."""
    loc = location.strip()
    m = _US_STATE_PATTERN.search(loc)
    if m:
        return loc[:m.start()].strip(), "US"
    for suffix in (", US", ", USA", ", United States"):
        if loc.upper().endswith(suffix.upper()):
            return loc[:len(loc) - len(suffix)].strip(), "US"
    return loc, None


def _geocode(location: str) -> tuple[float | None, float | None, str, str]:
    """Return (lat, lon, display_name, error)."""
    city_name, country_override = _strip_region_suffix(location)
    code = country_override or _infer_country_code(location) or _default_country_code()

    # Try with countryCode first; if no results, retry without (Open-Meteo's filter misses some cities).
    url = _GEO_BASE.format(name=quote(city_name))
    if code and code.strip():
        url_filtered = url + "&countryCode=" + code
    else:
        url_filtered = url

    ok, data, err = _fetch_json(url_filtered)
    if not ok:
        return None, None, "", f"Geocoding failed: {err}"
    results = data.get("results") or []

    if not results and url_filtered != url:
        ok2, data2, err2 = _fetch_json(url)
        if ok2:
            all_results = data2.get("results") or []
            if code:
                results = [r for r in all_results if (r.get("country_code") or "").upper() == code.upper()]
            if not results:
                results = all_results
    if not results:
        return None, None, "", f"Location not found: {location!r}. Try a more specific location (e.g. 'San Francisco, CA')."
    r = results[0]
    name = r.get("name", "")
    country = r.get("country", "")
    admin = r.get("admin1", "")
    display = f"{name}, {admin}, {country}".strip(", ")
    return r["latitude"], r["longitude"], display, ""


@tool
def get_weather(location: str = "") -> str:
    """Get the current weather for a location using the free Open-Meteo API (no API key needed).

    Returns temperature, apparent temperature, conditions, wind speed, and humidity.
    Useful during heartbeats and event reminders (e.g. 'Meeting at 3pm — 72°F, partly cloudy').

    Args:
        location: City name or city + country (e.g. 'Portland, OR' or 'London, UK').
                  If empty, uses the default_location from [weather] config in mugi_config.toml.
    """
    _emit_input_status("get_weather")
    loc = (location or "").strip() or _default_location().strip()
    if not loc:
        return (
            "No location provided. Pass a location (e.g. 'San Francisco, CA') or set "
            "default_location under [weather] in mugi_config.toml."
        )

    _debug_log(f"[weather] get_weather for: {loc!r}")
    lat, lon, display_name, geo_err = _geocode(loc)
    if geo_err:
        return f"ERROR: {geo_err}{_fallback_message(loc)}"

    temp_unit, wind_unit = _weather_units()
    url = _WEATHER_URL_TEMPLATE.format(lat=lat, lon=lon, temp_unit=temp_unit, wind_unit=wind_unit)
    ok, data, err = _fetch_json(url)
    if not ok:
        return f"ERROR: Weather API failed: {err}{_fallback_message(display_name)}"

    current = data.get("current", {})
    if not current:
        return f"ERROR: No current weather data in API response.{_fallback_message(display_name)}"

    temp = current.get("temperature_2m")
    feels_like = current.get("apparent_temperature")
    wmo = current.get("weather_code")
    wind = current.get("wind_speed_10m")
    humidity = current.get("relative_humidity_2m")

    conditions = _WMO_CODES.get(wmo, f"code {wmo}") if wmo is not None else "unknown"
    temp_sym = "°F" if temp_unit == "fahrenheit" else "°C"
    wind_sym = "mph" if wind_unit == "mph" else ("km/h" if wind_unit == "kmh" else wind_unit)

    parts = [f"{temp}{temp_sym}" if temp is not None else f"?{temp_sym}"]
    if feels_like is not None and abs(feels_like - (temp or 0)) >= 3:
        parts.append(f"feels like {feels_like:.0f}{temp_sym}")
    parts.append(conditions)
    if wind is not None:
        parts.append(f"{wind:.0f} {wind_sym} wind")
    if humidity is not None:
        parts.append(f"{humidity}% humidity")

    return f"{display_name}: {', '.join(parts)}."
