defmodule SnmpKit.SnmpLib.Manager do
  @moduledoc """
  High-level SNMP management operations providing a simplified interface for common SNMP tasks.

  This module builds on the core SnmpLib functionality to provide production-ready SNMP
  management capabilities including GET, GETBULK, SET operations with intelligent error
  handling, connection reuse, and performance optimizations.

  ## Features

  - **Simple API**: High-level functions for common SNMP operations
  - **Connection Reuse**: Efficient socket management for multiple operations
  - **Error Handling**: Comprehensive error handling with meaningful messages
  - **Performance**: Optimized for bulk operations and large-scale polling
  - **Timeout Management**: Configurable timeouts with sensible defaults
  - **Community Support**: Support for different community strings per device

  ## Quick Start

      # Simple GET operation
      {:ok, {type, value}} = SnmpKit.SnmpLib.Manager.get("192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0])

      # GET with custom community and timeout
      {:ok, {type, value}} = SnmpKit.SnmpLib.Manager.get("192.168.1.1", "1.3.6.1.2.1.1.1.0",
                                         community: "private", timeout: 10_000)

      # Bulk operations for efficiency
      {:ok, results} = SnmpKit.SnmpLib.Manager.get_bulk("192.168.1.1", [1, 3, 6, 1, 2, 1, 2, 2],
                                                 max_repetitions: 20)

      # SET operation
      {:ok, :success} = SnmpKit.SnmpLib.Manager.set("192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 5, 0],
                                            {:string, "New System Name"})

  ## Configuration Options

  - `community`: SNMP community string (default: "public")
  - `version`: SNMP version (:v1, :v2c) (default: :v2c)
  - `timeout`: Operation timeout in milliseconds (default: 5000)
  - `retries`: Number of retry attempts (default: 3)
  - `port`: SNMP port (default: 161)
  - `local_port`: Local source port (default: 0 for random)
  """

  alias SnmpKit.SnmpLib.OID
  alias SnmpKit.SnmpLib.PDU
  alias SnmpKit.SnmpLib.Transport
  alias SnmpKit.SnmpLib.Utils

  require Logger

  @default_community "public"
  @default_version :v2c
  @default_timeout 5_000
  @default_retries 3
  @default_port 161
  @default_local_port 0
  @default_max_repetitions 30
  @default_non_repeaters 0

  @type host :: binary() | :inet.ip_address()
  @type oid :: [non_neg_integer()] | binary()
  @type snmp_value :: any()
  @type community :: binary()
  @type version :: :v1 | :v2c
  @type operation_result :: {:ok, snmp_value()} | {:error, atom() | {atom(), any()}}
  @type bulk_result :: {:ok, [varbind()]} | {:error, atom() | {atom(), any()}}
  @type varbind :: {oid(), atom(), snmp_value()}

  @type manager_opts :: [
          community: community(),
          version: version(),
          timeout: pos_integer(),
          retries: non_neg_integer(),
          port: pos_integer(),
          local_port: non_neg_integer()
        ]

  @type bulk_opts :: [
          community: community(),
          version: version(),
          timeout: pos_integer(),
          retries: non_neg_integer(),
          port: pos_integer(),
          local_port: non_neg_integer(),
          max_repetitions: pos_integer(),
          non_repeaters: non_neg_integer()
        ]

  ## Public API

  @doc """
  Performs an SNMP GET operation to retrieve a single value.

  ## Parameters

  - `host`: Target device IP address or hostname
  - `oid`: Object identifier as list or string (e.g., [1,3,6,1,2,1,1,1,0] or "1.3.6.1.2.1.1.1.0")
  - `opts`: Configuration options (see module docs for available options)

  ## Returns

  - `{:ok, {type, value}}`: Successfully retrieved the value with its SNMP type
  - `{:error, reason}`: Operation failed with reason

  ## Examples

      # Basic GET operation (would succeed with real device)
      # SnmpKit.SnmpLib.Manager.get("192.168.1.1", [1, 3, 6, 1, 2, 1, 1, 1, 0])
      # {:ok, {:octet_string, "Cisco IOS Software"}}

      # GET with custom community and timeout (would succeed with real device)
      # SnmpKit.SnmpLib.Manager.get("192.168.1.1", "1.3.6.1.2.1.1.1.0", community: "private", timeout: 10_000)
      # {:ok, {:octet_string, "Private System Description"}}

      # Test that function exists and handles invalid input properly
      iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.get("invalid.host", [1, 3, 6, 1, 2, 1, 1, 3, 0], timeout: 100))
      true
  """
  @spec get(host(), oid(), manager_opts()) ::
          {:ok, {atom(), any()}}
          | {:error, atom() | {:network_error, atom()} | {:socket_error, atom()}}
  def get(host, oid, opts \\ []) do
    opts = merge_default_opts(opts)
    normalized_oid = normalize_oid(oid)

    Logger.debug("Starting GET operation: host=#{inspect(host)}, oid=#{inspect(normalized_oid)}")

    case create_socket(opts) do
      {:ok, socket} ->
        Logger.debug("Socket created successfully")

        case perform_get_operation(socket, host, normalized_oid, opts) do
          {:ok, response} ->
            Logger.debug("GET operation completed, extracting result")
            :ok = close_socket(socket)
            result = extract_get_result(response)
            Logger.debug("Final GET result: #{inspect(result)}")
            result

          {:error, reason} ->
            Logger.debug("GET operation failed: #{inspect(reason)}")
            :ok = close_socket(socket)
            {:error, reason}
        end

      {:error, reason} ->
        Logger.debug("Socket creation failed: #{inspect(reason)}")
        {:error, reason}
    end
  end

  @doc """
  Performs an SNMP GETNEXT operation to retrieve the next value in the MIB tree.

  GETNEXT is used to traverse the MIB tree by retrieving the next available
  object after the specified OID. This is essential for MIB walking operations
  and discovering available objects on SNMP devices.

  ## Parameters

  - `host`: Target device IP address or hostname
  - `oid`: Object identifier to get the next value after
  - `opts`: Configuration options

  ## Implementation Details

  - **SNMP v1**: Uses proper GETNEXT PDU for compatibility
  - **SNMP v2c+**: Uses optimized GETBULK with max_repetitions=1

  ## Returns

  - `{:ok, {next_oid, type, value}}`: Next OID and its value as a tuple
  - `{:error, reason}`: Operation failed with reason

  ## Examples

      # Get next OID after system description
      {:ok, {next_oid, type, value}} = SnmpKit.SnmpLib.Manager.get_next("192.168.1.1", "1.3.6.1.2.1.1.1.0")

      # SNMP v1 compatibility
      {:ok, {next_oid, type, value}} = SnmpKit.SnmpLib.Manager.get_next("192.168.1.1", "1.3.6.1.2.1.1.1.0", version: :v1)

      # With custom community
      {:ok, {next_oid, type, value}} = SnmpKit.SnmpLib.Manager.get_next("192.168.1.1", "1.3.6.1.2.1.1.1.0",
                                                           community: "private", timeout: 10_000)
  """
  @spec get_next(host(), oid(), manager_opts()) ::
          {:ok, {oid(), atom(), any()}} | {:error, atom() | {atom(), any()}}
  def get_next(host, oid, opts \\ []) do
    opts = merge_default_opts(opts)
    normalized_oid = normalize_oid(oid)

    Logger.debug(
      "Starting GETNEXT operation: host=#{inspect(host)}, oid=#{inspect(normalized_oid)}, version=#{opts[:version]}"
    )

    case opts[:version] do
      :v1 ->
        # Use proper GETNEXT PDU for SNMP v1 compatibility
        perform_get_next_v1(host, normalized_oid, opts)

      _ ->
        # Use GETBULK with max_repetitions=1 for v2c+ efficiency
        perform_get_next_v2c(host, normalized_oid, opts)
    end
  end

  @doc """
  Performs an SNMP GETBULK operation for efficient bulk data retrieval.

  GETBULK is more efficient than multiple GET operations when retrieving
  multiple consecutive values, especially for table walking operations.

  ## Parameters

  - `host`: Target device IP address or hostname
  - `base_oid`: Base OID to start the bulk operation
  - `opts`: Configuration options including bulk-specific options

  ## Bulk-Specific Options

  - `max_repetitions`: Maximum number of repetitions (default: 10)
  - `non_repeaters`: Number of non-repeating variables (default: 0)

  ## Returns

  - `{:ok, varbinds}`: List of {oid, type, value} tuples
  - `{:error, reason}`: Operation failed with reason

  ## Examples

      # Test that get_bulk function exists and handles invalid input properly
      iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.get_bulk("invalid.host", [1, 3, 6, 1, 2, 1, 2, 2], timeout: 100))
      true

      # High-repetition bulk for large tables
      # SnmpKit.SnmpLib.Manager.get_bulk("192.168.1.1", "1.3.6.1.2.1.2.2", max_repetitions: 50)
      # {:ok, [...]} Returns up to 50 interface entries
  """
  @spec get_bulk(host(), oid(), bulk_opts()) :: bulk_result()
  def get_bulk(host, base_oid, opts \\ []) do
    opts = merge_bulk_opts(opts)
    normalized_oid = normalize_oid(base_oid)

    # GETBULK requires SNMPv2c or higher
    if opts[:version] == :v1 do
      {:error, :getbulk_requires_v2c}
    else
      with {:ok, socket} <- create_socket(opts),
           {:ok, response} <- perform_bulk_operation(socket, host, normalized_oid, opts),
           :ok <- close_socket(socket) do
        extract_bulk_result(response)
      end
    end
  end

  @doc """
  Performs an SNMP SET operation to modify a value on the target device.

  ## Parameters

  - `host`: Target device IP address or hostname
  - `oid`: Object identifier to modify
  - `value`: New value as {type, data} tuple (e.g., {:string, "new name"})
  - `opts`: Configuration options

  ## Supported Value Types

  - `{:string, binary()}`: OCTET STRING
  - `{:integer, integer()}`: INTEGER
  - `{:counter32, non_neg_integer()}`: Counter32
  - `{:gauge32, non_neg_integer()}`: Gauge32
  - `{:timeticks, non_neg_integer()}`: TimeTicks
  - `{:ip_address, binary()}`: IpAddress (4 bytes)

  ## Returns

  - `{:ok, :success}`: SET operation completed successfully
  - `{:error, reason}`: Operation failed with reason

  ## Examples

      # Test that SET function exists and handles invalid input properly
      iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.set("invalid.host", [1, 3, 6, 1, 2, 1, 1, 5, 0], {:string, "test"}, timeout: 100))
      true
  """
  @spec set(host(), oid(), {atom(), any()}, manager_opts()) :: {:ok, :success} | {:error, any()}
  def set(host, oid, {type, value}, opts \\ []) do
    opts = merge_default_opts(opts)
    normalized_oid = normalize_oid(oid)

    with {:ok, socket} <- create_socket(opts),
         {:ok, response} <-
           perform_set_operation(socket, host, normalized_oid, {type, value}, opts),
         :ok <- close_socket(socket) do
      extract_set_result(response)
    end
  end

  @doc """
  Performs multiple GET operations efficiently with connection reuse.

  More efficient than individual get/3 calls when retrieving multiple values
  from the same device by reusing the same socket connection.

  ## Parameters

  - `host`: Target device IP address or hostname
  - `oids`: List of OIDs to retrieve
  - `opts`: Configuration options

  ## Returns

  - `{:ok, results}`: List of {oid, type, value} or {oid, {:error, reason}} tuples
  - `{:error, reason}`: Connection or overall operation failed

  ## Examples

      # Test that get_multi function exists and handles invalid input properly
      iex> oids = ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.3.0", "1.3.6.1.2.1.1.5.0"]
      iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.get_multi("invalid.host", oids, timeout: 100))
      true
  """
  @spec get_multi(host(), [oid()], manager_opts()) ::
          {:ok, [{oid(), atom(), any() | {:error, any()}}]} | {:error, any()}
  def get_multi(host, oids, opts \\ []) when is_list(oids) do
    # Validate input parameters
    case oids do
      [] ->
        {:error, :empty_oids}

      _ ->
        opts = merge_default_opts(opts)
        normalized_oids = Enum.map(oids, &normalize_oid/1)

        with {:ok, socket} <- create_socket(opts) do
          results = get_multi_with_socket(socket, host, normalized_oids, opts)
          :ok = close_socket(socket)
          process_multi_results(results)
        end
    end
  end

  @doc """
  Interprets SNMP errors with enhanced semantics for common cases.

  Provides more specific error interpretation when generic errors like `:gen_err`
  are returned by devices that should return more specific SNMP error codes.

  ## Parameters

  - `error`: The original error returned by SNMP operations
  - `operation`: The SNMP operation type (`:get`, `:set`, `:get_bulk`)
  - `version`: SNMP version (`:v1`, `:v2c`, `:v3`)

  ## Returns

  More specific error atom when possible, otherwise the original error.

  ## Examples

      # Interpret genErr for GET operations
      iex> SnmpKit.SnmpLib.Manager.interpret_error(:gen_err, :get, :v2c)
      :no_such_object

      iex> SnmpKit.SnmpLib.Manager.interpret_error(:gen_err, :get, :v1)
      :no_such_name

      iex> SnmpKit.SnmpLib.Manager.interpret_error(:too_big, :get, :v2c)
      :too_big
  """
  @spec interpret_error(atom(), atom(), atom()) :: atom()
  def interpret_error(:gen_err, :get, :v1) do
    # In SNMPv1, genErr for GET operations commonly means OID doesn't exist
    :no_such_name
  end

  def interpret_error(:gen_err, :get, version) when version in [:v2c, :v2, :v3] do
    # In SNMPv2c+, genErr for GET operations commonly means object doesn't exist
    :no_such_object
  end

  def interpret_error(:gen_err, :get_bulk, version) when version in [:v2c, :v2, :v3] do
    # For bulk operations, genErr often indicates end of MIB or missing objects
    :no_such_object
  end

  def interpret_error(error, _operation, _version) do
    # Return original error for all other cases
    error
  end

  @doc """
  Checks if a host is reachable via SNMP by performing a basic GET operation.

  Useful for device discovery and health checking. Attempts to retrieve
  sysUpTime (1.3.6.1.2.1.1.3.0) which should be available on all SNMP devices.

  ## Parameters

  - `host`: Target device IP address or hostname
  - `opts`: Configuration options (typically just community and timeout)

  ## Returns

  - `{:ok, :reachable}`: Device responded to SNMP request
  - `{:error, reason}`: Device not reachable or SNMP not available

  ## Examples

      # Test that ping function exists and handles invalid input properly
      iex> match?({:error, _}, SnmpKit.SnmpLib.Manager.ping("invalid.host", timeout: 100))
      true
  """
  @spec ping(host(), manager_opts()) :: {:ok, :reachable} | {:error, any()}
  def ping(host, opts \\ []) do
    # Use sysUpTime OID as it should be available on all SNMP devices
    sys_uptime_oid = [1, 3, 6, 1, 2, 1, 1, 3, 0]

    case get(host, sys_uptime_oid, opts) do
      {:ok, {_type, _value}} -> {:ok, :reachable}
      {:error, reason} -> {:error, reason}
    end
  end

  ## Private Implementation

  # Socket management
  defp create_socket(_opts) do
    case Transport.create_client_socket() do
      {:ok, socket} -> {:ok, socket}
      {:error, reason} -> {:error, {:socket_error, reason}}
    end
  end

  defp close_socket(socket) do
    Transport.close_socket(socket)
  end

  # Operation implementations
  defp perform_get_operation(socket, host, oid, opts) do
    request_id = generate_request_id()
    pdu = PDU.build_get_request(oid, request_id)
    perform_snmp_request(socket, host, pdu, opts)
  end

  defp perform_bulk_operation(socket, host, base_oid, opts) do
    request_id = generate_request_id()
    max_reps = opts[:max_repetitions] || @default_max_repetitions
    non_reps = opts[:non_repeaters] || @default_non_repeaters

    pdu = PDU.build_get_bulk_request(base_oid, request_id, non_reps, max_reps)
    perform_snmp_request(socket, host, pdu, opts)
  end

  defp perform_set_operation(socket, host, oid, value, opts) do
    request_id = generate_request_id()
    pdu = PDU.build_set_request(oid, value, request_id)
    perform_snmp_request(socket, host, pdu, opts)
  end

  defp perform_snmp_request(socket, host, pdu, opts) do
    community = opts[:community] || @default_community
    version = opts[:version] || @default_version
    timeout = opts[:timeout] || @default_timeout
    port_option = opts[:port] || @default_port

    {parsed_host, parsed_port} = parse_host_and_port(host, port_option)
    message = PDU.build_message(pdu, community, version)
    Logger.debug("Built SNMP message: #{inspect(message)}")

    with {:ok, packet} <- encode_pdu_message(message),
         {:ok, response_packet} <- send_and_receive(socket, parsed_host, parsed_port, packet, timeout) do
      decode_response_message(response_packet)
    end
  end

  defp parse_host_and_port(host, port_option) do
    case Utils.parse_target(host) do
      {:ok, %{host: h, port: p}} ->
        # Check if host contained a port specification
        if host_contains_port?(host) do
          # Host:port format - use parsed port (backward compatibility)
          {h, p}
        else
          # Host without port - use :port option
          {h, port_option}
        end

      {:error, _} ->
        # Parse failed - use original host and :port option
        {host, port_option}
    end
  end

  defp encode_pdu_message(message) do
    case PDU.encode_message(message) do
      {:ok, packet} ->
        Logger.debug("Encoded PDU packet for transmission")
        {:ok, packet}

      {:error, encode_reason} = encode_error ->
        Logger.error("PDU encode failed: #{inspect(encode_reason)}")
        encode_error
    end
  end

  defp decode_response_message(response_packet) do
    Logger.debug("Received response packet from network")

    case PDU.decode_message(response_packet) do
      {:ok, response_message} ->
        Logger.debug("Decoded response message: #{inspect(response_message)}")
        {:ok, response_message}

      {:error, decode_reason} = decode_error ->
        Logger.error("PDU decode failed: #{inspect(decode_reason)}")
        decode_error
    end
  end

  defp send_and_receive(socket, host, port, packet, timeout) do
    # Normal send-and-receive flow
    case Transport.send_packet(socket, host, port, packet) do
      :ok ->
        Logger.debug("Packet sent successfully, waiting for response (timeout: #{timeout}ms)")

        case Transport.receive_packet(socket, timeout) do
          {:ok, {response_packet, _from_addr, _from_port}} ->
            Logger.debug("Received response packet: #{byte_size(response_packet)} bytes")
            {:ok, response_packet}

          {:error, :timeout} = timeout_error ->
            Logger.debug("Timeout waiting for response after #{timeout}ms")
            timeout_error

          {:error, reason} ->
            Logger.debug("Error receiving response: #{inspect(reason)}")
            {:error, {:network_error, reason}}
        end

      {:error, reason} ->
        Logger.debug("Error sending packet: #{inspect(reason)}")
        {:error, {:network_error, reason}}
    end
  end

  # Multi-get implementation with connection reuse
  defp get_multi_with_socket(socket, host, oids, opts) do
    Enum.map(oids, fn oid ->
      perform_single_get(socket, host, oid, opts)
    end)
  end

  defp perform_single_get(socket, host, oid, opts) do
    case perform_get_operation(socket, host, oid, opts) do
      {:ok, response} ->
        case extract_get_result_with_oid(response) do
          {:ok, {oid, type, value}} -> {oid, type, value}
          {:error, reason} -> {oid, {:error, reason}}
        end

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

  # Result extraction
  defp extract_get_result(%{pdu: %{error_status: error_status}} = response) when error_status != 0 do
    Logger.debug("Extracting error result - error_status: #{error_status}")
    Logger.debug("Full response PDU: #{inspect(response.pdu)}")
    {:error, decode_error_status(error_status)}
  end

  # Handle responses with one or more varbinds - some devices return extra varbinds
  defp extract_get_result(%{pdu: %{varbinds: [{oid, type, value} | _rest]}} = response) do
    Logger.debug("Extracting successful result - PDU: #{inspect(response.pdu)}")

    Logger.debug("Varbind details - oid: #{inspect(oid)}, type: #{inspect(type)}, value: #{inspect(value)}")

    # Check for SNMPv2c exception values in both type and value fields
    case {type, value} do
      # Exception values in type field (from simulator)
      {:no_such_object, _} ->
        Logger.debug("Found exception in type field: no_such_object")
        {:error, :no_such_object}

      {:no_such_instance, _} ->
        Logger.debug("Found exception in type field: no_such_instance")
        {:error, :no_such_instance}

      {:end_of_mib_view, _} ->
        Logger.debug("Found exception in type field: end_of_mib_view")
        {:error, :end_of_mib_view}

      # Exception values in value field (standard format)
      {_, {:no_such_object, _}} ->
        Logger.debug("Found exception in value field: no_such_object")
        {:error, :no_such_object}

      {_, {:no_such_instance, _}} ->
        Logger.debug("Found exception in value field: no_such_instance")
        {:error, :no_such_instance}

      {_, {:end_of_mib_view, _}} ->
        Logger.debug("Found exception in value field: end_of_mib_view")
        {:error, :end_of_mib_view}

      # Normal value - return type and value only (OID is known from input)
      _ ->
        Logger.debug("Returning successful value with type: #{inspect({type, value})}")
        {:ok, {type, value}}
    end
  end

  defp extract_get_result(response) do
    Logger.error("Invalid response format: #{inspect(response)}")
    {:error, :invalid_response}
  end

  defp extract_get_result_with_oid(%{pdu: %{error_status: error_status}} = response) when error_status != 0 do
    Logger.debug("Extracting error result - error_status: #{error_status}")
    Logger.debug("Full response PDU: #{inspect(response.pdu)}")
    {:error, decode_error_status(error_status)}
  end

  # Handle responses with one or more varbinds - some devices return extra varbinds
  defp extract_get_result_with_oid(%{pdu: %{varbinds: [{oid, type, value} | _rest]}} = response) do
    Logger.debug("Extracting successful result - PDU: #{inspect(response.pdu)}")

    Logger.debug("Varbind details - oid: #{inspect(oid)}, type: #{inspect(type)}, value: #{inspect(value)}")

    # Check for SNMPv2c exception values in both type and value fields
    case {type, value} do
      # Exception values in type field (from simulator)
      {:no_such_object, _} ->
        Logger.debug("Found exception in type field: no_such_object")
        {:error, :no_such_object}

      {:no_such_instance, _} ->
        Logger.debug("Found exception in type field: no_such_instance")
        {:error, :no_such_instance}

      {:end_of_mib_view, _} ->
        Logger.debug("Found exception in type field: end_of_mib_view")
        {:error, :end_of_mib_view}

      # Exception values in value field (standard format)
      {_, {:no_such_object, _}} ->
        Logger.debug("Found exception in value field: no_such_object")
        {:error, :no_such_object}

      {_, {:no_such_instance, _}} ->
        Logger.debug("Found exception in value field: no_such_instance")
        {:error, :no_such_instance}

      {_, {:end_of_mib_view, _}} ->
        Logger.debug("Found exception in value field: end_of_mib_view")
        {:error, :end_of_mib_view}

      # Normal value - return full 3-tuple for multi operations
      _ ->
        Logger.debug("Returning successful varbind with type: #{inspect({oid, type, value})}")
        {:ok, {oid, type, value}}
    end
  end

  defp extract_get_result_with_oid(response) do
    Logger.error("Invalid response format: #{inspect(response)}")
    {:error, :invalid_response}
  end

  defp extract_bulk_result(%{pdu: %{error_status: error_status}}) when error_status != 0 do
    {:error, decode_error_status(error_status)}
  end

  defp extract_bulk_result(%{pdu: %{varbinds: varbinds}}) do
    valid_varbinds =
      Enum.filter(varbinds, fn {_oid, type, value} ->
        # Check for SNMPv2c exception values in both type and value fields
        case {type, value} do
          # Exception values in type field (from simulator)
          {:no_such_object, _} -> false
          {:no_such_instance, _} -> false
          {:end_of_mib_view, _} -> false
          # Exception values in value field (standard format)
          {_, {:no_such_object, _}} -> false
          {_, {:no_such_instance, _}} -> false
          {_, {:end_of_mib_view, _}} -> false
          # Valid varbind
          _ -> true
        end
      end)

    # Return documented 3-tuple varbind format {oid, type, value}
    {:ok, valid_varbinds}
  end

  defp extract_bulk_result(_), do: {:error, :invalid_response}

  defp extract_set_result(%{pdu: %{error_status: 0}}) do
    {:ok, :success}
  end

  defp extract_set_result(%{pdu: %{error_status: error_status}}) when error_status != 0 do
    {:error, decode_error_status(error_status)}
  end

  defp extract_set_result(_), do: {:error, :invalid_response}

  # GETNEXT implementation for SNMP v1
  defp perform_get_next_v1(host, oid, opts) do
    case create_socket(opts) do
      {:ok, socket} ->
        Logger.debug("Socket created successfully for GETNEXT v1")

        case perform_get_next_operation(socket, host, oid, opts) do
          {:ok, response} ->
            Logger.debug("GETNEXT v1 operation completed, extracting result")
            :ok = close_socket(socket)
            result = extract_get_next_result(response)
            Logger.debug("Final GETNEXT v1 result: #{inspect(result)}")
            result

          {:error, reason} ->
            Logger.debug("GETNEXT v1 operation failed: #{inspect(reason)}")
            :ok = close_socket(socket)
            {:error, reason}
        end

      {:error, reason} ->
        Logger.debug("Socket creation failed for GETNEXT v1: #{inspect(reason)}")
        {:error, reason}
    end
  end

  # GETNEXT implementation for SNMP v2c+
  defp perform_get_next_v2c(host, oid, opts) do
    # Use get_bulk with max_repetitions=1 for efficiency
    bulk_opts = Keyword.merge(opts, max_repetitions: 1, non_repeaters: 0)

    case get_bulk(host, oid, bulk_opts) do
      {:ok, [{next_oid, type, value}]} ->
        Logger.debug("GETNEXT v2c+ via GETBULK successful: #{inspect({next_oid, type, value})}")
        {:ok, {next_oid, type, value}}

      {:ok, []} ->
        Logger.debug("GETNEXT v2c+ reached end of MIB")
        {:error, :end_of_mib_view}

      {:ok, results} when is_list(results) ->
        # Take the first result if multiple returned
        case List.first(results) do
          {next_oid, type, value} -> {:ok, {next_oid, type, value}}
          _ -> {:error, :invalid_response}
        end

      {:error, reason} ->
        Logger.debug("GETNEXT v2c+ via GETBULK failed: #{inspect(reason)}")
        {:error, reason}
    end
  end

  defp perform_get_next_operation(socket, host, oid, opts) do
    request_id = generate_request_id()
    pdu = PDU.build_get_next_request(oid, request_id)
    perform_snmp_request(socket, host, pdu, opts)
  end

  # Handle responses with one or more varbinds - some devices return extra varbinds
  defp extract_get_next_result(%{pdu: %{varbinds: [{next_oid, type, value} | _rest]}} = response) do
    Logger.debug("Extracting GETNEXT result - PDU: #{inspect(response.pdu)}")

    Logger.debug("Varbind details - next_oid: #{inspect(next_oid)}, type: #{inspect(type)}, value: #{inspect(value)}")

    # Check for SNMPv2c exception values in both type and value fields
    case {type, value} do
      # Exception values in type field (from simulator)
      {:no_such_object, _} ->
        Logger.debug("Found exception in type field: no_such_object")
        {:error, :no_such_object}

      {:no_such_instance, _} ->
        Logger.debug("Found exception in type field: no_such_instance")
        {:error, :no_such_instance}

      {:end_of_mib_view, _} ->
        Logger.debug("Found exception in type field: end_of_mib_view")
        {:error, :end_of_mib_view}

      # Exception values in value field (standard format)
      {_, {:no_such_object, _}} ->
        Logger.debug("Found exception in value field: no_such_object")
        {:error, :no_such_object}

      {_, {:no_such_instance, _}} ->
        Logger.debug("Found exception in value field: no_such_instance")
        {:error, :no_such_instance}

      {_, {:end_of_mib_view, _}} ->
        Logger.debug("Found exception in value field: end_of_mib_view")
        {:error, :end_of_mib_view}

      # Normal value - return both next OID and value
      _ ->
        Logger.debug("Returning successful GETNEXT result: #{inspect({next_oid, type, value})}")
        {:ok, {next_oid, type, value}}
    end
  end

  defp extract_get_next_result(response) do
    Logger.error("Invalid GETNEXT response format: #{inspect(response)}")
    {:error, :invalid_response}
  end

  # Helper functions
  defp normalize_oid(oid) when is_list(oid) do
    # Validate the list OID before returning
    case OID.valid_oid?(oid) do
      :ok -> oid
      {:error, :empty_oid} -> [1, 3, 6, 1]
      {:error, _} -> [1, 3, 6, 1]
    end
  end

  defp normalize_oid(oid) when is_binary(oid) do
    # First try MIB symbolic name resolution
    case SnmpKit.SnmpLib.MIB.Registry.resolve_name(oid) do
      {:ok, oid_list} ->
        oid_list

      {:error, _} ->
        # Fallback to numeric string parsing
        case OID.string_to_list(oid) do
          {:ok, oid_list} -> oid_list
          # Safe fallback
          {:error, _} -> [1, 3, 6, 1]
        end
    end
  end

  defp normalize_oid(_), do: [1, 3, 6, 1]

  defp generate_request_id do
    :rand.uniform(2_147_483_647)
  end

  defp decode_error_status(0), do: :no_error
  defp decode_error_status(1), do: :too_big
  defp decode_error_status(2), do: :no_such_name
  defp decode_error_status(3), do: :bad_value
  defp decode_error_status(4), do: :read_only
  defp decode_error_status(5), do: :gen_err
  # SNMPv2c additional error codes (RFC 3416)
  defp decode_error_status(6), do: :no_access
  defp decode_error_status(7), do: :wrong_type
  defp decode_error_status(8), do: :wrong_length
  defp decode_error_status(9), do: :wrong_encoding
  defp decode_error_status(10), do: :wrong_value
  defp decode_error_status(11), do: :no_creation
  defp decode_error_status(12), do: :inconsistent_value
  defp decode_error_status(13), do: :resource_unavailable
  defp decode_error_status(14), do: :commit_failed
  defp decode_error_status(15), do: :undo_failed
  defp decode_error_status(16), do: :authorization_error
  defp decode_error_status(17), do: :not_writable
  defp decode_error_status(18), do: :inconsistent_name
  defp decode_error_status(error), do: {:unknown_error, error}

  defp merge_default_opts(opts) do
    Keyword.merge(
      [
        community: @default_community,
        version: @default_version,
        timeout: @default_timeout,
        retries: @default_retries,
        port: @default_port,
        local_port: @default_local_port
      ],
      opts
    )
  end

  defp merge_bulk_opts(opts) do
    opts
    |> merge_default_opts()
    |> Keyword.merge(
      max_repetitions: @default_max_repetitions,
      non_repeaters: @default_non_repeaters
    )
    |> Keyword.merge(opts)
  end

  # Helper to determine if host string contains port specification
  defp host_contains_port?(host) when is_binary(host) do
    cond do
      # RFC 3986 bracket notation: [IPv6]:port
      String.starts_with?(host, "[") and String.contains?(host, "]:") ->
        check_bracketed_ipv6_port(host)

      # Plain IPv6 addresses (contain :: or multiple colons) - no port embedded
      String.contains?(host, "::") ->
        false

      host |> String.graphemes() |> Enum.count(&(&1 == ":")) > 1 ->
        false

      # IPv4 or simple hostname with port
      String.contains?(host, ":") ->
        check_simple_host_port(host)

      # No colon at all
      true ->
        false
    end
  end

  defp host_contains_port?(_), do: false

  # Check if bracketed IPv6 address has valid port
  defp check_bracketed_ipv6_port(host) do
    case String.split(host, "]:", parts: 2) do
      [_ipv6_part, port_part] -> valid_port?(port_part)
      _ -> false
    end
  end

  # Check if simple host:port format has valid port
  defp check_simple_host_port(host) do
    case String.split(host, ":", parts: 2) do
      [_host_part, port_part] -> valid_port?(port_part)
      _ -> false
    end
  end

  # Validate port number
  defp valid_port?(port_string) do
    case Integer.parse(port_string) do
      {port, ""} when port > 0 and port <= 65_535 -> true
      _ -> false
    end
  end

  # Check if all results failed with the same network-related error
  defp process_multi_results(results) do
    errors =
      Enum.filter(results, fn
        {_oid, {:error, _}} -> true
        _ -> false
      end)

    # If all results are errors, check if they're all network-related
    case {length(errors), length(results)} do
      {same, same} when same > 0 ->
        # All operations failed, check if it's a consistent network error
        network_errors =
          Enum.filter(errors, fn
            {_oid, {:error, {:network_error, _}}} -> true
            _ -> false
          end)

        case length(network_errors) do
          ^same ->
            # All errors are network errors, return the first one as global failure
            {_oid, {:error, reason}} = hd(errors)
            {:error, reason}

          _ ->
            {:ok, results}
        end

      _ ->
        {:ok, results}
    end
  end
end
