defmodule Sippet do
  @moduledoc """
  Holds the Sippet stack.

  Network transport protocols should be registered during initialization:

      def init(_) do
        Sippet.register_transport(:udp, false)
        ...
      end

  Messages are dispatched to transports by sending the following message:

      send(pid, {:send_message, message, host, port, transaction})

  Whenever a message is received by a transport, the function
  `Sippet.handle_transport_message` is called, which will validate and route
  messages through the transaction layer or send directly to the core.
  """

  use Supervisor

  import Kernel, except: [send: 2]

  alias Sippet.{Message, Transactions}
  alias Sippet.Message.{RequestLine, StatusLine}

  require Logger

  @typedoc "A SIP message request"
  @type request :: Message.request()

  @typedoc "A SIP message response"
  @type response :: Message.response()

  @typedoc "An network error that occurred while sending a message"
  @type reason :: term

  @typedoc "A client transaction identifier"
  @type client_key :: Transactions.Client.Key.t()

  @typedoc "A server transaction identifier"
  @type server_key :: Transactions.Server.Key.t()

  @typedoc "Sippet identifier"
  @type sippet :: atom

  @doc """
  Handles the sigil `~K`.

  It returns a client or server transaction key depending on the number of
  parameters passed.

  ## Examples

      iex> import Sippet, only: [sigil_K: 2]

      iex> Sippet.Transactions.Client.Key.new("z9hG4bK230f2.1", :invite)
      ~K[z9hG4bK230f2.1|:invite]

      iex> ~K[z9hG4bK230f2.1|INVITE]
      ~K[z9hG4bK230f2.1|:invite]

      iex> Sippet.Transactions.Server.Key.new("z9hG4bK74b21", :invite, {"client.biloxi.example.com", 5060})
      ~K[z9hG4bK74b21|:invite|client.biloxi.example.com:5060]

      iex> ~K[z9hG4bK74b21|INVITE|client.biloxi.example.com:5060]
      ~K[z9hG4bK74b21|:invite|client.biloxi.example.com:5060]

  """
  def sigil_K(string, _) do
    case String.split(string, "|") do
      [branch, method] ->
        Transactions.Client.Key.new(branch, sigil_to_method(method))

      [branch, method, sentby] ->
        [host, port] = String.split(sentby, ":")

        Transactions.Server.Key.new(
          branch,
          sigil_to_method(method),
          {host, String.to_integer(port)}
        )
    end
  end

  defp sigil_to_method(method) do
    case method do
      ":" <> rest -> Message.to_method(rest)
      other -> Message.to_method(other)
    end
  end

  @doc """
  Sends a message (request or response) using transactions if possible.

  Requests of method `:ack` is sent directly to the transport layer.

  A `Sippet.Transactions.Client` is created for requests to handle client
  retransmissions, when the transport presumes it, and match response
  retransmissions, so the `Sippet.Core` doesn't get retransmissions other than
  200 OK for `:invite` requests.

  In case of success, returns `:ok`.
  """
  @spec send(sippet, request | response) :: :ok | {:error, reason}
  def send(sippet, message) when is_atom(sippet) do
    unless Message.valid?(message) do
      raise ArgumentError, "expected :message argument to be a valid SIP message"
    end

    do_send(sippet, message)
  end

  defp do_send(sippet, %Message{start_line: %RequestLine{method: :ack}} = request),
    do: Sippet.Router.send_transport_message(sippet, request, nil)

  defp do_send(sippet, %Message{start_line: %RequestLine{}} = outgoing_request),
    do: Sippet.Router.send_transaction_request(sippet, outgoing_request)

  defp do_send(sippet, %Message{start_line: %StatusLine{}} = outgoing_response),
    do: Sippet.Router.send_transaction_response(sippet, outgoing_response)

  @doc """
  Verifies if the transport protocol used to send the given message is
  reliable.
  """
  @spec reliable?(sippet, Message.t()) :: boolean
  def reliable?(sippet, %Message{headers: %{via: [via | _]}})
      when is_atom(sippet) do
    {_version, protocol, _host_and_port, _params} = via

    case Registry.lookup(sippet, {:transport, protocol}) do
      [{_, reliable}] ->
        reliable

      _ ->
        raise ArgumentError, message: "protocol not registered"
    end
  end

  @doc """
  Registers a transport for a given protocol.
  """
  @spec register_transport(sippet, atom, boolean) :: :ok | {:error, :already_registered}
  def register_transport(sippet, protocol, reliable)
      when is_atom(sippet) and is_atom(protocol) and is_boolean(reliable) do
    case Registry.register(sippet, {:transport, protocol}, reliable) do
      {:ok, _} ->
        :ok

      {:error, {:already_registered, _}} ->
        {:error, :already_registered}
    end
  end

  @doc """
  Registers the stack core.
  """
  @spec register_core(sippet, atom) :: :ok
  def register_core(sippet, module)
      when is_atom(sippet) and is_atom(module) do
    Registry.put_meta(sippet, :core, module)
  end

  @doc """
  Terminates a client or server transaction forcefully.

  This function is not generally executed by entities; there is a single case
  where it is fundamental, which is when a client transaction is in proceeding
  state for a long time, and the transaction has to be finished forcibly, or it
  will never finish by itself.

  If a transaction with such a key does not exist, it will be silently ignored.
  """
  @spec terminate(sippet, client_key | server_key) :: :ok
  def terminate(sippet, key) do
    case Registry.lookup(sippet, {:transaction, key}) do
      [] ->
        :ok

      [{pid, _}] ->
        # Send the response through the existing server key.
        case key do
          %Transactions.Client.Key{} ->
            Transactions.Client.terminate(pid)

          %Transactions.Server.Key{} ->
            Transactions.Server.terminate(pid)
        end
    end
  end

  @doc false
  def start_link(options) when is_list(options) do
    case Keyword.fetch(options, :name) do
      {:ok, name} when is_atom(name) ->
        :ok

      {:ok, other} ->
        raise ArgumentError, "expected :name to be an atom, got: #{inspect(other)}"

      :error ->
        raise ArgumentError, "expected :name option to be present"
    end

    Supervisor.start_link(__MODULE__, options)
  end

  @doc false
  def child_spec(options) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [options]}
    }
  end

  @impl true
  def init(options) do
    children = [
      {Registry, name: options[:name], keys: :unique, partitions: System.schedulers_online()},
      {DynamicSupervisor, strategy: :one_for_one, name: supervisor_name(options[:name])}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  @doc false
  def supervisor_name(name) do
    Module.concat(name, "Supervisor")
  end
end
