"""Pydantic models for AccuWeather forecast and conditions responses.

Models use snake_case attribute names with PascalCase aliases matching the
raw API payload, so both ``model.icon_phrase`` and parsing of
``"IconPhrase"`` work. Every response model offers:

* readable ``__str__`` output for quick printing,
* ``to_dict()`` for plain-Python consumption,
* ``to_pandas_df()`` for analysis (requires the ``pandas`` extra).
"""

from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, ConfigDict, Field, computed_field

from accuweather_client.icons import icon_to_emoji

if TYPE_CHECKING:  # pragma: no cover
    import pandas as pd


def _require_pandas() -> "pd":
    """Import pandas lazily, with a helpful error if it is missing."""
    try:
        import pandas as pd
    except ImportError as exc:  # pragma: no cover
        raise ImportError(
            "pandas is required for DataFrame export. Install it with: "
            "pip install 'accuweather-client[pandas]'"
        ) from exc
    return pd


class _ApiModel(BaseModel):
    """Base model: tolerate and preserve unknown fields from the API."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)


class TemperatureModel(_ApiModel):
    """A measurement with value, unit and AccuWeather unit type."""

    value: float = Field(..., alias="Value")
    unit: str = Field(..., alias="Unit")
    unit_type: int | None = Field(None, alias="UnitType")

    def __str__(self) -> str:
        return f"{self.value}{self.unit}"


class WindSpeedModel(_ApiModel):
    """Wind speed measurement."""

    value: float = Field(..., alias="Value")
    unit: str = Field(..., alias="Unit")
    unit_type: int | None = Field(None, alias="UnitType")


class WindDirectionModel(_ApiModel):
    """Wind direction in degrees and as a compass point."""

    degrees: int = Field(..., alias="Degrees")
    localized: str = Field(..., alias="Localized")
    english: str | None = Field(None, alias="English")


class WindModel(_ApiModel):
    """Wind speed and direction."""

    speed: WindSpeedModel = Field(..., alias="Speed")
    direction: WindDirectionModel | None = Field(None, alias="Direction")


class WindGustModel(_ApiModel):
    """Wind gust speed."""

    speed: WindSpeedModel = Field(..., alias="Speed")


class DayNightForecastModel(_ApiModel):
    """Forecast details for the day or night half of a daily forecast."""

    icon: int = Field(..., alias="Icon")
    icon_phrase: str = Field(..., alias="IconPhrase")
    has_precipitation: bool = Field(..., alias="HasPrecipitation")
    wind: WindModel | None = Field(None, alias="Wind")
    wind_gust: WindGustModel | None = Field(None, alias="WindGust")
    precipitation_probability: int | None = Field(
        None, alias="PrecipitationProbability"
    )

    @property
    def emoji(self) -> str:
        """Emoji matching this forecast's weather icon."""
        return icon_to_emoji(self.icon)


class DailyForecastModel(_ApiModel):
    """One day inside a daily forecast response."""

    date: str = Field(..., alias="Date")
    epoch_date: int = Field(..., alias="EpochDate")
    temperature: dict = Field(..., alias="Temperature")
    day: DayNightForecastModel = Field(..., alias="Day")
    night: DayNightForecastModel = Field(..., alias="Night")
    sources: list[str] | None = Field(None, alias="Sources")
    mobile_link: str | None = Field(None, alias="MobileLink")
    link: str | None = Field(None, alias="Link")

    def __str__(self) -> str:
        maximum = self.temperature.get("Maximum", {})
        return (
            f"{self.date[:10]}: {self.day.emoji} {self.day.icon_phrase}, "
            f"max {maximum.get('Value')}{maximum.get('Unit', '')}"
        )


class HeadlineModel(_ApiModel):
    """The headline (most significant weather event) of a daily forecast."""

    effective_date: str = Field(..., alias="EffectiveDate")
    effective_epoch_date: int = Field(..., alias="EffectiveEpochDate")
    severity: int = Field(..., alias="Severity")
    text: str = Field(..., alias="Text")
    category: str | None = Field(None, alias="Category")
    end_date: str | None = Field(None, alias="EndDate")
    end_epoch_date: int | None = Field(None, alias="EndEpochDate")
    mobile_link: str | None = Field(None, alias="MobileLink")
    link: str | None = Field(None, alias="Link")


