from __future__ import annotations

import math

_KT_TO_MPH = 1.15078
_KT_TO_MS = 0.514444


def _go_round(x: float, decimals: int = 0) -> float:
    """Round half-away-from-zero to match Go's math.Round behavior."""
    multiplier = 10**decimals
    return math.floor(x * multiplier + 0.5) / multiplier


def convert_temp(value: float | None, units: str) -> float | None:
    """Convert temperature. raw/imperial=no-op, metric=F->C."""
    if value is None:
        return None
    if units == "metric":
        return _go_round((value - 32) * 5 / 9, 1)
    return value


def convert_wind(value: int | None, units: str) -> float | int | None:
    """Convert wind speed. raw=no-op, metric=kt->m/s, imperial=kt->mph."""
    if value is None:
        return None
    if units == "metric":
        return _go_round(value * _KT_TO_MS, 1)
    if units == "imperial":
        return _go_round(value * _KT_TO_MPH, 1)
    return value


def convert_pressure(value: float | None, units: str) -> float | None:
    """Convert altimeter pressure. raw/imperial=no-op, metric=inHg->hPa.

    NOTE: This is ONLY for altimeter_inhg.
    sea_level_pressure_mb is already metric and must NOT be converted.
    """
    if value is None:
        return None
    if units == "metric":
        return _go_round(value * 33.8639, 2)
    return value


def convert_visibility(value: float | None, units: str) -> float | None:
    """Convert visibility. raw/imperial=no-op, metric=mi->km."""
    if value is None:
        return None
    if units == "metric":
        return _go_round(value * 1.60934, 2)
    return value


def convert_precipitation(value: float | None, units: str) -> float | None:
    """Convert precipitation. raw/imperial=no-op, metric=in->mm."""
    if value is None:
        return None
    if units == "metric":
        return _go_round(value * 25.4, 1)
    return value


# August-Roche-Magnus approximation (Alduchov & Eskridge 1996)
_MAGNUS_A = 17.625
_MAGNUS_B = 243.04  # °C


def compute_relative_humidity(
    temp_c: float | None, dewp_c: float | None
) -> float | None:
    """Compute RH from temp and dewpoint (Celsius) via Magnus formula.

    Returns value in [0, 100] rounded to 1 decimal, or None if inputs invalid.
    """
    if temp_c is None or dewp_c is None:
        return None
    try:
        t = float(temp_c)
        td = float(dewp_c)
        if not (math.isfinite(t) and math.isfinite(td)):
            return None
        rh = 100.0 * math.exp(
            (_MAGNUS_A * td) / (_MAGNUS_B + td)
        ) / math.exp(
            (_MAGNUS_A * t) / (_MAGNUS_B + t)
        )
        return _go_round(max(0.0, min(rh, 100.0)), 1)
    except (ValueError, TypeError, OverflowError, ZeroDivisionError):
        return None


def compute_feels_like(
    temp_f: float | None, wind_kt: int | None, rh: float | None,
) -> float | None:
    """Compute feels-like temperature in Fahrenheit (full NWS algorithm).

    Uses wind chill at or below 50F with wind > 3 mph, heat index at or
    above 80F with known RH, plain temp otherwise.
    Ref: https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml
    """
    if temp_f is None:
        return None
    try:
        t = float(temp_f)
        w_mph = float(wind_kt) * _KT_TO_MPH if wind_kt is not None else 0.0

        # Wind Chill (NWS): valid for temp <= 50F and wind > 3 mph
        if t <= 50.0 and w_mph > 3.0:
            wc = (
                35.74 + 0.6215 * t
                - 35.75 * (w_mph ** 0.16)
                + 0.4275 * t * (w_mph ** 0.16)
            )
            return _go_round(wc, 1)

        # Heat Index (NWS): requires known RH
        if t >= 80.0 and rh is not None:
            h = float(rh)
            # Step 1: Steadman simplified formula
            simple = 0.5 * (t + 61.0 + (t - 68.0) * 1.2 + h * 0.094)
            if (simple + t) / 2.0 < 80.0:
                return _go_round(simple, 1)

            # Step 2: Rothfusz regression
            hi = (
                -42.379
                + 2.04901523 * t
                + 10.14333127 * h
                - 0.22475541 * t * h
                - 0.00683783 * t * t
                - 0.05481717 * h * h
                + 0.00122874 * t * t * h
                + 0.00085282 * t * h * h
                - 0.00000199 * t * t * h * h
            )

            # Step 3: NWS adjustments
            if h < 13.0 and 80.0 <= t <= 112.0:
                hi -= ((13.0 - h) / 4.0) * math.sqrt((17.0 - abs(t - 95.0)) / 17.0)
            elif h > 85.0 and 80.0 <= t <= 87.0:
                hi += ((h - 85.0) / 10.0) * ((87.0 - t) / 5.0)

            return _go_round(hi, 1)

        return _go_round(t, 1)
    except (ValueError, TypeError, OverflowError):
        return None


def convert_observation(obs: Observation, units: str) -> Observation:
    """Apply all unit conversions to create a new Observation.

    Does NOT convert sea_level_pressure_mb (already metric).
    """
    from therminal.models.observation import Observation

    if units == "raw":
        return obs

    return Observation(
        station_code=obs.station_code,
        observed_at=obs.observed_at,
        observation_type=obs.observation_type,
        source=obs.source,
        temp_f=convert_temp(obs.temp_f, units),
        dewpoint_f=convert_temp(obs.dewpoint_f, units),
        relative_humidity=obs.relative_humidity,
        wind_dir_degrees=obs.wind_dir_degrees,
        wind_speed_kt=convert_wind(obs.wind_speed_kt, units),
        wind_gust_kt=convert_wind(obs.wind_gust_kt, units),
        altimeter_inhg=convert_pressure(obs.altimeter_inhg, units),
        sea_level_pressure_mb=obs.sea_level_pressure_mb,
        sky_cover_1=obs.sky_cover_1,
        sky_base_1_ft=obs.sky_base_1_ft,
        sky_cover_2=obs.sky_cover_2,
        sky_base_2_ft=obs.sky_base_2_ft,
        sky_cover_3=obs.sky_cover_3,
        sky_base_3_ft=obs.sky_base_3_ft,
        sky_cover_4=obs.sky_cover_4,
        sky_base_4_ft=obs.sky_base_4_ft,
        visibility_miles=convert_visibility(obs.visibility_miles, units),
        weather_codes=obs.weather_codes,
        precip_1hr_inches=convert_precipitation(obs.precip_1hr_inches, units),
        peak_wind_gust_kt=convert_wind(obs.peak_wind_gust_kt, units),
        peak_wind_dir=obs.peak_wind_dir,
        peak_wind_time=obs.peak_wind_time,
        feels_like_f=convert_temp(obs.feels_like_f, units),
        snow_depth_inches=convert_precipitation(obs.snow_depth_inches, units),
        raw_metar=obs.raw_metar,
    )
