defmodule Towerops.Monitoring.Ping do
  @moduledoc """
  Handles ping operations for device monitoring using raw ICMP sockets.

  This implementation uses raw ICMP echo request/reply packets instead of
  relying on the system `ping` command, making it suitable for containerized
  environments where the ping command may not be available.

  Note: Raw ICMP sockets require elevated privileges (CAP_NET_RAW on Linux).
  The application should be run with appropriate capabilities or use setcap
  on the beam executable.
  """

  @behaviour Towerops.Monitoring.PingBehaviour

  import Bitwise

  require Logger

  # ICMP protocol number
  @icmp_proto 1

  # ICMP message types
  @icmp_echo_request 8
  @icmp_echo_reply 0

  @doc """
  Pings an IP address using raw ICMP echo request/reply.

  Returns {:ok, response_time_ms} on success, {:error, reason} on failure.
  """
  @impl true
  def ping(ip_address, timeout_ms \\ 5000) do
    start_time = System.monotonic_time(:millisecond)

    case send_icmp_echo(ip_address, timeout_ms) do
      :ok ->
        end_time = System.monotonic_time(:millisecond)
        response_time = end_time - start_time
        {:ok, response_time}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp send_icmp_echo(ip_address, timeout_ms) do
    # Parse IP address
    case parse_ip_address(ip_address) do
      {:ok, ip_tuple} ->
        send_icmp_packet(ip_tuple, timeout_ms)

      {:error, reason} ->
        {:error, reason}
    end
  end

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

  defp send_icmp_packet(ip_tuple, timeout_ms) do
    # Generate unique identifier and sequence number
    identifier = :rand.uniform(65_535)
    sequence = :rand.uniform(65_535)

    # Build ICMP echo request packet
    packet = build_icmp_echo_request(identifier, sequence)

    # Open raw socket
    case :gen_udp.open(0, [:binary, {:ip, ip_tuple}, {:active, false}, {:raw, @icmp_proto, 0, <<1::32>>}]) do
      {:ok, socket} ->
        try do
          # Send ICMP echo request
          :ok = :gen_udp.send(socket, ip_tuple, 0, packet)

          # Wait for ICMP echo reply
          case :gen_udp.recv(socket, 0, timeout_ms) do
            {:ok, {^ip_tuple, 0, reply_packet}} ->
              case parse_icmp_reply(reply_packet, identifier, sequence) do
                :ok -> :ok
                {:error, reason} -> {:error, reason}
              end

            {:error, :timeout} ->
              {:error, :timeout}

            {:error, reason} ->
              {:error, reason}
          end
        after
          :gen_udp.close(socket)
        end

      {:error, :eacces} ->
        Logger.warning(
          "ICMP ping requires elevated privileges. Run with CAP_NET_RAW capability or as root. Falling back to unavailable."
        )

        {:error, :insufficient_privileges}

      {:error, reason} ->
        {:error, reason}
    end
  rescue
    e ->
      Logger.error("ICMP ping failed with exception: #{Exception.message(e)}")
      {:error, {:exception, Exception.message(e)}}
  end

  defp build_icmp_echo_request(identifier, sequence) do
    # ICMP Echo Request format:
    # Type (8) | Code (0) | Checksum (16) | Identifier (16) | Sequence (16) | Data (variable)
    type = @icmp_echo_request
    code = 0
    # Placeholder checksum
    checksum = 0
    # Payload data (typically timestamp or pattern)
    data = "towerops_ping"

    # Build packet without checksum
    packet_without_checksum =
      <<type::8, code::8, checksum::16, identifier::16, sequence::16, data::binary>>

    # Calculate checksum
    calculated_checksum = icmp_checksum(packet_without_checksum)

    # Rebuild packet with correct checksum
    <<type::8, code::8, calculated_checksum::16, identifier::16, sequence::16, data::binary>>
  end

  defp parse_icmp_reply(packet, expected_identifier, expected_sequence) do
    # ICMP reply might be wrapped in IP header in some cases
    # Try to parse as raw ICMP first
    case packet do
      <<@icmp_echo_reply::8, 0::8, _checksum::16, ^expected_identifier::16, ^expected_sequence::16, _rest::binary>> ->
        :ok

      # Handle IP header wrapping (20 bytes minimum)
      <<_ip_header::binary-size(20), @icmp_echo_reply::8, 0::8, _checksum::16, ^expected_identifier::16,
        ^expected_sequence::16, _rest::binary>> ->
        :ok

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

  defp icmp_checksum(data) do
    # ICMP checksum is the 16-bit one's complement of the one's complement sum
    # of the ICMP message starting with the ICMP Type
    sum = checksum_sum(data, 0)

    # Fold carry bits into the sum
    sum = (sum &&& 0xFFFF) + (sum >>> 16)
    sum = (sum &&& 0xFFFF) + (sum >>> 16)

    # One's complement
    bnot(sum) &&& 0xFFFF
  end

  defp checksum_sum(<<>>, acc), do: acc

  defp checksum_sum(<<word::16, rest::binary>>, acc) do
    checksum_sum(rest, acc + word)
  end

  # Handle odd number of bytes
  defp checksum_sum(<<byte::8>>, acc) do
    # Pad with zero
    acc + (byte <<< 8)
  end
end
