defmodule Agala.Backbone do
  @moduledoc """
  This behaviour specifies protocol, that should be implemented for each and every backbone,
  that can be used with `Agala` framework.
  """

  @typedoc """
  Backbone is represented by it's name as Atom
  """
  @type t :: atom()

  @doc """
  This function is given to configure the backbone.
  """
  @callback bake_backbone_config() :: :ok | no_return()

  @doc """
  This function is used to retrieve backbone's config
  """
  @callback get_config() :: any()

  @doc """
  This method is used to show bot's **receive <-> handle** load.

  * **Active Receivers** can use this information in order to stop retrieving new updates from third-parties.
  * **Passive Receivers** can use this information to stop serving for a moment until load will not decrease.

  Example:

      # For active receivers

      def get_updates() do
        # check if service is overloaded
        case Agala.Backbone.Foo.get_load(MyApp.MyBot) do
          {:ok, overload} when overload > 1000 ->
            # This server is overloaded
            # waiting a bit, to let handlers deal with overload
            :timer.sleep(10_000)
            download_updates()
          {:ok, normal} ->
            # We should not wait - load is normal
            download_updates()
        end
      end

      # For passive receivers
      def call(conn, opts) do
        # check if service is overloaded
        case Agala.Backbone.Foo.get_load(MyApp.MyBot) do
          {:ok, overload} when overload > 1000 ->
            # This server is overloaded
            # Stop serving
            send_500_http_error(conn)
          {:ok, normal} ->
            # We should not wait - load is normal
            proceed_update(conn)
        end
      end
  """
  @callback get_load(bot_name :: atom()) :: {:ok, integer} | {:error, any()}

  @doc """
  This method is uused to initialize bot. It should be probably used upon bot initialization
  """
  @callback init_bot(bot_name :: atom()) :: :ok | {:error, any()}
  @doc """
  This method is used to add new element to the end of queue, defined by `Agala.Bot` and queue's **CID**
  """
  @callback push(bot_name :: Agala.Bot.name(), cid :: any(), value :: any()) ::
              :ok | {:error, any()}

  @doc """
  This method is used to pull available element from the queue, defined by `Agala.Bot`
  """
  @callback pull(bot_name :: Agala.Bot.name()) ::
              {:ok, any()} | {:error, :empty} | {:error, any()}

  # @doc """
  # This method will subscribe caller process to get new events from the bot.

  # Messages will be of type `Agala.Conn.t()`.
  # """
  # @callback subscribe(bot_name :: Agala.Bot.name()) :: {:ok, any()} | {:error, any()}

  ### -------------------------------------------------------------------------------------------------------------------------------------------------
  ### API
  ### -------------------------------------------------------------------------------------------------------------------------------------------------

  @doc """
  Returns supervision config for specififed backbone.

  If the backbone is not specified - returns empty list
  """
  @spec supervisor() :: [atom()] | []
  def supervisor() do
    case bake_backbone_config() do
      {:ok, :empty} -> []
      {:ok, backbone} -> [backbone]
    end
  end

  @doc """
  This function will check backbone configuration.

  If everything is right - `ok tuple` with `Agala.Backbone` implementation module will be returned.
  If backbone is not specified -

  If it's not specified correct - function will rise.
  """
  @spec bake_backbone_config() :: {:ok, t} | {:ok, :empty} | no_return()
  def bake_backbone_config() do
    # Checking backbone
    case Application.get_env(:agala, :backbone, nil) do
      nil ->
        {:ok, :empty}

      backbone ->
        unless Code.ensure_loaded?(backbone) do
          raise ArgumentError,
                "backbone #{inspect(backbone)} was not compiled, " <>
                  "ensure it is correct and it is included as a module in the project"
        end

        unless Agala.Backbone in Agala.Util.behaviours_list(backbone) do
          raise ArgumentError,
                "backbone #{inspect(backbone)} does not implement Agala.Backbone behaviour, " <>
                  "ensure it is correct and it is included as a module in the project"
        end

        # All ok. Backing backbone specific config
        :ok = backbone.bake_backbone_config()
        {:ok, backbone}
    end
  end
end
