"""WeatherClient provider for OpenWeatherMap (OWM) API.

Refer to the documentation for details on the endpoints used:
https://nickelseyspelloc.github.io/sc_utility/reference/weather_client/

"""
from __future__ import annotations

from datetime import UTC, datetime
from typing import Any  # noqa: F401

import requests  # noqa: F401
from pyowm import OWM
from pyowm.commons.exceptions import APIRequestError, UnauthorizedError
from pyowm.weatherapi30.weather import Weather

from weather_client.models import (
    SkyCondition,
    Temperature,
    WeatherData,
    WeatherReading,
    WeatherStation,
    Wind,
)

_SKY_ICONS: list[tuple[str, str]] = [
    # Exact / most-specific first
    ("thunderstorm", "⛈️"),
    ("heavy rain", "🌧️"),
    ("shower rain", "🌧️"),
    ("rain", "🌧️"),
    ("drizzle", "🌦️"),
    ("snow", "❄️"),
    ("sleet", "🌨️"),
    ("fog", "🌫️"),
    ("mist", "🌫️"),
    ("haze", "🌫️"),
    ("smoke", "🌫️"),
    ("sand", "🌫️"),
    ("dust", "🌫️"),
    ("overcast", "☁️"),
    ("broken clouds", "⛅"),
    ("scattered clouds", "⛅"),
    ("partly cloudy", "⛅"),
    ("few clouds", "🌤️"),
    ("mainly clear", "🌤️"),
    ("clear", "☀️"),
]


_COMPASS = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
            "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]


