defmodule Microwaveprop.RoverPlanning.Mission do
  @moduledoc """
  A planned rover mission. Owned by a user but globally visible. Holds
  the band + height parameters that drive path-profile computation, the
  list of stationary endpoints (`stations`), and the matrix of computed
  paths from each rover-location to each station (`paths`).

  `only_known_good` toggles whether the rover endpoint set is the
  `:good` rover-locations (default) or every rover-location.
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Microwaveprop.Accounts.User
  alias Microwaveprop.RoverPlanning.Path
  alias Microwaveprop.RoverPlanning.Station

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "rover_missions" do
    field :name, :string
    field :band_mhz, :integer, default: 10_000
    field :bands_mhz, {:array, :integer}, default: []
    field :only_known_good, :boolean, default: true
    field :rover_height_ft, :float, default: 8.0
    field :station_height_ft, :float, default: 30.0
    field :notes, :string

    belongs_to :user, User
    has_many :stations, Station, foreign_key: :mission_id, on_replace: :delete
    has_many :paths, Path, foreign_key: :mission_id

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{}

  @castable_fields [
    :name,
    :band_mhz,
    :bands_mhz,
    :only_known_good,
    :rover_height_ft,
    :station_height_ft,
    :notes
  ]

  @doc """
  Returns the bands the mission's path matrix should iterate over.
  Prefers `bands_mhz` (canonical, possibly empty) and falls back to
  `[band_mhz]` for legacy rows. Always returns a non-empty list when
  the mission is valid.
  """
  @spec bands(t()) :: [integer()]
  def bands(%__MODULE__{bands_mhz: list}) when is_list(list) and list != [], do: Enum.uniq(list)

  def bands(%__MODULE__{band_mhz: primary}) when is_integer(primary), do: [primary]
  def bands(_), do: []

  @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  def changeset(mission, attrs) do
    mission
    |> cast(prepare_bands_attr(attrs), @castable_fields)
    |> normalize_bands()
    |> cast_assoc(:stations, with: &Station.changeset/2, required: true)
    |> validate_required([:name, :band_mhz])
    |> validate_length(:name, min: 1, max: 100)
    |> validate_number(:band_mhz, greater_than: 0)
    |> validate_bands_mhz()
    |> validate_number(:rover_height_ft, greater_than_or_equal_to: 0.0)
    |> validate_number(:station_height_ft, greater_than_or_equal_to: 0.0)
    |> validate_length(:notes, max: 4000)
    |> validate_at_least_one_station()
  end

  # The multi-checkbox form sends a hidden empty-string sentinel so
  # the key always exists even when nothing is checked. Drop those
  # sentinels here so Ecto's `{:array, :integer}` cast doesn't choke.
  defp prepare_bands_attr(%{"bands_mhz" => list} = attrs) when is_list(list) do
    cleaned =
      list
      |> Enum.reject(fn v -> v == "" or v == nil end)
      |> Enum.uniq()

    Map.put(attrs, "bands_mhz", cleaned)
  end

  defp prepare_bands_attr(attrs), do: attrs

  # Treats `bands_mhz` as the canonical multi-band list. When the
  # form submits it (multi-checkbox), use that. When only `band_mhz`
  # is present (legacy single-band form path), seed bands_mhz from it.
  # Also keeps `band_mhz` in sync as the "primary band" so older
  # readers stay correct.
  defp normalize_bands(changeset) do
    bands = get_change(changeset, :bands_mhz) || get_field(changeset, :bands_mhz)
    primary = get_change(changeset, :band_mhz) || get_field(changeset, :band_mhz)

    cond do
      is_list(bands) and bands != [] ->
        cleaned = bands |> Enum.uniq() |> Enum.sort()

        changeset
        |> put_change(:bands_mhz, cleaned)
        |> put_change(:band_mhz, hd(cleaned))

      is_integer(primary) and primary > 0 ->
        put_change(changeset, :bands_mhz, [primary])

      true ->
        changeset
    end
  end

  defp validate_bands_mhz(changeset) do
    case get_field(changeset, :bands_mhz) do
      list when is_list(list) and list != [] ->
        if Enum.all?(list, &(is_integer(&1) and &1 > 0)),
          do: changeset,
          else: add_error(changeset, :bands_mhz, "must be a list of positive integers")

      _ ->
        add_error(changeset, :bands_mhz, "must include at least one band")
    end
  end

  defp validate_at_least_one_station(changeset) do
    case fetch_field(changeset, :stations) do
      {_source, list} when is_list(list) and list != [] ->
        kept = Enum.reject(list, fn s -> match?(%Ecto.Changeset{action: :replace}, s) end)
        if kept == [], do: add_error(changeset, :stations, "must include at least one station"), else: changeset

      _ ->
        add_error(changeset, :stations, "must include at least one station")
    end
  end
end
