defmodule AMQP.Application.Channel do
  @moduledoc false

  # This module will stay as a private module at least during 2.0.x.
  # There might be non backward compatible changes on this module on 2.1.x.

  use GenServer
  require Logger
  alias AMQP.Channel

  @default_interval 5_000

  @doc """
  Starts a GenServer process linked to the current process.

  ## Examples

  Combines name and retry interval with the connection options.

      iex> opts = [proc_name: :my_chan, retry_interval: 10_000, connection: :my_conn]
      iex> :ok = AMQP.Application.Channel.start_link(opts)
      iex> {:ok, chan} = AMQP.Application.Channel.get_channel(:my_chan)

  If you omit the proc_name, it uses :default.

      iex> :ok = AMQP.Application.Channel.start_link([])
      iex> {:ok, chan} = AMQP.Application.Channel.get_channel()
      iex> {:ok, chan} = AMQP.Application.Channel.get_channel(:default)
  """
  @spec start_link(keyword) :: GenServer.on_start()
  def start_link(opts) do
    {name, init_arg} = link_opts_to_init_arg(opts)

    GenServer.start_link(__MODULE__, init_arg, name: name)
  end

  defp link_opts_to_init_arg(opts) do
    proc_name = Keyword.get(opts, :proc_name, :default)
    server_name = get_server_name(proc_name)
    retry_interval = Keyword.get(opts, :retry_interval, @default_interval)
    connection = Keyword.get(opts, :connection, proc_name)

    init_arg = %{
      retry_interval: retry_interval,
      connection: connection,
      name: proc_name,
      channel: nil
    }

    {server_name, init_arg}
  end

  @doc """
  Returns a GenServer reference for the channel name.
  """
  @spec get_server_name(binary() | atom()) :: atom()
  def get_server_name(name) do
    :"#{__MODULE__}::#{name}"
  end

  @doc false
  def get_state(name \\ :default) do
    GenServer.call(get_server_name(name), :get_state)
  catch
    :exit, {:timeout, _} -> %{}
  end

  @doc """
  Returns pid for the server referred by the name.

  It is a wrapper of `GenServer.whereis/1`.
  """
  @spec whereis(binary() | atom()) :: pid() | {atom(), node()} | nil
  def whereis(name) do
    name
    |> get_server_name()
    |> GenServer.whereis()
  end

  @doc """
  Returns a channel referred by the name.
  """
  @spec get_channel(binary | atom) :: {:ok, Channel.t()} | {:error, any}
  def get_channel(name \\ :default) do
    with false <- whereis(name) |> is_nil(),
         channel = %{} <- GenServer.call(get_server_name(name), :get_channel) do
      {:ok, channel}
    else
      true ->
        {:error, :channel_not_found}

      nil ->
        {:error, :channel_not_ready}
    end
  catch
    :exit, {:timeout, _} ->
      # This would happen when the connection or channel is stuck when opening.
      # See handle_info(:open, _) to understand - it can block the GenSever.
      {:error, :timeout}
  end

  @impl true
  def init(state) do
    Process.flag(:trap_exit, true)
    {:ok, state, {:continue, :open}}
  end

  @impl true
  def handle_continue(:open, state) do
    case AMQP.Application.Connection.get_connection(state[:connection]) do
      {:ok, conn} ->
        case Channel.open(conn) do
          {:ok, chan} ->
            true = Process.link(chan.pid)
            {:noreply, %{state | channel: chan}}

          {:error, error} ->
            Logger.error("Failed to open an AMQP channel(#{state[:name]}) - #{inspect(error)}")
            Process.send_after(self(), :open, state[:retry_interval])
            {:noreply, state}
        end

      _error ->
        Logger.error(
          "Failed to open an AMQP channel(#{state[:name]}). Connection (#{state[:connection]}) is not ready."
        )

        Process.send_after(self(), :open, state[:retry_interval])
        {:noreply, state}
    end
  end

  @impl true
  def handle_call(:get_state, _, state) do
    {:reply, state, state}
  end

  def handle_call(:get_channel, _, state) do
    if state[:channel] && Process.alive?(state[:channel].pid) do
      {:reply, state[:channel], state}
    else
      {:reply, nil, state}
    end
  end

  @impl true
  def handle_info(:open, state) do
    {:noreply, state, {:continue, :open}}
  end

  def handle_info({:EXIT, pid, _reason}, %{channel: %Channel{pid: pid}} = state)
      when is_pid(pid) do
    Logger.info("AMQP channel is gone (#{state[:name]}). Reopening...")
    {:noreply, %{state | channel: nil}, {:continue, :open}}
  end

  def handle_info({:EXIT, _from, reason}, state) do
    close(state)
    {:stop, reason, %{state | channel: nil}}
  end

  # When GenServer call gets timeout and the message arrives later,
  # it attempts to deliver the message to the server inbox.
  # AMQP handles the message but simply ignores it.
  #
  # See `GenServer.call/3` for more details.
  def handle_info({ref, _res}, state) when is_reference(ref) do
    {:noreply, state}
  end

  @impl true
  def terminate(_reason, state) do
    close(state)
    %{state | channel: nil}
  end

  defp close(%{channel: %Channel{} = channel}) do
    if Process.alive?(channel.pid) do
      Process.unlink(channel.pid)
      Channel.close(channel)
    end
  end

  defp close(_), do: :ok
end
