defmodule Tracer.Server do
  @moduledoc """
  Orchestrates the tracing session
  """

  use GenServer
  alias __MODULE__
  alias Tracer.{AgentCmds, ToolServer, ProbeList, Tool}

  @server_name __MODULE__
  defstruct tool_server_pid: nil,
            tracing: false,
            forward_pid: nil,
            probes: [],
            node: nil,
            agent_pids: [],
            tool: nil

  defmacro ensure_server_up(do: clauses) do
    quote do
      case Process.whereis(@server_name) do
        nil ->
          start()
          unquote(clauses)
        pid ->
          unquote(clauses)
      end
    end
  end

  def start_link(params) do
    GenServer.start_link(__MODULE__, params, [name: @server_name])
  end

  def start do
    Tracer.Supervisor.start_server()
  end

  def stop do
    case Process.whereis(@server_name) do
      nil -> {:error, :not_running}
      pid ->
        GenServer.call(@server_name, :stop_tool)
        Tracer.Supervisor.stop_server(pid)
    end
  end

  [:stop_tool]
  |> Enum.each(fn cmd ->
    def unquote(cmd)() do
      ensure_server_up do
        GenServer.call(@server_name, unquote(cmd))
      end
    end
  end)

  def set_tool(%{"__tool__": _} = tool) do
    with :ok <- Tool.valid?(tool) do
      ensure_server_up do
        GenServer.call(@server_name, {:set_tool, tool})
      end
    end
  end
  def set_tool(_) do
    raise ArgumentError, message: "Argument is not a tool"
  end

  def start_tool(%{"__tool__": _} = tool) do
    with :ok <- Tool.valid?(tool) do
      ensure_server_up do
        GenServer.call(@server_name, {:start_tool, tool})
      end
    end
  end

  def init(_params) do
    Process.flag(:trap_exit, true)
    {:ok, %Server{}}
  end

  def handle_call({:set_tool, tool}, _from, %Server{} = state) do
    {:reply, :ok, put_in(state.tool, tool)}
  end

  def handle_call({:start_tool, tool}, _from, %Server{} = state) do
    with  %Server{} = state <- stop_if_tracing(state),
          %Server{} = state <- get_running_config(state, tool),
          :ok <- ProbeList.valid?(state.probes),
          ret when is_pid(ret) <- ToolServer.start(tool) do
      state = state
      |> Map.put(:tool_server_pid, ret)

      agent_opts = Tool.get_agent_opts(tool)
      node = Keyword.get(agent_opts, :node, state.node)
      {ret, new_state} = case AgentCmds.start(node,
                                           state.probes,
                                           [forward_pid: state.tool_server_pid]
                                              ++ agent_opts) do
        {:error, error} ->
          {{:error, error}, state}
        agent_pids ->
          new_state = state
          |> Map.put(:agent_pids, agent_pids)
          |> Map.put(:node, node)
          |> Map.put(:tracing, true)
          # TODO get a notification from agents
          :timer.sleep(5)
          report_message(state,
                         :started_tracing,
                         "started tracing")
          {:ok, new_state}
      end

      {:reply, ret, new_state}
    else
      error ->
        {:reply, error, state}
    end
  end

  def handle_call(:stop_tool, _from, %Server{} = state) do
    {ret, state} = handle_stop_trace(state)
    {:reply, ret, state}
  end

  def handle_info({:EXIT, pid, :done_reporting},
      %Server{} = state) do
    if state.tool_server_pid == pid do
      {:noreply, put_in(state.tool_server_pid, nil)}
    else
      {:noreply, state}
    end
  end
  def handle_info({:EXIT, pid, {:done_tracing, exit_status}},
      %Server{} = state) do
    state = handle_agent_exit(state, pid)
    report_message(state,
                   {:done_tracing, exit_status},
                   "done tracing: #{inspect exit_status}")
    {:noreply, state}
  end
  def handle_info({:EXIT, pid, {:done_tracing, key, val}},
      %Server{} = state) do
    state = handle_agent_exit(state, pid)
    report_message(state,
                   {:done_tracing, key, val},
                   "done tracing: #{to_string(key)} #{val}")
    {:noreply, state}
  end
  def handle_info({:EXIT, pid, exit_code},
      %Server{} = state) do
    state = handle_agent_exit(state, pid)
    report_message(state,
                   {:done_tracing, exit_code},
                   "done tracing: #{inspect exit_code}")
    {:noreply, state}
  end

  defp get_running_config(state, tool) do
    tool
    |> Tool.get_probes()
    |> case do
      nil -> state
      probes when is_list(probes) ->
        put_in(state.probes, probes)
      probe ->
        put_in(state.probes, [probe])
    end
    |> Map.put(:forward_pid, Tool.get_forward_to(tool))
  end

  defp report_message(state, event, message) do
    if is_pid(state.forward_pid) do
      send state.forward_pid, event
    else
      IO.puts(message)
    end
  end

  defp handle_stop_trace(state) do
    {ret, state} = case AgentCmds.stop(state.agent_pids) do
      {:error, error} ->
        {{:error, error}, state}
      _ ->
        new_state = state
        |> Map.put(:agent_pids, [])
        |> Map.put(:node, nil)
        |> Map.put(:tracing, false)
        {:ok, new_state}
    end

    ToolServer.stop(state.tool_server_pid)
    state = put_in(state.tool_server_pid, nil)
    {ret, state}
  end

  defp handle_agent_exit(state, pid) do
    agent_pids = state.agent_pids -- [pid]
    state = put_in(state.agent_pids, agent_pids)
    if Enum.empty?(agent_pids) do
      {_ret, state} = handle_stop_trace(state)
      state
    else
      state
    end
  end

  defp stop_if_tracing(%Server{tracing: false} = state), do: state
  defp stop_if_tracing(state), do: elem(handle_stop_trace(state), 1)
end
