defmodule PhoenixSwagger.Path do
  @moduledoc """
  Defines the swagger path DSL for specifying Controller actions.

  This module should not be imported directly, it will be automatically imported
  in the scope of a `swagger_path` macro body.

  ## Examples

      use PhoenixSwagger

      swagger_path :index do
        get "/users"
        produces "application/json"
        paging
        parameter :id, :query, :integer, "user id", required: true
        tag "Users"
        response 200 "User resource" :User
        response 404 "User not found"
      end

  """

  alias PhoenixSwagger.Schema

  defmodule Parameter do
    @moduledoc """
    A swagger parameter definition, similar to a `Schema`, but swagger defines
    parameter name (and some other options) to be part of the parameter object itself.

    See http://swagger.io/specification/#parameterObject
    """

    defstruct(
      name: "",
      in: "",
      description: "",
      required: false,
      schema: nil,
      type: nil,
      format: nil,
      allowEmptyValue: nil,
      items: nil,
      collectionFormat: nil,
      default: nil,
      maximum: nil,
      exclusiveMaximum: nil,
      minimum: nil,
      exclusiveMinimum: nil,
      maxLength: nil,
      minLength: nil,
      pattern: nil,
      maxItems: nil,
      minItems: nil,
      uniqueItems: nil,
      enum: nil,
      multipleOf: nil
    )
  end

  defmodule ResponseObject do
    @moduledoc """
    A swagger response definition.

    The response status (200, 404, etc.) is the key in the containing map.

    See http://swagger.io/specification/#responseObject
    """
    defstruct description: "", schema: nil, headers: nil, examples: nil
  end

  defmodule OperationObject do
    @moduledoc """
    A swagger operation object ties together parameters, responses, etc.

    See http://swagger.io/specification/#operationObject
    """
    defstruct(
      tags: [],
      summary: "",
      description: "",
      externalDocs: nil,
      operationId: "",
      consumes: nil,
      produces: nil,
      parameters: [],
      responses: %{},
      deprecated: nil,
      security: nil
    )
  end

  defmodule PathObject do
    @moduledoc """
    The DSL builds paths out of individual operations, so this is a flattened version
    of a swagger Path.

    The `nest` function will convert this to a nested map before final
    conversion to a JSON map.

    See http://swagger.io/specification/#pathsObject
    """
    defstruct path: nil, verb: nil, operation: %OperationObject{}
  end

  @doc "Initializes a PathObject with verb \"get\" and given path"
  def get(path_obj = %PathObject{}, path), do: %{path_obj | path: path, verb: "get"}

  @doc "Initializes a PathObject with verb \"post\" and given path"
  def post(path_obj = %PathObject{}, path), do: %{path_obj | path: path, verb: "post"}

  @doc "Initializes a PathObject with verb \"put\" and given path"
  def put(path_obj = %PathObject{}, path), do: %{path_obj | path: path, verb: "put"}

  @doc "Initializes a PathObject with verb \"patch\" and given path"
  def patch(path_obj = %PathObject{}, path), do: %{path_obj | path: path, verb: "patch"}

  @doc "Initializes a PathObject with verb \"delete\" and given path"
  def delete(path_obj = %PathObject{}, path), do: %{path_obj | path: path, verb: "delete"}

  @doc "Initializes a PathObject with verb \"head\" and given path"
  def head(path_obj = %PathObject{}, path), do: %{path_obj | path: path, verb: "head"}

  @doc "Initializes a PathObject with verb \"options\" and given path"
  def options(path_obj = %PathObject{}, path), do: %{path_obj | path: path, verb: "options"}

  @doc """
  Adds the summary section to the operation of a swagger `%PathObject{}`.
  """
  def summary(path = %PathObject{}, summary) do
    put_in(path.operation.summary, summary)
  end

  @doc """
  Adds the description section to the operation of a swagger `%PathObject{}`.
  """
  def description(path = %PathObject{}, description) do
    put_in(path.operation.description, description)
  end

  @doc """
  Adds a mime-type to the consumes list of the operation of a swagger `%PathObject{}`.
  """
  def consumes(path = %PathObject{}, mimetype) do
    put_in(path.operation.consumes, (path.operation.consumes || []) ++ [mimetype])
  end

  @doc """
  Adds a mime-type to the produces list of the operation of a swagger `%PathObject{}`.
  """
  def produces(path = %PathObject{}, mimetype) do
    put_in(path.operation.produces, (path.operation.produces || []) ++ [mimetype])
  end

  @doc """
  Adds a tag to the operation of a swagger `%PathObject{}`.
  """
  def tag(path = %PathObject{}, tag) do
    put_in(path.operation.tags, path.operation.tags ++ [tag])
  end

  @doc """
  Adds the operationId section to the operation of a swagger `%PathObject{}`.
  """
  def operation_id(path = %PathObject{}, id) do
    put_in(path.operation.operationId, id)
  end

  @doc """
  Adds the security section to the operation of a swagger `%PathObject{}`.
  """
  def security(path = %PathObject{}, security) do
    put_in(path.operation.security, security)
  end

  @doc """
  Defines multiple parameters for an operation with a custom DSL syntax.

  ## Examples

      swagger_path :create do
        post "/api/v1/{team}/users"
        summary "Create a new user"
        parameters do
          user :body, Schema.ref(:User), "user attributes"
          team :path, :string, "Users team ID"
        end
        response 200, "OK", :User
      end

  """
  defmacro parameters(path, block) do
    exprs =
      case block do
        [do: {:__block__, _, exprs}] -> exprs
        [do: expr] -> [expr]
      end

    exprs
    |> Enum.map(fn {name, line, args} -> {:parameter, line, [name | args]} end)
    |> Enum.reduce(path, fn expr, acc ->
      quote do
        unquote(acc) |> unquote(expr)
      end
    end)
  end

  @doc """
  Adds a parameter to the operation of a swagger `%PathObject{}`.
  """
  def parameter(path = %PathObject{}, name, location, type, description, opts \\ []) do
    param = %Parameter{
      name: name,
      in: location,
      description: description
    }

    param =
      case location do
        :body -> %{param | schema: type}
        :path -> %{param | type: type, required: true}
        _ -> %{param | type: type}
      end

    param = Map.merge(param, opts |> Enum.into(%{}, &translate_parameter_opt/1))
    params = path.operation.parameters
    put_in(path.operation.parameters, params ++ [param])
  end

  @doc """
  Adds the deprecation section to the operation of a swagger `%PathObject{}`.
  """
  def deprecated(path = %PathObject{}, status) do
    put_in(path.operation.deprecated, status)
  end

  defp translate_parameter_opt({:example, v}), do: {:"x-example", v}

  defp translate_parameter_opt({:items, items_schema}) when is_list(items_schema) do
    {:items, Enum.into(items_schema, %{})}
  end

  defp translate_parameter_opt({k, v}), do: {k, v}

  @doc """
  Adds page size, number and offset parameters to the operation of a swagger `%PathObject{}`.

  The names default to  "page_size" and "page" for ease of use with `scriviner_ecto`, but can be overridden.

  ## Examples

      get "/api/pets/"
      paging
      response 200, "OK"

      get "/api/pets/dogs"
      paging size: "page_size", number: "count"
      response 200, "OK"

      get "/api/pets/cats"
      paging size: "limit", offset: "offset"
      response 200, "OK"

  """
  def paging(path = %PathObject{}, opts \\ [size: "page_size", number: "page"]) do
    Enum.reduce(opts, path, fn
      {:size, size}, path ->
        parameter(path, size, :query, :integer, "Number of elements per page", minimum: 1)

      {:number, number}, path ->
        parameter(path, number, :query, :integer, "Number of the page", minimum: 1)

      {:offset, offset}, path ->
        parameter(path, offset, :query, :integer, "Offset of first element in the page")
    end)
  end

  @doc """
  Adds a response to the operation of a swagger `%PathObject{}`, without a schema.
  """
  def response(path = %PathObject{}, status, description) do
    resp = %ResponseObject{description: description}
    put_in(path.operation.responses[to_string(status)], resp)
  end

  @doc """
  Adds a response to the operation of a swagger `%PathObject{}`, with a schema.

  Optional keyword args can be provided for `headers` and `examples`.

  If the mime-type is known from the `produces` list, then a single can be given as a shorthand.

  ## Examples

      get "/users/{id}"
      produces "application/json"
      parameter :id, :path, :integer, "user id", required: true
      response 200, "Success", :User, examples: %{"application/json": %{id: 1, name: "Joe"}}

      get "/users/{id}"
      produces "application/json"
      parameter :id, :path, :integer, "user id", required: true
      response 200, "Success", :User, example: %{id: 1, name: "Joe"}

  """
  def response(path, status, description, schema, opts \\ [])

  def response(path = %PathObject{}, status, description, schema, opts) when is_atom(schema) do
    response(path, status, description, Schema.ref(schema), opts)
  end

  def response(path = %PathObject{}, status, description, schema = %{}, opts) do
    opts = expand_response_example(path, opts)
    resp = struct(ResponseObject, [description: description, schema: schema] ++ opts)
    put_in(path.operation.responses[status |> to_string], resp)
  end

  def expand_response_example(%PathObject{operation: %{produces: [mimetype | _]}}, opts) do
    Enum.map(opts, fn
      {:example, e} -> {:examples, %{mimetype => e}}
      opt -> opt
    end)
  end

  def expand_response_example(%PathObject{}, opts), do: opts

  @doc """
  Converts the `%PathObject{}` struct into the nested JSON form expected by swagger.
  """
  def nest(path = %PathObject{}) do
    %{path.path => %{path.verb => path.operation}}
  end
end
