defmodule MicrowavepropWeb.RoverPlanningLive.Show do
  @moduledoc """
  `/rover-planning/:id` mission detail. Renders the planned stations
  and the matrix of computed (or pending) terrain path profiles from
  every candidate rover-location.
  """
  use MicrowavepropWeb, :live_view

  alias Microwaveprop.Accounts.User
  alias Microwaveprop.Radio.Maidenhead
  alias Microwaveprop.Repo
  alias Microwaveprop.Rover
  alias Microwaveprop.Rover.Location
  alias Microwaveprop.RoverPlanning
  alias Microwaveprop.RoverPlanning.Mission
  alias MicrowavepropWeb.LocationResolver

  @impl true
  def mount(%{"id" => id}, _session, socket) do
    case RoverPlanning.get_mission_with_paths(id) do
      nil ->
        {:ok,
         socket
         |> put_flash(:error, "Mission not found.")
         |> push_navigate(to: ~p"/rover-planning")}

      %Mission{} = mission ->
        if connected?(socket) do
          Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "rover_planning:#{mission.id}")
        end

        {:ok,
         assign(socket,
           page_title: "Mission · #{mission.name}",
           mission: mission,
           paths: RoverPlanning.list_paths(mission),
           rover_sites: RoverPlanning.candidate_rover_locations(mission),
           rover_site_input: "",
           rover_site_input_matches: []
         )}
    end
  end

  @impl true
  def handle_info({:rover_path_updated, _path_id}, socket) do
    {:noreply, assign(socket, paths: RoverPlanning.list_paths(socket.assigns.mission))}
  end

  @impl true
  def handle_event("rover_site_input_change", %{"input" => input}, socket) do
    {:noreply,
     assign(socket,
       rover_site_input: input,
       rover_site_input_matches: find_grid_matches(input)
     )}
  end

  def handle_event("add_rover_site", %{"input" => input}, socket) do
    with {:ok, user} <- authenticated(socket),
         {:ok, %{lat: lat, lon: lon}} <- LocationResolver.resolve(input),
         attrs = %{lat: lat, lon: lon, status: :good, notes: String.trim(input)},
         {:ok, _loc} <- Rover.create_location(user, attrs) do
      {:noreply, refresh_after_site_change(socket, "Rover site added — recomputing paths.")}
    else
      {:error, :unauthenticated} -> {:noreply, put_flash(socket, :error, "Sign in to add rover sites.")}
      :empty -> {:noreply, put_flash(socket, :error, "Enter a callsign, grid, or lat,lon.")}
      {:error, msg} when is_binary(msg) -> {:noreply, put_flash(socket, :error, msg)}
      {:error, %Ecto.Changeset{} = cs} -> {:noreply, put_flash(socket, :error, error_summary(cs))}
    end
  end

  def handle_event("delete_rover_site", %{"id" => id}, socket) do
    with {:ok, user} <- authenticated(socket),
         {:ok, _} <- Rover.delete_location(user, id) do
      {:noreply, refresh_after_site_change(socket, "Rover site removed — recomputing paths.")}
    else
      {:error, :unauthenticated} -> {:noreply, put_flash(socket, :error, "Sign in to remove rover sites.")}
      {:error, :not_found} -> {:noreply, put_flash(socket, :error, "You can only delete rover sites you created.")}
    end
  end

  def handle_event("delete", _params, socket) do
    with {:ok, user} <- authenticated(socket),
         {:ok, _} <- RoverPlanning.delete_mission(user, socket.assigns.mission.id) do
      {:noreply,
       socket
       |> put_flash(:info, "Mission deleted.")
       |> push_navigate(to: ~p"/rover-planning")}
    else
      {:error, :unauthenticated} -> {:noreply, put_flash(socket, :error, "Sign in required.")}
      {:error, :not_found} -> {:noreply, put_flash(socket, :error, "You can only delete your own missions.")}
    end
  end

  # Re-enqueues the mission's path matrix and reloads the show-page assigns
  # that depend on the rover-site set.
  defp refresh_after_site_change(socket, flash_message) do
    mission = socket.assigns.mission
    :ok = RoverPlanning.enqueue_reconcile(mission)

    socket
    |> assign(
      rover_sites: RoverPlanning.candidate_rover_locations(mission),
      paths: RoverPlanning.list_paths(mission),
      rover_site_input: "",
      rover_site_input_matches: []
    )
    |> put_flash(:info, flash_message)
  end

  defp authenticated(%Phoenix.LiveView.Socket{assigns: %{current_scope: %{user: %User{} = user}}}), do: {:ok, user}

  defp authenticated(_), do: {:error, :unauthenticated}

  # Resolve the user-typed input and surface any existing rover-locations
  # that share its 6-char Maidenhead grid (sub-grid resolution ≈ 5 km).
  # Cheap enough for ~hundreds of locations: we map them in memory rather
  # than maintaining a stored grid column.
  defp find_grid_matches(input) when is_binary(input) do
    trimmed = String.trim(input)

    case trimmed != "" && LocationResolver.resolve(trimmed) do
      {:ok, %{lat: lat, lon: lon}} when is_number(lat) and is_number(lon) ->
        target = Maidenhead.from_latlon(lat, lon, 6)

        Location
        |> Repo.all()
        |> Enum.filter(fn loc ->
          Maidenhead.from_latlon(loc.lat, loc.lon, 6) == target
        end)

      _ ->
        []
    end
  end

  defp find_grid_matches(_), do: []

  defp can_modify?(%{current_scope: %{user: %User{is_admin: true}}}, _), do: true

  defp can_modify?(%{current_scope: %{user: %User{id: id}}}, %Mission{user_id: id}) when not is_nil(id), do: true

  defp can_modify?(_, _), do: false

  defp can_delete_site?(%{current_scope: %{user: %User{is_admin: true}}}, _), do: true

  defp can_delete_site?(%{current_scope: %{user: %User{id: id}}}, %Location{user_id: id}) when not is_nil(id), do: true

  defp can_delete_site?(_, _), do: false

  defp error_summary(%Ecto.Changeset{errors: []}), do: "Could not save rover site."

  defp error_summary(%Ecto.Changeset{errors: errors}),
    do: Enum.map_join(errors, "; ", fn {field, {msg, _}} -> "#{field} #{msg}" end)

  defp band_label(mhz) when is_integer(mhz) and mhz >= 1000 do
    ghz = mhz / 1000
    if ghz == trunc(ghz), do: "#{trunc(ghz)} GHz", else: "#{Float.round(ghz, 1)} GHz"
  end

  defp band_label(mhz), do: "#{mhz} MHz"

  defp bands_label(%Mission{} = mission) do
    case Mission.bands(mission) do
      [] -> "—"
      [single] -> band_label(single)
      list -> Enum.map_join(list, ", ", &band_label/1)
    end
  end

  defp status_badge(:complete) do
    assigns = %{}
    ~H|<span class="badge badge-success badge-sm">Complete</span>|
  end

  defp status_badge(:computing) do
    assigns = %{}
    ~H|<span class="badge badge-info badge-sm">Computing</span>|
  end

  defp status_badge(:failed) do
    assigns = %{}
    ~H|<span class="badge badge-error badge-sm">Failed</span>|
  end

  defp status_badge(_) do
    assigns = %{}
    ~H|<span class="badge badge-ghost badge-sm">Pending</span>|
  end

  defp verdict_badge("CLEAR") do
    assigns = %{}
    ~H|<span class="badge badge-success badge-xs">Clear</span>|
  end

  defp verdict_badge("BLOCKED") do
    assigns = %{}
    ~H|<span class="badge badge-error badge-xs">Blocked</span>|
  end

  defp verdict_badge("FRESNEL_PARTIAL") do
    assigns = %{}
    ~H|<span class="badge badge-warning badge-xs">Fresnel partial</span>|
  end

  defp verdict_badge("FRESNEL_MINOR") do
    assigns = %{}
    ~H|<span class="badge badge-info badge-xs">Fresnel minor</span>|
  end

  defp verdict_badge(_), do: ""

  defp propagation_badge(score) when is_integer(score) and score >= 70 do
    assigns = %{score: score}
    ~H|<span class="badge badge-success badge-sm font-mono">{@score}</span>|
  end

  defp propagation_badge(score) when is_integer(score) and score >= 40 do
    assigns = %{score: score}
    ~H|<span class="badge badge-warning badge-sm font-mono">{@score}</span>|
  end

  defp propagation_badge(score) when is_integer(score) do
    assigns = %{score: score}
    ~H|<span class="badge badge-error badge-sm font-mono">{@score}</span>|
  end

  defp propagation_badge(_), do: "—"

  defp progress_summary(paths) do
    Enum.reduce(paths, %{total: 0, complete: 0, pending: 0, failed: 0}, &tally_path/2)
  end

  defp tally_path(%{status: :complete}, acc), do: %{acc | total: acc.total + 1, complete: acc.complete + 1}

  defp tally_path(%{status: status}, acc) when status in [:pending, :computing],
    do: %{acc | total: acc.total + 1, pending: acc.pending + 1}

  defp tally_path(%{status: :failed}, acc), do: %{acc | total: acc.total + 1, failed: acc.failed + 1}
  defp tally_path(_, acc), do: %{acc | total: acc.total + 1}

  # Bucket paths by their rover-location and sort each bucket so stations
  # appear in the order the user defined on the mission. The outer order
  # is by rover-location lat (north → south) so the same rover location
  # keeps the same slot across re-renders.
  defp paths_by_rover_location(paths) do
    paths
    |> Enum.group_by(& &1.rover_location_id)
    |> Enum.map(&group_to_pair/1)
    |> Enum.sort_by(&group_lat/1, :desc)
  end

  defp group_to_pair({_id, [%{rover_location: location} | _] = group}) do
    {location, Enum.sort_by(group, &sort_key/1)}
  end

  defp group_to_pair({_id, []}), do: {nil, []}

  # Sort within each rover-location group by station position (so all
  # bands for a station cluster together) then ascending band — gives
  # 902 → 1296 → 2304 → … in each station block.
  defp sort_key(path), do: {station_position(path), path.band_mhz || 0}

  defp station_position(%{station: %{position: pos}}) when is_integer(pos), do: pos
  defp station_position(_), do: 0

  defp group_lat({%{lat: lat}, _}) when is_number(lat), do: lat
  defp group_lat(_), do: 0

  # JSONB round-trips bare zeros as integers (e.g. `0` not `0.0`), and
  # `Float.round/2` only accepts floats. Coerce with `* 1.0` everywhere
  # we round a value that came out of a `:map` column.
  defp format_distance(km) when is_number(km), do: "#{Float.round(km * 1.0, 1)} km / #{Float.round(km * 0.621371, 1)} mi"

  defp format_distance(_), do: "—"

  defp format_loss(db) when is_number(db), do: "#{Float.round(db * 1.0, 1)} dB"
  defp format_loss(_), do: "—"

  # Two-line station identity for the path table. Returns
  # `{primary, secondary}` — primary is the callsign (or grid when no
  # callsign), secondary is a Maidenhead grid (stored or derived from
  # lat/lon) when it adds info beyond `primary`.
  defp station_lines(%{station: %{} = station}), do: station_lines_for(station)
  defp station_lines(_), do: {"—", nil}

  defp station_lines_for(%{callsign: c} = s) when is_binary(c) and c != "", do: {c, station_grid(s)}

  defp station_lines_for(%{grid: g}) when is_binary(g) and g != "", do: {g, nil}

  defp station_lines_for(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon),
    do: {Maidenhead.from_latlon(lat, lon, 6), nil}

  defp station_lines_for(_), do: {"—", nil}

  defp station_grid(%{grid: g}) when is_binary(g) and g != "", do: g

  defp station_grid(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon), do: Maidenhead.from_latlon(lat, lon, 6)

  defp station_grid(_), do: nil

  # Inline label for the "Stationary stations" summary list. Prefers the
  # callsign (uppercased — legacy data was stored mixed-case), then the
  # grid, then the input string when neither is set. Bare lat/lon
  # coordinates are intentionally NOT used as a label here — the grid
  # half (via `stationary_grid/1`) carries that location info.
  defp stationary_label(%{callsign: c}) when is_binary(c) and c != "", do: String.upcase(c)
  defp stationary_label(%{grid: g}) when is_binary(g) and g != "", do: g

  defp stationary_label(%{input: i}) when is_binary(i) and i != "" do
    if LocationResolver.coordinate_pair?(i), do: nil, else: String.upcase(i)
  end

  defp stationary_label(_), do: nil

  defp stationary_grid(%{grid: g}) when is_binary(g) and g != "", do: g

  defp stationary_grid(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon),
    do: Maidenhead.from_latlon(lat, lon, 6)

  defp stationary_grid(_), do: nil

  # Endpoint string used for /path?destination=…. Prefers callsign, then
  # Row click goes straight to /path with the cached rover-path ID.
  # PathLive detects the param, deserializes the worker-stored
  # PathCompute result, and renders without re-running the pipeline.
  defp path_url(_mission, %{id: path_id}) when not is_nil(path_id), do: "/path?rover_path_id=#{path_id}"

  defp path_url(_, _), do: nil

  # Wraps a single table cell's content in a real <a> when an href is
  # available, so the browser shows the destination on hover and
  # right-click / middle-click work natively. Falls back to a plain
  # <div> when the row has no path (e.g. compute hasn't started).
  attr :href, :string, default: nil
  slot :inner_block, required: true

  defp path_cell(%{href: href} = assigns) when is_binary(href) do
    ~H"""
    <.link navigate={@href} class="block px-3 py-2 text-inherit no-underline">
      {render_slot(@inner_block)}
    </.link>
    """
  end

  defp path_cell(assigns) do
    ~H"""
    <div class="px-3 py-2">
      {render_slot(@inner_block)}
    </div>
    """
  end

  @impl true
  def render(assigns) do
    summary = progress_summary(assigns.paths)
    grouped = paths_by_rover_location(assigns.paths)
    assigns = assign(assigns, summary: summary, grouped_paths: grouped)

    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-6xl">
      <.header>
        {@mission.name}
        <:subtitle>
          {bands_label(@mission)} · {length(@mission.stations)} station(s) ·
          rover {@mission.rover_height_ft}ft / station {@mission.station_height_ft}ft
        </:subtitle>
        <:actions>
          <.link navigate={~p"/rover-planning"} class="btn btn-ghost btn-sm">
            <.icon name="hero-arrow-left" class="w-4 h-4" /> All missions
          </.link>
          <.link
            :if={can_modify?(assigns, @mission)}
            navigate={~p"/rover-planning/#{@mission.id}/edit"}
            class="btn btn-ghost btn-sm"
          >
            <.icon name="hero-pencil-square" class="w-4 h-4" /> Edit
          </.link>
          <button
            :if={can_modify?(assigns, @mission)}
            type="button"
            phx-click="delete"
            data-confirm="Delete this mission?"
            class="btn btn-ghost btn-sm text-error"
          >
            <.icon name="hero-trash" class="w-4 h-4" /> Delete
          </button>
        </:actions>
      </.header>

      <div class="card bg-base-100 border border-base-300 p-4 mb-4">
        <h3 class="font-semibold mb-2">Stationary stations</h3>
        <ul class="text-sm space-y-1">
          <li :for={station <- @mission.stations} class="flex flex-wrap gap-2">
            <span :if={stationary_label(station)} class="font-mono">
              {stationary_label(station)}
            </span>
            <span :if={stationary_grid(station)} class="text-base-content/60 font-mono">
              {stationary_grid(station)}
            </span>
          </li>
        </ul>

        <p :if={@mission.notes && @mission.notes != ""} class="mt-3 text-sm whitespace-pre-line">
          {@mission.notes}
        </p>

        <p class="text-xs text-base-content/60 mt-3">
          Scope: {if @mission.only_known_good,
            do: "Known good locations only (status = good)",
            else: "All rover locations"} · Owner: {(@mission.user && @mission.user.callsign) || "—"}
        </p>
      </div>

      <div class="card bg-base-100 border border-base-300 p-4 mb-4">
        <div class="flex items-center justify-between mb-2 gap-2 flex-wrap">
          <h3 class="font-semibold">Add rover site</h3>
          <span class="text-xs text-base-content/60">
            {length(@rover_sites)} site(s) — paths recompute on add/remove
          </span>
        </div>

        <%!--
          The add form takes the same flexible input the station inputs use:
          callsign, Maidenhead grid, or `lat, lon`. Sign-in required.
          The site list itself is rendered inline as path-profile group
          headings below — each heading carries its own delete button.
        --%>
        <form
          :if={@current_scope && @current_scope.user}
          phx-submit="add_rover_site"
          phx-change="rover_site_input_change"
          class="flex flex-wrap gap-2 items-stretch"
        >
          <input
            type="text"
            name="input"
            value={@rover_site_input}
            placeholder="Callsign, EM12kp, or 32.91, -97.06"
            class="input input-sm flex-1 min-w-[14rem]"
          />
          <button type="submit" class="btn btn-primary btn-sm" disabled={@rover_site_input == ""}>
            <.icon name="hero-plus" class="w-4 h-4" /> Add rover site
          </button>
        </form>

        <div :if={@rover_site_input_matches != []} class="mt-2 text-xs text-warning">
          Already saved as:
          <.link
            :for={match <- @rover_site_input_matches}
            navigate={~p"/rover-locations/#{match.id}"}
            class="link link-warning font-mono ml-1"
          >
            {Maidenhead.from_latlon(match.lat, match.lon, 6)}{if match.notes && match.notes != "",
              do: " (#{match.notes})"}
          </.link>
        </div>

        <p :if={!(@current_scope && @current_scope.user)} class="text-xs text-base-content/60">
          <.link navigate={~p"/users/log-in"} class="link">Sign in</.link>
          to add or remove rover sites.
        </p>
      </div>

      <div class="card bg-base-100 border border-base-300 p-4">
        <div class="flex items-center justify-between mb-3 gap-2 flex-wrap">
          <h3 class="font-semibold">Path profiles</h3>
          <div class="text-xs text-base-content/70">
            {@summary.complete}/{@summary.total} complete
            <span :if={@summary.pending > 0}>· {@summary.pending} pending</span>
            <span :if={@summary.failed > 0} class="text-error">
              · {@summary.failed} failed
            </span>
          </div>
        </div>

        <div :if={@paths == []} class="text-sm text-base-content/60 py-4 text-center">
          No paths yet — they'll appear here as the workers run.
        </div>

        <div :if={@paths != []} class="space-y-4">
          <div
            :for={{location, group_paths} <- @grouped_paths}
            data-rover-group
            class="border border-base-300 rounded-lg overflow-hidden"
          >
            <div class="bg-base-200 px-3 py-2 flex flex-wrap items-center justify-between gap-2">
              <div class="font-mono text-sm">
                <.link
                  :if={location && location.id}
                  navigate={~p"/rover-locations/#{location.id}"}
                  class="link link-hover"
                >
                  {rover_location_heading(location)}
                </.link>
                <span :if={!(location && location.id)}>
                  {rover_location_heading(location)}
                </span>
                <span
                  :if={location && location.notes && location.notes != ""}
                  class="text-base-content/60 text-xs"
                >
                  · {location.notes}
                </span>
              </div>
              <div class="flex items-center gap-2">
                <span class="text-xs text-base-content/70">
                  {group_progress(group_paths)}
                </span>
                <button
                  :if={location && location.id && can_delete_site?(assigns, location)}
                  type="button"
                  phx-click="delete_rover_site"
                  phx-value-id={location.id}
                  data-confirm="Remove this rover site? This affects every mission that uses it."
                  class="btn btn-ghost btn-xs text-error"
                  title="Remove rover site"
                >
                  <.icon name="hero-trash" class="w-4 h-4" />
                </button>
              </div>
            </div>

            <div class="overflow-x-auto">
              <table class="table table-sm">
                <thead>
                  <tr>
                    <th>Station</th>
                    <th>Band</th>
                    <th>Status</th>
                    <th>Distance</th>
                    <th>Loss</th>
                    <th>Verdict</th>
                    <th>Propagation</th>
                  </tr>
                </thead>
                <tbody>
                  <%!-- Each cell wraps its content in <.link>, so the row
                  behaves as a real anchor: native href hover preview,
                  right-click "open in new tab", middle-click, etc. The
                  row-hover highlight is preserved via :has(a:hover) so
                  hovering any cell still tints the whole row. --%>
                  <tr
                    :for={path <- group_paths}
                    class={[
                      path_url(@mission, path) &&
                        "[&:has(a:hover)]:bg-base-200 transition-colors"
                    ]}
                  >
                    <td class="font-mono text-xs p-0">
                      <% {primary, secondary} = station_lines(path) %>
                      <.path_cell href={path_url(@mission, path)}>
                        <div>{primary}</div>
                        <div :if={secondary} class="opacity-60">{secondary}</div>
                      </.path_cell>
                    </td>
                    <td class="font-mono text-xs p-0">
                      <.path_cell href={path_url(@mission, path)}>
                        {band_label(path.band_mhz || @mission.band_mhz)}
                      </.path_cell>
                    </td>
                    <td class="p-0">
                      <.path_cell href={path_url(@mission, path)}>
                        {status_badge(path.status)}
                      </.path_cell>
                    </td>
                    <td class="text-xs p-0">
                      <.path_cell href={path_url(@mission, path)}>
                        {if path.result, do: format_distance(path.result["distance_km"]), else: "—"}
                      </.path_cell>
                    </td>
                    <td class="text-xs p-0">
                      <.path_cell href={path_url(@mission, path)}>
                        {if path.result,
                          do: format_loss(path.result["total_baseline_loss_db"]),
                          else: "—"}
                      </.path_cell>
                    </td>
                    <td class="p-0">
                      <.path_cell href={path_url(@mission, path)}>
                        {if path.result, do: verdict_badge(path.result["verdict"]), else: "—"}
                      </.path_cell>
                    </td>
                    <td class="p-0">
                      <.path_cell href={path_url(@mission, path)}>
                        {if path.result,
                          do: propagation_badge(path.result["propagation_score"]),
                          else: "—"}
                      </.path_cell>
                    </td>
                  </tr>
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>
    </Layouts.app>
    """
  end

  defp rover_location_heading(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
    # 10-char grid is the maximum useful Maidenhead precision; anything
    # finer is below the float lat/lon's resolution. The user wants the
    # full precision, not a 6-char truncation.
    grid = Maidenhead.from_latlon(lat, lon, 10)
    "#{grid} (#{Float.round(lat, 4)}, #{Float.round(lon, 4)})"
  end

  defp rover_location_heading(_), do: "—"

  defp group_progress(paths) do
    total = length(paths)
    complete = Enum.count(paths, &(&1.status == :complete))
    "#{complete}/#{total} paths"
  end
end
