defmodule Microwaveprop.RoverPlanning.Station do
  @moduledoc """
  A stationary endpoint for a rover mission. The user provides one of:
  callsign, Maidenhead grid, or `lat, lon`; the changeset normalizes
  the input into all three when possible (callsigns geocode via
  `MicrowavepropWeb.LocationResolver`).
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Microwaveprop.RoverPlanning.Mission
  alias MicrowavepropWeb.LocationResolver

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id
  schema "rover_mission_stations" do
    field :callsign, :string
    field :grid, :string
    field :input, :string
    field :lat, :float
    field :lon, :float
    field :position, :integer, default: 0

    belongs_to :mission, Mission

    timestamps(type: :utc_datetime)
  end

  @type t :: %__MODULE__{}

  @spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
  def changeset(station, attrs) do
    station
    |> cast(attrs, [:callsign, :grid, :input, :lat, :lon, :position])
    |> validate_required([:input])
    |> resolve_input()
    |> validate_required([:lat, :lon])
    |> validate_number(:lat, greater_than_or_equal_to: -90.0, less_than_or_equal_to: 90.0)
    |> validate_number(:lon, greater_than_or_equal_to: -180.0, less_than_or_equal_to: 180.0)
  end

  # When `input` looks like a callsign or grid, geocode it and populate
  # the matching field. If the user TYPED a new input (changeset has a
  # `:input` change), wipe any previously-resolved callsign / grid /
  # lat / lon first — otherwise an early prefix that resolved
  # successfully (e.g. "AA5" matched a callsign) would lock those
  # fields and the user's later keystrokes would never re-resolve.
  defp resolve_input(changeset) do
    cond do
      not changeset.valid? ->
        changeset

      input_changed?(changeset) ->
        changeset
        |> clear_resolved()
        |> resolve_from_input()

      get_field(changeset, :lat) && get_field(changeset, :lon) ->
        changeset

      true ->
        resolve_from_input(changeset)
    end
  end

  defp input_changed?(changeset), do: not is_nil(get_change(changeset, :input))

  defp clear_resolved(changeset) do
    changeset
    |> put_change(:callsign, nil)
    |> put_change(:grid, nil)
    |> put_change(:lat, nil)
    |> put_change(:lon, nil)
  end

  defp resolve_from_input(changeset) do
    case get_field(changeset, :input) do
      input when is_binary(input) and input != "" ->
        apply_resolution(changeset, LocationResolver.resolve(String.trim(input)))

      _ ->
        changeset
    end
  end

  defp apply_resolution(changeset, {:ok, %{kind: :callsign} = res}) do
    changeset
    |> put_change(:callsign, res.label)
    |> put_change(:grid, Map.get(res, :grid))
    |> put_change(:lat, res.lat)
    |> put_change(:lon, res.lon)
  end

  defp apply_resolution(changeset, {:ok, %{kind: :grid} = res}) do
    changeset
    |> put_change(:grid, res.label)
    |> put_change(:lat, res.lat)
    |> put_change(:lon, res.lon)
  end

  defp apply_resolution(changeset, {:ok, %{kind: :coordinates} = res}) do
    changeset
    |> put_change(:lat, res.lat)
    |> put_change(:lon, res.lon)
  end

  defp apply_resolution(changeset, {:error, msg}), do: add_error(changeset, :input, msg)
  defp apply_resolution(changeset, :empty), do: add_error(changeset, :input, "is required")
end
