defmodule Microwaveprop.Weather.ThetaE do
  @moduledoc """
  Equivalent potential temperature (θₑ) computation using the Bolton
  (1980) approximation.

  θₑ combines temperature and moisture into a single conserved variable
  that characterizes how "juicy" an air parcel is. A sharp vertical
  θₑ gradient across the inversion top means strong thermodynamic
  decoupling between the boundary layer and the free atmosphere — which
  the meteorologist identified as a first-class propagation predictor.

  All inputs are SI: T in Kelvin, specific humidity in kg/kg, P in Pa.
  """

  @doc """
  Compute equivalent potential temperature (K) from temperature,
  specific humidity, and pressure.

  Uses the Bolton (1980) formulation:
    θₑ = θ_dry × exp((Lv × r) / (cp × T_lcl))

  where r is mixing ratio, T_lcl is the lifting condensation level
  temperature, and θ_dry is the dry potential temperature.
  """
  @spec compute(float, float, float) :: float
  def compute(temp_k, spfh, pressure_pa) do
    # Guard: zero or negative humidity makes the log terms undefined.
    # Clamp to a tiny positive value (equivalent to extremely dry air).
    spfh = max(spfh, 1.0e-8)

    # Mixing ratio from specific humidity: r = q / (1 - q)
    r = spfh / (1.0 - spfh)

    # Water vapor pressure (Pa): e = q * P / (0.622 + 0.378 * q)
    e = spfh * pressure_pa / (0.622 + 0.378 * spfh)

    # Dry potential temperature: θ = T * (P0/P_dry)^(Rd/cp)
    # where P_dry = P - e
    p_dry = pressure_pa - e
    theta_dry = temp_k * :math.pow(100_000.0 / p_dry, 0.2854)

    # Bolton (1980) LCL temperature (Eq. 15)
    t_lcl = 2840.0 / (3.5 * :math.log(temp_k) - :math.log(e / 100.0) - 4.805) + 55.0

    # Bolton (1980) θₑ (Eq. 38, simplified)
    lv_cp = 2.5e6 / 1005.7
    theta_dry * :math.exp(lv_cp * r / t_lcl)
  end

  @doc """
  Dewpoint temperature (K) from specific humidity and pressure.

  Computes water vapor pressure from (q, P), then inverts the
  Magnus-Tetens formula to get Td.
  """
  @spec dewpoint_from_spfh(float, float) :: float
  def dewpoint_from_spfh(spfh, pressure_pa) do
    # e = q * P / (0.622 + 0.378 * q) in Pa
    e = spfh * pressure_pa / (0.622 + 0.378 * spfh)
    e_hpa = e / 100.0

    # Invert Magnus-Tetens: Td(°C) = (243.04 * ln(e/6.1078)) / (17.625 - ln(e/6.1078))
    ln_ratio = :math.log(e_hpa / 6.1078)
    td_c = 243.04 * ln_ratio / (17.625 - ln_ratio)
    td_c + 273.15
  end

  @doc """
  θₑ jump (K) between two levels in a profile.

  Returns θₑ(top) − θₑ(base). A large positive value means the
  inversion top is much warmer+drier (thermodynamically decoupled)
  than the air below — which is what the meteorologist says we need.
  """
  @spec theta_e_jump(map, non_neg_integer, non_neg_integer) :: float | nil
  def theta_e_jump(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),
         q_base when is_float(q_base) <- Enum.at(profile.spfh, base_idx),
         q_top when is_float(q_top) <- Enum.at(profile.spfh, 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) do
      compute(t_top, q_top, p_top) - compute(t_base, q_base, p_base)
    else
      _ -> nil
    end
  end
end
