defmodule OpenApiSpex.Operation2 do
  @moduledoc """
  Casts and validates a request from a Plug conn.
  """
  alias OpenApiSpex.{
    Cast,
    CastParameters,
    Components,
    OpenApi,
    Operation,
    Reference,
    RequestBody
  }

  alias OpenApiSpex.Cast.Error
  alias Plug.Conn

  @spec cast(
          OpenApi.t(),
          Operation.t(),
          Conn.t(),
          String.t() | nil,
          opts :: [OpenApiSpex.cast_opt()]
        ) ::
          {:error, [Error.t()]} | {:ok, Conn.t()}
  def cast(
        spec = %OpenApi{components: components},
        operation = %Operation{},
        conn = %Conn{},
        content_type,
        opts \\ []
      ) do
    replace_params = Keyword.get(opts, :replace_params, true)

    with {:ok, conn} <- cast_parameters(conn, operation, spec, opts),
         {:ok, body} <-
           cast_request_body(
             operation.requestBody,
             conn.body_params,
             content_type,
             components,
             opts
           ) do
      {:ok,
       conn
       |> cast_conn(body)
       |> maybe_replace_body(body, replace_params)}
    end
  end

  ## Private functions

  defp cast_conn(conn, body) do
    private_data =
      conn
      |> Map.get(:private)
      |> Map.get(:open_api_spex, %{})
      |> Map.put(:body_params, body)

    Plug.Conn.put_private(conn, :open_api_spex, private_data)
  end

  defp maybe_replace_body(conn, _body, false), do: conn
  defp maybe_replace_body(conn, body, true), do: %{conn | body_params: body}

  defp cast_parameters(conn, operation, spec, opts) do
    CastParameters.cast(conn, operation, spec, opts)
  end

  defp cast_request_body(ref = %Reference{}, body_params, content_type, components, opts) do
    request_body = Reference.resolve_request_body(ref, components.requestBodies)

    cast_request_body(request_body, body_params, content_type, components, opts)
  end

  defp cast_request_body(nil, _, _, _, _), do: {:ok, %{}}

  defp cast_request_body(%{required: false}, _, nil, _, _), do: {:ok, %{}}

  defp cast_request_body(%{required: true}, _, nil, _, _) do
    {:error, [Error.new(%{path: [], value: nil}, {:missing_header, "content-type"})]}
  end

  # Special case to handle strings or arrays in request body that come inside _json
  # https://hexdocs.pm/plug/Plug.Parsers.JSON.html
  defp cast_request_body(
         request_body,
         %{"_json" => body_params},
         content_type,
         components = %Components{},
         opts
       ) do
    case cast_request_body(request_body, body_params, content_type, components, opts) do
      {:ok, body_params} -> {:ok, %{"_json" => body_params}}
      error -> error
    end
  end

  defp cast_request_body(
         %RequestBody{content: content},
         params,
         content_type,
         components = %Components{},
         opts
       ) do
    case content do
      %{^content_type => media_type} ->
        Cast.cast(media_type.schema, params, components.schemas, opts)

      _ ->
        {:error, [Error.new(%{path: [], value: content_type}, {:invalid_header, "content-type"})]}
    end
  end
end
