defmodule MicrowavepropWeb.WeatherMapLive do
  @moduledoc "`/weather` — HRRR-derived forecast fields (temp, Td, wind, refractivity) on a map."
  use MicrowavepropWeb, :live_view

  alias Microwaveprop.Weather
  alias Microwaveprop.Weather.MapLayers

  @layers MapLayers.all()
  @default_layer MapLayers.default_id()

  # Default map viewport. Used when no `lat`/`lon`/`zoom` URL params are
  # provided (the deep-link case). Centered on DFW (the project's home
  # area) at z=7 — same defaults the JS hook used to hardcode.
  @default_center_lat 32.897
  @default_center_lon -97.038
  @default_zoom 7
  @min_zoom 4
  @max_zoom 10

  @impl true
  def mount(params, _session, socket) do
    _ =
      if connected?(socket) do
        :ok = Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "weather:updated")
        Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:pipeline")
      end

    # Default the timeline cursor to the valid_time closest to "now" so
    # users land on current conditions, not a +11h forecast hour (which
    # the underlying cache can advance to while the worker is walking
    # f00→f18).
    #
    # Mount stays cheap — no grid materialisation. The dead render
    # would otherwise spend ~5–10 s materialising 92k cells through
    # `WeatherLayers.derive`, blocking the HTTP response and tripping
    # the LiveView socket join timeout. The client now paints weather
    # through a tile endpoint, so mount only needs the selected layer
    # and valid_time metadata.
    valid_times = recent_valid_times(Weather.available_weather_valid_times())
    initial_vt = pick_initial_valid_time(valid_times)

    {lat, lon, zoom} = parse_viewport_params(params)

    {:ok,
     assign(socket,
       page_title: "Weather Map",
       layers: @layers,
       selected_layer: @default_layer,
       valid_time: initial_vt,
       valid_times: valid_times,
       initial_valid_times_json: Jason.encode!(Enum.map(valid_times, &DateTime.to_iso8601/1)),
       initial_selected_time: initial_vt && DateTime.to_iso8601(initial_vt),
       initial_utc_clock: Calendar.strftime(DateTime.utc_now(), "%H:%M UTC"),
       initial_center_lat: lat,
       initial_center_lon: lon,
       initial_zoom: zoom,
       current_lat: lat,
       current_lon: lon,
       current_zoom: zoom,
       grid_visible: false,
       radar_visible: false
     )}
  end

  @impl true
  def handle_params(params, _uri, socket) do
    selected = pick_layer(params["layer"], socket.assigns[:selected_layer])

    # Update current viewport so subsequent push_patch URLs combine
    # `layer=` with whatever lat/lon/zoom the user has navigated to.
    # The map hook is `phx-update="ignore"` so re-rendering with new
    # data attributes won't re-init Leaflet.
    {lat, lon, zoom} = parse_viewport_params(params)

    {:noreply,
     socket
     |> assign(:selected_layer, selected)
     |> assign(:current_lat, lat)
     |> assign(:current_lon, lon)
     |> assign(:current_zoom, zoom)}
  end

  defp pick_layer(layer, current) when is_binary(layer) do
    if MapLayers.valid_id?(layer), do: layer, else: pick_layer(nil, current)
  end

  defp pick_layer(_layer, current) when is_binary(current), do: current
  defp pick_layer(_layer, _current), do: @default_layer

  # Parse `lat` / `lon` / `zoom` query params, falling back to defaults
  # when missing or out of range. Lat in [-90, 90], lon in [-180, 180],
  # zoom in [@min_zoom, @max_zoom]. A bad single param doesn't poison
  # the others — each clamps independently.
  defp parse_viewport_params(params) do
    lat = parse_float_param(params["lat"], -90.0, 90.0, @default_center_lat)
    lon = parse_float_param(params["lon"], -180.0, 180.0, @default_center_lon)

    zoom =
      case Integer.parse(params["zoom"] || "") do
        {z, ""} when z >= @min_zoom and z <= @max_zoom -> z
        _ -> @default_zoom
      end

    {lat, lon, zoom}
  end

  defp parse_float_param(value, min, max, default) when is_binary(value) do
    case Float.parse(value) do
      {f, ""} when f >= min and f <= max -> f
      _ -> default
    end
  end

  defp parse_float_param(_value, _min, _max, default), do: default

  @doc false
  # Public for direct unit testing: anchoring an integration test on a
  # truncated `utc_now()` would only assert the new behavior past minute
  # 30 of the wall clock.
  def pick_initial_valid_time(valid_times, now \\ DateTime.utc_now())
  def pick_initial_valid_time([], _now), do: nil

  def pick_initial_valid_time(valid_times, now) do
    # Prefer the most recent valid_time that is <= now (the "current
    # hour" — what HRRR analysis just delivered and what the HRDPS chain
    # seeds when fired this hour). Fall back to the closest absolute
    # match only when nothing on disk is yet at-or-before now.
    past_or_equal =
      Enum.filter(valid_times, fn vt -> DateTime.compare(vt, now) != :gt end)

    case past_or_equal do
      [] -> Enum.min_by(valid_times, fn vt -> abs(DateTime.diff(vt, now, :second)) end)
      list -> Enum.max_by(list, &DateTime.to_unix/1)
    end
  end

  # Drop valid_times older than one hour ago. The HRRR scalar cache can
  # hold the previous run's forecast hours past their useful life;
  # showing 3-hour-old "current conditions" on the map is more
  # misleading than helpful.
  defp recent_valid_times(valid_times) do
    cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
    Enum.filter(valid_times, fn vt -> DateTime.compare(vt, cutoff) != :lt end)
  end

  # Build a `/weather?…` path that preserves the current
  # layer + lat/lon/zoom. `overrides` lets a specific event (e.g.
  # `select_layer`) tweak one field without losing the others.
  defp build_path(socket, overrides \\ []) do
    layer = Keyword.get(overrides, :layer, socket.assigns.selected_layer)
    lat = Keyword.get(overrides, :lat, socket.assigns.current_lat)
    lon = Keyword.get(overrides, :lon, socket.assigns.current_lon)
    zoom = Keyword.get(overrides, :zoom, socket.assigns.current_zoom)

    # List form (not map) so the param order is stable — tests
    # asserting on the exact URL, and humans reading shareable links,
    # both benefit from layer first then viewport.
    query =
      URI.encode_query([
        {"layer", layer},
        {"lat", lat},
        {"lon", lon},
        {"zoom", zoom}
      ])

    "/weather?#{query}"
  end

  @impl true
  def handle_event("select_layer", %{"layer" => layer_id}, socket) do
    if MapLayers.valid_id?(layer_id) do
      {:noreply, push_patch(socket, to: build_path(socket, layer: layer_id))}
    else
      {:noreply, socket}
    end
  end

  # Debounced moveend/zoomend from the JS hook. Every viewport change
  # patches the URL so a refresh / share-link lands the user back at
  # the same view. Stays out of the LiveView assigns hot path —
  # `phx-update="ignore"` on the map div means the hook isn't re-run.
  def handle_event("viewport_changed", %{"lat" => lat, "lon" => lon, "zoom" => zoom}, socket)
      when is_number(lat) and is_number(lon) and is_number(zoom) do
    new_lat = lat |> max(-90.0) |> min(90.0) |> Float.round(4)
    new_lon = lon |> max(-180.0) |> min(180.0) |> Float.round(4)
    new_zoom = zoom |> max(@min_zoom) |> min(@max_zoom) |> trunc()

    socket =
      socket
      |> assign(:current_lat, new_lat)
      |> assign(:current_lon, new_lon)
      |> assign(:current_zoom, new_zoom)

    {:noreply, push_patch(socket, to: build_path(socket), replace: true)}
  end

  def handle_event("viewport_changed", _params, socket), do: {:noreply, socket}

  def handle_event("toggle_grid", _params, socket) do
    visible = !socket.assigns.grid_visible

    socket =
      socket
      |> assign(:grid_visible, visible)
      |> push_event("toggle_grid", %{visible: visible})

    {:noreply, socket}
  end

  def handle_event("toggle_radar", _params, socket) do
    visible = !socket.assigns.radar_visible

    socket =
      socket
      |> assign(:radar_visible, visible)
      |> push_event("toggle_radar", %{visible: visible})

    {:noreply, socket}
  end

  def handle_event("select_time", %{"time" => time_str}, socket) do
    with {:ok, time, _} <- DateTime.from_iso8601(time_str),
         true <- time in socket.assigns.valid_times do
      socket =
        socket
        |> assign(:valid_time, time)
        |> push_event("update_weather_overlay", %{selected: DateTime.to_iso8601(time)})

      {:noreply, socket}
    else
      _ -> {:noreply, socket}
    end
  end

  # `/map`'s PropagationMap hook pushes `map_bounds` on moveend/zoomend.
  # When a user navigates /map → /weather, an in-flight bounds push from
  # the old hook can land on the new WeatherMapLive (LV reuses sockets
  # across live navigation). WeatherMapLive doesn't need bounds — the
  # /weather/cells HTTP endpoint computes them client-side — so swallow
  # the event instead of crashing the LV process.
  def handle_event("map_bounds", _params, socket), do: {:noreply, socket}

  def handle_event("point_detail", %{"lat" => lat, "lon" => lon}, socket) do
    detail =
      if socket.assigns.valid_time do
        Weather.weather_point_detail(lat, lon, socket.assigns.valid_time)
      end

    payload = detail || %{}
    {:noreply, push_event(socket, "point_detail", payload)}
  end

  @impl true
  def handle_info({:weather_updated, _valid_time}, socket) do
    valid_times = recent_valid_times(Weather.available_weather_valid_times())

    # Keep whatever the user has scrubbed to if it's still available,
    # otherwise fall back to the latest on-disk time (GridCache-backed
    # hot path). Never block mount/refresh on a ProfilesFile read for
    # the happy path.
    selected =
      cond do
        socket.assigns.valid_time in valid_times -> socket.assigns.valid_time
        valid_times != [] -> List.last(valid_times)
        true -> nil
      end

    socket =
      socket
      |> assign(:valid_times, valid_times)
      |> assign(:valid_time, selected)
      |> push_event("update_weather_overlay", %{selected: selected && DateTime.to_iso8601(selected)})
      |> push_event("update_timeline", %{
        times: Enum.map(valid_times, &DateTime.to_iso8601/1),
        selected: selected && DateTime.to_iso8601(selected)
      })

    {:noreply, socket}
  end

  # A new forecast hour has landed. Re-enumerate the available times
  # so the timeline picks up the extra hour, but keep the currently
  # selected time where it is.
  def handle_info({:propagation_pipeline_progress, _progress}, socket) do
    valid_times = recent_valid_times(Weather.available_weather_valid_times())

    if valid_times == socket.assigns.valid_times do
      {:noreply, socket}
    else
      socket =
        socket
        |> assign(:valid_times, valid_times)
        |> push_event("update_timeline", %{
          times: Enum.map(valid_times, &DateTime.to_iso8601/1),
          selected: socket.assigns.valid_time && DateTime.to_iso8601(socket.assigns.valid_time)
        })

      {:noreply, socket}
    end
  end

  @group_order ["Surface", "Upper Air", "Ducting"]

  defp group_layers(layers) do
    layers
    |> Enum.group_by(& &1.group)
    |> Enum.sort_by(fn {group, _} -> Enum.find_index(@group_order, &(&1 == group)) || 99 end)
  end

  defp layer_description(layers, selected_id) do
    case Enum.find(layers, &(&1.id == selected_id)) do
      %{desc: desc} -> desc
      _ -> nil
    end
  end

  defp selected_layer_label(layers, selected_id) do
    case Enum.find(layers, &(&1.id == selected_id)) do
      %{label: label} -> label
      _ -> ""
    end
  end

  defp selected_layer_group_label(layers, selected_id) do
    case Enum.find(layers, &(&1.id == selected_id)) do
      %{group: group} -> group
      _ -> ""
    end
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="flex w-screen h-screen overflow-hidden">
      <%!-- Map container --%>
      <div class="relative flex-1 min-w-0">
        <%!-- Full-page map --%>
        <div
          id="weather-map"
          phx-hook="WeatherMap"
          phx-update="ignore"
          data-layers={Jason.encode!(@layers)}
          data-selected-layer={@selected_layer}
          data-valid-times={@initial_valid_times_json}
          data-selected-time={@initial_selected_time}
          data-initial-lat={@initial_center_lat}
          data-initial-lon={@initial_center_lon}
          data-initial-zoom={@initial_zoom}
          class="absolute inset-0 z-0"
        >
        </div>

        <%!-- Bottom forecast timeline --%>
        <div
          id="weather-forecast-timeline"
          phx-update="ignore"
          class="absolute bottom-2 left-1/2 -translate-x-1/2 z-[1000] max-w-[calc(100vw-1rem)] md:bottom-4"
        >
        </div>

        <%!-- Mobile-only floating controls --%>
        <div
          id="weather-controls"
          class="md:hidden absolute top-2 left-12 z-[1000] flex flex-col gap-2 max-w-[calc(100vw-4rem)]"
        >
          <div
            data-theme="dark"
            class="bg-neutral text-neutral-content shadow rounded-box border border-base-300 p-2 flex flex-col gap-2"
          >
            <div class="font-bold text-sm leading-tight px-1 flex items-center justify-between gap-2">
              <div class="min-w-0">
                <span>NTMS</span>
                <div class="font-normal text-xs opacity-70">Weather Map</div>
                <div class="font-normal text-[10px] opacity-60">
                  <%= if @valid_time do %>
                    {Calendar.strftime(@valid_time, "%Y-%m-%d %H:%M UTC")}
                  <% else %>
                    No data available
                  <% end %>
                </div>
              </div>
              <div class="flex items-center gap-1.5 shrink-0">
                <div
                  id="weather-utc-clock-mobile"
                  phx-hook="UtcClock"
                  phx-update="ignore"
                  class="font-mono text-xs opacity-70 tabular-nums"
                >
                  {@initial_utc_clock}
                </div>
                <button
                  id="weather-mobile-panel-toggle"
                  class="btn btn-xs btn-square btn-ghost"
                  onclick="document.getElementById('weather-panel-extras').classList.toggle('hidden')"
                >
                  <.icon name="hero-bars-3" class="size-4" />
                </button>
              </div>
            </div>

            <div id="weather-panel-extras" class="hidden flex-col gap-2">
              <%!-- Layer pill buttons grouped --%>
              <div class="flex flex-col gap-1.5">
                <div :for={{group, layers} <- group_layers(@layers)}>
                  <div class="text-[10px] font-semibold opacity-50 uppercase tracking-wider px-1 mb-0.5">
                    {group}
                  </div>
                  <div class="flex flex-wrap gap-1">
                    <button
                      :for={layer <- layers}
                      phx-click="select_layer"
                      phx-value-layer={layer.id}
                      class={[
                        "btn btn-xs rounded-full",
                        if(@selected_layer == layer.id, do: "btn-primary", else: "btn-ghost")
                      ]}
                    >
                      {layer.label}
                    </button>
                  </div>
                </div>
              </div>

              <div class="text-[11px] opacity-70 px-1 leading-snug">
                {layer_description(@layers, @selected_layer)}
              </div>

              <div class="flex flex-col gap-1 border-t border-base-300 pt-2">
                <label class="flex items-center gap-2 cursor-pointer px-1">
                  <input
                    type="checkbox"
                    class="toggle toggle-sm toggle-primary"
                    checked={@grid_visible}
                    phx-click="toggle_grid"
                  />
                  <span class="text-sm">Grid squares</span>
                </label>
                <label class="flex items-center gap-2 cursor-pointer px-1">
                  <input
                    type="checkbox"
                    class="toggle toggle-sm toggle-primary"
                    checked={@radar_visible}
                    phx-click="toggle_radar"
                  />
                  <span class="text-sm">Weather radar</span>
                </label>
              </div>

              <div class="flex flex-col gap-1 border-t border-base-300 pt-2">
                <.link navigate="/map" class="btn btn-xs btn-ghost justify-start">
                  Propagation Map
                </.link>
                <.link navigate="/path" class="btn btn-xs btn-ghost justify-start">
                  Path Calculator
                </.link>
                <.link navigate="/eme" class="btn btn-xs btn-ghost justify-start">
                  EME
                </.link>
                <.link navigate="/beacons" class="btn btn-xs btn-ghost justify-start">
                  Beacons
                </.link>
                <.link navigate="/submit" class="btn btn-xs btn-ghost justify-start">
                  Submit a Contact
                </.link>
                <.link navigate="/contacts" class="btn btn-xs btn-ghost justify-start">
                  Contact Training Data
                </.link>
                <.link navigate="/contacts/map" class="btn btn-xs btn-ghost justify-start">
                  Contact Map
                </.link>
                <.link navigate="/algo" class="btn btn-xs btn-ghost justify-start">
                  Scoring Algorithm
                </.link>
                <.link navigate="/about" class="btn btn-xs btn-ghost justify-start">
                  About
                </.link>
              </div>
            </div>
          </div>
        </div>

        <%!-- Point detail panel --%>
        <div
          id="weather-detail-panel"
          phx-update="ignore"
          class={[
            "bg-neutral text-neutral-content shadow-lg border border-base-300 overflow-hidden overflow-y-auto z-[1001]",
            "fixed bottom-0 left-0 right-0 max-h-[50vh] rounded-t-2xl",
            "md:absolute md:top-auto md:right-auto md:bottom-4 md:left-3 md:max-h-[70vh] md:w-80 md:rounded-box"
          ]}
          style="display:none;"
        >
        </div>

        <%!-- Sidebar expand button (visible when sidebar is collapsed) --%>
        <button
          id="weather-sidebar-expand"
          class="hidden md:hidden absolute top-3 right-3 z-[1000] btn btn-sm btn-neutral shadow-lg"
          onclick="document.getElementById('weather-sidebar').style.display='flex';this.style.display='none';window.dispatchEvent(new Event('sidebar-toggle'));"
        >
          <.icon name="hero-bars-3" class="size-4" />
        </button>
      </div>

      <%!-- Desktop right sidebar --%>
      <div
        id="weather-sidebar"
        data-theme="dark"
        class="hidden md:flex flex-col w-56 shrink-0 h-full bg-neutral text-neutral-content border-l border-base-300 overflow-hidden"
      >
        <div class="p-3 flex flex-col gap-2 shrink-0 overflow-y-auto flex-1">
          <div class="font-bold text-sm leading-tight px-1 flex items-center justify-between gap-2">
            <div class="min-w-0">
              <span>NTMS</span>
              <div class="font-normal text-xs opacity-70">Weather Map</div>
              <div class="font-normal text-[10px] opacity-60">
                <%= if @valid_time do %>
                  {Calendar.strftime(@valid_time, "%Y-%m-%d %H:%M UTC")}
                <% else %>
                  No data available
                <% end %>
              </div>
            </div>
            <div class="flex items-center gap-1.5 shrink-0">
              <div
                id="weather-utc-clock-desktop"
                phx-hook="UtcClock"
                phx-update="ignore"
                class="font-mono text-xs opacity-70 tabular-nums"
              >
                {@initial_utc_clock}
              </div>
              <button
                id="weather-sidebar-collapse"
                class="btn btn-xs btn-square btn-ghost"
                title="Collapse sidebar"
                onclick="document.getElementById('weather-sidebar').style.display='none';document.getElementById('weather-sidebar-expand').style.display='flex';window.dispatchEvent(new Event('sidebar-toggle'));"
              >
                <.icon name="hero-chevron-right" class="size-4" />
              </button>
            </div>
          </div>

          <%!-- Layer pill buttons grouped --%>
          <div class="flex flex-col gap-1.5">
            <div :for={{group, layers} <- group_layers(@layers)}>
              <div class="text-[10px] font-semibold opacity-50 uppercase tracking-wider px-1 mb-0.5">
                {group}
              </div>
              <div class="flex flex-wrap gap-1">
                <button
                  :for={layer <- layers}
                  phx-click="select_layer"
                  phx-value-layer={layer.id}
                  class={[
                    "btn btn-xs rounded-full",
                    if(@selected_layer == layer.id, do: "btn-primary", else: "btn-ghost")
                  ]}
                >
                  {layer.label}
                </button>
              </div>
            </div>
          </div>

          <%!-- Layer description (group · name header + body) --%>
          <div class="px-1">
            <div class="text-[10px] font-semibold uppercase tracking-wider opacity-60 mb-1">
              {selected_layer_group_label(@layers, @selected_layer)} &middot; {selected_layer_label(
                @layers,
                @selected_layer
              )}
            </div>
            <div class="text-[11px] opacity-70 leading-snug">
              {layer_description(@layers, @selected_layer)}
            </div>
          </div>

          <%!-- Overlay toggles --%>
          <div class="flex flex-col gap-1 border-t border-base-300 pt-2">
            <label class="flex items-center gap-2 cursor-pointer px-1">
              <input
                type="checkbox"
                class="toggle toggle-sm toggle-primary"
                checked={@grid_visible}
                phx-click="toggle_grid"
              />
              <span class="text-sm">Grid squares</span>
            </label>
            <label class="flex items-center gap-2 cursor-pointer px-1">
              <input
                type="checkbox"
                class="toggle toggle-sm toggle-primary"
                checked={@radar_visible}
                phx-click="toggle_radar"
              />
              <span class="text-sm">Weather radar</span>
            </label>
          </div>

          <%!-- Navigation --%>
          <ul class="menu menu-xs border-t border-base-300 pt-2 px-0">
            <li>
              <.link navigate="/map">Propagation Map</.link>
            </li>
            <li>
              <.link navigate="/path">Path Calculator</.link>
            </li>
            <li>
              <.link navigate="/eme">EME</.link>
            </li>
            <li>
              <.link navigate="/beacons">Beacons</.link>
            </li>
            <li>
              <.link navigate="/submit">Submit a Contact</.link>
            </li>
            <li>
              <.link navigate="/contacts">Contact Training Data</.link>
            </li>
            <li>
              <.link navigate="/contacts/map">Contact Map</.link>
            </li>
            <li>
              <.link navigate="/algo">Scoring Algorithm</.link>
            </li>
            <li>
              <.link navigate="/about">About</.link>
            </li>
          </ul>

          <%!-- Auth --%>
          <ul class="menu menu-xs border-t border-base-300 pt-2 px-0">
            <%= if @current_scope && @current_scope.user do %>
              <li>
                <.link navigate={~p"/u/#{@current_scope.user.callsign}"} class="font-semibold">
                  {@current_scope.user.callsign}
                </.link>
              </li>
              <li>
                <.link href={~p"/users/settings"}>Settings</.link>
              </li>
              <li>
                <.link href={~p"/users/log-out"} method="delete">Log out</.link>
              </li>
            <% else %>
              <li>
                <.link href={~p"/users/register"}>Register</.link>
              </li>
              <li>
                <.link href={~p"/users/log-in"}>Log in</.link>
              </li>
            <% end %>
          </ul>
        </div>
      </div>
    </div>

    <Layouts.flash_group flash={@flash} />
    """
  end
end