class ForecastModel(_ApiModel):
    """One hour inside an hourly forecast response."""

    date_time: str = Field(..., alias="DateTime")
    epoch_date_time: int = Field(..., alias="EpochDateTime")
    weather_icon: int = Field(..., alias="WeatherIcon")
    icon_phrase: str = Field(..., alias="IconPhrase")
    has_precipitation: bool = Field(..., alias="HasPrecipitation")
    precipitation_type: str | None = Field(None, alias="PrecipitationType")
    precipitation_intensity: str | None = Field(
        None, alias="PrecipitationIntensity"
    )
    is_daylight: bool | None = Field(None, alias="IsDaylight")
    temperature: TemperatureModel = Field(..., alias="Temperature")
    real_feel_temperature: TemperatureModel | None = Field(
        None, alias="RealFeelTemperature"
    )
    real_feel_temperature_shade: TemperatureModel | None = Field(
        None, alias="RealFeelTemperatureShade"
    )
    wet_bulb_temperature: TemperatureModel | None = Field(
        None, alias="WetBulbTemperature"
    )
    dew_point: TemperatureModel | None = Field(None, alias="DewPoint")
    wind: WindModel | None = Field(None, alias="Wind")
    wind_gust: WindGustModel | None = Field(None, alias="WindGust")
    relative_humidity: int | None = Field(None, alias="RelativeHumidity")
    indoor_relative_humidity: int | None = Field(
        None, alias="IndoorRelativeHumidity"
    )
    visibility: TemperatureModel | None = Field(None, alias="Visibility")
    cloud_cover: int | None = Field(None, alias="CloudCover")
    ceiling: TemperatureModel | None = Field(None, alias="Ceiling")
    uv_index: int | None = Field(None, alias="UVIndex")
    uv_index_text: str | None = Field(None, alias="UVIndexText")
    precipitation_probability: int | None = Field(
        None, alias="PrecipitationProbability"
    )
    thunderstorm_probability: int | None = Field(
        None, alias="ThunderstormProbability"
    )
    rain_probability: int | None = Field(None, alias="RainProbability")
    snow_probability: int | None = Field(None, alias="SnowProbability")
    ice_probability: int | None = Field(None, alias="IceProbability")

    @property
    def emoji(self) -> str:
        """Emoji matching this hour's weather icon."""
        return icon_to_emoji(self.weather_icon)

    def __str__(self) -> str:
        return (
            f"{self.date_time}: {self.emoji} {self.icon_phrase}, "
            f"{self.temperature}"
        )


class ForecastModel5Days(_ApiModel):
    """A daily forecast response (1, 5, 10 or 15 days).

    The class name is kept from v1 for backwards compatibility; since v2
    it is used for every daily-forecast length.

    Example:
        >>> forecast = client.get_5day_forecast()
        >>> forecast.forecast_tomorrow
        'Partly sunny, max temp is 18.9C and 25% chance of rain.'
        >>> df = forecast.to_pandas_df()  # requires the pandas extra
    """

    headline: HeadlineModel = Field(..., alias="Headline")
    daily_forecasts: list[DailyForecastModel] = Field(
        ..., alias="DailyForecasts"
    )

    @classmethod
    def from_api_response(cls, data: dict) -> "ForecastModel5Days":
        """Build the model from a raw daily-forecast API payload."""
        return cls(**data)

    @computed_field  # type: ignore[prop-decorator]
    @property
    def text(self) -> str:
        """The headline text, e.g. an incoming-storm warning."""
        return self.headline.text

    @computed_field  # type: ignore[prop-decorator]
    @property
    def forecast_tomorrow(self) -> str:
        """A one-line, human-readable forecast for tomorrow.

        Falls back to the first available day when the response contains
        only a single day (1-day forecasts).
        """
        day_dict = self.daily_forecasts[
            1 if len(self.daily_forecasts) > 1 else 0
        ]
        text = day_dict.day.icon_phrase.replace("w/", "with")
        temp = day_dict.temperature["Maximum"]["Value"]
        temp_unit = day_dict.temperature["Maximum"]["Unit"]
        rain = day_dict.day.precipitation_probability
        return (
            f"{text}, max temp is {temp}{temp_unit} "
            f"and {rain}% chance of rain."
        )

    def to_dict(self) -> dict[str, Any]:
        """Return the forecast as a plain dict using API field names."""
        return self.model_dump(by_alias=True)

    def to_pandas_df(self) -> "pd.DataFrame":
        """Return one row per forecast day as a flat pandas DataFrame.

        Raises:
            ImportError: If pandas is not installed
                (``pip install 'accuweather-client[pandas]'``).
        """
        pd = _require_pandas()
        forecast_dicts = [
            forecast.model_dump(by_alias=True)
            for forecast in self.daily_forecasts
        ]
        return pd.json_normalize(forecast_dicts)

    def __str__(self) -> str:
        days = "\n".join(str(d) for d in self.daily_forecasts)
        return f"{self.headline.text}\n{days}"


#: Alias with a length-neutral name; identical to :class:`ForecastModel5Days`.
DailyForecastsModel = ForecastModel5Days


