defmodule SnmpKit.SnmpLib.Transport do
  @moduledoc """
  UDP transport layer for SNMP communications.

  Provides socket management, connection utilities, and network operations
  for both SNMP managers and agents/simulators.

  ## Features

  - UDP socket creation and management
  - Address resolution and validation
  - Connection pooling and reuse
  - Timeout handling
  - Error recovery
  - Performance optimizations

  ## Examples

      # Create and use a socket
      {:ok, socket} = SnmpKit.SnmpLib.Transport.create_socket("0.0.0.0", 161)
      {:ok, data} = SnmpKit.SnmpLib.Transport.receive_packet(socket, 5000)
      :ok = SnmpKit.SnmpLib.Transport.send_packet(socket, "192.168.1.100", 161, packet_data)
      :ok = SnmpKit.SnmpLib.Transport.close_socket(socket)

      # Address utilities
      {:ok, {192, 168, 1, 100}} = SnmpKit.SnmpLib.Transport.resolve_address("192.168.1.100")
      true = SnmpKit.SnmpLib.Transport.validate_port(161)
  """

  require Logger

  @type socket :: :gen_udp.socket()
  @type address :: :inet.socket_address() | :inet.hostname() | binary()
  @type port_number :: :inet.port_number()
  @type packet_data :: binary()
  @type socket_options :: [:gen_udp.option()]

  # Default socket options
  @default_socket_options [
    {:active, false},
    {:reuseaddr, true}
  ]

  # Standard SNMP ports
  @snmp_agent_port 161
  @snmp_trap_port 162

  ## Socket Management

  @doc """
  Creates a UDP socket bound to the specified address and port.

  ## Parameters

  - `bind_address`: Address to bind to (use "0.0.0.0" for all interfaces)
  - `port`: Port number to bind to
  - `options`: Additional socket options (optional)

  ## Returns

  - `{:ok, socket}` on success
  - `{:error, reason}` on failure

  ## Examples

      {:ok, socket} = SnmpKit.SnmpLib.Transport.create_socket("0.0.0.0", 161)
      {:ok, client_socket} = SnmpKit.SnmpLib.Transport.create_socket("0.0.0.0", 0, [{:active, true}])
  """
  @spec create_socket(binary() | :inet.socket_address(), port_number(), socket_options()) ::
          {:ok, socket()} | {:error, atom()}
  def create_socket(bind_address, port, options \\ []) do
    with {:ok, resolved_address} <- resolve_address(bind_address),
         :ok <- validate_port_result(port) do
      open_udp_socket(resolved_address, port, options)
    end
  end

  defp validate_port_result(port) do
    if validate_port(port), do: :ok, else: {:error, :invalid_port}
  end

  defp open_udp_socket(resolved_address, port, options) do
    merged_options = Keyword.merge(@default_socket_options, options)

    case :gen_udp.open(port, [:binary, {:ip, resolved_address} | merged_options]) do
      {:ok, socket} ->
        Logger.debug("Created UDP socket bound to #{format_endpoint(resolved_address, port)}")
        {:ok, socket}

      {:error, reason} ->
        Logger.error("Failed to create UDP socket: #{inspect(reason)}")
        {:error, reason}
    end
  end

  @doc """
  Creates a client socket for outgoing SNMP requests.

  Uses an ephemeral port and optimizes settings for client use.

  ## Examples

      {:ok, socket} = SnmpKit.SnmpLib.Transport.create_client_socket()
      {:ok, socket} = SnmpKit.SnmpLib.Transport.create_client_socket([{:recbuf, 65536}])
  """
  @spec create_client_socket(socket_options()) :: {:ok, socket()} | {:error, atom()}
  def create_client_socket(options \\ []) do
    # Use ephemeral port (0) for client connections - bypass validation for ephemeral ports
    bind_address = "0.0.0.0"

    case resolve_address(bind_address) do
      {:ok, resolved_address} ->
        client_options =
          Keyword.merge(
            @default_socket_options,
            [
              {:active, false},
              # Larger receive buffer for responses
              {:recbuf, 65_536},
              # Smaller send buffer for requests
              {:sndbuf, 8192}
            ] ++ options
          )

        # Add IP binding if not 0.0.0.0
        final_options =
          if resolved_address == {0, 0, 0, 0} do
            client_options
          else
            [{:ip, resolved_address} | client_options]
          end

        case :gen_udp.open(0, [:binary | final_options]) do
          {:ok, socket} ->
            Logger.debug("Created UDP socket bound to #{inspect(resolved_address)}:0")
            {:ok, socket}

          {:error, reason} ->
            Logger.error("Failed to create UDP socket: #{inspect(reason)}")
            {:error, reason}
        end

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

  @doc """
  Creates a server socket for incoming SNMP requests.

  Optimizes settings for server use with proper buffer sizes.

  ## Examples

      {:ok, socket} = SnmpKit.SnmpLib.Transport.create_server_socket(161)
      {:ok, socket} = SnmpKit.SnmpLib.Transport.create_server_socket(161, "192.168.1.10")
  """
  @spec create_server_socket(port_number(), binary()) :: {:ok, socket()} | {:error, atom()}
  def create_server_socket(port, bind_address \\ "0.0.0.0") do
    server_options = [
      {:active, false},
      # Smaller receive buffer for requests
      {:recbuf, 8192},
      # Larger send buffer for responses
      {:sndbuf, 65_536}
    ]

    # Allow port 0 for ephemeral ports in testing, but validate others
    if port == 0 do
      create_ephemeral_server_socket(bind_address, port, server_options)
    else
      create_socket(bind_address, port, server_options)
    end
  end

  defp create_ephemeral_server_socket(bind_address, port, server_options) do
    with {:ok, resolved_address} <- resolve_address(bind_address) do
      open_udp_socket(resolved_address, port, server_options)
    end
  end

  @doc """
  Sends a packet to the specified destination.

  ## Parameters

  - `socket`: UDP socket to send from
  - `dest_address`: Destination IP address or hostname
  - `dest_port`: Destination port number
  - `data`: Binary data to send

  ## Returns

  - `:ok` on success
  - `{:error, reason}` on failure

  ## Examples

      :ok = SnmpKit.SnmpLib.Transport.send_packet(socket, "192.168.1.100", 161, packet_data)
      :ok = SnmpKit.SnmpLib.Transport.send_packet(socket, {192, 168, 1, 100}, 161, packet_data)
  """
  @spec send_packet(socket(), address(), port_number(), packet_data()) ::
          :ok | {:error, atom()}
  def send_packet(socket, dest_address, dest_port, data) when is_binary(data) do
    Logger.debug(
      "send_packet called with: socket=#{inspect(socket)}, dest_address=#{inspect(dest_address)}, dest_port=#{inspect(dest_port)}, data_size=#{byte_size(data)}"
    )

    case {resolve_address(dest_address), validate_port(dest_port)} do
      {{:ok, resolved_address}, true} ->
        Logger.debug("Address resolved to: #{inspect(resolved_address)}, port validated: #{dest_port}")

        Logger.debug(
          "About to call :gen_udp.send with: socket=#{inspect(socket)}, ip=#{inspect(resolved_address)}, port=#{dest_port}, data_type=#{inspect(data)}"
        )

        result = :gen_udp.send(socket, resolved_address, dest_port, data)

        case result do
          :ok ->
            Logger.debug("Sent #{byte_size(data)} bytes to #{format_endpoint(resolved_address, dest_port)}")

            :ok

          {:error, reason} ->
            Logger.error("Failed to send packet: #{inspect(reason)}")

            Logger.error(
              "gen_udp.send failed with: socket=#{inspect(socket)}, ip=#{inspect(resolved_address)}, port=#{dest_port}, data_size=#{byte_size(data)}"
            )

            Logger.error("Socket info: #{inspect(:inet.sockname(socket))}")

            Logger.error("Data sample: #{inspect(binary_part(data, 0, min(50, byte_size(data))))}")

            {:error, reason}
        end

      {{:error, reason}, _} ->
        Logger.error("Address resolution failed: #{inspect(reason)} for address: #{inspect(dest_address)}")

        {:error, reason}

      {_, false} ->
        Logger.error("Port validation failed for port: #{inspect(dest_port)}")
        {:error, :invalid_port}
    end
  end

  def send_packet(_, _, _, _), do: {:error, :invalid_data}

  @doc """
  Receives a packet from the socket with optional timeout.

  ## Parameters

  - `socket`: UDP socket to receive from
  - `timeout`: Timeout in milliseconds (default: 5000)

  ## Returns

  - `{:ok, {data, from_address, from_port}}` on success
  - `{:error, reason}` on failure or timeout

  ## Examples

      {:ok, {data, from_ip, from_port}} = SnmpKit.SnmpLib.Transport.receive_packet(socket)
      {:ok, {data, from_ip, from_port}} = SnmpKit.SnmpLib.Transport.receive_packet(socket, 10000)
  """
  @spec send_and_receive_packet(
          :inet.socket_address(),
          port_number(),
          packet_data(),
          non_neg_integer()
        ) ::
          {:ok, {packet_data(), :inet.socket_address(), port_number()}} | {:error, atom()}
  def send_and_receive_packet(_dest_address, _dest_port, _data, _timeout \\ 5000) do
    {:error, :not_supported}
  end

  @spec receive_packet(socket(), non_neg_integer()) ::
          {:ok, {packet_data(), :inet.socket_address(), port_number()}} | {:error, atom()}
  def receive_packet(socket, timeout \\ 5000) when is_integer(timeout) and timeout >= 0 do
    case :gen_udp.recv(socket, 0, timeout) do
      {:ok, {from_address, from_port, data}} when is_binary(data) ->
        Logger.debug("Received #{byte_size(data)} bytes from #{format_endpoint(from_address, from_port)}")

        {:ok, {data, from_address, from_port}}

      {:ok, invalid_response} ->
        Logger.error("Invalid UDP response format: #{inspect(invalid_response)}")
        {:error, :invalid_response}

      {:error, :timeout} ->
        Logger.debug("Socket receive timeout after #{timeout}ms")
        {:error, :timeout}

      {:error, reason} ->
        Logger.error("Failed to receive packet: #{inspect(reason)}")
        {:error, reason}
    end
  end

  @doc """
  Receives a packet with a custom filter function.

  Continues receiving until a packet matches the filter or timeout occurs.

  ## Parameters

  - `socket`: UDP socket to receive from
  - `filter_fn`: Function that returns true for desired packets
  - `timeout`: Total timeout in milliseconds
  - `per_recv_timeout`: Timeout per receive attempt (default: 1000ms)

  ## Examples

      # Wait for packet from specific address
      filter_fn = fn {_data, from_addr, _from_port} -> from_addr == {192, 168, 1, 100} end
      {:ok, {data, addr, port}} = SnmpKit.SnmpLib.Transport.receive_packet_filtered(socket, filter_fn, 5000)
  """
  @spec receive_packet_filtered(socket(), function(), non_neg_integer(), non_neg_integer()) ::
          {:ok, {packet_data(), :inet.socket_address(), port_number()}} | {:error, atom()}
  def receive_packet_filtered(socket, filter_fn, timeout, per_recv_timeout \\ 1000) when is_function(filter_fn, 1) do
    start_time = System.monotonic_time(:millisecond)
    receive_packet_filtered_loop(socket, filter_fn, timeout, per_recv_timeout, start_time)
  end

  @doc """
  Closes a UDP socket.

  ## Examples

      :ok = SnmpKit.SnmpLib.Transport.close_socket(socket)
  """
  @spec close_socket(socket()) :: :ok
  def close_socket(socket) do
    :ok = :gen_udp.close(socket)
    Logger.debug("Closed UDP socket")
    :ok
  end

  ## Address and Network Utilities

  @doc """
  Resolves an address to an IP tuple.

  Accepts:
  - IP address strings (e.g., "192.168.1.1")
  - Hostnames (e.g., "localhost")
  - IP tuples (e.g., {192, 168, 1, 1})

  ## Examples

      iex> SnmpKit.SnmpLib.Transport.resolve_address("192.168.1.1")
      {:ok, {192, 168, 1, 1}}

      iex> SnmpKit.SnmpLib.Transport.resolve_address({192, 168, 1, 1})
      {:ok, {192, 168, 1, 1}}
  """
  @spec resolve_address(address()) :: {:ok, :inet.socket_address()} | {:error, atom()}
  def resolve_address(address) when is_tuple(address) do
    if valid_ipv4_tuple?(address) do
      {:ok, address}
    else
      {:error, :invalid_ip_tuple}
    end
  end

  def resolve_address(address) when is_binary(address) do
    charlist = String.to_charlist(address)

    case :inet.parse_address(charlist) do
      {:ok, ip_tuple} ->
        {:ok, ip_tuple}

      {:error, :einval} ->
        resolve_hostname(charlist, address)
    end
  end

  def resolve_address(address) when is_list(address) do
    case :inet.parse_address(address) do
      {:ok, ip_tuple} ->
        {:ok, ip_tuple}

      {:error, :einval} ->
        address_str = List.to_string(address)
        resolve_hostname(address, address_str)
    end
  end

  def resolve_address(_), do: {:error, :invalid_address_format}

  defp valid_ipv4_tuple?({a, b, c, d}) when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) do
    a in 0..255 and b in 0..255 and c in 0..255 and d in 0..255
  end

  defp valid_ipv4_tuple?(_), do: false

  defp resolve_hostname(charlist, display_address) do
    case :inet.gethostbyname(charlist) do
      {:ok, {:hostent, _name, _aliases, :inet, 4, [ip_tuple | _]}} ->
        {:ok, ip_tuple}

      {:error, reason} ->
        Logger.error("Failed to resolve hostname #{display_address}: #{inspect(reason)}")
        {:error, :hostname_resolution_failed}
    end
  end

  @doc """
  Validates a port number.

  ## Examples

      true = SnmpKit.SnmpLib.Transport.validate_port(161)
      true = SnmpKit.SnmpLib.Transport.validate_port(65535)
      true = SnmpKit.SnmpLib.Transport.validate_port(0)
      false = SnmpKit.SnmpLib.Transport.validate_port(65536)
  """
  @spec validate_port(term()) :: boolean()
  def validate_port(port) when is_integer(port) and port > 0 and port <= 65_535, do: true
  def validate_port(_), do: false

  @doc """
  Formats an endpoint (address and port) as a string.

  ## Examples

      "192.168.1.100:161" = SnmpKit.SnmpLib.Transport.format_endpoint({192, 168, 1, 100}, 161)
      "localhost:162" = SnmpKit.SnmpLib.Transport.format_endpoint("localhost", 162)
  """
  @spec format_endpoint(address(), port_number()) :: binary()
  def format_endpoint(address, port) do
    address_str =
      case address do
        {a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}"
        addr when is_binary(addr) -> addr
        _ -> inspect(address)
      end

    "#{address_str}:#{port}"
  end

  @doc """
  Gets the local address and port of a socket.

  ## Examples

      {:ok, {{127, 0, 0, 1}, 12345}} = SnmpKit.SnmpLib.Transport.get_socket_address(socket)
  """
  @spec get_socket_address(socket()) ::
          {:ok, {:inet.socket_address(), port_number()}} | {:error, atom()}
  def get_socket_address(socket) do
    case :inet.sockname(socket) do
      {:ok, {address, port}} ->
        {:ok, {address, port}}

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

  ## Connection Management

  @doc """
  Tests connectivity to a destination by sending a test packet.

  This creates a temporary socket, sends a small packet, and waits for any response
  to verify network connectivity.

  ## Parameters

  - `dest_address`: Destination IP address or hostname
  - `dest_port`: Destination port number
  - `timeout`: Timeout in milliseconds (default: 3000)

  ## Returns

  - `:ok` if connectivity is confirmed
  - `{:error, reason}` if connectivity fails

  ## Examples

      :ok = SnmpKit.SnmpLib.Transport.test_connectivity("192.168.1.100", 161)
      {:error, :timeout} = SnmpKit.SnmpLib.Transport.test_connectivity("10.0.0.1", 161, 1000)
  """
  @spec test_connectivity(address(), port_number(), non_neg_integer()) :: :ok | {:error, atom()}
  def test_connectivity(dest_address, dest_port, timeout \\ 3000) do
    case create_client_socket() do
      {:ok, socket} ->
        try do
          # Send a minimal packet to test connectivity
          # Minimal ASN.1 sequence
          test_packet = <<0x30, 0x02, 0x01, 0x00>>

          case send_packet(socket, dest_address, dest_port, test_packet) do
            :ok ->
              # Wait for any response (even an error response indicates connectivity)
              case receive_packet(socket, timeout) do
                {:ok, _} -> :ok
                {:error, :timeout} -> {:error, :timeout}
                {:error, reason} -> {:error, reason}
              end

            {:error, reason} ->
              {:error, reason}
          end
        after
          close_socket(socket)
        end

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

  @doc """
  Sends a request packet and waits for a response.

  This creates a temporary socket, sends a packet, waits for the response,
  and returns the response data. Useful for SNMPv3 discovery and security operations.

  ## Parameters

  - `dest_address`: Destination IP address or hostname
  - `dest_port`: Destination port number
  - `request_data`: Binary request data to send
  - `timeout`: Timeout in milliseconds (default: 5000)

  ## Returns

  - `{:ok, response_data}` if request succeeds and response received
  - `{:error, reason}` if request fails or times out

  ## Examples

      {:ok, response} = SnmpKit.SnmpLib.Transport.send_request("192.168.1.100", 161, request_packet, 5000)
      {:error, :timeout} = SnmpKit.SnmpLib.Transport.send_request("10.0.0.1", 161, request_packet, 1000)
  """
  @spec send_request(address(), port_number(), packet_data(), non_neg_integer()) ::
          {:ok, packet_data()} | {:error, atom()}
  def send_request(dest_address, dest_port, request_data, timeout \\ 5000) do
    case create_client_socket() do
      {:ok, socket} ->
        try do
          case send_packet(socket, dest_address, dest_port, request_data) do
            :ok ->
              case receive_packet(socket, timeout) do
                {:ok, {response_data, _from_addr, _from_port}} ->
                  {:ok, response_data}

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

            {:error, reason} ->
              {:error, reason}
          end
        after
          close_socket(socket)
        end

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

  @doc """
  Gets socket statistics and information.

  ## Examples

      {:ok, stats} = SnmpKit.SnmpLib.Transport.get_socket_stats(socket)
      # stats contains buffer sizes, packet counts, etc.
  """
  @spec get_socket_stats(socket()) :: {:ok, map()} | {:error, atom()}
  def get_socket_stats(socket) do
    case collect_socket_stats(socket) do
      {:ok, stats} -> {:ok, stats}
      {:error, reason} -> {:error, reason}
    end
  rescue
    error ->
      {:error, {:stats_error, error}}
  end

  ## Utility Functions

  @doc """
  Returns standard SNMP port numbers.
  """
  @spec snmp_agent_port() :: port_number()
  def snmp_agent_port, do: @snmp_agent_port

  @spec snmp_trap_port() :: port_number()
  def snmp_trap_port, do: @snmp_trap_port

  @doc """
  Checks if a port number is a standard SNMP port.

  ## Examples

      true = SnmpKit.SnmpLib.Transport.snmp_port?(161)
      true = SnmpKit.SnmpLib.Transport.snmp_port?(162)
      false = SnmpKit.SnmpLib.Transport.snmp_port?(80)
  """
  @spec snmp_port?(port_number()) :: boolean()
  def snmp_port?(port) when port in [@snmp_agent_port, @snmp_trap_port], do: true
  def snmp_port?(_), do: false

  @doc """
  Calculates network MTU considerations for SNMP packets.

  Returns recommended maximum payload size to avoid fragmentation.

  ## Examples

      1472 = SnmpKit.SnmpLib.Transport.max_snmp_payload_size()  # Ethernet MTU - headers
  """
  @spec max_snmp_payload_size() :: non_neg_integer()
  def max_snmp_payload_size do
    # Ethernet MTU (1500) - IP header (20) - UDP header (8)
    1472
  end

  @doc """
  Validates if a packet size is suitable for SNMP transmission.

  ## Examples

      true = SnmpKit.SnmpLib.Transport.valid_packet_size?(500)
      false = SnmpKit.SnmpLib.Transport.valid_packet_size?(2000)
  """
  @spec valid_packet_size?(non_neg_integer()) :: boolean()
  def valid_packet_size?(size) when is_integer(size) and size > 0 do
    size <= max_snmp_payload_size()
  end

  def valid_packet_size?(_), do: false

  ## Private Helper Functions

  defp receive_packet_filtered_loop(socket, filter_fn, timeout, per_recv_timeout, start_time) do
    current_time = System.monotonic_time(:millisecond)
    elapsed = current_time - start_time

    if elapsed >= timeout do
      {:error, :timeout}
    else
      remaining_timeout = min(per_recv_timeout, timeout - elapsed)

      case receive_packet(socket, remaining_timeout) do
        {:ok, packet_info} ->
          handle_received_packet(socket, filter_fn, timeout, per_recv_timeout, start_time, packet_info)

        {:error, :timeout} ->
          # Continue waiting if we haven't exceeded total timeout
          receive_packet_filtered_loop(socket, filter_fn, timeout, per_recv_timeout, start_time)

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

  defp handle_received_packet(
         socket,
         filter_fn,
         timeout,
         per_recv_timeout,
         start_time,
         {data, from_addr, from_port} = packet_info
       ) do
    if filter_fn.(packet_info) do
      {:ok, {data, from_addr, from_port}}
    else
      # Packet didn't match filter, continue waiting
      receive_packet_filtered_loop(socket, filter_fn, timeout, per_recv_timeout, start_time)
    end
  end

  defp collect_socket_stats(socket) do
    stats = %{
      socket_info: :inet.info(socket),
      port_info: :erlang.port_info(socket),
      statistics: :inet.getstat(socket)
    }

    {:ok, stats}
  rescue
    _ -> {:error, :stats_unavailable}
  end
end
