defmodule OpenAPI.Spec.Path.Item do
  @moduledoc "Raw operation path from the OpenAPI spec"
  import OpenAPI.Reader.State

  alias OpenAPI.Spec.Path.Operation
  alias OpenAPI.Spec.Path.Parameter
  alias OpenAPI.Spec.Server

  @type t :: %__MODULE__{
          summary: String.t() | nil,
          description: String.t() | nil,
          get: Operation.t() | nil,
          put: Operation.t() | nil,
          post: Operation.t() | nil,
          delete: Operation.t() | nil,
          options: Operation.t() | nil,
          head: Operation.t() | nil,
          patch: Operation.t() | nil,
          trace: Operation.t() | nil,
          servers: [Server.t()],
          parameters: [Parameter.t()]
        }

  defstruct [
    :summary,
    :description,
    :get,
    :put,
    :post,
    :delete,
    :options,
    :head,
    :patch,
    :trace,
    :servers,
    :parameters
  ]

  @doc false
  @spec decode(map, map) :: {map, t}
  def decode(state, yaml) do
    {state, parameters} = decode_parameters(state, yaml)
    {state, servers} = decode_servers(state, yaml)

    state = %{state | path_parameters: parameters}
    {state, get} = decode_get(state, yaml)
    {state, put} = decode_put(state, yaml)
    {state, post} = decode_post(state, yaml)
    {state, delete} = decode_delete(state, yaml)
    {state, options} = decode_options(state, yaml)
    {state, head} = decode_head(state, yaml)
    {state, patch} = decode_patch(state, yaml)
    {state, trace} = decode_trace(state, yaml)
    state = %{state | path_parameters: []}

    item = %__MODULE__{
      summary: Map.get(yaml, "summary"),
      description: Map.get(yaml, "description"),
      get: get,
      put: put,
      post: post,
      delete: delete,
      options: options,
      head: head,
      patch: patch,
      trace: trace,
      servers: servers,
      parameters: parameters
    }

    {state, item}
  end

  @spec decode_get(map, map) :: {map, Operation.t() | nil}
  def decode_get(state, %{"get" => get}) do
    with_path(state, get, "get", &Operation.decode/2)
  end

  def decode_get(state, _yaml), do: {state, nil}

  @spec decode_put(map, map) :: {map, Operation.t() | nil}
  def decode_put(state, %{"put" => put}) do
    with_path(state, put, "put", &Operation.decode/2)
  end

  def decode_put(state, _yaml), do: {state, nil}

  @spec decode_post(map, map) :: {map, Operation.t() | nil}
  def decode_post(state, %{"post" => post}) do
    with_path(state, post, "post", &Operation.decode/2)
  end

  def decode_post(state, _yaml), do: {state, nil}

  @spec decode_delete(map, map) :: {map, Operation.t() | nil}
  def decode_delete(state, %{"delete" => delete}) do
    with_path(state, delete, "delete", &Operation.decode/2)
  end

  def decode_delete(state, _yaml), do: {state, nil}

  @spec decode_options(map, map) :: {map, Operation.t() | nil}
  def decode_options(state, %{"options" => options}) do
    with_path(state, options, "options", &Operation.decode/2)
  end

  def decode_options(state, _yaml), do: {state, nil}

  @spec decode_head(map, map) :: {map, Operation.t() | nil}
  def decode_head(state, %{"head" => head}) do
    with_path(state, head, "head", &Operation.decode/2)
  end

  def decode_head(state, _yaml), do: {state, nil}

  @spec decode_patch(map, map) :: {map, Operation.t() | nil}
  def decode_patch(state, %{"patch" => patch}) do
    with_path(state, patch, "patch", &Operation.decode/2)
  end

  def decode_patch(state, _yaml), do: {state, nil}

  @spec decode_trace(map, map) :: {map, Operation.t() | nil}
  def decode_trace(state, %{"trace" => trace}) do
    with_path(state, trace, "trace", &Operation.decode/2)
  end

  def decode_trace(state, _yaml), do: {state, nil}

  @spec decode_parameters(map, map) :: {map, [Parameter.t()]}
  def decode_parameters(state, %{"parameters" => parameters}) do
    with_path(state, parameters, "parameters", fn state, parameters ->
      parameters
      |> Enum.with_index()
      |> Enum.reverse()
      |> Enum.reduce({state, []}, fn {parameter, index}, {state, list} ->
        {state, element} =
          with_path(state, parameter, index, fn state, parameter ->
            with_ref(state, parameter, &Parameter.decode/2)
          end)

        {state, [element | list]}
      end)
    end)
  end

  def decode_parameters(state, _yaml), do: {state, []}

  @spec decode_servers(map, map) :: {map, [Server.t()]}
  defp decode_servers(state, %{"servers" => servers}) when is_list(servers) do
    with_path(state, servers, "servers", fn state, servers ->
      {state, servers} =
        servers
        |> Enum.with_index()
        |> Enum.reduce({state, []}, fn {server, index}, {state, servers} ->
          {state, server} = with_path(state, server, index, &Server.decode/2)
          {state, [server | servers]}
        end)

      {state, Enum.reverse(servers)}
    end)
  end

  defp decode_servers(state, _yaml), do: {state, [%Server{url: "/"}]}

  def merge(item_one, item_two) do
    %__MODULE__{
      summary: item_two.summary || item_one.summary,
      description: item_two.description || item_one.description,
      get: item_two.get || item_one.get,
      put: item_two.put || item_one.put,
      post: item_two.post || item_one.post,
      delete: item_two.delete || item_one.delete,
      options: item_two.options || item_one.options,
      head: item_two.head || item_one.head,
      patch: item_two.patch || item_one.patch,
      trace: item_two.trace || item_one.trace,
      servers: Enum.uniq_by(item_two.servers ++ item_one.servers, & &1.url),
      parameters: Enum.uniq_by(item_two.parameters ++ item_one.parameters, &{&1.name, &1.in})
    }
  end
end
