defmodule Microwaveprop.Beacons do
  @moduledoc """
  The Beacons context. Anyone can submit a beacon — authenticated or not —
  but submissions are held as unapproved until an admin approves them.
  Only approved beacons appear in the public list.
  """

  import Ecto.Query, warn: false

  alias Microwaveprop.Accounts.User
  alias Microwaveprop.Beacons.Beacon
  alias Microwaveprop.Repo

  @topic "beacons"

  @doc """
  Subscribes to beacon change notifications. Messages:

    * `{:created, %Beacon{}}`
    * `{:updated, %Beacon{}}`
    * `{:deleted, %Beacon{}}`
  """
  @spec subscribe_beacons() :: :ok | {:error, term()}
  def subscribe_beacons do
    Phoenix.PubSub.subscribe(Microwaveprop.PubSub, @topic)
  end

  defp broadcast(message) do
    Phoenix.PubSub.broadcast(Microwaveprop.PubSub, @topic, message)
  end

  @doc "Returns approved beacons ordered by most recently added."
  @spec list_beacons() :: [Beacon.t()]
  def list_beacons do
    Repo.all(
      from b in Beacon,
        where: b.approved == true,
        order_by: [desc: b.inserted_at]
    )
  end

  @doc """
  Base Ecto query for approved beacons. Used as a `live_table`
  `data_provider` so sort/search/pagination can be applied on top.
  """
  @spec approved_beacons_query() :: Ecto.Query.t()
  def approved_beacons_query do
    from b in Beacon, as: :resource, where: b.approved == true
  end

  @doc "Returns unapproved beacons awaiting admin review."
  @spec list_pending_beacons() :: [Beacon.t()]
  def list_pending_beacons do
    Repo.all(
      from b in Beacon,
        where: b.approved == false,
        order_by: [asc: b.inserted_at]
    )
  end

  @doc """
  Returns every beacon (approved or pending) that the given user submitted,
  newest first. Used by the public `/u/:callsign` profile page.
  """
  @spec list_beacons_for_user(User.t()) :: [Beacon.t()]
  def list_beacons_for_user(%User{id: user_id}) do
    Repo.all(
      from b in Beacon,
        where: b.user_id == ^user_id,
        order_by: [desc: b.inserted_at]
    )
  end

  @doc "Gets a single beacon. Raises if not found."
  @spec get_beacon!(Ecto.UUID.t()) :: Beacon.t()
  def get_beacon!(id), do: Beacon |> Repo.get!(id) |> Repo.preload(:user)

  @doc """
  Creates a beacon. When a user is provided they are recorded as the
  creator; anonymous submissions pass `nil` and leave `user_id` unset.
  """
  @spec create_beacon(User.t() | nil, map()) :: {:ok, Beacon.t()} | {:error, Ecto.Changeset.t()}
  def create_beacon(user, attrs)

  def create_beacon(%User{} = user, attrs) do
    %Beacon{user_id: user.id}
    |> Beacon.changeset(attrs)
    |> Repo.insert()
    |> broadcast_if_ok(:created)
  end

  def create_beacon(nil, attrs) do
    %Beacon{}
    |> Beacon.changeset(attrs)
    |> Repo.insert()
    |> broadcast_if_ok(:created)
  end

  @doc "Updates a beacon."
  @spec update_beacon(Beacon.t(), map()) :: {:ok, Beacon.t()} | {:error, Ecto.Changeset.t()}
  def update_beacon(%Beacon{} = beacon, attrs) do
    beacon
    |> Beacon.changeset(attrs)
    |> Repo.update()
    |> broadcast_if_ok(:updated)
  end

  @doc "Marks a beacon as approved, making it visible in the public list."
  @spec approve_beacon(Beacon.t()) :: {:ok, Beacon.t()} | {:error, Ecto.Changeset.t()}
  def approve_beacon(%Beacon{} = beacon) do
    beacon
    |> Ecto.Changeset.change(approved: true)
    |> Repo.update()
    |> broadcast_if_ok(:updated)
  end

  @doc "Deletes a beacon."
  @spec delete_beacon(Beacon.t()) :: {:ok, Beacon.t()} | {:error, Ecto.Changeset.t()}
  def delete_beacon(%Beacon{} = beacon) do
    case Repo.delete(beacon) do
      {:ok, beacon} ->
        broadcast({:deleted, beacon})
        {:ok, beacon}

      other ->
        other
    end
  end

  @doc "Returns an `%Ecto.Changeset{}` for tracking beacon changes."
  @spec change_beacon(Beacon.t(), map()) :: Ecto.Changeset.t()
  def change_beacon(%Beacon{} = beacon, attrs \\ %{}) do
    Beacon.changeset(beacon, attrs)
  end

  defp broadcast_if_ok({:ok, beacon} = result, type) do
    broadcast({type, beacon})
    result
  end

  defp broadcast_if_ok(other, _type), do: other
end
