defmodule Microwaveprop.Propagation.Inversion do
  @moduledoc """
  Detects temperature inversions in a native HRRR vertical profile.

  An inversion exists where temperature increases with height (dT/dz > 0),
  violating the normal atmospheric lapse rate. The "inversion top" is the
  level where temperature peaks before resuming its normal decrease — this
  is the boundary that acts as a mirror for RF propagation when it's
  smooth enough (see the meteorologist's April 2026 review).

  This module also computes the stability and turbulence properties at
  the inversion top that Phase 2 needs for scoring:

  - **Bulk Richardson number** — ratio of thermal stratification to
    wind shear; Ri < 0.25 → turbulent, Ri > 1 → laminar (good for
    propagation).
  - **θₑ jump** — change in equivalent potential temperature across
    the inversion; sharper jump = stronger decoupling.
  - **Wind shear magnitude** at the inversion top.
  """

  @g 9.80665

  @doc """
  Find the first (lowest) temperature inversion in a native profile.

  Returns `{:ok, %{height_m, level_idx, strength_k, base_idx}}` where
  `strength_k` is the total temperature increase from the inversion base
  to its top, or `:none` if no inversion is found.

  The profile map must have at least `:heights_m` and `:temp_k` arrays.
  """
  @spec find_inversion_top(map()) ::
          {:ok,
           %{
             height_m: float(),
             level_idx: non_neg_integer(),
             base_idx: non_neg_integer(),
             strength_k: float()
           }}
          | :none
  def find_inversion_top(%{level_count: n}) when n < 2, do: :none

  def find_inversion_top(%{heights_m: heights, temp_k: temps}) do
    levels = heights |> Enum.zip(temps) |> Enum.with_index()

    # Walk upward looking for the first level where T stops increasing.
    # "Inversion base" is the level where dT/dz first turns positive.
    # "Inversion top" is the level where dT/dz turns negative again.
    case find_inversion_region(levels) do
      nil ->
        :none

      {base_idx, top_idx} ->
        {:ok,
         %{
           height_m: Enum.at(heights, top_idx),
           level_idx: top_idx,
           base_idx: base_idx,
           strength_k: Enum.at(temps, top_idx) - Enum.at(temps, base_idx)
         }}
    end
  end

  def find_inversion_top(_), do: :none

  defp find_inversion_region(levels) do
    pairs = Enum.chunk_every(levels, 2, 1, :discard)

    # Find the first transition from cooling to warming (inversion base),
    # then the first transition back to cooling (inversion top).
    find_region(pairs, nil)
  end

  defp find_region([], _base), do: nil

  defp find_region([pair | rest], _base) do
    [{{_h1, t1}, _i1}, {{_h2, t2}, i2}] = pair

    if t2 > t1 do
      find_region_top(rest, i2 - 1, i2)
    else
      find_region(rest, nil)
    end
  end

  # Inside the inversion, looking for dT < 0 (= the top)
  defp find_region_top([], base_idx, last_warm_idx), do: {base_idx, last_warm_idx}

  defp find_region_top([pair | rest], base_idx, _last_warm_idx) do
    [{{_h1, t1}, _i1}, {{_h2, t2}, i2}] = pair

    if t2 < t1 do
      # Temperature dropping — previous level is the top
      {base_idx, i2 - 1}
    else
      find_region_top(rest, base_idx, i2)
    end
  end

  @doc """
  Bulk Richardson number across a layer from `base_idx` to `top_idx`.

  Ri = (g / θ_ref) * Δθ * Δz / (ΔU² + ΔV²)

  where Δθ is the potential temperature difference, ΔU/ΔV are wind
  component differences, and θ_ref is the mean potential temperature.

  Returns `nil` if inputs are missing or shear is zero (infinite Ri
  is clamped to 100.0 for practical use).
  """
  @spec bulk_richardson(map(), non_neg_integer(), non_neg_integer()) :: float() | nil
  def bulk_richardson(profile, base_idx, top_idx) do
    with t_base when is_float(t_base) <- Enum.at(profile.temp_k, base_idx),
         t_top when is_float(t_top) <- Enum.at(profile.temp_k, top_idx),
         p_base when is_float(p_base) <- Enum.at(profile.pressure_pa, base_idx),
         p_top when is_float(p_top) <- Enum.at(profile.pressure_pa, top_idx),
         h_base when is_float(h_base) <- Enum.at(profile.heights_m, base_idx),
         h_top when is_float(h_top) <- Enum.at(profile.heights_m, top_idx),
         u_base when is_float(u_base) <- Enum.at(profile.u_wind_ms, base_idx),
         u_top when is_float(u_top) <- Enum.at(profile.u_wind_ms, top_idx),
         v_base when is_float(v_base) <- Enum.at(profile.v_wind_ms, base_idx),
         v_top when is_float(v_top) <- Enum.at(profile.v_wind_ms, top_idx) do
      theta_base = potential_temperature(t_base, p_base)
      theta_top = potential_temperature(t_top, p_top)
      theta_ref = (theta_base + theta_top) / 2.0

      delta_theta = theta_top - theta_base
      delta_z = h_top - h_base
      delta_u_sq = :math.pow(u_top - u_base, 2) + :math.pow(v_top - v_base, 2)

      if delta_u_sq < 1.0e-6 or delta_z < 1.0 do
        # No shear or degenerate layer → effectively infinite stability
        100.0
      else
        ri = @g / theta_ref * delta_theta * delta_z / delta_u_sq
        min(ri, 100.0)
      end
    else
      _ -> nil
    end
  end

  @doc """
  Wind shear magnitude (m/s) across a layer from `base_idx` to `top_idx`.
  """
  @spec shear_magnitude(map(), non_neg_integer(), non_neg_integer()) :: float() | nil
  def shear_magnitude(profile, base_idx, top_idx) do
    with u_base when is_float(u_base) <- Enum.at(profile.u_wind_ms, base_idx),
         u_top when is_float(u_top) <- Enum.at(profile.u_wind_ms, top_idx),
         v_base when is_float(v_base) <- Enum.at(profile.v_wind_ms, base_idx),
         v_top when is_float(v_top) <- Enum.at(profile.v_wind_ms, top_idx) do
      :math.sqrt(:math.pow(u_top - u_base, 2) + :math.pow(v_top - v_base, 2))
    else
      _ -> nil
    end
  end

  @doc """
  Potential temperature θ = T * (P₀/P)^(R/cp) where P₀ = 100000 Pa.
  """
  @spec potential_temperature(float(), float()) :: float()
  def potential_temperature(temp_k, pressure_pa) do
    temp_k * :math.pow(100_000.0 / pressure_pa, 0.286)
  end
end
