defmodule Bundlex.CNode.Server do
  @moduledoc false

  use GenServer
  require Logger
  alias Bundlex.CNode.NameStore

  @impl true
  def init(opts) do
    Process.flag(:trap_exit, true)
    if opts.link?, do: Process.monitor(opts.caller)
    :ok = ensure_node_distributed()
    {name, creation} = NameStore.get_name()
    cnode = :"#{name}@#{host_name()}"
    cookie = Node.get_cookie() |> Atom.to_charlist()

    port =
      Port.open(
        {:spawn_executable, Bundlex.build_path(opts.app, opts.native_name, :cnode)},
        args: [host_name(), name, cnode, "#{creation}"],
        line: 2048,
        env: [{~c"BUNDLEX_ERLANG_COOKIE", cookie}]
      )

    Process.send_after(self(), :timeout, 5000)

    {:ok,
     %{
       port: port,
       status: :waiting,
       caller: opts.caller,
       link?: opts.link?,
       cnode: cnode,
       msg_part?: false
     }}
  catch
    err, reason -> {:stop, {err, reason}}
  end

  @impl true
  def handle_info(
        {port, {:data, {:eol, ~c"ready"}}},
        %{port: port, status: :waiting, msg_part?: false} = state
      ) do
    case Node.connect(state.cnode) do
      true ->
        send(state.caller, {self(), {:ok, %Bundlex.CNode{server: self(), node: state.cnode}}})
        {:noreply, %{state | status: :connected}}

      _connect_failed ->
        send(state.caller, {self(), {:error, :connect_to_cnode}})
        {:stop, :normal, state}
    end
  end

  def handle_info({port, {:data, {flag, data}}}, %{port: port} = state) do
    Logger.info("cnode#{inspect(self())}: #{data}")
    {:noreply, %{state | msg_part?: flag == :noeol}}
  end

  def handle_info(:timeout, state) do
    case state.status do
      :waiting ->
        send(state.caller, {self(), {:error, :spawn_cnode}})
        {:stop, :normal, state}

      _status ->
        {:noreply, state}
    end
  end

  def handle_info({:DOWN, _ref, :process, pid, _reason}, %{caller: pid} = state) do
    disconnect(state.cnode)
    {:stop, :normal, state}
  end

  def handle_info({:EXIT, port, :normal}, %{port: port} = state) do
    {:stop, :normal, state}
  end

  def handle_info({:EXIT, port, reason}, %{port: port} = state) do
    if state.link?, do: Process.exit(state.caller, :shutdown)
    {:stop, reason, state}
  end

  def handle_info({:EXIT, _from, :normal}, state) do
    {:noreply, state}
  end

  def handle_info({:EXIT, _from, reason}, state) do
    if state.link?, do: Process.exit(state.caller, reason)
    {:stop, reason, state}
  end

  @impl true
  def handle_call(:stop, _from, state) do
    {:stop, :normal, disconnect(state.cnode), state}
  end

  defp ensure_node_distributed(empd_status \\ :unknown) do
    if Node.alive?() do
      :ok
    else
      case Node.start(NameStore.get_self_name(), :shortnames) do
        {:ok, _pid} ->
          Node.set_cookie(:bundlex_cookie)
          :ok

        {:error, {:already_started, _pid}} ->
          # In case the node has been started after the `Node.alive?` check
          :ok

        {:error, _reason} when empd_status == :unknown ->
          Logger.info("Trying to start epmd...")
          System.cmd("epmd", ~w(-daemon))
          # ensure epmd finished starting
          System.cmd("epmd", ~w(-names))
          ensure_node_distributed(:start_tried)

        {:error, reason} ->
          {:error, reason}
      end
    end
  end

  defp disconnect(cnode) do
    case Node.disconnect(cnode) do
      true ->
        NameStore.return_name(cnode |> node_name)
        :ok

      _disconnect_failed ->
        {:error, :disconnect_cnode}
    end
  end

  defp node_name(node) do
    node |> to_string() |> String.split("@") |> List.first()
  end

  defp host_name(node \\ Node.self()) do
    node |> to_string() |> String.split("@") |> List.last()
  end
end
