defmodule MicrowavepropWeb.RoverLive do
  @moduledoc """
  Rover planner: enter stationary stations you want to work, select a band,
  and the map shows the HRRR propagation heatmap with your stations plotted.
  """
  use MicrowavepropWeb, :live_view

  alias Microwaveprop.Propagation
  alias Microwaveprop.Propagation.BandConfig
  alias Microwaveprop.Radio.CallsignClient
  alias Microwaveprop.Radio.Maidenhead

  @band_options BandConfig.band_options()

  @default_band 10_000
  @initial_bounds %{
    "south" => 29.5,
    "north" => 36.3,
    "west" => -101.5,
    "east" => -92.5
  }

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")
    end

    initial_scores = Propagation.latest_scores(@default_band, @initial_bounds)

    {:ok,
     assign(socket,
       page_title: "Rover Planner",
       band_options: @band_options,
       band: @default_band,
       stations: [],
       station_input: "",
       resolving: false,
       url_loaded: false,
       initial_scores_json: Jason.encode!(initial_scores),
       bounds: @initial_bounds
     )}
  end

  @impl true
  def handle_params(params, _uri, socket) do
    if socket.assigns.url_loaded do
      {:noreply, socket}
    else
      band = if params["band"], do: String.to_integer(params["band"]), else: socket.assigns.band

      socket = assign(socket, band: band, url_loaded: true)

      # Load stations from URL: ?stations=W5ISP,K5TR,EM00cd
      case params["stations"] do
        nil ->
          {:noreply, socket}

        stations_str ->
          calls = stations_str |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == ""))
          send(self(), {:resolve_station_list, calls})
          {:noreply, assign(socket, resolving: true)}
      end
    end
  end

  # ── Events ──

  @impl true
  def handle_event("add_station", %{"callsign" => input}, socket) do
    input = String.trim(input)

    if input == "" do
      {:noreply, socket}
    else
      send(self(), {:resolve_station, input})
      {:noreply, assign(socket, resolving: true, station_input: "")}
    end
  end

  def handle_event("remove_station", %{"index" => idx_str}, socket) do
    idx = String.to_integer(idx_str)
    stations = List.delete_at(socket.assigns.stations, idx)
    socket = assign(socket, stations: stations)
    socket = push_event(socket, "stations_updated", %{stations: station_data(stations)})
    {:noreply, push_url(socket)}
  end

  def handle_event("select_band", %{"band" => band_str}, socket) do
    band = String.to_integer(band_str)
    scores = Propagation.latest_scores(band, socket.assigns.bounds)

    socket =
      socket
      |> assign(band: band)
      |> push_event("update_scores", %{scores: scores})

    {:noreply, push_url(socket)}
  end

  def handle_event("map_bounds", bounds, socket) do
    scores = Propagation.latest_scores(socket.assigns.band, bounds)

    socket =
      socket
      |> assign(:bounds, bounds)
      |> push_event("update_scores", %{scores: scores})

    {:noreply, socket}
  end

  # ── Async ──

  def handle_info({:resolve_station_list, calls}, socket) do
    stations =
      calls
      |> Task.async_stream(&resolve_station/1, max_concurrency: 4, timeout: 10_000)
      |> Enum.flat_map(fn
        {:ok, {:ok, s}} -> [s]
        _ -> []
      end)

    socket =
      socket
      |> assign(stations: stations, resolving: false)
      |> push_event("stations_updated", %{stations: station_data(stations)})

    {:noreply, socket}
  end

  @impl true
  def handle_info({:resolve_station, input}, socket) do
    case resolve_station(input) do
      {:ok, station} ->
        stations = socket.assigns.stations ++ [station]
        socket = assign(socket, stations: stations, resolving: false)
        socket = push_event(socket, "stations_updated", %{stations: station_data(stations)})
        {:noreply, push_url(socket)}

      {:error, _reason} ->
        {:noreply, socket |> assign(resolving: false) |> put_flash(:error, "Could not find: #{input}")}
    end
  end

  def handle_info({:propagation_updated, _valid_times}, socket) do
    scores = Propagation.latest_scores(socket.assigns.band, socket.assigns.bounds)
    {:noreply, push_event(socket, "update_scores", %{scores: scores})}
  end

  # ── Helpers ──

  defp resolve_station(input) do
    input = String.trim(input)

    if Regex.match?(~r/^[A-Ra-r]{2}[0-9]{2}/i, input) and String.length(input) >= 4 do
      case Maidenhead.to_latlon(input) do
        {:ok, {lat, lon}} ->
          {:ok, %{label: String.upcase(input), lat: lat, lon: lon, type: :grid}}

        :error ->
          {:error, "invalid grid"}
      end
    else
      case CallsignClient.locate(input) do
        {:ok, info} ->
          {:ok, %{label: String.upcase(info.callsign), lat: info.lat, lon: info.lon, type: :callsign}}

        {:error, reason} ->
          {:error, reason}
      end
    end
  end

  defp push_url(socket) do
    labels = Enum.map_join(socket.assigns.stations, ",", & &1.label)

    params =
      then(%{"band" => to_string(socket.assigns.band)}, fn p ->
        if labels == "", do: p, else: Map.put(p, "stations", labels)
      end)

    push_patch(socket, to: ~p"/rover?#{params}", replace: true)
  end

  defp station_data(stations) do
    Enum.map(stations, fn s -> %{label: s.label, lat: s.lat, lon: s.lon} end)
  end

  # ── Render ──

  @impl true
  def render(assigns) do
    ~H"""
    <div class="relative w-screen h-screen overflow-hidden">
      <div
        id="rover-map"
        phx-hook="RoverMap"
        phx-update="ignore"
        data-band={@band}
        data-scores={@initial_scores_json}
        class="absolute inset-0 z-0"
      >
      </div>

      <%!-- Sidebar --%>
      <div class="absolute top-2 left-2 z-[1000] w-80 max-h-[calc(100vh-1rem)] overflow-y-auto bg-base-100/95 shadow-lg rounded-box border border-base-300 p-3 flex flex-col gap-3">
        <div class="font-bold text-sm">Rover Planner</div>

        <select phx-change="select_band" name="band" class="select select-bordered select-sm w-full">
          <%= for {label, value} <- @band_options do %>
            <option value={value} selected={value == to_string(@band)}>{label}</option>
          <% end %>
        </select>

        <form phx-submit="add_station" class="flex gap-1">
          <input
            type="text"
            name="callsign"
            value={@station_input}
            placeholder="Add station (call or grid)"
            class="input input-bordered input-sm flex-1"
          />
          <button type="submit" class="btn btn-sm btn-primary" disabled={@resolving}>
            <%= if @resolving do %>
              <span class="loading loading-spinner loading-xs"></span>
            <% else %>
              Add
            <% end %>
          </button>
        </form>

        <%= if @stations != [] do %>
          <div class="text-xs opacity-60">Stationary Stations ({length(@stations)})</div>
          <div class="flex flex-wrap gap-1">
            <%= for {station, idx} <- Enum.with_index(@stations) do %>
              <div class="badge badge-outline gap-1">
                {station.label}
                <button
                  type="button"
                  phx-click="remove_station"
                  phx-value-index={idx}
                  class="cursor-pointer opacity-50 hover:opacity-100"
                >
                  x
                </button>
              </div>
            <% end %>
          </div>
        <% end %>
      </div>
    </div>
    """
  end
end
