defmodule Forge.EPMD do
  @moduledoc false

  # From Erlang/OTP 23+
  @epmd_dist_version 6

  @doc ~S"""
  This is the distribution port of the current node.

  The parent node must be named `expert-manager-*`.
  The child node must be named `expert-project-*`.

  When the parent boots the child, it must pass
  its node name and port as the respective environment
  variables `EXPERT_PARENT_NODE` and `EXPERT_PARENT_PORT`.

  The parent must have this as a child in its supervision tree:

      {Forge.NodePortMapper, []}

  The child, in turn, must have this:

      {Task, &Forge.NodePortMapper.register/0}

  This will register the child within the parent, so they can
  find each other.

  ## Example

  In order to manually simulate the connections, run `elixirc epmd.ex` to compile
  this file and follow the steps below. Notice we call the functions in the
  `Forge.NodePortMapper` module directly, while in practice they will be called
  as part of the app's supervision tree.

      # In one node
      $ iex --erl "-start_epmd false -epmd_module Elixir.Forge.EPMD" --sname expert-manager-foo
      iex(expert_parent_foo@macstudio)> Forge.NodePortMapper.start_link([])
      iex(expert_parent_foo@macstudio)> Forge.EPMD.dist_port()
      52914

  Get the port name from the step above and then, in another terminal, do:

      $ EXPERT_PARENT_NODE=expert_parent_foo@macstudio EXPERT_PARENT_PORT=52914 \
          iex --erl "-start_epmd false -epmd_module Elixir.Forge.EPMD" --sname expert-project-bar
      iex> Forge.NodePortMapper.register()

  And in another terminal:

      $ EXPERT_PARENT_NODE=expert_parent_foo@macstudio EXPERT_PARENT_PORT=52914 \
          iex --erl "-start_epmd false -epmd_module Elixir.Forge.EPMD" --sname expert-project-baz
      iex> Forge.NodePortMapper.register()

  If you try `Node.ping(:expert-project-bar@HOSTNAME)` from the last node, it should work.
  The child nodes will find each other even without EPMD.
  """
  def dist_port do
    :persistent_term.get(:expert_dist_port, nil)
  end

  # EPMD callbacks

  def register_node(name, port), do: register_node(name, port, :inet)

  def register_node(_name, port, _family) do
    # We never connect to EPMD, so we pretend the registration is successful without doing anything.
    # We only care about the distribution port
    :persistent_term.put(:expert_dist_port, port)

    {:ok, -1}
  end

  def port_please(name, host), do: port_please(name, host, :infinity)

  def port_please(~c"expert-manager-" ++ _ = _name, _host, _timeout) do
    # We only return the port if we have the parent port env var set.
    # We must NOT fall back to :erl_epmd.port_please/3 because the system
    # EPMD daemon may have stale entries with incorrect port numbers.
    if port = System.get_env("EXPERT_PARENT_PORT") do
      {:port, String.to_integer(port), @epmd_dist_version}
    else
      :noport
    end
  end

  def port_please(~c"expert-project-" ++ _ = name, host, _timeout) do
    host_string = format_host(host)
    full_node = :"#{name}@#{host_string}"

    if port = Forge.NodePortMapper.get_port(full_node) do
      {:port, port, @epmd_dist_version}
    else
      :noport
    end
  end

  def port_please(_name, _host, _timeout) do
    :noport
  end

  defp format_host({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}"
  defp format_host(host) when is_list(host), do: List.to_string(host)

  def names(_host_name), do: {:error, :address}

  defdelegate start_link, to: :erl_epmd
  defdelegate listen_port_please(name, host), to: :erl_epmd
  defdelegate address_please(name, host, family), to: :erl_epmd
end
