"""AWC METAR helpers ported from therminal-ingest/src/sources/nws/awc-client.ts."""

from __future__ import annotations

import math
from datetime import UTC, datetime
from typing import Any

from therminal._convert import compute_relative_humidity


def icao_to_station_code(icao: str) -> str:
    """Strip leading K for 4-letter CONUS ICAO codes."""
    if icao.startswith("K") and len(icao) == 4:
        return icao[1:]
    return icao


def station_to_icao(code: str) -> str:
    """Prepend K to 3-letter CONUS station codes."""
    if len(code) == 3:
        return "K" + code
    return code


def parse_awc_visibility(vis: Any) -> float | None:
    """Parse AWC visibility: '10+', '1/2', '2 1/4', '3/4', plain numbers.

    Returns miles or None. Caps at 99.99.
    """
    if vis is None:
        return None

    s = str(vis)
    if s == "" or s == "null":
        return None

    # "10+" -> 10
    if s.endswith("+"):
        try:
            n = float(s[:-1])
        except (ValueError, OverflowError):
            return None
        if not math.isfinite(n):
            return None
        return min(n, 99.99)

    # Mixed number: "1 1/2", "2 1/4"
    if " " in s and "/" in s:
        parts = s.split(" ", 1)
        if len(parts) != 2:
            return None
        frac_parts = parts[1].split("/")
        if len(frac_parts) != 2:
            return None
        try:
            w = float(parts[0])
            n = float(frac_parts[0])
            d = float(frac_parts[1])
        except (ValueError, OverflowError):
            return None
        if not (math.isfinite(w) and math.isfinite(n) and math.isfinite(d) and d != 0):
            return None
        return math.floor((w + n / d) * 100 + 0.5) / 100

    # Simple fraction: "1/2", "1/4", "3/4"
    if "/" in s:
        frac_parts = s.split("/")
        if len(frac_parts) != 2:
            return None
        try:
            n = float(frac_parts[0])
            d = float(frac_parts[1])
        except (ValueError, OverflowError):
            return None
        if not (math.isfinite(n) and math.isfinite(d) and d != 0):
            return None
        return math.floor((n / d) * 100 + 0.5) / 100

    # Plain number
    try:
        n = float(s)
    except (ValueError, OverflowError):
        return None
    if not math.isfinite(n):
        return None
    return min(n, 99.99)


def map_cloud_cover(cover: str | None) -> str | None:
    """Map AWC cloud cover code to standard abbreviation."""
    if cover is None:
        return None
    upper = cover.upper()
    if upper in ("CLR", "SKC", "FEW", "SCT", "BKN", "OVC", "VV"):
        return upper
    if upper == "CAVOK":
        return "CLR"
    return None


def celsius_to_f(c: float | None) -> float | None:
    """Celsius to Fahrenheit with Go-matched rounding."""
    if c is None or not math.isfinite(c):
        return None
    return math.floor((c * 9 / 5 + 32) * 10 + 0.5) / 10


def _safe_int(v: Any) -> int | None:
    """Convert to int with Go-matched rounding. Returns None on bad input."""
    if v is None:
        return None
    try:
        f = float(v)
        if not math.isfinite(f):
            return None
        return int(math.floor(f + 0.5))
    except (ValueError, TypeError, OverflowError):
        return None


def _safe_float(v: Any) -> float | None:
    """Convert to float. Returns None on bad input."""
    if v is None:
        return None
    try:
        f = float(v)
        return f if math.isfinite(f) else None
    except (ValueError, TypeError, OverflowError):
        return None


def _safe_altim(v: Any) -> float | None:
    """Convert AWC altimeter (hPa) to inHg with Go-matched rounding."""
    if v is None:
        return None
    try:
        f = float(v)
        if not math.isfinite(f):
            return None
        return math.floor(f * 0.02953 * 100 + 0.5) / 100
    except (ValueError, TypeError, OverflowError):
        return None


