defmodule Qtrace.Tracer do
  @moduledoc false

  use GenServer

  require Logger

  alias DogSketch.SimpleDog

  @inactivity_timeout 120_000

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def get_quantile(pid, quantile, module, function, arity) do
    GenServer.call(pid, {:get_quantile, quantile, module, function, arity})
  end

  def get_sketch(pid, module, function, arity) do
    GenServer.call(pid, {:get_sketch, module, function, arity})
  end

  def list_functions(pid) do
    GenServer.call(pid, {:list_functions})
  end

  def start_supervised(opts \\ []) do
    DynamicSupervisor.start_child(
      Qtrace.TracerSupervisor,
      Supervisor.child_spec({__MODULE__, opts}, restart: :temporary)
    )
  end

  def stop(pid) when is_pid(pid) do
    DynamicSupervisor.terminate_child(Qtrace.TracerSupervisor, pid)
  end

  def init(_opts) do
    {:ok, %{ongoing: %{}, completed: %{}}, @inactivity_timeout}
  end

  def handle_info({:trace_ts, pid, :call, {m, f, a}, ts}, state) when pid != self() do
    state = %{
      state
      | ongoing:
          Map.put(
            state.ongoing,
            {pid, m, f, length(a)},
            :erlang.convert_time_unit(ts, :nanosecond, :microsecond)
          )
    }

    {:noreply, state}
  end

  def handle_info({:trace_ts, pid, :return_from, {m, f, a}, _val, ts}, state)
      when pid != self() do
    {started_at, ongoing} = Map.pop(state.ongoing, {pid, m, f, a})

    if is_nil(started_at) do
      {:noreply, state}
    else
      sketch =
        state.completed
        |> Map.get_lazy({m, f, a}, fn -> DogSketch.SimpleDog.new(error: 0.04) end)
        |> DogSketch.SimpleDog.insert(
          max(1, :erlang.convert_time_unit(ts, :nanosecond, :microsecond) - started_at)
        )

      {:noreply,
       %{state | ongoing: ongoing, completed: Map.put(state.completed, {m, f, a}, sketch)},
       @inactivity_timeout}
    end
  end

  def handle_info(:timeout, state) do
    Logger.debug("Tracer #{inspect(self())} terminating due to inactivity")
    {:stop, :normal, state}
  end

  def handle_info(_msg, state) do
    {:noreply, state, @inactivity_timeout}
  end

  def handle_call({:get_quantile, quantile, m, f, a}, _from, state) do
    case Map.get(state.completed, {m, f, a}) do
      nil ->
        {:reply, nil, state, @inactivity_timeout}

      sketch ->
        {:reply, SimpleDog.quantile(sketch, quantile), state, @inactivity_timeout}
    end
  end

  def handle_call({:get_sketch, m, f, a}, _from, state) do
    case Map.get(state.completed, {m, f, a}) do
      nil ->
        {:reply, nil, state, @inactivity_timeout}

      sketch ->
        {:reply, sketch, state, @inactivity_timeout}
    end
  end

  def handle_call({:list_functions}, _from, state) do
    {:reply, Map.keys(state.completed), state, @inactivity_timeout}
  end
end
