defmodule MusicLibraryWeb.Telemetry.Storage do
  @moduledoc false

  use GenServer

  @buffer_size Application.compile_env(
                 :music_library,
                 [__MODULE__, :buffer_size],
                 50
               )

  def metrics_history(metric) do
    GenServer.call(__MODULE__, {:data, metric})
  end

  def start_link(args) do
    GenServer.start_link(__MODULE__, args, name: __MODULE__)
  end

  @impl true
  def init(metrics) do
    Process.flag(:trap_exit, true)

    for metric <- metrics do
      attach_handler(metric)
    end

    {:ok, metrics}
  end

  @impl true
  def terminate(_, metrics) do
    for metric <- metrics do
      :telemetry.detach({__MODULE__, metric, self()})
    end

    :ok
  end

  defp attach_handler(%{event_name: name_list} = metric) do
    :telemetry.attach(
      {__MODULE__, metric, self()},
      name_list,
      &__MODULE__.handle_event/4,
      metric
    )
  end

  def handle_event(_event_name, data, metadata, metric) do
    if data = Phoenix.LiveDashboard.extract_datapoint_for_metric(metric, data, metadata) do
      GenServer.cast(__MODULE__, {:telemetry_metric, data, metric})
    end
  end

  @impl true
  def handle_cast({:telemetry_metric, data, metric}, state) do
    key = metric_key(metric)
    label = if is_map(data), do: Map.get(data, :label)
    measurement = if is_map(data), do: Map.get(data, :measurement, 0), else: 0

    time =
      if is_map(data),
        do: Map.get(data, :time, System.system_time(:microsecond)),
        else: System.system_time(:microsecond)

    insert_and_prune(key, label, measurement, time)

    {:noreply, state}
  end

  @impl true
  def handle_call({:data, metric}, _from, state) do
    key = metric_key(metric)
    datapoints = fetch_datapoints(key)
    {:reply, datapoints, state}
  end

  defp metric_key(metric) do
    Enum.join(
      [
        inspect(metric.__struct__),
        Enum.join(metric.name, "."),
        Enum.join(metric.tags, ".")
      ],
      ":"
    )
  end

  defp insert_and_prune(key, label, measurement, time) do
    MusicLibrary.TelemetryRepo.query(
      "INSERT INTO telemetry_datapoints (metric_key, label, measurement, time) VALUES (?1, ?2, ?3, ?4)",
      [key, label, measurement, time]
    )

    MusicLibrary.TelemetryRepo.query(
      """
      DELETE FROM telemetry_datapoints
      WHERE metric_key = ?1
        AND id NOT IN (
          SELECT id FROM telemetry_datapoints
          WHERE metric_key = ?1
          ORDER BY id DESC
          LIMIT ?2
        )
      """,
      [key, @buffer_size]
    )
  catch
    _, _ -> :ok
  end

  defp fetch_datapoints(key) do
    case MusicLibrary.TelemetryRepo.query(
           "SELECT label, measurement, time FROM telemetry_datapoints WHERE metric_key = ?1 ORDER BY time ASC",
           [key]
         ) do
      {:ok, %{rows: rows}} ->
        Enum.map(rows, fn [label, measurement, time] ->
          %{label: label, measurement: measurement, time: time}
        end)

      _ ->
        []
    end
  catch
    _, _ -> []
  end
end