def awc_to_observation(m: dict[str, Any]) -> Observation | None:
    """Convert a parsed AWC METAR dict to an Observation.

    Returns None if icaoId or obsTime is invalid.
    """
    from therminal.models.observation import Observation

    icao_id = m.get("icaoId")
    if not isinstance(icao_id, str) or not icao_id:
        return None

    obs_time = m.get("obsTime")
    if not isinstance(obs_time, (int, float)):
        return None

    station_code = icao_to_station_code(icao_id)

    dt = datetime.fromtimestamp(obs_time, tz=UTC)
    observed_at = dt.strftime("%Y-%m-%dT%H:%M:%SZ")

    metar_type = (m.get("metarType") or "METAR").upper()
    observation_type = "SPECI" if metar_type == "SPECI" else "METAR"

    # Wind direction: handle "VRB" -> None
    wdir: int | None = None
    raw_wdir = m.get("wdir")
    if raw_wdir is not None:
        if isinstance(raw_wdir, (int, float)):
            wdir = int(math.floor(raw_wdir + 0.5))
        elif raw_wdir != "VRB":
            try:
                parsed = float(raw_wdir)
                if math.isfinite(parsed):
                    wdir = int(math.floor(parsed + 0.5))
            except (ValueError, TypeError):
                pass

    # Wind speed / gust (guarded against non-numeric AWC values)
    wspd = _safe_int(m.get("wspd"))
    wgst = _safe_int(m.get("wgst"))

    # Altimeter: AWC altim is in hPa, convert to inHg
    altim = _safe_altim(m.get("altim"))

    # Sea-level pressure (already in mb/hPa)
    slp = _safe_float(m.get("slp"))

    # Cloud layers
    clouds = m.get("clouds") or []
    c1 = clouds[0] if len(clouds) > 0 else None
    c2 = clouds[1] if len(clouds) > 1 else None
    c3 = clouds[2] if len(clouds) > 2 else None
    c4 = clouds[3] if len(clouds) > 3 else None

    # Raw METAR (truncate to 2048)
    raw_ob = m.get("rawOb")
    raw_metar: str | None = None
    if isinstance(raw_ob, str):
        raw_metar = raw_ob[:2048]

    # Weather codes (truncate to 256)
    raw_wx = m.get("wxString")
    weather_codes: str | None = None
    if isinstance(raw_wx, str):
        weather_codes = raw_wx[:256]

    # Relative humidity from temp + dewpoint (Magnus formula)
    temp_c = m.get("temp")
    dewp_c = m.get("dewp")

    return Observation(
        station_code=station_code,
        observed_at=observed_at,
        observation_type=observation_type,
        source="awc",
        temp_f=celsius_to_f(temp_c),
        dewpoint_f=celsius_to_f(dewp_c),
        relative_humidity=compute_relative_humidity(temp_c, dewp_c),
        wind_dir_degrees=wdir,
        wind_speed_kt=wspd,
        wind_gust_kt=wgst,
        altimeter_inhg=altim,
        sea_level_pressure_mb=slp,
        sky_cover_1=map_cloud_cover(c1.get("cover")) if c1 else None,
        sky_base_1_ft=c1.get("base") if c1 else None,
        sky_cover_2=map_cloud_cover(c2.get("cover")) if c2 else None,
        sky_base_2_ft=c2.get("base") if c2 else None,
        sky_cover_3=map_cloud_cover(c3.get("cover")) if c3 else None,
        sky_base_3_ft=c3.get("base") if c3 else None,
        sky_cover_4=map_cloud_cover(c4.get("cover")) if c4 else None,
        sky_base_4_ft=c4.get("base") if c4 else None,
        visibility_miles=parse_awc_visibility(m.get("visib")),
        weather_codes=weather_codes,
        precip_1hr_inches=None,
        peak_wind_gust_kt=None,
        peak_wind_dir=None,
        peak_wind_time=None,
        feels_like_f=None,
        snow_depth_inches=None,
        raw_metar=raw_metar,
    )
