defmodule Quenya.Plug.RoutePlug do
  @moduledoc """
  Plug for executing a route. To use it:

      get "/todo/:todoId", to: Quenya.Plug.RoutePlug, init_opts: [
        preprocessors: [{RequestValidatorPlug, []}],
        postprocessors: [{ResponseValidatorPlug, []}],
        handlers: [{RouteHandler, []}]
      ])

  Note this plug shall be used after Plug.Parsers since it will access request body (if any)
  ## Options

    * `:preprocessors` - a list of plugs, execute in order before executing handlers
    * `:handlers` - a list of plugs to process request and generate response
    * `:postprocessors` - a list of plugs that will be executed in order before send
  """

  alias Plug.Conn

  @behaviour Plug

  @spec init(keyword()) :: keyword()
  def init(opts) do
    handlers = opts[:handlers] || []

    if Enum.empty?(handlers) do
      raise "You must define at least one handler"
    end

    opts
  end

  @spec call(Conn.t(), keyword()) :: Conn.t()
  def call(conn, opts) do
    conn = register_postprocessors(conn, opts[:postprocessors])

    conn = Enum.reduce_while(opts[:preprocessors] || [], conn, &apply_pipe(&1, &2))

    case conn.halted do
      false -> Enum.reduce_while(opts[:handlers] || [], conn, &apply_pipe(&1, &2))
      _ -> conn
    end
  end

  defp apply_pipe({mod, opts}, conn) do
    case apply(mod, :call, [conn, opts]) do
      %Plug.Conn{halted: true} = result -> {:halt, result}
      %Plug.Conn{} = result -> {:cont, result}
      other -> raise "All pipes must return Plug.Conn.t. Got #{inspect(other)}"
    end
  end

  defp register_postprocessors(conn, processors) when is_list(processors) do
    processors
    |> Enum.reverse()
    |> Enum.reduce(conn, fn {mod, opts}, acc ->
      Conn.register_before_send(acc, &apply(mod, :call, [&1, opts]))
    end)
  end

  defp register_postprocessors(conn, _), do: conn
end
