defmodule Microwaveprop.Radio.BandResolver do
  @moduledoc """
  Converts user-supplied band input — ADIF wavelength labels ("33cm",
  "13cm"), numeric frequencies in MHz ("903.100", "10368"), or bare
  channel numbers ("902", "1296") — into one of the site's canonical
  MHz values.

  Used by `Microwaveprop.Radio.AdifImport`, `Microwaveprop.Radio.CsvImport`,
  and the `SubmitLive` manual form so all four entry points agree on
  what "33cm" means.
  """

  # Keep in sync with:
  #   * `Microwaveprop.Radio.Contact.@allowed_bands`
  #   * `Microwaveprop.Propagation.BandConfig.@band_configs`
  @allowed_bands [
    50,
    144,
    222,
    432,
    902,
    1296,
    2304,
    3400,
    5760,
    10_000,
    24_000,
    47_000,
    68_000,
    75_000,
    122_000,
    134_000,
    241_000
  ]

  # ADIF amateur wavelength band labels → our MHz channel centers.
  # The `normalize/1` helper strips whitespace and lower-cases before
  # looking up, so "33 CM", "33cm", " 33Cm " all resolve.
  @band_name_to_mhz %{
    "6m" => 50,
    "2m" => 144,
    "1.25m" => 222,
    "70cm" => 432,
    "33cm" => 902,
    "23cm" => 1_296,
    "13cm" => 2_304,
    "9cm" => 3_400,
    "6cm" => 5_760,
    "3cm" => 10_000,
    "1.25cm" => 24_000,
    "6mm" => 47_000,
    "4mm" => 75_000,
    "2.5mm" => 122_000,
    "2mm" => 134_000,
    "1mm" => 241_000
  }

  @doc "All MHz band centers the site accepts, ascending."
  @spec allowed_bands() :: [pos_integer()]
  def allowed_bands, do: @allowed_bands

  @doc """
  Resolve any user-supplied band input to a canonical MHz integer.

  Accepts:

    * ADIF wavelength labels ("33cm", "1.25cm", "6mm" — case/whitespace
      insensitive)
    * numeric frequencies in MHz as a string or number ("903.100",
      10368, "10368.000") — returns the nearest allowed band
    * pre-canonicalized band values (902, "902") — passes through

  Returns `nil` for unparseable input and for values below the 6m
  amateur band (<50 MHz).
  """
  @spec resolve(term()) :: pos_integer() | nil
  def resolve(nil), do: nil
  def resolve(""), do: nil

  def resolve(mhz) when is_integer(mhz), do: nearest_band(mhz * 1.0)
  def resolve(mhz) when is_float(mhz), do: nearest_band(mhz)

  def resolve(value) when is_binary(value) do
    normalized = normalize(value)

    cond do
      mhz = @band_name_to_mhz[normalized] -> mhz
      freq = parse_float(normalized) -> nearest_band(freq)
      true -> nil
    end
  end

  def resolve(_), do: nil

  @doc "Return `resolve/1` as a string, or `nil`."
  @spec resolve_as_string(term()) :: String.t() | nil
  def resolve_as_string(value) do
    case resolve(value) do
      nil -> nil
      mhz -> Integer.to_string(mhz)
    end
  end

  defp normalize(s) do
    s
    |> String.trim()
    |> String.downcase()
    |> String.replace(~r/\s+/, "")
  end

  defp parse_float(s) do
    case Float.parse(s) do
      {f, ""} -> f
      {f, _rest} -> f
      :error -> nil
    end
  end

  # Amateur allocations: 6m is 50-54 MHz, 2m is 144-148 MHz, 1.25m is
  # 222-225 MHz (US-only), 70cm is 420-450 MHz (US). Accept a little slop on
  # each side to tolerate ADIF FREQ fields that are slightly off-band or
  # informal "220" labels.
  defp nearest_band(freq_mhz) when freq_mhz >= 44 and freq_mhz <= 60, do: 50
  defp nearest_band(freq_mhz) when freq_mhz >= 140 and freq_mhz <= 150, do: 144
  defp nearest_band(freq_mhz) when freq_mhz >= 218 and freq_mhz <= 226, do: 222
  defp nearest_band(freq_mhz) when freq_mhz >= 420 and freq_mhz <= 450, do: 432

  defp nearest_band(freq_mhz) when freq_mhz >= 900 do
    Enum.min_by(@allowed_bands, &abs(&1 - freq_mhz))
  end

  defp nearest_band(_), do: nil
end
