defmodule MicrowavepropWeb.BeaconLive.Show do
  @moduledoc false
  use MicrowavepropWeb, :live_view

  alias Microwaveprop.Beacons
  alias Microwaveprop.Beacons.Beacon
  alias Microwaveprop.Beacons.RangeEstimate
  alias Microwaveprop.Radio.BandResolver
  alias Microwaveprop.Radio.Maidenhead

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope}>
      <.header>
        {@beacon.callsign}
        <:subtitle>
          {Beacon.format_freq(@beacon.frequency_mhz)} MHz &middot; {@beacon.grid}
          <span :if={not @beacon.approved} class="badge badge-warning badge-sm ml-2">
            Pending approval
          </span>
        </:subtitle>
        <:actions>
          <.button
            :if={admin?(@current_scope) and not @beacon.approved}
            variant="primary"
            phx-click="approve"
          >
            <.icon name="hero-check" /> Approve
          </.button>
          <.button navigate={~p"/beacons"}>
            <.icon name="hero-arrow-left" />
          </.button>
          <.button navigate={path_calc_link(@beacon)}>
            <.icon name="hero-map" /> Plot path to beacon
          </.button>
          <.button
            :if={admin?(@current_scope)}
            variant="primary"
            navigate={~p"/beacons/#{@beacon}/edit?return_to=show"}
          >
            <.icon name="hero-pencil-square" /> Edit
          </.button>
        </:actions>
      </.header>

      <div
        id={"beacon-map-" <> @beacon.id}
        phx-hook="BeaconMap"
        phx-update="ignore"
        class="h-96 w-full rounded-lg border border-base-300 mb-2"
        data-lat={@beacon.lat}
        data-lon={@beacon.lon}
        data-label={"#{@beacon.callsign} · #{Beacon.format_freq(@beacon.frequency_mhz)} MHz"}
        data-on-the-air={to_string(@beacon.on_the_air)}
      >
      </div>

      <div class="mb-2 flex items-center justify-between flex-wrap gap-2">
        <label class={[
          "label gap-2 py-0",
          if(@coverage_supported, do: "cursor-pointer", else: "cursor-not-allowed opacity-60")
        ]}>
          <input
            type="checkbox"
            class="toggle toggle-sm toggle-primary"
            checked={@show_coverage}
            disabled={not @coverage_supported}
            phx-click={if @coverage_supported, do: "toggle_coverage"}
          />
          <span class="label-text text-sm">Show estimated current coverage</span>
          <span :if={not @coverage_supported} class="label-text-alt text-xs opacity-70">
            (5.76 GHz and up only — atmospheric loss dominates on microwave bands)
          </span>
        </label>
      </div>

      <div
        :if={@show_coverage && @estimate}
        class="text-xs opacity-70 mb-2 flex flex-wrap items-center gap-3"
      >
        <span>
          Band <strong>{@estimate.band_label}</strong> &middot;
          EIRP <strong>{@estimate.eirp_dbm} dBm</strong> &middot;
          Atm loss <strong>{@estimate.atm_per_km} dB/km</strong>
        </span>
        <span>
          Beacon grid score <strong>{@estimate.center_score}</strong>
          <%= if @estimate.valid_time do %>
            at {Calendar.strftime(@estimate.valid_time, "%Y-%m-%d %H:%M UTC")}
          <% else %>
            (no HRRR data — defaulting cells to 50)
          <% end %>
          &middot; <strong>{length(@estimate.cells)}</strong>
          cells rendered
        </span>
      </div>

      <div :if={@show_coverage && @estimate} class="flex flex-wrap gap-3 text-xs mb-4">
        <%= for tier <- @estimate.tiers do %>
          <div class="flex items-center gap-1.5">
            <span
              class="inline-block w-3 h-3 rounded-sm border"
              style={"background-color: #{tier.color}; border-color: #{tier.color}"}
            >
            </span>
            <span>
              <strong>{tier.label}</strong> (≥ {tier.min_dbm} dBm)
            </span>
          </div>
        <% end %>
      </div>

      <div class="card card-border bg-base-200/40">
        <div class="card-body p-3 text-sm">
          <dl class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-x-4 gap-y-2">
            <.stat_field label="Frequency">
              {Beacon.format_freq(@beacon.frequency_mhz)} MHz
            </.stat_field>
            <.stat_field label="Callsign">{@beacon.callsign}</.stat_field>
            <.stat_field label="Keying">{Beacon.keying_label(@beacon.keying)}</.stat_field>
            <.stat_field label="On the air">
              <span class={[
                "badge badge-sm",
                (@beacon.on_the_air && "badge-success") || "badge-ghost"
              ]}>
                {if @beacon.on_the_air, do: "Yes", else: "No"}
              </span>
            </.stat_field>

            <.stat_field label="Grid">{@beacon.grid}</.stat_field>
            <.stat_field label="Latitude">{format_coord(@beacon.lat)}</.stat_field>
            <.stat_field label="Longitude">{format_coord(@beacon.lon)}</.stat_field>
            <.stat_field label="EIRP">{Beacon.format_mw(@beacon.power_mw)} mW</.stat_field>

            <.stat_field label="Height AGL">{@beacon.height_ft} ft</.stat_field>
            <.stat_field label="Bearing">{bearing_label(@beacon.bearing)}</.stat_field>
            <.stat_field :if={@beacon.beamwidth_deg} label="Beamwidth">
              {Beacon.format_mw(@beacon.beamwidth_deg)}°
            </.stat_field>
          </dl>

          <div :if={@beacon.notes && @beacon.notes != ""} class="mt-3 pt-3 border-t border-base-300">
            <div class="font-semibold text-[11px] uppercase tracking-wider opacity-60 mb-1">
              Notes
            </div>
            <div class="whitespace-pre-wrap text-sm">{@beacon.notes}</div>
          </div>

          <div :if={@beacon.user && @beacon.user.callsign} class="mt-3 pt-3 border-t border-base-300">
            <span class="text-xs opacity-60">Submitted by</span>
            <span class="text-sm font-semibold">{@beacon.user.callsign}</span>
          </div>
        </div>
      </div>
    </Layouts.app>
    """
  end

  @impl true
  def mount(%{"id" => id}, _session, socket) do
    if connected?(socket), do: Beacons.subscribe_beacons()

    beacon = Beacons.get_beacon!(id)

    {:ok,
     socket
     |> assign(:page_title, "Beacon")
     |> assign(:beacon, beacon)
     |> assign(:show_coverage, false)
     |> assign(:coverage_supported, coverage_supported?(beacon))
     # Estimate is lazy — computed on the first toggle-on to avoid
     # paying the bbox grid cost on every page load.
     |> assign(:estimate, nil)}
  end

  # Estimated current coverage only makes sense from 5.76 GHz upward,
  # where atmospheric attenuation meaningfully shapes the reach. Below
  # that the free-space-loss-dominated map is misleading.
  defp coverage_supported?(%{frequency_mhz: mhz}) when is_number(mhz), do: mhz >= 5760
  defp coverage_supported?(_), do: false

  @impl true
  def handle_event("toggle_coverage", _params, socket) do
    cond do
      not socket.assigns.coverage_supported ->
        {:noreply, socket}

      socket.assigns.show_coverage ->
        # Already on, user is turning it off.
        {:noreply,
         socket
         |> assign(:show_coverage, false)
         |> push_event("toggle_coverage", %{show: false})}

      true ->
        # First time turning on (or re-enabling after off) — compute
        # the estimate if we don't already have it, then push the
        # cell payload to the hook.
        estimate = socket.assigns.estimate || RangeEstimate.estimate(socket.assigns.beacon)

        {:noreply,
         socket
         |> assign(:show_coverage, true)
         |> assign(:estimate, estimate)
         |> push_event("load_coverage", %{
           grid_step: estimate.grid_step,
           cells: estimate.cells,
           show: true
         })}
    end
  end

  def handle_event("approve", _params, socket) do
    if admin?(socket.assigns.current_scope) do
      {:ok, approved} = Beacons.approve_beacon(socket.assigns.beacon)

      {:noreply,
       socket
       |> assign(:beacon, approved)
       |> put_flash(:info, "Beacon approved.")}
    else
      {:noreply, put_flash(socket, :error, "Admins only.")}
    end
  end

  @impl true
  def handle_info({:updated, %Beacon{id: id} = beacon}, %{assigns: %{beacon: %{id: id}}} = socket) do
    supported = coverage_supported?(beacon)
    show? = socket.assigns.show_coverage and supported

    # Only recompute the estimate if coverage was already displayed —
    # otherwise leave it nil and let the next toggle lazy-load it.
    estimate =
      if show? and socket.assigns.estimate do
        RangeEstimate.estimate(beacon)
      else
        socket.assigns.estimate
      end

    socket =
      socket
      |> assign(:beacon, beacon)
      |> assign(:coverage_supported, supported)
      |> assign(:show_coverage, show?)
      |> assign(:estimate, estimate)

    # Re-push fresh cells to the hook if the map is currently visible.
    if show? and estimate do
      {:noreply,
       push_event(socket, "load_coverage", %{
         grid_step: estimate.grid_step,
         cells: estimate.cells,
         show: true
       })}
    else
      {:noreply, socket}
    end
  end

  def handle_info({:deleted, %Beacon{id: id}}, %{assigns: %{beacon: %{id: id}}} = socket) do
    {:noreply,
     socket
     |> put_flash(:error, "This beacon was deleted.")
     |> push_navigate(to: ~p"/beacons")}
  end

  def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do
    {:noreply, socket}
  end

  defp admin?(%{user: %{is_admin: true}}), do: true
  defp admin?(_), do: false

  defp bearing_label(nil), do: "Omni"
  defp bearing_label("omni"), do: "Omni"
  defp bearing_label(value), do: "#{value}°"

  defp format_coord(value) when is_float(value), do: :erlang.float_to_binary(value, decimals: 6)
  defp format_coord(value), do: to_string(value)

  defp path_calc_link(%Beacon{} = beacon) do
    base = %{
      "destination" => precise_grid(beacon),
      "dst_height_ft" => to_string(beacon.height_ft)
    }

    {tx_power_dbm, src_gain_dbi} = baseline_tx_for_eirp(beacon.power_mw)

    params =
      base
      |> maybe_put("band", BandResolver.resolve_as_string(beacon.frequency_mhz))
      |> maybe_put("tx_power_dbm", tx_power_dbm)
      |> maybe_put("src_gain_dbi", src_gain_dbi)

    "/path?" <> URI.encode_query(params)
  end

  # Splits the beacon's EIRP (the beacon schema's `power_mw` field is
  # already an EIRP value, per Beacons.RangeEstimate) into a
  # tx_power_dbm + src_gain_dbi pair so the path calculator's TX chain
  # emits the same EIRP as the beacon. We default the antenna gain to
  # 30 dBi (typical 2-3 ft microwave dish) and derive the TX power from
  # there, clamping to a 0 dBm / 1 mW minimum so very-low-EIRP beacons
  # produce sane baseline values instead of -10 dBm absurdities.
  defp baseline_tx_for_eirp(nil), do: {nil, nil}
  defp baseline_tx_for_eirp(power_mw) when power_mw <= 0, do: {nil, nil}

  defp baseline_tx_for_eirp(power_mw) do
    eirp_dbm = 10.0 * :math.log10(power_mw)
    default_gain = 30.0
    min_tx_dbm = 0.0

    tx_dbm = max(min_tx_dbm, eirp_dbm - default_gain)
    gain_dbi = eirp_dbm - tx_dbm

    {format_dbm(tx_dbm), format_dbi(gain_dbi)}
  end

  defp format_dbm(v), do: v |> Float.round(1) |> to_string()
  defp format_dbi(v), do: v |> Float.round(1) |> to_string()

  defp precise_grid(%Beacon{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
    Maidenhead.from_latlon(lat, lon, 8)
  end

  defp precise_grid(%Beacon{grid: grid}), do: grid

  defp maybe_put(map, _key, nil), do: map
  defp maybe_put(map, key, value), do: Map.put(map, key, value)

  attr :label, :string, required: true
  slot :inner_block, required: true

  defp stat_field(assigns) do
    ~H"""
    <div class="min-w-0">
      <dt class="font-semibold text-[11px] uppercase tracking-wider opacity-60">
        {@label}
      </dt>
      <dd class="truncate">{render_slot(@inner_block)}</dd>
    </div>
    """
  end
end
