defmodule PhoenixSwagger.ConnValidator do
  alias PhoenixSwagger.Validator

  @table :validator_table

  @doc """
  Validate a request.

  Feel free to use it in your own Plugs. Returns:
  * `{:ok, conn}` on success
  * `{:error, :no_matching_path}` if the request path could not be mapped to a schema
  * `{:error, message, path}` if the request was mapped but failed validation
  * `{:error, [{message, path}], path}` if more than one validation error has been detected
  """
  def validate(conn) do
    with {:ok, path} <- find_matching_path(conn),
         :ok <- validate_body_params(path, conn),
         :ok <- validate_query_params(path, conn),
         do: {:ok, conn}
  end

  defp find_matching_path(conn) do
    found =
      :ets.tab2list(@table)
      |> Enum.sort()
      |> Enum.find(fn {path, base_path, _} ->
        base_path_segments = String.split(base_path || "", "/") |> tl
        path_segments = String.split(path, "/") |> tl
        path_info_without_base = remove_base_path(conn.path_info, base_path_segments)
        req_path_segments = [String.downcase(conn.method) | path_info_without_base]
        equal_paths?(path_segments, req_path_segments)
      end)

    case found do
      nil -> {:error, :no_matching_path}
      {path, _, _} -> {:ok, path}
    end
  end

  defp validate_array(name, values, items = %{"type" => type}, parameters) do
    with :ok <-
           String.split(values, ",")
           |> Enum.map(&{type, name, &1, false, Map.get(items, "enum"), nil})
           |> validate_query_params() do
      validate_query_params(parameters)
    else
      error -> error
    end
  end

  defp validate_enum(name, value, enum, parameters) do
    cond do
      value in enum -> validate_query_params(parameters)
      true -> {:error, "Value #{inspect(value)} is not allowed in enum.", "#/#{name}"}
    end
  end

  defp validate_boolean(_name, value, parameters) when value in [true, false, "true", "false"] do
    validate_query_params(parameters)
  end

  defp validate_boolean(name, _value, _parameters) do
    {:error, "Type mismatch. Expected Boolean but got something else.", "#/#{name}"}
  end

  defp validate_integer(name, value, parameters) do
    _ = String.to_integer(value)
    validate_query_params(parameters)
  rescue
    ArgumentError ->
      {:error, "Type mismatch. Expected Integer but got String.", "#/#{name}"}
  end

  defp validate_number(name, value, parameters) do
    {_, ""} = Float.parse(value)
    validate_query_params(parameters)
  rescue
    MatchError ->
      {:error, "Type mismatch. Expected Number but got String.", "#/#{name}"}
  end

  defp validate_query_params([]), do: :ok

  defp validate_query_params([{_type, _name, nil, false, _, _} | parameters]) do
    validate_query_params(parameters)
  end

  defp validate_query_params([{_type, name, nil, true, _, _} | _]) do
    {:error, "Required property #{name} was not present.", "#"}
  end

  defp validate_query_params([{_type, name, val, _, enum, _} | parameters])
       when not is_nil(enum) do
    validate_enum(name, val, enum, parameters)
  end

  defp validate_query_params([{"string", _name, _val, _, _, _} | parameters]) do
    validate_query_params(parameters)
  end

  defp validate_query_params([{"integer", name, val, _, _, _} | parameters]) do
    validate_integer(name, val, parameters)
  end

  defp validate_query_params([{"number", name, val, _, _, _} | parameters]) do
    validate_number(name, val, parameters)
  end

  defp validate_query_params([{"boolean", name, val, _, _, _} | parameters]) do
    validate_boolean(name, val, parameters)
  end

  defp validate_query_params([{"array", name, vals, _, _, items} | parameters]) do
    validate_array(name, vals, items, parameters)
  end

  defp validate_query_params(path, conn) do
    [{_path, _basePath, schema}] = :ets.lookup(@table, path)

    parameters =
      for parameter <- schema.schema["parameters"],
          parameter["type"] != nil,
          parameter["in"] in ["query", "path"] do
        {parameter["type"], parameter["name"], get_param_value(conn.params, parameter["name"]),
         parameter["required"], parameter["enum"], parameter["items"]}
      end

    validate_query_params(parameters)
  end

  defp get_in_nested(params = nil, _), do: params
  defp get_in_nested(params, nil), do: params
  defp get_in_nested(params, ""), do: params

  defp get_in_nested(params, nested_map) when map_size(nested_map) == 1 do
    [{key, child_nested_map}] = Map.to_list(nested_map)

    get_in_nested(params[key], child_nested_map)
  end

  defp get_param_value(params, nested_name) when is_binary(nested_name) do
    nested_map = Plug.Conn.Query.decode(nested_name)
    get_in_nested(params, nested_map)
  end

  defp validate_body_params(path, conn) do
    Validator.validate(path, conn.body_params)
  end

  defp equal_paths?([], []), do: true

  defp equal_paths?([head | orig_path_rest], [head | req_path_rest]),
    do: equal_paths?(orig_path_rest, req_path_rest)

  defp equal_paths?(["{" <> _ | orig_path_rest], [_ | req_path_rest]),
    do: equal_paths?(orig_path_rest, req_path_rest)

  defp equal_paths?(_, _), do: false

  # It is pretty safe to strip request path by base path. They can't be
  # non-equal. In this way, the router even will not execute this plug.
  defp remove_base_path(path, []), do: path

  defp remove_base_path([_path | rest], [_base_path | base_path_rest]) do
    remove_base_path(rest, base_path_rest)
  end
end
