defmodule Plug.Cowboy.Translator do
  @moduledoc false

  # Cowboy 2.12.0 and below error format
  @doc """
  The `translate/4` function expected by custom Logger translators.
  """
  def translate(
        min_level,
        :error,
        :format,
        {~c"Ranch listener" ++ _, [ref, conn_pid, stream_id, stream_pid, reason, stack]}
      ) do
    extra = [" (connection ", inspect(conn_pid), ", stream id ", inspect(stream_id), ?)]
    translate_ranch(min_level, ref, extra, stream_pid, reason, stack)
  end

  # Cowboy 2.13.0 error format
  def translate(
        min_level,
        :error,
        :format,
        {~c"Ranch listener" ++ _, [ref, conn_pid, stream_id, stream_pid, {reason, stack}]}
      ) do
    extra = [" (connection ", inspect(conn_pid), ", stream id ", inspect(stream_id), ?)]
    translate_ranch(min_level, ref, extra, stream_pid, reason, stack)
  end

  def translate(_min_level, _level, _kind, _data) do
    :none
  end

  ## Ranch/Cowboy

  defp translate_ranch(
         min_level,
         _ref,
         extra,
         pid,
         {reason, {mod, :call, [%Plug.Conn{} = conn, _opts]}},
         _stack
       ) do
    if log_exception?(reason) do
      message = [
        inspect(pid),
        " running ",
        inspect(mod),
        extra,
        " terminated\n",
        conn_info(min_level, conn)
        | Exception.format(:exit, reason, [])
      ]

      crash_reason =
        case reason do
          {exception, _stack} when is_exception(exception) -> reason
          {{:nocatch, _value}, _stack} -> reason
          exit_reason -> {exit_reason, []}
        end

      metadata =
        [
          crash_reason: crash_reason,
          domain: [:cowboy]
        ] ++ maybe_conn_metadata(conn)

      {:ok, message, metadata}
    else
      :skip
    end
  end

  defp translate_ranch(_min_level, ref, extra, pid, reason, stack) do
    {:ok,
     [
       "Ranch protocol ",
       inspect(pid),
       " of listener ",
       inspect(ref),
       extra,
       " terminated\n"
       | Exception.format_exit({reason, stack})
     ], crash_reason: {reason, stack}, domain: [:cowboy]}
  end

  defp log_exception?({%{__exception__: true} = exception, _}) do
    status_ranges =
      Application.get_env(:plug_cowboy, :log_exceptions_with_status_code, [500..599])

    status = Plug.Exception.status(exception)

    Enum.any?(status_ranges, &(status in &1))
  end

  defp log_exception?(_), do: true

  defp conn_info(_min_level, conn) do
    [server_info(conn), request_info(conn)]
  end

  defp server_info(%Plug.Conn{host: host, port: :undefined, scheme: scheme}) do
    ["Server: ", host, ?\s, ?(, Atom.to_string(scheme), ?), ?\n]
  end

  defp server_info(%Plug.Conn{host: host, port: port, scheme: scheme}) do
    ["Server: ", host, ":", Integer.to_string(port), ?\s, ?(, Atom.to_string(scheme), ?), ?\n]
  end

  defp request_info(%Plug.Conn{method: method, query_string: query_string} = conn) do
    ["Request: ", method, ?\s, path_to_iodata(conn.request_path, query_string), ?\n]
  end

  defp maybe_conn_metadata(conn) do
    if Application.get_env(:plug_cowboy, :conn_in_exception_metadata, true) do
      [conn: conn]
    else
      []
    end
  end

  defp path_to_iodata(path, ""), do: path
  defp path_to_iodata(path, qs), do: [path, ??, qs]
end
