defmodule Microwaveprop.Rover do
  @moduledoc """
  Context for managing a user's fixed stations and providing the
  rover-planning defaults.

  All mutations are scoped to a user — `update_station/3`, `delete_station/2`,
  and `toggle_selected/2` return `{:error, :not_found}` when invoked on
  a station belonging to a different user.
  """

  import Ecto.Query

  alias Microwaveprop.Accounts.User
  alias Microwaveprop.Radio.Maidenhead
  alias Microwaveprop.Repo
  alias Microwaveprop.Rover.FixedStation
  alias Microwaveprop.Rover.Location

  @default_stations [
    %{callsign: "W5LUA", grid: "EM13qc"},
    %{callsign: "W5HN", grid: "EM13nf"},
    %{callsign: "N5XU", grid: "EM10cl"}
  ]

  @spec list_stations(User.t()) :: [FixedStation.t()]
  def list_stations(%User{id: user_id}) do
    FixedStation
    |> where([s], s.user_id == ^user_id)
    |> order_by([s], asc: s.position, asc: s.inserted_at)
    |> Repo.all()
  end

  @spec create_station(User.t(), map()) :: {:ok, FixedStation.t()} | {:error, Ecto.Changeset.t()}
  def create_station(%User{} = user, attrs) do
    attrs_with_position = put_position_default(attrs, next_position(user))

    %FixedStation{user_id: user.id}
    |> FixedStation.changeset(attrs_with_position)
    |> Repo.insert()
    |> maybe_enqueue_elevation()
  end

  defp put_position_default(attrs, position) when is_map(attrs) do
    cond do
      Map.has_key?(attrs, :position) -> attrs
      Map.has_key?(attrs, "position") -> attrs
      string_keyed?(attrs) -> Map.put(attrs, "position", position)
      true -> Map.put(attrs, :position, position)
    end
  end

  defp string_keyed?(attrs) do
    Enum.any?(attrs, fn {k, _v} -> is_binary(k) end)
  end

  @spec update_station(User.t(), Ecto.UUID.t(), map()) ::
          {:ok, FixedStation.t()} | {:error, :not_found | Ecto.Changeset.t()}
  def update_station(%User{} = user, id, attrs) do
    case fetch_owned(user, id) do
      {:ok, station} ->
        station
        |> FixedStation.changeset(attrs)
        |> Repo.update()

      {:error, :not_found} ->
        {:error, :not_found}
    end
  end

  @spec delete_station(User.t(), Ecto.UUID.t()) ::
          {:ok, FixedStation.t()} | {:error, :not_found}
  def delete_station(%User{} = user, id) do
    case fetch_owned(user, id) do
      {:ok, station} ->
        Repo.delete(station)

      {:error, :not_found} ->
        {:error, :not_found}
    end
  end

  @spec toggle_selected(User.t(), Ecto.UUID.t()) ::
          {:ok, FixedStation.t()} | {:error, :not_found | Ecto.Changeset.t()}
  def toggle_selected(%User{} = user, id) do
    case fetch_owned(user, id) do
      {:ok, station} ->
        station
        |> Ecto.Changeset.change(selected: not station.selected)
        |> Repo.update()

      {:error, :not_found} ->
        {:error, :not_found}
    end
  end

  @doc """
  Three hardcoded NTMS stations used as the anonymous-user fallback in
  the rover planner. Plain maps shaped like `FixedStation`, with lat/lon
  derived from each grid via `Maidenhead.to_latlon/1`.
  """
  @spec default_stations() :: [map()]
  def default_stations do
    @default_stations
    |> Enum.with_index()
    |> Enum.map(fn {%{callsign: callsign, grid: grid}, index} ->
      {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)

      %{
        callsign: callsign,
        grid: grid,
        lat: lat,
        lon: lon,
        elevation_m: nil,
        selected: true,
        position: index
      }
    end)
  end

  defp fetch_owned(%User{id: user_id}, id) do
    case Repo.get(FixedStation, id) do
      %FixedStation{user_id: ^user_id} = station -> {:ok, station}
      _ -> {:error, :not_found}
    end
  end

  # ── Rover locations (globally visible, logged-in to mutate) ──────────

  @doc """
  Returns every rover location across all users, newest first.
  Anonymous visitors can read this list; mutations require a User.
  """
  @spec list_locations() :: [Location.t()]
  def list_locations do
    Location
    |> order_by([l], desc: l.inserted_at)
    |> Repo.all()
  end

  @spec create_location(User.t(), map()) ::
          {:ok, Location.t()} | {:error, Ecto.Changeset.t()}
  def create_location(%User{} = user, attrs) do
    %Location{user_id: user.id}
    |> Location.changeset(attrs)
    |> Repo.insert()
  end

  @spec update_location(User.t(), Ecto.UUID.t(), map()) ::
          {:ok, Location.t()} | {:error, :not_found | Ecto.Changeset.t()}
  def update_location(%User{} = user, id, attrs) do
    case fetch_owned_location(user, id) do
      {:ok, location} ->
        location
        |> Location.changeset(attrs)
        |> Repo.update()

      {:error, :not_found} ->
        {:error, :not_found}
    end
  end

  @spec delete_location(User.t(), Ecto.UUID.t()) ::
          {:ok, Location.t()} | {:error, :not_found}
  def delete_location(%User{} = user, id) do
    case fetch_owned_location(user, id) do
      {:ok, location} -> Repo.delete(location)
      {:error, :not_found} -> {:error, :not_found}
    end
  end

  defp fetch_owned_location(%User{is_admin: true}, id) do
    case Repo.get(Location, id) do
      %Location{} = loc -> {:ok, loc}
      _ -> {:error, :not_found}
    end
  end

  defp fetch_owned_location(%User{id: user_id}, id) do
    case Repo.get(Location, id) do
      %Location{user_id: ^user_id} = loc -> {:ok, loc}
      _ -> {:error, :not_found}
    end
  end

  defp next_position(%User{id: user_id}) do
    query =
      from s in FixedStation,
        where: s.user_id == ^user_id,
        select: count(s.id)

    Repo.one(query) || 0
  end

  defp maybe_enqueue_elevation({:ok, %FixedStation{elevation_m: nil, id: id}} = result) do
    enqueue_station_elevation(id)
    result
  end

  defp maybe_enqueue_elevation(other), do: other

  # Wrapped behind a function-exported guard so this module compiles
  # cleanly while Task 8's worker is still being authored — the worker
  # is referenced by atom and resolved at runtime once it's loaded.
  defp enqueue_station_elevation(id) do
    worker = Microwaveprop.Workers.StationElevationWorker

    if Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do
      %{id: id} |> worker.new() |> Oban.insert()
    end
  end
end
