defmodule Microwaveprop.Rover.FixedStation do
  @moduledoc """
  A fixed station owned by a user. Used as a known endpoint for the rover
  planner — the rover predicts SNR margins from each candidate cell back
  to the selected fixed stations.

  Either an explicit `lat`/`lon` pair or a Maidenhead `grid` is required;
  when only `grid` is provided the changeset derives the centre lat/lon
  via `Microwaveprop.Radio.Maidenhead.to_latlon/1`. Elevation is *not*
  filled in by the changeset — `Microwaveprop.Workers.StationElevationWorker`
  enriches it after insert.
  """
  use Ecto.Schema

  import Ecto.Changeset

  alias Microwaveprop.Radio.Maidenhead

  @callsign_regex ~r/^[A-Z0-9\/]{3,12}$/

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

    belongs_to :user, Microwaveprop.Accounts.User

    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, :lat, :lon, :elevation_m, :selected, :position])
    |> normalize_callsign()
    |> normalize_grid()
    |> validate_required([:callsign])
    |> validate_format(:callsign, @callsign_regex)
    |> validate_grid_format()
    |> derive_latlon_from_grid()
    |> 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)
    |> unique_constraint(:callsign, name: :fixed_stations_user_id_callsign_index)
  end

  defp normalize_callsign(changeset) do
    case get_change(changeset, :callsign) do
      nil ->
        changeset

      call when is_binary(call) ->
        put_change(changeset, :callsign, call |> String.trim() |> String.upcase())
    end
  end

  defp normalize_grid(changeset) do
    case get_change(changeset, :grid) do
      nil -> changeset
      "" -> put_change(changeset, :grid, nil)
      grid when is_binary(grid) -> put_change(changeset, :grid, normalize_grid_string(grid))
    end
  end

  # Field + Square uppercase, subsquare lowercase, extended square uppercase.
  # Maidenhead convention: A-R / 0-9 / a-x / 0-9 / A-X / ...
  defp normalize_grid_string(grid) do
    grid
    |> String.trim()
    |> String.graphemes()
    |> Enum.with_index()
    |> Enum.map_join(fn {ch, idx} ->
      pair = div(idx, 2)

      letter_pair_index = div(pair, 2)

      cond do
        # Pair 0 (Field, letters) + Pair 1 (Square, digits) +
        # Pair 2 (Subsquare, letters) + Pair 3 (Extended, digits) ...
        # Letter pairs alternate uppercase / lowercase / uppercase ...
        rem(pair, 2) == 1 -> ch
        rem(letter_pair_index, 2) == 0 -> String.upcase(ch)
        true -> String.downcase(ch)
      end
    end)
  end

  defp validate_grid_format(changeset) do
    case get_field(changeset, :grid) do
      nil ->
        changeset

      grid ->
        if Maidenhead.valid?(grid) do
          changeset
        else
          add_error(changeset, :grid, "is not a valid Maidenhead grid")
        end
    end
  end

  defp derive_latlon_from_grid(changeset) do
    lat = get_field(changeset, :lat)
    lon = get_field(changeset, :lon)
    grid = get_field(changeset, :grid)

    cond do
      not changeset.valid? ->
        changeset

      is_number(lat) and is_number(lon) ->
        changeset

      is_binary(grid) ->
        case Maidenhead.to_latlon(grid) do
          {:ok, {derived_lat, derived_lon}} ->
            changeset
            |> put_change(:lat, derived_lat)
            |> put_change(:lon, derived_lon)

          :error ->
            changeset
        end

      true ->
        changeset
    end
  end
end
