defmodule Wobserver.Util.Metrics.Prometheus do
  @moduledoc ~S"""
  Prometheus formatter.

  Formats metrics in a for Prometheus readable way.
  See: [https://prometheus.io/docs/instrumenting/writing_exporters/](https://prometheus.io/docs/instrumenting/writing_exporters/)
  """

  @behaviour Wobserver.Util.Metrics.Formatter

  @doc ~S"""
  Format a set of `data` with a `label` for a Prometheus.

  The `data` must be given as a `list` of tuples with the following format: `{value, labels}`, where `labels` is a keyword list with labels and their values.

  The following options can also be given:
    - `type`, the type of the metric. The following values are currently supported: `:gauge`, `:counter`.
    - `help`, a single line text description of the metric.
  """
  @spec format_data(
    name :: String.t,
    data :: [{integer | float, keyword}],
    type :: :atom,
    help :: String.t
  ) :: String.t
  def format_data(name, data, type, help) do
    "#{format_help name, help}#{format_type name, type}#{format_values name, data}"
  end

  @doc ~S"""
  Combines formatted metrics together.

  Arguments:
    - `metrics`, a list of formatted metrics for one node.

  Example:
      iex> combine_metrics ["metric1{node="127.0.0.1"} 5\n", "metric2{node="127.0.0.1"} 5\n"]
      "metric1{node="127.0.0.1"} 5\n", "metric2{node="127.0.0.1"} 5\n"
  """
  @spec combine_metrics(
    metrics :: list[String.t]
  ) :: String.t
  def combine_metrics(metrics), do: Enum.join(metrics)

  @doc ~S"""
  Merges formatted sets of metrics from different nodes together.

  The merge will filter out double declarations of help and type.

  Arguments:
    - `metrics`, a list of formatted sets metrics for multiple node.

  Example:
      iex> combine_metrics ["metric{node="192.168.0.6"} 5\n", "metric{node="192.168.0.5"} 5\n"]
      "metric{node="192.168.0.6"} 5\n", "metric{node="192.168.0.7"} 5\n"
  """
  @spec merge_metrics(
    metrics :: list[String.t]
  ) :: String.t
  def merge_metrics(metrics) do
    {combined, _} = Enum.reduce(metrics, {"", []}, &filter/2)

    combined
  end

  # Helpers

  defp format_help(_name, nil), do: ""
  defp format_help(name, help) do
    "\# HELP #{name} #{help}\n"
  end

  defp format_type(_name, nil), do: ""
  defp format_type(name, type) do
    "\# TYPE #{name} #{type}\n"
  end

  defp format_labels(labels) do
    labels
    |> Enum.map(fn {label, value} -> "#{label}=\"#{value}\"" end)
    |> Enum.join(",")
  end

  defp format_values(name, data) do
    data
    |> Enum.map(fn {value, labels} -> "#{name}{#{format_labels labels}} #{value}\n" end)
    |> Enum.join
  end

  defp analyze_metrics(metrics) do
    help =
      ~r/^\# HELP ([a-zA-Z_]+\ )/m
      |> Regex.scan(metrics)
      |> Enum.map(fn [match | _] -> match end)

    type =
      ~r/^\# TYPE ([a-zA-Z_]+\ )/m
      |> Regex.scan(metrics)
      |> Enum.map(fn [match | _] -> match end)

    help ++ type
  end

  defp filter_line(line, filter) do
    filter
    |> Enum.find_value(false, &String.starts_with?(line, &1))
    |> Kernel.!
  end

  defp filter(metric, {metrics, filter}) do
    filtered_metric =
      metric
      |> String.split("\n")
      |> Enum.filter(&filter_line(&1, filter))
      |> Enum.join("\n")

    updated_filter =
      filtered_metric
      |> analyze_metrics()
      |> Kernel.++(filter)

    {metrics <> filtered_metric, updated_filter}
  end
end
