defmodule MicrowavepropWeb.RoverPlanningLive.Form do
  @moduledoc "New / edit form for `/rover-planning` missions."
  use MicrowavepropWeb, :live_view

  alias Microwaveprop.Accounts.User
  alias Microwaveprop.RoverPlanning
  alias Microwaveprop.RoverPlanning.Mission

  @band_options [
    {"902 MHz", 902},
    {"1.3 GHz", 1296},
    {"2.3 GHz", 2304},
    {"3.4 GHz", 3400},
    {"5.7 GHz", 5760},
    {"10 GHz", 10_000},
    {"24 GHz", 24_000},
    {"47 GHz", 47_000}
  ]

  @impl true
  def mount(params, _session, socket) do
    case current_user(socket) do
      nil ->
        {:ok,
         socket
         |> put_flash(:error, "Sign in to plan a mission.")
         |> push_navigate(to: ~p"/rover-planning")}

      %User{} = user ->
        {:ok,
         socket
         |> assign(
           page_title: "Rover Mission",
           band_options: @band_options,
           current_user: user
         )
         |> apply_action(socket.assigns.live_action, params)}
    end
  end

  defp apply_action(socket, :new, _params) do
    # Seed the initial changeset PARAMS (not just the struct) with one
    # station row so add_station / remove_station have something to work
    # with. Without this seed, cast_assoc treats the first add_station
    # click as a replacement of the unseeded default — visually a no-op.
    mission = %Mission{}
    cs = Mission.changeset(mission, %{"stations" => %{"0" => %{"position" => "0"}}})

    assign(socket,
      mission: mission,
      form: to_form(cs),
      mode: :new
    )
  end

  defp apply_action(socket, :edit, %{"id" => id}) do
    user = socket.assigns.current_user

    case RoverPlanning.get_mission(id) do
      nil ->
        socket
        |> put_flash(:error, "Mission not found.")
        |> push_navigate(to: ~p"/rover-planning")

      mission ->
        if can_modify?(user, mission) do
          cs = Mission.changeset(mission, %{})

          assign(socket,
            mission: mission,
            form: to_form(cs),
            mode: :edit
          )
        else
          socket
          |> put_flash(:error, "You can only edit your own missions.")
          |> push_navigate(to: ~p"/rover-planning/#{mission.id}")
        end
    end
  end

  @impl true
  def handle_event("validate", %{"mission" => params}, socket) do
    cs =
      socket.assigns.mission
      |> Mission.changeset(params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, form: to_form(cs))}
  end

  def handle_event("add_station", _params, socket) do
    params = current_params(socket)
    stations = stations_params(socket, params)
    next_index = next_station_index(stations)

    # Don't include "input" in the new station's params — `used_input?/1`
    # treats any field present in params as "used" and would render the
    # `validate_required([:input])` error before the user has typed
    # anything. Leaving the key out keeps the row pristine until edited.
    stations =
      Map.put(stations, Integer.to_string(next_index), %{
        "position" => Integer.to_string(next_index)
      })

    cs =
      socket.assigns.mission
      |> Mission.changeset(Map.put(params, "stations", stations))
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, form: to_form(cs))}
  end

  def handle_event("remove_station", %{"index" => index}, socket) do
    params = current_params(socket)
    stations = socket |> stations_params(params) |> Map.delete(index)

    cs =
      socket.assigns.mission
      |> Mission.changeset(Map.put(params, "stations", stations))
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, form: to_form(cs))}
  end

  def handle_event("save", %{"mission" => params}, socket) do
    user = socket.assigns.current_user

    result =
      case socket.assigns.mode do
        :new -> RoverPlanning.create_mission(user, params)
        :edit -> RoverPlanning.update_mission(user, socket.assigns.mission.id, params)
      end

    case result do
      {:ok, mission} ->
        {:noreply,
         socket
         |> put_flash(:info, "Mission saved — computing path profiles.")
         |> push_navigate(to: ~p"/rover-planning/#{mission.id}")}

      {:error, :not_found} ->
        {:noreply, put_flash(socket, :error, "Mission not found.")}

      {:error, %Ecto.Changeset{} = cs} ->
        {:noreply, assign(socket, form: to_form(cs))}
    end
  end

  # When the form changeset already carries a "stations" params map
  # (every keystroke fires phx-change="validate" which builds one),
  # trust it. Otherwise — e.g. the user clicked add/remove BEFORE
  # touching any other input on an :edit-mode form — fall back to
  # rebuilding the params from the persisted mission's stations so
  # `cast_assoc(on_replace: :delete)` doesn't wipe every existing row.
  defp stations_params(_socket, %{"stations" => stations}) when is_map(stations) and stations != %{}, do: stations

  defp stations_params(socket, _params), do: stations_from_mission(socket.assigns.mission)

  defp stations_from_mission(%Mission{stations: list}) when is_list(list) do
    list
    |> Enum.with_index()
    |> Map.new(fn {s, i} ->
      {Integer.to_string(i),
       %{
         "id" => s.id,
         "input" => s.input || s.callsign || s.grid,
         "callsign" => s.callsign,
         "grid" => s.grid,
         "lat" => s.lat,
         "lon" => s.lon,
         "position" => Integer.to_string(s.position || i)
       }}
    end)
  end

  defp stations_from_mission(_), do: %{}

  defp current_params(socket) do
    case socket.assigns.form.source do
      %Ecto.Changeset{params: params} when is_map(params) -> params
      _ -> %{}
    end
  end

  defp next_station_index(stations) when map_size(stations) == 0, do: 0

  defp next_station_index(stations) do
    stations
    |> Map.keys()
    |> Enum.map(fn k ->
      case Integer.parse(to_string(k)) do
        {n, _} -> n
        :error -> -1
      end
    end)
    |> Enum.max()
    |> Kernel.+(1)
  end

  defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}) do
    case assigns[:current_scope] do
      %{user: %User{} = user} -> user
      _ -> nil
    end
  end

  defp can_modify?(%User{is_admin: true}, _), do: true
  defp can_modify?(%User{id: id}, %Mission{user_id: id}) when not is_nil(id), do: true
  defp can_modify?(_, _), do: false

  # Multi-checkbox values come from the form as a list of strings (or
  # ints, depending on whether the changeset has cast them yet). Tolerate
  # both, falling back to "not checked" for nil / empty bands lists.
  defp band_checked?(values, target) when is_list(values) do
    Enum.any?(values, fn
      v when is_integer(v) -> v == target
      v when is_binary(v) -> v == Integer.to_string(target)
      _ -> false
    end)
  end

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

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-3xl">
      <.header>
        {if @mode == :edit, do: "Edit mission", else: "New rover mission"}
        <:subtitle>
          Stationary stations can be callsigns, Maidenhead grids, or <code>lat, lon</code> pairs.
        </:subtitle>
      </.header>

      <.form
        for={@form}
        id="mission-form"
        phx-change="validate"
        phx-submit="save"
        class="space-y-4"
      >
        <.input field={@form[:name]} type="text" label="Mission name" placeholder="DFW August rove" />

        <div>
          <label class="label">
            <span class="label-text">Bands</span>
            <span class="label-text-alt text-base-content/60">
              Pick one or more — paths recompute per band
            </span>
          </label>
          <%!--
            Hidden zero-value field so an unchecked-everywhere submit
            still parses as an empty list (Phoenix drops the key
            otherwise). Mission.changeset rejects an empty bands_mhz
            with a friendly error.
          --%>
          <input type="hidden" name="mission[bands_mhz][]" value="" />
          <div class="flex flex-wrap gap-2">
            <label
              :for={{label, value} <- @band_options}
              class="cursor-pointer flex items-center gap-2 border border-base-300 rounded px-3 py-1 text-sm"
            >
              <input
                type="checkbox"
                name="mission[bands_mhz][]"
                value={value}
                checked={band_checked?(@form[:bands_mhz].value, value)}
                class="checkbox checkbox-sm checkbox-primary"
              />
              <span>{label}</span>
            </label>
          </div>
          <p :if={@form[:bands_mhz].errors != []} class="text-error text-sm mt-1">
            {Phoenix.HTML.html_escape(Enum.map_join(@form[:bands_mhz].errors, ", ", &elem(&1, 0)))}
          </p>
        </div>

        <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
          <.input
            field={@form[:rover_height_ft]}
            type="number"
            step="any"
            label="Rover antenna (ft)"
          />
          <.input
            field={@form[:station_height_ft]}
            type="number"
            step="any"
            label="Station antenna (ft)"
          />
        </div>

        <label class="flex items-center gap-2 cursor-pointer">
          <input type="hidden" name="mission[only_known_good]" value="false" />
          <input
            type="checkbox"
            name="mission[only_known_good]"
            value="true"
            checked={Phoenix.HTML.Form.normalize_value("checkbox", @form[:only_known_good].value)}
            class="checkbox checkbox-primary"
          />
          <span>
            Only check against known good locations
            <span class="text-xs opacity-70 block">
              When checked, paths are computed only against rover-locations marked Good.
            </span>
          </span>
        </label>

        <div>
          <div class="flex items-center justify-between mb-2">
            <h3 class="font-semibold">Stationary stations</h3>
            <button
              type="button"
              phx-click="add_station"
              class="btn btn-xs btn-ghost"
            >
              <.icon name="hero-plus" class="w-4 h-4" /> Add station
            </button>
          </div>

          <.inputs_for :let={s} field={@form[:stations]}>
            <div class="card bg-base-200 p-3 mb-2">
              <%!--
                `items-start` keeps the lat/lon row top-aligned even when the
                callsign cell grows (e.g. an error message under the input).
                The trash button uses `mt-6` to sit beside the input — matches
                the label height so it visually aligns with the input field.
              --%>
              <div class="grid grid-cols-1 md:grid-cols-2 gap-2 items-start">
                <.input
                  field={s[:input]}
                  type="text"
                  label="Callsign / grid / lat,lon"
                  placeholder="W5LUA or EM13qc or 32.91, -97.06"
                  phx-debounce="600"
                />
                <div class="flex items-start gap-2">
                  <div class="flex-1">
                    <.input field={s[:lat]} type="number" step="any" label="Lat" />
                  </div>
                  <div class="flex-1">
                    <.input field={s[:lon]} type="number" step="any" label="Lon" />
                  </div>
                  <button
                    type="button"
                    phx-click="remove_station"
                    phx-value-index={s.index}
                    class="btn btn-ghost btn-sm text-error mt-6"
                    title="Remove station"
                  >
                    <.icon name="hero-trash" class="w-4 h-4" />
                  </button>
                </div>
              </div>
              <p :if={s[:callsign].value} class="text-xs opacity-70 mt-1 px-1">
                Resolved callsign: {s[:callsign].value}
                <span :if={s[:grid].value}>· grid {s[:grid].value}</span>
              </p>
            </div>
          </.inputs_for>

          <p :if={@form[:stations].errors != []} class="text-error text-sm">
            {Phoenix.HTML.html_escape(Enum.map_join(@form[:stations].errors, ", ", &elem(&1, 0)))}
          </p>
        </div>

        <.input field={@form[:notes]} type="textarea" label="Notes" rows="3" />

        <div class="flex gap-2">
          <button type="submit" class="btn btn-primary">
            {if @mode == :edit, do: "Save changes", else: "Create mission"}
          </button>
          <.link navigate={~p"/rover-planning"} class="btn btn-ghost">Cancel</.link>
        </div>
      </.form>
    </Layouts.app>
    """
  end
end
