lib/ami/action.ex

defmodule AMI.Action do
  @typedoc """
  AMI Action type is a map of headers and values.
  Values are lists because AMI protocol allows to
  have multiple headers with the same name.

  ## Example

  ```
    iex> {:ok, action} = AMI.Action.new("QueueStatus")
    iex> {:ok, action} = AMI.Action.add_field(action, "Queue", "Sales")
    {:ok,
      %{
        "Action" => ["QueueStatus"],
        "ActionID" => ["o3webr9ub391qq9hzdrqatztk"],
        "Queue" => ["Sales"]
      }}
  ```

  Automatically adds ActionID field if not yet exists. If it is required to
  have custom ActionID then use `new/2` function.
  """
  @type t :: map()

  @doc """
  Create new AMI Action and automatically add ActionID
  """
  @spec new(action :: String.t()) :: {:ok, AMI.Action.t()} | {:invalid}
  def new(action) when is_bitstring(action) do
    new(action, [])
  end

  @doc """
  Create new AMI Action with headers list. If needed to add custom
  ActionID and skip auto-generated ActionID it can be done here.

  For example:
  ```
    iex> AMI.Action.new("QueueStatus", [
      {"ActionID", "FOO-bar1"},
      {"Queue", "Sales"}
    ])
    {:ok,
     %{
      "Action" => ["QueueStatus"],
      "ActionID" => ["FOO-bar1"],
      "Queue" => ["Sales"]
    }}
  ```
  """
  def new(action, list)
      when is_bitstring(action) and is_list(list) do
    action = String.trim(action)

    cond do
      String.length(action) == 0 ->
        {:invalid}

      true ->
        map = %{"Action" => [action]}
        create(map, list)
    end
  end

  def new(_action, _list) do
    {:invalid}
  end

  @doc """
  Create login AMI Action with given parameters.

  `user` and `passwd` will be inserted in login Action and
  the Action will be returned as string
  """
  @spec login(user :: String.t(), passwd :: String.t()) :: String.t()
  def login(user, passwd) do
    {:ok, pack} = AMI.Action.new("Login", [])
    {:ok, pack} = AMI.Action.add_field(pack, "Username", user)
    {:ok, pack} = AMI.Action.add_field(pack, "Secret", passwd)
    AMI.Action.to_string(pack)
  end

  defp create(%{} = map, [h | tail]) do
    {k, v} = h

    case add_field(map, k, v) do
      {:ok, map} -> create(map, tail)
      _ -> {:invalid}
    end
  end

  defp create(%{} = map, []) do
    if Map.has_key?(map, "ActionID") do
      {:ok, map}
    else
      add_field(map, "ActionID", action_id())
    end
  end

  def add_field(_map, "", _value) do
    {:invalid}
  end

  @doc """
  Add new field to the Action
  """
  @spec add_field(action :: AMI.Action.t(), name :: String.t(), value :: String.t()) ::
          {:ok, AMI.Action.t()}
  def add_field(%{} = action, name, value)
      when is_bitstring(name) and is_bitstring(value) do
    action =
      case Map.fetch(action, name) do
        {:ok, val} -> Map.put(action, name, [value | val])
        _ -> Map.put(action, name, [value])
      end

    {:ok, action}
  end

  @doc """
  Convert AMI Action to string
  """
  @spec to_string(action :: AMI.Action.t()) :: String.t()
  def to_string(%{} = action) do
    action
    |> Enum.map(fn {k, v} ->
      Enum.map(v, fn h -> ~s(#{k}: #{h}) end)
      |> Enum.reverse()
    end)
    |> List.flatten()
    |> List.foldr("\r\n", fn h, acc -> ~s(#{h}\r\n) <> acc end)
  end

  defp action_id() do
    base = Enum.to_list(?a..?z) ++ Enum.to_list(?0..?9)

    List.foldr(
      Enum.to_list(0..24),
      "",
      fn _, acc -> acc <> <<Enum.random(base)>> end
    )
  end
end