defmodule Mobilizon.GraphQL.Error do
  @moduledoc """
  Module to handle errors in GraphQL
  """

  require Logger
  alias __MODULE__
  alias Mobilizon.Web.Gettext, as: GettextBackend
  import Mobilizon.Web.Gettext, only: [dgettext: 2]

  @type t :: %{code: atom(), message: String.t(), status_code: pos_integer(), field: atom()}

  defstruct [:code, :message, :status_code, :field]

  @type error :: {:error, any()} | {:error, any(), any(), any()} | atom()

  @doc """
  Normalize an error to return `t`.
  """
  # Error Tuples
  # ------------
  # Regular errors
  @spec normalize(any()) :: t() | list(t())
  def normalize({:error, reason}) do
    handle(reason)
  end

  # Ecto transaction errors
  def normalize({:error, _operation, reason, _changes}) do
    handle(reason)
  end

  # It's unclear why returned errors are now binaries instead of atoms
  # but we can still convert them back
  def normalize(string) when is_binary(string) do
    string
    |> String.to_existing_atom()
    |> handle()
  rescue
    ArgumentError ->
      handle(string)
  end

  # Unhandled errors
  def normalize(other) do
    handle(other)
  end

  # Handle Different Errors
  # -----------------------
  defp handle(code) when is_atom(code) do
    {status, message} = metadata(code)

    %Error{
      code: code,
      message: message,
      status_code: status
    }
  end

  defp handle(errors) when is_list(errors) do
    Enum.map(errors, &handle/1)
  end

  defp handle(%Ecto.Changeset{} = changeset) do
    changeset
    |> Ecto.Changeset.traverse_errors(&translate_error/1)
    |> Enum.map(fn {k, v} ->
      %Error{
        code: :validation,
        message: v,
        field: k,
        status_code: 422
      }
    end)
  end

  defp handle(reason) when is_binary(reason) do
    Logger.debug("Unknown error")
    Logger.debug(reason)

    %Error{
      code: :unknown_error,
      message: reason,
      status_code: 500
    }
  end

  defp handle(%Error{} = error), do: error

  # ... Handle other error types here ...
  defp handle(other) do
    Logger.error("Unhandled error term:\n#{inspect(other)}")
    handle(:unknown)
  end

  # Build Error Metadata
  # --------------------
  defp metadata(:unknown_resource), do: {400, dgettext("errors", "Unknown Resource")}
  defp metadata(:invalid_argument), do: {400, dgettext("errors", "Invalid arguments passed")}
  defp metadata(:unauthenticated), do: {401, dgettext("errors", "You need to be logged in")}

  defp metadata(:password_hash_missing),
    do: {401, dgettext("errors", "Reset your password to login")}

  defp metadata(:incorrect_password), do: {401, dgettext("errors", "Invalid credentials")}

  defp metadata(:unauthorized),
    do: {403, dgettext("errors", "You don't have permission to do this")}

  defp metadata(:not_found), do: {404, dgettext("errors", "Resource not found")}
  defp metadata(:user_not_found), do: {404, dgettext("errors", "User not found")}
  defp metadata(:post_not_found), do: {404, dgettext("errors", "Post not found")}
  defp metadata(:event_not_found), do: {404, dgettext("errors", "Event not found")}
  defp metadata(:group_not_found), do: {404, dgettext("errors", "Group not found")}
  defp metadata(:resource_not_found), do: {404, dgettext("errors", "Resource not found")}
  defp metadata(:discussion_not_found), do: {404, dgettext("errors", "Discussion not found")}
  defp metadata(:application_not_found), do: {404, dgettext("errors", "Application not found")}

  defp metadata(:application_token_not_found),
    do: {404, dgettext("errors", "Application token not found")}

  defp metadata(:unknown), do: {500, dgettext("errors", "Something went wrong")}

  defp metadata(code) do
    Logger.warn("Unhandled error code: #{inspect(code)}")
    {422, to_string(code)}
  end

  # Translates an error message using gettext.
  defp translate_error({msg, opts}) do
    # Because error messages were defined within Ecto, we must
    # call the Gettext module passing our Gettext backend. We
    # also use the "errors" domain as translations are placed
    # in the errors.po file.
    # Ecto will pass the :count keyword if the error message is
    # meant to be pluralized.
    # On your own code and templates, depending on whether you
    # need the message to be pluralized or not, this could be
    # written simply as:
    #
    #     dngettext "errors", "1 file", "%{count} files", count
    #     dgettext "errors", "is invalid"
    #

    if count = opts[:count] do
      Gettext.dngettext(GettextBackend, "errors", msg, msg, count, opts)
    else
      Gettext.dgettext(GettextBackend, "errors", msg, opts)
    end
  end
end
