## The contents of this file are subject to the Mozilla Public License
## Version 1.1 (the "License"); you may not use this file except in
## compliance with the License. You may obtain a copy of the License
## at http://www.mozilla.org/MPL/
##
## Software distributed under the License is distributed on an "AS IS"
## basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
## the License for the specific language governing rights and
## limitations under the License.
##
## The Original Code is RabbitMQ.
##
## The Initial Developer of the Original Code is GoPivotal, Inc.
## Copyright (c) 2007-2019 Pivotal Software, Inc.  All rights reserved.

defmodule RabbitMQ.CLI.Diagnostics.Commands.CheckPortConnectivityCommand do
  @moduledoc """
  Displays all listeners on a node.

  Returns a code of 0 unless there were connectivity and authentication
  errors. This command is not meant to be used in health checks.
  """

  alias RabbitMQ.CLI.Core.Helpers

  import RabbitMQ.CLI.Diagnostics.Helpers,
    only: [
      listeners_on: 2,
      listener_lines: 1,
      listener_map: 1,
      listener_maps: 1,
      check_listener_connectivity: 3
    ]

  @behaviour RabbitMQ.CLI.CommandBehaviour

  @default_timeout 30_000

  use RabbitMQ.CLI.Core.AcceptsDefaultSwitchesAndTimeout

  def merge_defaults(args, opts) do
    timeout =
      case opts[:timeout] do
        nil -> @default_timeout
        :infinity -> @default_timeout
        other -> other
      end

    {args, Map.merge(opts, %{timeout: timeout})}
  end

  use RabbitMQ.CLI.Core.AcceptsNoPositionalArguments
  use RabbitMQ.CLI.Core.RequiresRabbitAppRunning

  def run([], %{node: node_name, timeout: timeout}) do
    case :rabbit_misc.rpc_call(node_name, :rabbit_networking, :active_listeners, [], timeout) do
      {:error, _} = err ->
        err

      {:error, _, _} = err ->
        err

      xs when is_list(xs) ->
        locals = listeners_on(xs, node_name)

        case locals do
          [] -> {true, locals}
          _ -> check_connectivity_of(locals, node_name, timeout)
        end

      other ->
        other
    end
  end

  def output({true, listeners}, %{node: node_name, formatter: "json"}) do
    {:ok, %{"result" => "ok", "node" => node_name, "listeners" => listener_maps(listeners)}}
  end

  def output({true, listeners}, %{node: node_name}) do
    ports =
      listeners
      |> listener_maps
      |> Enum.map(fn %{port: p} -> p end)
      |> Enum.sort()
      |> Enum.join(", ")

    {:ok, "Successfully connected to ports #{ports} on node #{node_name}."}
  end

  def output({false, failures}, %{formatter: "json", node: node_name}) do
    {:error, %{"result" => "error", "node" => node_name, "failures" => listener_maps(failures)}}
  end

  def output({false, failures}, %{node: node_name}) do
    lines = [
      "Connection to ports of the following listeners on node #{node_name} failed: "
      | listener_lines(failures)
    ]

    {:error, Enum.join(lines, Helpers.line_separator())}
  end

  def usage, do: "check_port_connectivity"

  def banner([], %{node: node_name}) do
    "Testing TCP connections to all active listeners on node #{node_name} ..."
  end

  #
  # Implementation
  #

  defp check_connectivity_of(listeners, node_name, timeout) do
    # per listener timeout
    t = Kernel.trunc(timeout / (length(listeners) + 1))

    failures =
      Enum.reject(
        listeners,
        fn l -> check_listener_connectivity(listener_map(l), node_name, t) end
      )

    case failures do
      [] -> {true, listeners}
      fs -> {false, fs}
    end
  end
end
