# SPDX-FileCopyrightText: 2023 Frank Hunleth
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Toolshed.Core.Tcping do
  @doc """
  Ping an IP address using TCP

  This tries to connect to the remote host using TCP instead of sending an ICMP
  echo request like normal ping. This sometimes works better than ping
  if the remote server or any machine in between drops ICMP messages.

  Options:

  * `:count` - number of pings to send (defaults to 3)
  * `:ifname` - Specify a network interface to use. (e.g., "eth0")
  * `:port` - Which TCP port to try (defaults to 80)

  ## Examples

      iex> tcping "nerves-project.org"
      Response from nerves-project.org (185.199.108.153:80): time=4.155ms
      Response from nerves-project.org (185.199.108.153:80): time=10.385ms
      Response from nerves-project.org (185.199.108.153:80): time=12.458ms

      iex> tcping "google.com", ifname: "wlp5s0"
      Response from google.com (172.217.7.206:80): time=88.602ms
  """
  @spec tcping(String.t(), keyword()) :: :"do not show this result in output"
  def tcping(address, options \\ []) do
    count = options[:count] || 3
    run_or_enter(fn -> repeat_tcping(address, options, 0, count) end)

    IEx.dont_display_result()
  end

  defp repeat_tcping(_address, _options, count, max_count)
       when count >= max_count,
       do: :ok

  defp repeat_tcping(address, options, count, max_count) do
    if count > 0, do: Process.sleep(1000)

    do_tcping(address, options)
    Process.sleep(1000)
    repeat_tcping(address, options, count + 1, max_count)
  end

  defp do_tcping(address, options) do
    case resolve_addr(address) do
      {:ok, ip} -> tcping_ip(address, ip, options)
      {:error, message} -> message
    end
    |> IO.puts()

    IEx.dont_display_result()
  end

  defp tcping_ip(address, ip, options) do
    port = Keyword.get(options, :port, 80)

    case try_connect(ip, port, options) do
      {:ok, micros} ->
        "Response from #{address} (#{pretty_ip_port(ip, port)}): time=#{micros / 1000}ms"

      {:error, reason} ->
        "#{address} (#{pretty_ip_port(ip, port)}): #{inspect(reason)}"
    end
  end

  defp pretty_ip_port({_, _, _, _} = ip, port), do: "#{:inet.ntoa(ip)}:#{port}"
  defp pretty_ip_port(ip, port), do: "[#{:inet.ntoa(ip)}]:#{port}"

  defp try_connect(address, port, options) do
    start = System.monotonic_time(:microsecond)

    connect_options =
      case Keyword.fetch(options, :ifname) do
        {:ok, ifname} ->
          family = ip_to_family(address)
          bind_to_device_option(ifname, family)

        :error ->
          []
      end

    case :gen_tcp.connect(address, port, connect_options) do
      {:ok, pid} ->
        :gen_tcp.close(pid)
        {:ok, System.monotonic_time(:microsecond) - start}

      {:error, :econnrefused} ->
        # If the connection is refused, the machine is up.
        {:ok, System.monotonic_time(:microsecond) - start}

      error ->
        error
    end
  end
end
