defmodule Towerops.Monitoring.Ping do
  @moduledoc """
  Handles ping operations for device monitoring using the system ping command.

  This implementation uses the system `ping` binary which is typically setuid root,
  allowing ICMP operations without requiring CAP_NET_RAW or elevated privileges
  for the application itself.
  """

  @behaviour Towerops.Monitoring.PingBehaviour

  require Logger

  @doc """
  Pings an IP address using the system ping command with default timeout (5 seconds).

  Returns {:ok, response_time_ms} on success, {:error, reason} on failure.
  Response time is returned as a float with millisecond precision.
  """
  @impl true
  def ping(ip_address) when is_binary(ip_address) do
    ping(ip_address, 5000)
  end

  @doc """
  Pings an IP address using the system ping command with custom timeout.

  Returns {:ok, response_time_ms} on success, {:error, reason} on failure.
  Response time is returned as a float with millisecond precision.

  Note: The system ping command has a minimum timeout of 1 second.
  Timeouts less than 1000ms will be rounded up to 1 second.
  """
  @impl true
  def ping(ip_address, timeout_ms) when is_binary(ip_address) and is_integer(timeout_ms) do
    case validate_ip_address(ip_address) do
      :ok ->
        do_ping(ip_address, timeout_ms)

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

  defp do_ping(ip_address, timeout_ms) do
    timeout_seconds = max(1, div(timeout_ms, 1000))

    # Use -W for timeout on macOS/BSD, -w for Linux
    # -c 1 sends a single ping
    args =
      case :os.type() do
        {:unix, :darwin} ->
          # macOS: -W is timeout in ms, but we use seconds for consistency
          ["-c", "1", "-W", to_string(timeout_seconds * 1000), ip_address]

        {:unix, _} ->
          # Linux: -W is timeout in seconds
          ["-c", "1", "-W", to_string(timeout_seconds), ip_address]

        _ ->
          # Fallback
          ["-c", "1", "-W", to_string(timeout_seconds), ip_address]
      end

    case System.cmd("ping", args, stderr_to_stdout: true) do
      {output, 0} ->
        parse_ping_output(output)

      {output, _exit_code} ->
        Logger.debug("Ping failed for #{ip_address}: #{String.trim(output)}")
        {:error, :timeout}
    end
  rescue
    e in ErlangError ->
      Logger.error("Ping command failed: #{inspect(e)}")
      {:error, :command_not_found}
  end

  # Validate IP address format using Erlang's inet module
  defp validate_ip_address(""), do: {:error, :invalid_ip}

  defp validate_ip_address(ip_address) do
    case :inet.parse_address(String.to_charlist(ip_address)) do
      {:ok, _} -> :ok
      {:error, _} -> {:error, :invalid_ip}
    end
  end

  # Parse the ping output to extract round-trip time
  # macOS format: "round-trip min/avg/max/stddev = 1.234/1.234/1.234/0.000 ms"
  # Linux format: "rtt min/avg/max/mdev = 1.234/1.234/1.234/0.000 ms"
  defp parse_ping_output(output) do
    cond do
      # macOS format
      match = Regex.run(~r/round-trip.*=\s*[\d.]+\/([\d.]+)\//, output) ->
        [_, avg_ms] = match
        {:ok, String.to_float(avg_ms)}

      # Linux format
      match = Regex.run(~r/rtt.*=\s*[\d.]+\/([\d.]+)\//, output) ->
        [_, avg_ms] = match
        {:ok, String.to_float(avg_ms)}

      # Alternative: extract from "time=X.XX ms" in the reply line
      match = Regex.run(~r/time[=<]([\d.]+)\s*ms/, output) ->
        [_, time_ms] = match
        {:ok, String.to_float(time_ms)}

      true ->
        Logger.warning("Could not parse ping output: #{String.trim(output)}")
        {:error, :parse_error}
    end
  end
end
