defmodule Microwaveprop.Weather.WeatherLayers do
  @moduledoc "Derives scalar weather map layers from HRRR profile data."

  alias Microwaveprop.Weather.SoundingParams

  @doc """
  Derives a map of scalar weather fields from a row containing HRRR profile data.

  Expects a map with keys: `:surface_temp_c`, `:surface_dewpoint_c`,
  `:surface_pressure_mb`, `:surface_refractivity`, `:profile`, `:duct_characteristics`.

  Returns a map with derived fields: `:surface_rh`, `:surface_refractivity`,
  `:temp_850mb`, `:dewpoint_850mb`, `:lapse_rate`, `:inversion_strength`,
  `:inversion_base_m`, `:duct_base_m`, `:duct_strength`.
  """
  @spec derive(map()) :: map()
  def derive(row) do
    sorted = sort_profile(row.profile)

    %{
      surface_rh: compute_rh(row.surface_temp_c, row.surface_dewpoint_c),
      surface_refractivity: row.surface_refractivity,
      temp_850mb: level_value(sorted, 850, "tmpc"),
      dewpoint_850mb: level_value(sorted, 850, "dwpc"),
      lapse_rate: compute_lapse_rate(sorted),
      inversion_strength: inversion_strength(sorted),
      inversion_base_m: inversion_base_m(sorted),
      duct_base_m: duct_field(row.duct_characteristics, "base"),
      duct_strength: duct_field(row.duct_characteristics, "strength")
    }
  end

  defp sort_profile(nil), do: []
  defp sort_profile([]), do: []

  defp sort_profile(profile) do
    profile
    |> Enum.filter(fn p -> p["pres"] != nil and p["tmpc"] != nil and p["hght"] != nil end)
    |> Enum.sort_by(fn p -> p["pres"] end, :desc)
  end

  defp compute_rh(t_c, td_c) do
    100.0 * SoundingParams.sat_vap_pres(td_c) / SoundingParams.sat_vap_pres(t_c)
  end

  defp level_value([], _target_pres, _key), do: nil

  defp level_value(sorted, target_pres, key) do
    nearest = Enum.min_by(sorted, fn p -> abs(p["pres"] - target_pres) end)

    if abs(nearest["pres"] - target_pres) <= 25.0 do
      nearest[key]
    end
  end

  defp compute_lapse_rate([]), do: nil

  defp compute_lapse_rate(sorted) when length(sorted) < 2, do: nil

  defp compute_lapse_rate(sorted) do
    surface = hd(sorted)
    top = List.last(sorted)
    dh_km = (top["hght"] - surface["hght"]) / 1000.0

    if dh_km > 0 do
      (surface["tmpc"] - top["tmpc"]) / dh_km
    end
  end

  defp inversion_strength([]), do: 0.0

  defp inversion_strength(sorted) do
    sorted
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.reduce(0.0, fn [lower, upper], max_str ->
      dt = upper["tmpc"] - lower["tmpc"]

      if dt > max_str do
        dt
      else
        max_str
      end
    end)
  end

  defp inversion_base_m([]), do: nil

  defp inversion_base_m(sorted) do
    sfc_hght = hd(sorted)["hght"]

    result =
      sorted
      |> Enum.chunk_every(2, 1, :discard)
      |> Enum.reduce({0.0, nil}, fn [lower, upper], {max_str, _base} = acc ->
        dt = upper["tmpc"] - lower["tmpc"]

        if dt > 0 and dt > max_str do
          {dt, lower["hght"] - sfc_hght}
        else
          acc
        end
      end)

    case result do
      {str, base} when str > 0 -> base
      _ -> nil
    end
  end

  defp duct_field(nil, _key), do: nil
  defp duct_field([], _key), do: nil

  defp duct_field(ducts, "base") do
    ducts
    |> Enum.map(fn d -> d["base"] end)
    |> Enum.min()
  end

  defp duct_field(ducts, "strength") do
    ducts
    |> Enum.map(fn d -> d["strength"] end)
    |> Enum.max()
  end
end