class OWMProvider:
    def __init__(self, api_key: str):
        self._api_key = api_key
        self._owm = OWM(api_key)
        self._mgr = self._owm.weather_manager()

    def fetch(self, lat: float, lon: float) -> WeatherData:  # noqa: PLR0914
        """Fetch weather data from OpenWeatherMap.

        Args:
            lat (float): Latitude of the location.
            lon (float): Longitude of the location.

        Raises:
            RuntimeError: If the OWM API key is not set or if the request fails.
            UnauthorizedError: If the OWM API key is invalid or does not have access to the One Call API.

        Returns:
            A WeatherData object containing the current reading, list of hourly readings, and weather station info.
        """
        try:
            one_call = self._mgr.one_call(lat=lat, lon=lon, units="celsius")
        except UnauthorizedError as e:
            # Many "free" OpenWeatherMap keys don't have access to One Call.
            # Fall back to free-tier endpoints (current + 5-day/3-hour forecast).
            # return self._fetch_free(lat, lon)
            error_msg = f"Unauthorized access to OpenWeatherMap One Call API: {e}"
            raise UnauthorizedError(error_msg) from e  # TO DO: Change this one free version implemented
        except APIRequestError as e:
            error_msg = f"OpenWeatherMap request failed: {e}"
            raise RuntimeError(error_msg) from e

        local_tz = datetime.now().astimezone().tzinfo
        utc_now = datetime.now(UTC)
        time_now = datetime.now(tz=local_tz)

        # Get the API response data
        hourly_data = one_call.forecast_hourly or []
        daily_data = one_call.forecast_daily or []
        today_data = daily_data[0] if daily_data else None

        # Build up the current reading. The One Call API doesn't provide high/low or feels-like for the current weather, so those will be None.
        current_data = one_call.current
        current_sky = SkyCondition(
            title=current_data.status,
            description=current_data.detailed_status,
            icon=self._sky_to_icon(current_data.detailed_status),
            icon_code=current_data.weather_icon_name,
            icon_png_url=self._get_icon_url(current_data.weather_icon_name),
            cloud_cover=current_data.clouds / 100 if current_data.clouds is not None else None,
            visibility=current_data.visibility_distance,
            uv_index=current_data.uvi / 100 if current_data.uvi is not None else None)

        current_reading = WeatherReading(
            utc_time=utc_now,
            local_time=utc_now.astimezone(),
            temperature=Temperature(
                reading=current_data.temperature("celsius")["temp"],
                high=today_data.temperature("celsius")["max"] if today_data else None,
                low=today_data.temperature("celsius")["min"] if today_data else None,
                feels_like=today_data.temperature("celsius")["feels_like_day"] if today_data else None,
            ),
            sky=current_sky,
            wind=Wind(
                speed=self._covert_wind_speed(current_data.wind()["speed"]),
                deg=current_data.wind().get("deg"),
                direction=self._deg_to_compass(current_data.wind().get("deg")),
                gust=self._covert_wind_speed(current_data.wind().get("gust", 0.0)) if "gust" in current_data.wind() else None,
            ),
            summary=current_data.detailed_status,   # We dould be getting this from today_data.summary, but pyowm currently doesn't return this. Raised issue 455.
            precip_probability=current_data.precipitation_probability if current_data.precipitation_probability is not None else None,
            rain=self._get_rain(today_data, "all"),
            pressure=current_data.pressure["press"] if current_data.pressure else None,
            humidity=current_data.humidity / 100 if current_data.humidity is not None else None,
            dew_point=self._convert_kelvin_to_celsius(current_data.dewpoint) if current_data.dewpoint else None,
            sunrise=self._convert_unix_time_to_datetime(current_data.sunrise_time("unix"), get_local=True) if current_data.sunrise_time() else None,  # pyright: ignore[reportArgumentType]
            sunset=self._convert_unix_time_to_datetime(current_data.sunset_time("unix"), get_local=True) if current_data.sunset_time() else None,  # pyright: ignore[reportArgumentType]
        )

        # Build up the hourly reading
        hourly: list[WeatherReading] = []
        for hour in hourly_data:
            utc_timestamp = self._convert_unix_time_to_datetime(hour.ref_time)
            local_timestamp = self._convert_unix_time_to_datetime(hour.ref_time, get_local=True)
            if local_timestamp < time_now:
                continue

            hour_sky = SkyCondition(
                title=hour.status,
                description=hour.detailed_status,
                icon=self._sky_to_icon(hour.detailed_status),
                icon_code=hour.weather_icon_name,
                icon_png_url=self._get_icon_url(hour.weather_icon_name),
                cloud_cover=hour.clouds / 100 if hour.clouds is not None else None,
                visibility=hour.visibility_distance,
                uv_index=hour.uvi / 100 if hour.uvi is not None else None)

            hourly.append(
                WeatherReading(
                    utc_time=utc_timestamp,
                    local_time=local_timestamp,
                    temperature=Temperature(
                        reading=hour.temperature("celsius")["temp"],
                        feels_like=hour.temperature("celsius").get("feels_like")),
                    sky=hour_sky,
                    wind=Wind(
                        speed=self._covert_wind_speed(hour.wind()["speed"]),
                        deg=hour.wind().get("deg"),
                        direction=self._deg_to_compass(hour.wind().get("deg"))),
                    summary=hour.detailed_status,   # We dould be getting this from today_data.summary, but pyowm currently doesn't return this. Raised issue 455.
                    precip_probability=hour.precipitation_probability if hour.precipitation_probability is not None else None,
                    rain=self._get_rain(hour, "1hr"),
                    pressure=hour.pressure["press"] if hour.pressure else None,
                    humidity=hour.humidity / 100 if hour.humidity is not None else None,
                    dew_point=self._convert_kelvin_to_celsius(hour.dewpoint) if hour.dewpoint else None,
                    sunrise=self._convert_unix_time_to_datetime(current_data.sunrise_time("unix"), get_local=True) if current_data.sunrise_time() else None,  # pyright: ignore[reportArgumentType]
                    sunset=self._convert_unix_time_to_datetime(current_data.sunset_time("unix"), get_local=True) if current_data.sunset_time() else None,  # pyright: ignore[reportArgumentType]
                )
            )

        daily: list[WeatherReading] = []
        for day in daily_data:
            utc_timestamp = self._convert_unix_time_to_datetime(day.ref_time)
            local_timestamp = self._convert_unix_time_to_datetime(day.ref_time, get_local=True)
            if local_timestamp < time_now:
                continue

            day_sky = SkyCondition(
                title=day.status,
                description=day.detailed_status,
                icon=self._sky_to_icon(day.detailed_status),
                icon_code=day.weather_icon_name,
                icon_png_url=self._get_icon_url(day.weather_icon_name),
                cloud_cover=day.clouds / 100 if day.clouds is not None else None,
                visibility=day.visibility_distance,
                uv_index=day.uvi / 100 if day.uvi is not None else None)

            daily.append(
                WeatherReading(
                    utc_time=utc_timestamp,
                    local_time=local_timestamp,
                    temperature=Temperature(
                        reading=day.temperature("celsius")["day"],
                        high=day.temperature("celsius")["max"],
                        low=day.temperature("celsius")["min"],
                        feels_like=day.temperature("celsius").get("feels_like_day")),
                    sky=day_sky,
                    wind=Wind(
                        speed=self._covert_wind_speed(day.wind()["speed"]),
                        deg=day.wind().get("deg"),
                        direction=self._deg_to_compass(day.wind().get("deg"))),
                    summary=day.detailed_status,   # We dould be getting this from today_data.summary, but pyowm currently doesn't return this. Raised issue 455.
                    precip_probability=day.precipitation_probability if day.precipitation_probability is not None else None,
                    rain=self._get_rain(day, "all"),
                    pressure=day.pressure["press"] if day.pressure else None,
                    humidity=day.humidity / 100 if day.humidity is not None else None,
                    dew_point=self._convert_kelvin_to_celsius(day.dewpoint) if day.dewpoint else None,
                    sunrise=self._convert_unix_time_to_datetime(current_data.sunrise_time("unix"), get_local=True) if current_data.sunrise_time() else None,  # pyright: ignore[reportArgumentType]
                    sunset=self._convert_unix_time_to_datetime(current_data.sunset_time("unix"), get_local=True) if current_data.sunset_time() else None,  # pyright: ignore[reportArgumentType]
                )
            )

        station = WeatherStation("OpenWeatherMap One Call API", lat, lon)

        # Build the final response object.
        return_data = WeatherData(current=current_reading, hourly=hourly, daily=daily, station=station, as_at=datetime.now(tz=local_tz))
        return return_data

    # def _fetch_free(self, lat: float, lon: float) -> WeatherData:
    #     """Use free-tier endpoints: current weather + 5 day / 3 hour forecast.

    #     Args:
    #         lat (float): Latitude of the location.
    #         lon (float): Longitude of the location.

    # raise except UnauthorizedError <<<<<<<<<<<<<<<<<

    #     Returns:
    #         tuple[WeatherReading, list[WeatherReading], WeatherStation]: Current reading, hourly forecast, and station info.
    #     """

    #     def _as_float(value: Any, *, field: str) -> float:
    #         try:
    #             return float(value)
    #         except (TypeError, ValueError) as e:
    #             error_msg = f"OpenWeatherMap response missing/invalid {field}"
    #             raise RuntimeError(error_msg) from e

    #     current_url = "https://api.openweathermap.org/data/2.5/weather"
    #     forecast_url = "https://api.openweathermap.org/data/2.5/forecast"
    #     params = {"lat": lat, "lon": lon, "appid": self._api_key, "units": "metric"}

    #     current_resp = requests.get(current_url, params=params, timeout=10)
    #     current_resp.raise_for_status()
    #     current_data: dict[str, Any] = current_resp.json()

    #     utc_now = datetime.now(UTC)
    #     dt_raw = current_data.get("dt")
    #     dt_unix = int(dt_raw) if isinstance(dt_raw, (int, float)) else int(utc_now.timestamp())
    #     utc_current_time = datetime.fromtimestamp(dt_unix, tz=UTC)
    #     current_weather = (current_data.get("weather") or [{}])[0] or {}
    #     current_wind = current_data.get("wind") or {}
    #     # Convert the wind speed from m/s to km/h
    #     if "speed" in current_wind:
    #         current_wind["speed"] = self._covert_wind_speed(_as_float(current_wind["speed"], field="wind.speed"))
    #     current_main = current_data.get("main") or {}

    #     current_reading = WeatherReading(
    #         utc_time=utc_current_time,
    #         local_time=utc_current_time.astimezone(),
    #         temperature=_as_float(current_main.get("temp"), field="main.temp"),
    #         temperature_high=_as_float(current_main.get("temp_max"), field="main.temp_max"),
    #         temperature_low=_as_float(current_main.get("temp_min"), field="main.temp_min"),
    #         feels_like=_as_float(current_main.get("feels_like"), field="main.feels_like"),
    #         sky=str(current_weather.get("description") or "unknown"),
    #         wind=Wind(
    #             speed=_as_float(current_wind.get("speed", 0.0), field="wind.speed"),
    #             deg=(float(current_wind["deg"]) if "deg" in current_wind else None),
    #             direction=self._deg_to_compass(current_data.wind().get("deg")),
    #             gusts=_as_float(current_wind["gust"], field="wind.gust") if "gust" in current_wind else None,
    #         ),
    #     )

    #     forecast_resp = requests.get(forecast_url, params=params, timeout=10)
    #     forecast_resp.raise_for_status()
    #     forecast_data: dict[str, Any] = forecast_resp.json()

    #     hourly: list[WeatherReading] = []
    #     for item in forecast_data.get("list", []):
    #         utc_ts = datetime.fromtimestamp(int(item.get("dt", 0)), tz=UTC)
    #         if utc_ts < utc_now:    # Issue 35
    #             continue

    #         weather = (item.get("weather") or [{}])[0] or {}
    #         wind = item.get("wind") or {}
    #         main = item.get("main") or {}

    #         hourly.append(
    #             WeatherReading(
    #                 utc_time=utc_ts,
    #                 local_time=utc_ts.astimezone(),
    #                 temperature=_as_float(main.get("temp"), field="forecast.main.temp"),
    #                 temperature_high=_as_float(main.get("temp_max"), field="forecast.main.temp_max"),
    #                 temperature_low=_as_float(main.get("temp_min"), field="forecast.main.temp_min"),
    #                 feels_like=_as_float(main.get("feels_like"), field="forecast.main.feels_like"),
    #                 sky=str(weather.get("description") or "unknown"),
    #                 wind=Wind(
    #                     speed=_as_float(wind.get("speed", 0.0), field="forecast.wind.speed"),
    #                     deg=(float(wind["deg"]) if "deg" in wind else None),
    #                     direction=self._deg_to_compass(float(wind["deg"])) if "deg" in wind else None,
    #                     gusts=_as_float(wind["gust"], field="forecast.wind.gust") if "gust" in wind else None,
    #                 ),
    #             )
    #         )

    #     station = WeatherStation("OpenWeatherMap (free)", lat, lon)
    #     return current_reading, hourly, station

    @staticmethod
    def _covert_wind_speed(wind: float) -> float:
        """Convert wind speed from m/s to km/h.

        Args:
            wind (float): Wind speed in meters per second.

        Returns:
            float: Wind speed in kilometers per hour.
        """
        return round(wind * 3.6, 2)

    @staticmethod
    def _get_rain(rain_data: list[Weather] | Weather | None, key: str) -> float | None:
        """Extract the rain volume from the OWM response.

        The OWM API may provide rain data in different formats (e.g., "1h" for last hour, "3h" for last 3 hours).
        This method will attempt to extract the most recent rain volume available.

        Args:
            rain_data (list[Weather] | Weather | None): The "rain" field from the OWM API response. Might be None.
            key (str): The key to look for in the rain data (e.g., "1h" or "3h").

        Returns:
            float: The rain volume in mm, or None if no rain data is available.
        """
        if not rain_data:
            return None
        if isinstance(rain_data, list):
            src_weather = rain_data[0]
        elif isinstance(rain_data, Weather):
            src_weather = rain_data
        else:
            return None

        rain = src_weather.rain
        if rain and key in rain:
            return round(float(rain[key]), 2)
        return None

    @staticmethod
    def _get_icon_url(icon_code: str | None) -> str | None:
        """Get the URL for an OpenWeatherMap icon code.

        OWM icons can be retrieved at https://openweathermap.org/payload/api/media/file/<icon_code>.png

        Args:
            icon_code (str): The OWM icon code (e.g., "10d").

        Returns:
            str: The URL to the corresponding PNG icon, or None if the icon code is not provided.
        """
        if not icon_code:
            return None
        return f"https://openweathermap.org/payload/api/media/file/{icon_code}.png"

    @staticmethod
    def _convert_unix_time_to_datetime(unix_time: int, get_local: bool = False) -> datetime:
        """Convert a Unix timestamp to a timezone-aware datetime object in UTC.

        Args:
            unix_time (int): The Unix timestamp to convert.
            get_local (bool): Whether to convert the time to the local timezone.

        Returns:
            datetime: The converted datetime object.
        """
        dt = datetime.fromtimestamp(unix_time, tz=UTC)
        if get_local:
            dt = dt.astimezone()
        return dt

    @staticmethod
    def _convert_kelvin_to_celsius(kelvin_temp: float) -> float:
        """Convert a temperature from Kelvin to Celsius.

        Args:
            kelvin_temp (float): The temperature in Kelvin.

        Returns:
            float: The temperature converted to Celsius.
        """
        return round(kelvin_temp - 273.15, 2)

    @staticmethod
    def _deg_to_compass(deg: float | None) -> str:
        if deg is None:
            return ""
        return _COMPASS[round(deg / 22.5) % 16]

    @staticmethod
    def _sky_to_icon(sky: str) -> str:
        sky_lower = sky.lower()
        for keyword, icon in _SKY_ICONS:
            if keyword in sky_lower:
                return icon
        return "🌡️"
