defmodule Spandex.Plug.StartTrace do
  @moduledoc """
  Starts a trace, skipping ignored routes or methods.
  Store info in Conn assigns if we actually trace the request.
  """
  @behaviour Plug

  alias Spandex.Plug.Utils
  alias Spandex.SpanContext

  @init_opts Optimal.schema(
               opts: [
                 ignored_methods: {:list, :string},
                 ignored_routes: {:list, [:regex, :string]},
                 tracer: :atom,
                 tracer_opts: :keyword,
                 span_name: :string
               ],
               defaults: [
                 ignored_methods: [],
                 ignored_routes: [],
                 tracer_opts: [],
                 span_name: "request"
               ],
               required: [:tracer],
               describe: [
                 ignored_methods:
                   "A list of strings representing methods to ignore. A good example would be `[\"OPTIONS\"]`",
                 ignored_routes: "A list of strings or regexes. If it is a string, it must match exactly.",
                 tracer: "The tracing module to be used to start the trace.",
                 tracer_opts: "Any opts to be passed to the tracer when starting or continuing the trace.",
                 span_name: "The name to be used for the top level span."
               ]
             )

  @doc """
  Accepts and validates opts for the plug, and underlying tracer.

  #{Optimal.Doc.document(@init_opts)}
  """
  @spec init(opts :: Keyword.t()) :: Keyword.t()
  def init(opts), do: Optimal.validate!(opts, @init_opts)

  @spec call(conn :: Plug.Conn.t(), opts :: Keyword.t()) :: Plug.Conn.t()
  def call(conn, opts) do
    if ignoring_request?(conn, opts) do
      Utils.trace(conn, false)
    else
      begin_tracing(conn, opts)
    end
  end

  @spec begin_tracing(conn :: Plug.Conn.t(), Keyword.t()) :: Plug.Conn.t()
  defp begin_tracing(conn, opts) do
    tracer = opts[:tracer]
    tracer_opts = opts[:tracer_opts]

    case tracer.distributed_context(conn, tracer_opts) do
      {:ok, %SpanContext{} = span_context} ->
        tracer.continue_trace("request", span_context, tracer_opts)
        Utils.trace(conn, true)

      {:error, :no_distributed_trace} ->
        tracer.start_trace(opts[:span_name], tracer_opts)
        Utils.trace(conn, true)

      _ ->
        conn
    end
  end

  @spec ignoring_request?(conn :: Plug.Conn.t(), Keyword.t()) :: boolean
  defp ignoring_request?(conn, opts) do
    ignored_method?(conn, opts) || ignored_route?(conn, opts)
  end

  @spec ignored_method?(conn :: Plug.Conn.t(), Keyword.t()) :: boolean
  defp ignored_method?(conn, opts) do
    conn.method in opts[:ignored_methods]
  end

  @spec ignored_route?(conn :: Plug.Conn.t(), Keyword.t()) :: boolean
  defp ignored_route?(conn, opts) do
    Enum.any?(opts[:ignored_routes], fn ignored_route ->
      match_route?(conn.request_path, ignored_route)
    end)
  end

  @spec match_route?(route :: String.t(), ignore :: %Regex{} | String.t()) :: boolean
  defp match_route?(ignore, ignore) when is_bitstring(ignore), do: true
  defp match_route?(_, ignore) when is_bitstring(ignore), do: false

  defp match_route?(route, ignore) do
    String.match?(route, ignore)
  end
end