class HourlyForecastModel(_ApiModel):
    """An hourly forecast response (1, 12, 24, 72 or 120 hours).

    Attributes:
        output: One :class:`ForecastModel` per forecast hour.
    """

    output: list[ForecastModel]

    def to_dict(self) -> list[dict[str, Any]]:
        """Return the forecast as a list of dicts using API field names."""
        return [hour.model_dump(by_alias=True) for hour in self.output]

    def to_pandas_df(self) -> "pd.DataFrame":
        """Return one row per forecast hour as a flat pandas DataFrame.

        Raises:
            ImportError: If pandas is not installed
                (``pip install 'accuweather-client[pandas]'``).
        """
        pd = _require_pandas()
        return pd.json_normalize(self.to_dict())

    def __str__(self) -> str:
        return "\n".join(str(hour) for hour in self.output)


class CurrentConditionsModel(_ApiModel):
    """A current-conditions response.

    Attributes:
        output: The raw list of observation dicts from the API (usually a
            single item).

    Example:
        >>> conditions = client.get_current_conditions()
        >>> conditions.current_conditions
        'At the moment: sunny, with a temperature of 22.0C. ...'
    """

    output: list[dict[str, Any]]

    @computed_field  # type: ignore[prop-decorator]
    @property
    def current_conditions(self) -> str:
        """A one-line, human-readable summary of the current weather."""
        info = self.output[0]
        text = info["WeatherText"]
        temp = info["Temperature"]["Metric"]["Value"]
        temp_unit = info["Temperature"]["Metric"]["Unit"]
        wind_speed = info["Wind"]["Speed"]["Metric"]["Value"]
        wind_direction = info["Wind"]["Direction"]["Localized"]
        wind_unit = info["Wind"]["Speed"]["Metric"]["Unit"]
        return (
            f"At the moment: {text.lower()}, with a temperature of "
            f"{temp}{temp_unit}. The wind is coming from the "
            f"{wind_direction} at {wind_speed}{wind_unit}."
        )

    @property
    def weather_text(self) -> str:
        """The current weather phrase, e.g. ``"Sunny"``."""
        return self.output[0]["WeatherText"]

    @property
    def temperature(self) -> float:
        """The current metric temperature in degrees Celsius."""
        return self.output[0]["Temperature"]["Metric"]["Value"]

    def to_dict(self) -> dict[str, Any]:
        """Return the first (current) observation as a plain dict."""
        return self.output[0]

    def to_pandas_df(self) -> "pd.DataFrame":
        """Return the observation(s) as a flat pandas DataFrame.

        Raises:
            ImportError: If pandas is not installed
                (``pip install 'accuweather-client[pandas]'``).
        """
        pd = _require_pandas()
        return pd.json_normalize(self.output)

    def __str__(self) -> str:
        return self.current_conditions


class HistoricalConditionsModel(_ApiModel):
    """A historical-conditions response (past 6 or 24 hours).

    Attributes:
        output: One observation dict per historical hour.
    """

    output: list[dict[str, Any]]

    def to_dict(self) -> list[dict[str, Any]]:
        """Return all historical observations as plain dicts."""
        return self.output

    def to_pandas_df(self) -> "pd.DataFrame":
        """Return one row per observation as a flat pandas DataFrame.

        Raises:
            ImportError: If pandas is not installed
                (``pip install 'accuweather-client[pandas]'``).
        """
        pd = _require_pandas()
        return pd.json_normalize(self.output)


class AlertsModel(_ApiModel):
    """A severe-weather-alerts response.

    Attributes:
        output: One dict per active alert; empty when there are none.
    """

    output: list[dict[str, Any]]

    @property
    def count(self) -> int:
        """The number of active alerts."""
        return len(self.output)

    @property
    def descriptions(self) -> list[str]:
        """The localized description of each active alert."""
        return [
            alert.get("Description", {}).get("Localized", "")
            for alert in self.output
        ]

    def to_dict(self) -> list[dict[str, Any]]:
        """Return all alerts as plain dicts."""
        return self.output

    def __str__(self) -> str:
        if not self.output:
            return "No active weather alerts."
        return "\n".join(f"⚠️ {d}" for d in self.descriptions)


class IndicesModel(_ApiModel):
    """A daily-indices response (pollen, UV, outdoor activities, ...).

    Attributes:
        output: One dict per lifestyle index.
    """

    output: list[dict[str, Any]]

    def get(self, name: str) -> list[dict[str, Any]]:
        """Return all indices whose name contains ``name`` (case-insensitive).

        Args:
            name: Full or partial index name, e.g. ``"running"``.
        """
        return [
            index
            for index in self.output
            if name.lower() in index.get("Name", "").lower()
        ]

    def to_dict(self) -> list[dict[str, Any]]:
        """Return all indices as plain dicts."""
        return self.output

    def to_pandas_df(self) -> "pd.DataFrame":
        """Return one row per index as a flat pandas DataFrame.

        Raises:
            ImportError: If pandas is not installed
                (``pip install 'accuweather-client[pandas]'``).
        """
        pd = _require_pandas()
        return pd.json_normalize(self.output)
