"""Retrieve hourly weather data from Open-Meteo."""

import json
import logging
from dataclasses import dataclass, field
from datetime import date, datetime
from time import perf_counter
from typing import cast
from urllib.error import URLError
from urllib.parse import urlencode
from urllib.request import urlopen
from zoneinfo import ZoneInfo

OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast"
DEFAULT_TIMEOUT_SECONDS = 10
logger = logging.getLogger(__name__)


class WeatherFetchError(RuntimeError):
    """Raised when weather data cannot be fetched from the remote API."""


@dataclass(frozen=True, slots=True)
class Forecast:
    time: list[str]
    temperature_2m: list[float]
    precipitation: list[float]
    precipitation_probability: list[float]
    rain: list[float]
    showers: list[float]
    cloudcover_low: list[float]
    cloudcover_mid: list[float]
    timezone: str
    utc_offset_seconds: int | None
    daily_time: list[str] = field(default_factory=list)
    daily_precipitation_sum: list[float] = field(default_factory=list)
    daily_sunrise: list[str] = field(default_factory=list)
    daily_sunset: list[str] = field(default_factory=list)
    hourly_datetimes: tuple[datetime, ...] = field(init=False, repr=False)
    daily_dates: tuple[date, ...] = field(init=False, repr=False)
    daily_sunrise_datetimes: tuple[datetime, ...] = field(init=False, repr=False)
    daily_sunset_datetimes: tuple[datetime, ...] = field(init=False, repr=False)
    forecast_timezone: ZoneInfo = field(init=False, repr=False)

    def __post_init__(self) -> None:
        forecast_timezone = _resolve_forecast_timezone(self.timezone)
        object.__setattr__(self, "forecast_timezone", forecast_timezone)
        object.__setattr__(
            self,
            "hourly_datetimes",
            _parse_forecast_datetimes(self.time, forecast_timezone),
        )
        object.__setattr__(
            self,
            "daily_dates",
            _parse_forecast_dates(self.daily_time),
        )
        object.__setattr__(
            self,
            "daily_sunrise_datetimes",
            _parse_forecast_datetimes(self.daily_sunrise, forecast_timezone),
        )
        object.__setattr__(
            self,
            "daily_sunset_datetimes",
            _parse_forecast_datetimes(self.daily_sunset, forecast_timezone),
        )


def _resolve_forecast_timezone(
    timezone_name: str,
) -> ZoneInfo:
    return ZoneInfo(timezone_name)


def _parse_forecast_datetime(
    timestamp_text: str,
    forecast_timezone: ZoneInfo,
) -> datetime:
    timestamp = datetime.fromisoformat(timestamp_text)

    if timestamp.tzinfo is not None:
        timestamp = timestamp.astimezone(forecast_timezone).replace(tzinfo=None)

    return timestamp


def _parse_forecast_date(timestamp_text: str) -> date:
    return date.fromisoformat(timestamp_text)


def _parse_forecast_datetimes(
    timestamps: list[str],
    forecast_timezone: ZoneInfo,
) -> tuple[datetime, ...]:
    return tuple(
        _parse_forecast_datetime(timestamp_text, forecast_timezone)
        for timestamp_text in timestamps
    )


def _parse_forecast_dates(timestamps: list[str]) -> tuple[date, ...]:
    return tuple(_parse_forecast_date(timestamp_text) for timestamp_text in timestamps)


def _coerce_float_series(values: object) -> list[float]:
    return [float(value) for value in cast("list[float | int | str]", values)]


def _extract_float_series(
    hourly: dict[str, object], *candidate_keys: str
) -> list[float]:
    for key in candidate_keys:
        values = hourly.get(key)
        if values is None:
            continue
        return _coerce_float_series(values)

    missing_keys = ", ".join(candidate_keys)
    msg = f"Missing expected hourly series: {missing_keys}"
    raise KeyError(msg)


def fetch_forecast_data(latitude: float, longitude: float) -> Forecast:
    query = urlencode(
        {
            "latitude": latitude,
            "longitude": longitude,
            "hourly": (
                "temperature_2m,precipitation,precipitation_probability,"
                "rain,showers,cloud_cover_low,cloud_cover_mid"
            ),
            "daily": "precipitation_sum,sunrise,sunset",
            "timezone": "auto",
            "forecast_hours": 24,
            "forecast_days": 7,
        }
    )
    url = f"{OPEN_METEO_URL}?{query}"
    start_time = perf_counter()

    try:
        with urlopen(url, timeout=DEFAULT_TIMEOUT_SECONDS) as response:  # noqa: S310
            payload = response.read()
    except URLError as exc:
        elapsed_ms = (perf_counter() - start_time) * 1000
        logger.warning(
            "Weather request failed after %.1f ms (latitude=%s, longitude=%s): %s",
            elapsed_ms,
            latitude,
            longitude,
            exc,
        )
        message = f"Unable to fetch weather:\n{exc}"
        raise WeatherFetchError(message) from exc

    elapsed_ms = (perf_counter() - start_time) * 1000
    logger.info(
        "Weather request completed in %.1f ms (latitude=%s, longitude=%s)",
        elapsed_ms,
        latitude,
        longitude,
    )

    # We deliberately trust Open-Meteo's payload contract and do not
    # validate schema here.
    raw_data = cast("dict[str, object]", json.loads(payload))
    hourly = cast("dict[str, object]", raw_data["hourly"])
    daily = cast("dict[str, object]", raw_data["daily"])
    timezone_name = cast("str", raw_data["timezone"])
    utc_offset_seconds = cast("int | float | None", raw_data["utc_offset_seconds"])

    return Forecast(
        time=cast("list[str]", hourly["time"]),
        temperature_2m=_coerce_float_series(hourly["temperature_2m"]),
        precipitation=_coerce_float_series(hourly["precipitation"]),
        precipitation_probability=_coerce_float_series(
            hourly["precipitation_probability"]
        ),
        rain=_coerce_float_series(hourly["rain"]),
        showers=_coerce_float_series(hourly["showers"]),
        cloudcover_low=_extract_float_series(
            hourly,
            "cloud_cover_low",
            "cloudcover_low",
        ),
        cloudcover_mid=_extract_float_series(
            hourly,
            "cloud_cover_mid",
            "cloudcover_mid",
        ),
        timezone=timezone_name,
        utc_offset_seconds=(
            int(utc_offset_seconds) if utc_offset_seconds is not None else None
        ),
        daily_time=cast("list[str]", daily["time"]),
        daily_precipitation_sum=_coerce_float_series(daily["precipitation_sum"]),
        daily_sunrise=cast("list[str]", daily["sunrise"]),
        daily_sunset=cast("list[str]", daily["sunset"]),
    )
