defmodule Phantom.ResourcePlug do
  @moduledoc false

  @behaviour Plug
  import Plug.Conn

  alias Phantom.Request
  alias Phantom.Resource
  alias Phantom.Session

  @impl Plug
  def init(opts), do: opts

  @impl Plug
  def call(fake_conn, _opts) do
    session = %{
      fake_conn.assigns.session
      | request: %{fake_conn.assigns.session.request | spec: fake_conn.assigns.resource_template}
    }

    result =
      try do
        apply(
          fake_conn.assigns.resource_template.handler,
          fake_conn.assigns.resource_template.function,
          [fake_conn.path_params, session]
        )
      rescue
        _e in FunctionClauseError ->
          {:error, Phantom.Request.resource_not_found(%{uri: fake_conn.assigns.uri}),
           fake_conn.assigns.session}
      end

    assign(
      fake_conn,
      :result,
      wrap(result, fake_conn.assigns.uri, fake_conn.assigns.session)
    )
  end

  defp wrap({:error, reason}, _uri, session) do
    {:error, reason, session}
  end

  defp wrap({:error, _reason, %Session{}} = result, _uri, _session), do: result

  defp wrap(nil, uri, session) do
    {:error, Request.resource_not_found(%{uri: uri}), session}
  end

  defp wrap({:noreply, %Session{}} = result, _uri, _session), do: result

  defp wrap({:reply, nil, %Session{} = session}, uri, _session) do
    {:error, Request.resource_not_found(%{uri: uri}), session}
  end

  defp wrap({:reply, results, %Session{} = session}, _uri, _session) do
    {:reply, Resource.response(results), session}
  end

  defmodule NotFound do
    @moduledoc false

    @behaviour Plug
    import Plug.Conn

    def init(opts), do: opts

    def call(conn, _opts) do
      result = Phantom.Request.resource_not_found(%{uri: conn.assigns.uri})
      assign(conn, :result, {:reply, result, conn.assigns.session})
    end
  end
end
