defmodule MicrowavepropWeb.EmeLive do
  @moduledoc """
  `/eme` Earth-Moon-Earth path-loss + antenna-aim calculator.

  Given a station's grid/callsign/coords, TX power, and antenna gain,
  the page computes:

  - Current Moon az / el from the station (real-time tick).
  - Slant range to the Moon.
  - Round-trip EME path loss at the operating band.
  - Link-budget received power for a self-echo bounce.
  - Indicative CW SNR in a narrow detection bandwidth.

  The Moon readout ticks once per 10 s so operators can watch the
  aim drift live. Moon az/el is computed with a low-precision
  ephemeris (±0.3°) — well inside the beamwidth of any amateur EME
  dish at the supported microwave bands.
  """
  use MicrowavepropWeb, :live_view

  import MicrowavepropWeb.LiveHelpers, only: [parse_float: 2, parse_int: 2]

  alias Microwaveprop.Moon
  alias Microwaveprop.Propagation.BandConfig
  alias Microwaveprop.Propagation.Eme

  @band_options BandConfig.band_options()
  @tick_ms 10_000

  @defaults %{
    "source" => "",
    "band" => "1296",
    "tx_power_dbm" => "50",
    "tx_gain_dbi" => "30",
    "bandwidth_hz" => "100",
    "t_sys_k" => "150"
  }

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      {:ok, _tref} = :timer.send_interval(@tick_ms, self(), :tick)
      :ok
    else
      :ok
    end

    {:ok,
     assign(socket,
       page_title: "EME Calculator",
       band_options: @band_options,
       source: "",
       source_resolved: nil,
       band: "1296",
       tx_power_dbm: "50",
       tx_gain_dbi: "30",
       bandwidth_hz: "100",
       t_sys_k: "150",
       moon: nil,
       link: nil,
       error: nil,
       now_utc: DateTime.utc_now()
     )}
  end

  @impl true
  def handle_params(params, _uri, socket) do
    p = Map.merge(@defaults, Map.take(params, Map.keys(@defaults)))

    socket =
      assign(socket,
        source: p["source"],
        band: p["band"],
        tx_power_dbm: p["tx_power_dbm"],
        tx_gain_dbi: p["tx_gain_dbi"],
        bandwidth_hz: p["bandwidth_hz"],
        t_sys_k: p["t_sys_k"]
      )

    {:noreply, recompute(socket)}
  end

  @impl true
  def handle_event("calculate", params, socket) do
    url_params =
      @defaults
      |> Map.keys()
      |> Enum.reduce(%{}, fn k, acc ->
        v = params[k] || ""
        if v == "" or v == @defaults[k], do: acc, else: Map.put(acc, k, v)
      end)

    {:noreply, push_patch(socket, to: ~p"/eme?#{url_params}", replace: true)}
  end

  @impl true
  def handle_info(:tick, socket) do
    {:noreply, socket |> assign(now_utc: DateTime.utc_now()) |> recompute_moon_only()}
  end

  # Recompute both the source resolution and the link budget (called on
  # form submit / URL patch).
  defp recompute(socket) do
    case resolve_source(socket.assigns.source) do
      {:ok, resolved} ->
        socket
        |> assign(source_resolved: resolved, error: nil)
        |> recompute_moon_only()

      {:error, reason} ->
        assign(socket, source_resolved: nil, error: reason, moon: nil, link: nil)

      :empty ->
        assign(socket, source_resolved: nil, error: nil, moon: nil, link: nil)
    end
  end

  # Refresh just the live-ticking numbers (moon pos + link budget).
  # Called from the tick timer so a stale error state doesn't get
  # stomped by a repeat source-resolution.
  defp recompute_moon_only(%{assigns: %{source_resolved: nil}} = socket), do: socket

  defp recompute_moon_only(socket) do
    %{lat: lat, lon: lon} = socket.assigns.source_resolved
    moon = Moon.observer_position(socket.assigns.now_utc, lat, lon)

    band_mhz = parse_int(socket.assigns.band, 1_296)
    tx_dbm = parse_float(socket.assigns.tx_power_dbm, 50.0)
    tx_gain = parse_float(socket.assigns.tx_gain_dbi, 30.0)
    bw = parse_float(socket.assigns.bandwidth_hz, 100.0)
    tsys = parse_float(socket.assigns.t_sys_k, 150.0)

    # Band-dependent lunar albedo feeds both the path-loss number
    # and the per-line-item display below.
    albedo = Eme.moon_albedo(band_mhz)

    fspl_rt = Eme.fspl_round_trip_db(band_mhz, moon.distance_km)
    moon_gain = Eme.moon_reflection_gain_db(band_mhz, albedo)
    path_loss = fspl_rt - moon_gain

    # Keep everything in dBm for display — EIRP(dBm) = TX(dBm) + G_tx(dBi).
    eirp_dbm = tx_dbm + tx_gain

    received_power = tx_dbm + tx_gain + tx_gain - path_loss
    noise_floor = Eme.noise_floor_dbm(bw, tsys)
    snr = received_power - noise_floor

    # Self-echo Doppler (Earth → Moon → Earth) for the current
    # geometry. Live-updates with the tick.
    v_radial_km_s = Moon.radial_velocity_km_s(socket.assigns.now_utc, lat, lon)
    doppler_hz = Eme.doppler_shift_hz(band_mhz, v_radial_km_s)

    link = %{
      # EIRP breakdown so the template can render "TX + gain = EIRP".
      tx_power_dbm: tx_dbm,
      tx_power_w: :math.pow(10.0, (tx_dbm - 30) / 10.0),
      tx_gain_dbi: tx_gain,
      eirp_dbm: eirp_dbm,
      eirp_w: :math.pow(10.0, (eirp_dbm - 30) / 10.0),

      # Path-loss decomposition: spreading vs moon-reflection gain.
      fspl_round_trip_db: fspl_rt,
      moon_albedo: albedo,
      moon_reflection_gain_db: moon_gain,
      path_loss_db: path_loss,

      # Final link numbers.
      received_power_dbm: received_power,
      noise_floor_dbm: noise_floor,
      snr_db: snr,

      # Doppler (2× one-way for self-echo round-trip).
      radial_velocity_km_s: v_radial_km_s,
      doppler_hz: doppler_hz
    }

    socket
    |> assign(moon: moon, link: link)
    |> push_event("eme:update", %{
      lat: lat,
      lon: lon,
      az: moon.azimuth,
      el: moon.elevation,
      moon_visible: moon.elevation >= 0.0
    })
  end

  # --- Input parsing --------------------------------------------------

  defp resolve_source(input), do: MicrowavepropWeb.LocationResolver.resolve(input)

  # --- Render helpers -------------------------------------------------
  #
  # Note on UTF-8: these use Elixir's native :erlang.float_to_binary/2
  # rather than :io_lib.format/2 because the latter treats each byte of
  # an inline UTF-8 codepoint (like "°" = 0xC2 0xB0) as a separate
  # Latin-1 character, producing mojibake (Â°) when the result is
  # joined back into a binary.

  defp format_deg(nil), do: "—"

  defp format_deg(v) when is_number(v) do
    :erlang.float_to_binary(v * 1.0, decimals: 2) <> "°"
  end

  defp format_distance_km(nil), do: "—"

  defp format_distance_km(v) when is_number(v) do
    # Integer km with thousands separators ("374,088 km").
    v |> round() |> Microwaveprop.Format.number() |> Kernel.<>(" km")
  end

  defp format_db(nil), do: "—"
  defp format_db(v) when is_number(v), do: :erlang.float_to_binary(v * 1.0, decimals: 1) <> " dB"

  defp format_dbm(nil), do: "—"

  defp format_dbm(v) when is_number(v) do
    :erlang.float_to_binary(v * 1.0, decimals: 1) <> " dBm"
  end

  defp format_doppler(nil), do: "—"

  defp format_doppler(hz) when is_number(hz) do
    abs_hz = abs(hz)

    body =
      cond do
        abs_hz >= 1000.0 -> :erlang.float_to_binary(hz / 1000.0, decimals: 2) <> " kHz"
        abs_hz >= 1.0 -> :erlang.float_to_binary(hz * 1.0, decimals: 0) <> " Hz"
        true -> :erlang.float_to_binary(hz * 1.0, decimals: 2) <> " Hz"
      end

    # Prepend a + sign for positive (rising) shifts; `float_to_binary`
    # already renders negatives with the leading − character.
    if hz > 0, do: "+" <> body, else: body
  end

  defp format_radial_velocity(nil), do: "—"

  defp format_radial_velocity(v_km_s) when is_number(v_km_s) do
    # Display in m/s at the ~kHz-scale Doppler levels EME operators
    # care about. Positive = receding.
    m_s = v_km_s * 1000.0
    suffix = " m/s"
    body = :erlang.float_to_binary(m_s, decimals: 1)
    if m_s > 0, do: "+" <> body <> suffix, else: body <> suffix
  end

  defp format_watts(nil), do: "—"

  defp format_watts(w) when is_number(w) and w >= 1000 do
    :erlang.float_to_binary(w / 1000.0, decimals: 2) <> " kW"
  end

  defp format_watts(w) when is_number(w) and w >= 1 do
    :erlang.float_to_binary(w * 1.0, decimals: 1) <> " W"
  end

  defp format_watts(w) when is_number(w) and w > 0 do
    :erlang.float_to_binary(w * 1000.0, decimals: 2) <> " mW"
  end

  defp format_watts(_), do: "—"

  defp format_albedo(nil), do: "—"

  defp format_albedo(v) when is_number(v) do
    :erlang.float_to_binary(v * 1.0, decimals: 3)
  end

  defp snr_badge_class(nil), do: "badge-ghost"
  defp snr_badge_class(snr) when snr >= 10, do: "badge-success"
  defp snr_badge_class(snr) when snr >= 0, do: "badge-warning"
  defp snr_badge_class(_), do: "badge-error"

  defp elevation_badge_class(nil), do: "badge-ghost"
  defp elevation_badge_class(el) when el >= 5.0, do: "badge-success"
  defp elevation_badge_class(el) when el >= 0.0, do: "badge-warning"
  defp elevation_badge_class(_), do: "badge-error"

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope}>
      <div class="mx-auto max-w-5xl px-4 py-6 space-y-6">
        <header>
          <h1 class="text-2xl font-semibold">EME Calculator</h1>
          <p class="opacity-70 text-sm">
            Earth-Moon-Earth path loss, link budget, and real-time Moon az / el for antenna aiming.
            Moon position updates every 10 seconds; refresh to force an immediate recompute.
          </p>
        </header>

        <form
          phx-submit="calculate"
          phx-change="calculate"
          class="bg-base-200 rounded-box p-4 grid grid-cols-1 md:grid-cols-3 gap-3"
        >
          <label class="form-control md:col-span-3">
            <div class="label">
              <span class="label-text">Station (callsign, grid, or "lat,lon")</span>
            </div>
            <input
              type="text"
              name="source"
              value={@source}
              placeholder="e.g. W5ISP / EM13ng / 32.9,-97.0"
              class="input input-bordered"
              autocomplete="off"
            />
          </label>

          <label class="form-control">
            <div class="label"><span class="label-text">Band (MHz)</span></div>
            <select name="band" class="select select-bordered">
              <%= for {label, value} <- @band_options do %>
                <option value={value} selected={value == @band}>{label}</option>
              <% end %>
            </select>
          </label>

          <label class="form-control">
            <div class="label"><span class="label-text">TX power (dBm)</span></div>
            <input
              type="number"
              name="tx_power_dbm"
              value={@tx_power_dbm}
              step="0.1"
              class="input input-bordered"
            />
          </label>

          <label class="form-control">
            <div class="label"><span class="label-text">Antenna gain (dBi)</span></div>
            <input
              type="number"
              name="tx_gain_dbi"
              value={@tx_gain_dbi}
              step="0.1"
              class="input input-bordered"
            />
          </label>

          <label class="form-control">
            <div class="label"><span class="label-text">Detection BW (Hz)</span></div>
            <input
              type="number"
              name="bandwidth_hz"
              value={@bandwidth_hz}
              step="1"
              class="input input-bordered"
            />
          </label>

          <label class="form-control">
            <div class="label"><span class="label-text">System noise Tsys (K)</span></div>
            <input
              type="number"
              name="t_sys_k"
              value={@t_sys_k}
              step="1"
              class="input input-bordered"
            />
          </label>

          <div class="md:col-span-1 flex items-end">
            <button type="submit" class="btn btn-primary w-full">Calculate</button>
          </div>
        </form>

        <%= if @error do %>
          <div class="alert alert-error">
            <span>{@error}</span>
          </div>
        <% end %>

        <%= if @source_resolved do %>
          <section class="grid grid-cols-1 md:grid-cols-2 gap-4">
            <div class="bg-base-100 border border-base-300 rounded-box p-4">
              <div class="flex items-center justify-between gap-2">
                <h2 class="text-base font-semibold">Antenna aim</h2>
                <span class="text-xs opacity-60 font-mono whitespace-nowrap">
                  {Calendar.strftime(@now_utc, "%Y-%m-%d %H:%M:%SZ")}
                </span>
              </div>
              <dl class="mt-3 text-sm space-y-1.5">
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">Station</dt>
                  <dd class="font-mono text-right">{@source_resolved.label}</dd>
                </div>
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">Lat / lon</dt>
                  <dd class="font-mono text-right">
                    {Float.round(@source_resolved.lat, 3)}, {Float.round(@source_resolved.lon, 3)}
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">Azimuth</dt>
                  <dd class="font-mono text-right">{format_deg(@moon && @moon.azimuth)}</dd>
                </div>
                <div class="flex items-baseline justify-between gap-3 flex-wrap">
                  <dt class="opacity-70 shrink-0">Elevation</dt>
                  <dd class="font-mono text-right flex items-center gap-2 flex-wrap justify-end">
                    <span class={"badge badge-sm whitespace-nowrap " <> elevation_badge_class(@moon && @moon.elevation)}>
                      <%= cond do %>
                        <% is_nil(@moon) -> %>
                          —
                        <% @moon.elevation >= 5.0 -> %>
                          above horizon
                        <% @moon.elevation >= 0.0 -> %>
                          low
                        <% true -> %>
                          below horizon
                      <% end %>
                    </span>
                    <span>{format_deg(@moon && @moon.elevation)}</span>
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">Slant range</dt>
                  <dd class="font-mono text-right">
                    {format_distance_km(@moon && @moon.distance_km)}
                  </dd>
                </div>
              </dl>
            </div>

            <div class="bg-base-100 border border-base-300 rounded-box p-4">
              <h2 class="text-base font-semibold">Link budget (Earth → Moon → Earth)</h2>
              <dl class="mt-3 text-sm space-y-1.5">
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">TX power</dt>
                  <dd class="font-mono text-right">
                    {format_dbm(@link && @link.tx_power_dbm)}
                    <span class="opacity-60 text-xs">
                      ({format_watts(@link && @link.tx_power_w)})
                    </span>
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">Antenna gain</dt>
                  <dd class="font-mono text-right">
                    +{@link && :erlang.float_to_binary(@link.tx_gain_dbi * 1.0, decimals: 1)} dBi
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3 border-t border-base-300 pt-1.5">
                  <dt class="opacity-70 shrink-0">= EIRP</dt>
                  <dd class="font-mono text-right font-semibold">
                    {format_dbm(@link && @link.eirp_dbm)}
                    <span class="opacity-60 text-xs">({format_watts(@link && @link.eirp_w)})</span>
                  </dd>
                </div>

                <div class="flex items-baseline justify-between gap-3 mt-2">
                  <dt class="opacity-70 shrink-0">Free-space spreading (×2)</dt>
                  <dd class="font-mono text-right">
                    {format_db(@link && @link.fspl_round_trip_db)}
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">
                    Moon reflection
                    <span class="text-xs opacity-70">
                      (ρ = {format_albedo(@link && @link.moon_albedo)})
                    </span>
                  </dt>
                  <dd class="font-mono text-right text-success">
                    −{format_db(@link && @link.moon_reflection_gain_db)}
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3 border-t border-base-300 pt-1.5">
                  <dt class="opacity-70 shrink-0">= Round-trip path loss</dt>
                  <dd class="font-mono text-right font-semibold">
                    {format_db(@link && @link.path_loss_db)}
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3 mt-2">
                  <dt class="opacity-70 shrink-0">Received power</dt>
                  <dd class="font-mono text-right">
                    {format_dbm(@link && @link.received_power_dbm)}
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">Noise floor</dt>
                  <dd class="font-mono text-right">{format_dbm(@link && @link.noise_floor_dbm)}</dd>
                </div>
                <div class="flex items-baseline justify-between gap-3">
                  <dt class="opacity-70 shrink-0">Doppler (self-echo)</dt>
                  <dd class="font-mono text-right">
                    {format_doppler(@link && @link.doppler_hz)}
                    <span class="opacity-60 text-xs">
                      ({format_radial_velocity(@link && @link.radial_velocity_km_s)})
                    </span>
                  </dd>
                </div>
                <div class="flex items-baseline justify-between gap-3 flex-wrap border-t border-base-300 pt-1.5">
                  <dt class="opacity-70 shrink-0">SNR</dt>
                  <dd class="font-mono text-right flex items-center gap-2 flex-wrap justify-end">
                    <span class={"badge badge-sm whitespace-nowrap " <> snr_badge_class(@link && @link.snr_db)}>
                      <%= cond do %>
                        <% is_nil(@link) -> %>
                          —
                        <% @link.snr_db >= 10 -> %>
                          strong
                        <% @link.snr_db >= 0 -> %>
                          marginal
                        <% true -> %>
                          below noise
                      <% end %>
                    </span>
                    <span class="font-semibold">{format_db(@link && @link.snr_db)}</span>
                  </dd>
                </div>
              </dl>

              <p class="text-xs opacity-60 mt-3">
                Round-trip path loss = 2 × free-space spreading minus the Moon's
                effective scattering gain <code>10·log(4π σ / λ²)</code>, with σ = ρ·π·R²<sub>moon</sub>.
                Albedo ρ varies by band (≈ 0.065 at VHF, ≈ 0.08 at microwave, dropping above 40 GHz).
                Cable, feed, and polarisation losses are on you.
              </p>
            </div>
          </section>

          <%= if @moon do %>
            <section class="bg-base-100 border border-base-300 rounded-box p-4">
              <div class="flex items-baseline justify-between gap-3 flex-wrap">
                <h2 class="text-base font-semibold">Earth–Moon geometry (not to scale)</h2>
                <span class="text-xs opacity-60">
                  Drag to rotate. Shaded hemisphere is everyone who cannot currently see the Moon — outside your bounce footprint.
                </span>
              </div>
              <div
                id="eme-globe"
                phx-hook="EmeGlobe"
                phx-update="ignore"
                data-station-lat={Float.round(@source_resolved.lat, 4)}
                data-station-lon={Float.round(@source_resolved.lon, 4)}
                data-moon-az={Float.round(@moon.azimuth, 3)}
                data-moon-el={Float.round(@moon.elevation, 3)}
                data-moon-visible={to_string(@moon.elevation >= 0.0)}
                class="mt-3 rounded-box overflow-hidden bg-[#050914]"
                style="min-height: 360px;"
              >
              </div>
              <%= if @moon.elevation < 0.0 do %>
                <p class="text-xs text-warning mt-2">
                  Moon is below your horizon right now — no line-of-sight path. Wait for moonrise.
                </p>
              <% end %>
              <p class="text-[10px] opacity-50 mt-2">
                Earth texture: NASA Blue Marble 2015 (public domain). Moon texture: NASA CGI Moon Kit, LROC colour mosaic (public domain).
              </p>
            </section>
          <% end %>
        <% else %>
          <div class="text-sm opacity-70">
            Enter your callsign or grid above to start the live Moon aim readout.
          </div>
        <% end %>
      </div>
    </Layouts.app>
    """
  end
end
