defmodule Oban.Met do
  @external_resource readme = Path.join([__DIR__, "../README.md"])

  @moduledoc readme
             |> File.read!()
             |> String.split("<!-- MDOC -->")
             |> Enum.fetch!(1)

  use Supervisor

  alias Oban.Met.{Cronitor, Examiner, Listener, Recorder, Reporter, Value}
  alias Oban.Registry

  @type counts :: %{optional(String.t()) => non_neg_integer()}
  @type sub_counts :: %{optional(String.t()) => non_neg_integer() | counts()}

  @type series :: atom() | String.t()
  @type value :: Value.t()
  @type label :: String.t()
  @type ts :: integer()

  @type filter_value :: label() | [label()]

  @type series_detail :: %{series: series(), labels: [label()], value: module()}

  @type operation :: :max | :sum | {:pct, float()}

  @type latest_opts :: [
          filters: keyword(filter_value()),
          group: nil | label(),
          lookback: pos_integer()
        ]

  @type timeslice_opts :: [
          by: pos_integer(),
          filters: keyword(filter_value()),
          group: nil | label(),
          label: nil | label(),
          lookback: pos_integer(),
          operation: operation(),
          since: pos_integer()
        ]

  @doc false
  @spec child_spec(Keyword.t()) :: Supervisor.child_spec()
  def child_spec(opts) do
    conf = Keyword.fetch!(opts, :conf)
    name = Registry.via(conf.name, __MODULE__)

    opts
    |> super()
    |> Map.put(:id, name)
  end

  @doc """
  Start a Met supervisor for an Oban instance.

  `Oban.Met` typically starts supervisors automatically when Oban instances initialize. However,
  starting a supervisor manually can be used if `auto_start` is disabled.

  ## Options

  These options are required; without them the supervisor won't start:

  * `:conf` — configuration for a running Oban instance, required

  * `:name` — an optional name for the supervisor, defaults to `Oban.Met`

  ## Example

  Start a supervisor for the default Oban instance:

      Oban.Met.start_link(conf: Oban.config())

  Start a supervisor with a custom name:

      Oban.Met.start_link(conf: Oban.config(), name: MyApp.MetSup)
  """
  @spec start_link(Keyword.t()) :: Supervisor.on_start()
  def start_link(opts) when is_list(opts) do
    conf = Keyword.fetch!(opts, :conf)
    name = Registry.via(conf.name, __MODULE__)

    Supervisor.start_link(__MODULE__, opts, name: name)
  end

  @doc """
  Retrieve stored producer checks.

  This mimics the output of the legacy `Oban.Web.Plugins.Stats.all_gossip/1` function.

  Checks are queried approximately every second and broadcast to all connected nodes, so each node
  is a replica of checks from the entire cluster. Checks are stored for 30 seconds before being
  purged.

  ## Output

  Checks are the result of `Oban.check_queue/1`, and the exact contents depends on which
  `Oban.Engine` is in use. A `Basic` engine check will look similar to this:

      %{
        uuid: "2dde4c0f-53b8-4f59-9a16-a9487454292d",
        limit: 10,
        node: "me@local",
        paused: false,
        queue: "default",
        running: [100, 102],
        started_at: ~D[2020-10-07 15:31:00],
        updated_at: ~D[2020-10-07 15:31:00]
      }

  ## Examples

  Get all current checks:

      Oban.Met.checks()

  Get current checks for a non-standard Oban isntance:

      Oban.Met.checks(MyOban)
  """
  @spec checks(Oban.name()) :: [map()]
  def checks(oban \\ Oban) do
    oban
    |> Registry.via(Examiner)
    |> Examiner.all_checks()
  end

  @doc """
  Get a normalized, unified crontab from all connected nodes.

  ## Examples

  Get a merged crontab:

      Oban.Met.crontab()
      [
        {"* * * * *", "Worker.A", []},
        {"* * * * *", "Worker.B", [["args", %{"mode" => "foo"}]]}
      ]

  Get the crontab for a non-standard Oban instance:

      Oban.Met.crontab(MyOban)
  """
  @spec crontab(Oban.name()) :: [{binary(), binary(), map()}]
  def crontab(oban \\ Oban) do
    oban
    |> Registry.via(Cronitor)
    |> Cronitor.merged_crontab()
  end

  @doc """
  Get all stored, unique values for a particular label.

  ## Examples

  Get all known queues:

      Oban.Met.labels("queue")
      ~w(alpha gamma delta)

  Get all known workers:

      Oban.Met.labels("worker")
      ~w(MyApp.Worker MyApp.OtherWorker)
  """
  @spec labels(Oban.name(), label(), keyword()) :: [label()]
  def labels(oban \\ Oban, label, opts \\ []) do
    oban
    |> Registry.via(Recorder)
    |> Recorder.labels(to_string(label), opts)
  end

  @doc """
  Get all stored values for a series without any filtering.
  """
  @spec lookup(Oban.name(), series()) :: [term()]
  def lookup(oban \\ Oban, series) do
    oban
    |> Registry.via(Recorder)
    |> Recorder.lookup(to_string(series))
  end

  @doc """
  Get the latest values for a gauge series, optionally subdivided by a label.

  Unlike queues and workers, states are static and constant, so they'll always show up in the
  counts or subdivision maps.

  ## Gauge Series

  Latest counts only apply to `Gauge` series. There are two gauges available (as reported by
  `series/1`:

  * `:exec_count` — jobs executing at that moment, including `node`, `queue`, `state`, and
    `worker` labels.

  * `:full_count` — jobs in the database, including `queue`, and `state` labels.

  ## Examples

  Get the `:full_count` value without any grouping:

      Oban.Met.latest(:full_count)
      %{"all" => 99}

  Group the `:full_count` value by state:

      Oban.Met.latest(:full_count, group: "state")
      %{"available" => 9, "completed" => 80, "executing" => 5, ...

  Group results by queue:

      Oban.Met.latest(:exec_count, group: "queue")
      %{"alpha" => 9, "gamma" => 3}

  Group results by node:

      Oban.Met.latest(:exec_count, group: "node")
      %{"worker.1" => 6, "worker.2" => 5}

  Filter values by node:

      Oban.Met.latest(:exec_count, filters: [node: "worker.1"])
      %{"all" => 6}

  Filter values by queue and state:

      Oban.Met.latest(:exec_count, filters: [node: "worker.1", "worker.2"])
  """
  @spec latest(Oban.name(), series()) :: counts() | sub_counts()
  def latest(oban \\ Oban, series, opts \\ []) do
    oban
    |> Registry.via(Recorder)
    |> Recorder.latest(to_string(series), opts)
  end

  @doc """
  List all recorded series along with their labels and value type.

  ## Examples

      Oban.Met.series()
      [
        %{series: "exec_time", labels: ["state", "queue", "worker"], value: Sketch},
        %{series: "wait_time", labels: ["state", "queue", "worker"], value: Sketch},
        %{series: "exec_count", labels: ["state", "queue", "worker"], value: Gauge},
        %{series: "full_count", labels: ["state", "queue"], value: Gauge}
      ]
  """
  @spec series(Oban.name()) :: [series_detail()]
  def series(oban \\ Oban) do
    oban
    |> Registry.via(Recorder)
    |> Recorder.series()
  end

  @doc """
  Summarize a series of data with an aggregate over a configurable window of time.

  ## Examples

  Retreive a 3 second timeslice of the `exec_time` sketch:

      Oban.Met.timeslice(Oban, :exec_time, lookback: 3)
      [
        {2, 16771374649.128689, nil},
        {1, 24040058779.3428, nil},
        {0, 22191534459.516357, nil},
      ]

  Group `exec_time` slices by the `queue` label:

      Oban.Met.timeslice(Oban, :exec_time, group: "queue")
      [
        {1, 9970235387.031698, "analysis"},
        {0, 11700429279.446463, "analysis"},
        {1, 23097311376.231316, "default"},
        {0, 23097311376.231316, "default"},
        {1, 1520977874.3348415, "events"},
        {0, 2558504265.2738624, "events"},
        ...
  """
  @spec timeslice(Oban.name(), series(), timeslice_opts()) :: [{ts(), value(), label()}]
  def timeslice(oban \\ Oban, series, opts \\ []) do
    oban
    |> Registry.via(Recorder)
    |> Recorder.timeslice(to_string(series), opts)
  end

  # Callbacks

  @impl Supervisor
  def init(opts) do
    conf = Keyword.fetch!(opts, :conf)

    children = [
      {Cronitor, with_opts(:cronitor, conf, opts)},
      {Examiner, with_opts(:examiner, conf, opts)},
      {Recorder, with_opts(:recorder, conf, opts)},
      {Listener, with_opts(:listener, conf, opts)},
      {Reporter, with_opts(:reporter, conf, opts)},
      {Task, fn -> :telemetry.execute([:oban, :met, :init], %{}, %{pid: self(), conf: conf}) end}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  defp with_opts(name, conf, opts) do
    real_name =
      name
      |> to_string()
      |> String.capitalize()
      |> then(&Module.safe_concat([Oban, Met, &1]))

    init_opts = Keyword.get(opts, name, [])

    :oban_met
    |> Application.get_env(name, [])
    |> Keyword.merge(init_opts)
    |> Keyword.put(:conf, conf)
    |> Keyword.put(:name, Registry.via(conf.name, real_name))
  end
end
