defmodule Towerops.Snmp.Profiles.Base do
  @moduledoc """
  Base SNMP device profile for generic device discovery.
  Uses standard MIBs: SNMPv2-MIB, IF-MIB, ENTITY-MIB, ENTITY-SENSOR-MIB.

  This profile works with any SNMP-capable device and discovers:
  - System information (sysDescr, sysObjectID, sysName, etc.)
  - Network interfaces from IF-MIB
  - Generic sensors from ENTITY-SENSOR-MIB (if available)
  """

  alias Towerops.Snmp.Client
  alias Towerops.Snmp.Discovery
  alias Towerops.Snmp.Sanitizer

  require Logger

  # Standard MIB OIDs
  @system_oids %{
    # SNMPv2-MIB::system group
    sys_descr: "1.3.6.1.2.1.1.1.0",
    sys_object_id: "1.3.6.1.2.1.1.2.0",
    sys_uptime: "1.3.6.1.2.1.1.3.0",
    sys_contact: "1.3.6.1.2.1.1.4.0",
    sys_name: "1.3.6.1.2.1.1.5.0",
    sys_location: "1.3.6.1.2.1.1.6.0"
  }

  @interface_oids %{
    # IF-MIB::ifTable - Standard interface table
    if_index: "1.3.6.1.2.1.2.2.1.1",
    if_descr: "1.3.6.1.2.1.2.2.1.2",
    if_type: "1.3.6.1.2.1.2.2.1.3",
    if_speed: "1.3.6.1.2.1.2.2.1.5",
    if_phys_address: "1.3.6.1.2.1.2.2.1.6",
    if_admin_status: "1.3.6.1.2.1.2.2.1.7",
    if_oper_status: "1.3.6.1.2.1.2.2.1.8",

    # IF-MIB::ifXTable - Extended interface table (64-bit counters, names)
    if_name: "1.3.6.1.2.1.31.1.1.1.1",
    if_high_speed: "1.3.6.1.2.1.31.1.1.1.15",
    if_alias: "1.3.6.1.2.1.31.1.1.1.18",

    # IF-MIB::ifXTable - High-Capacity 64-bit counters (for 10G+ interfaces)
    if_hc_in_octets: "1.3.6.1.2.1.31.1.1.1.6",
    if_hc_out_octets: "1.3.6.1.2.1.31.1.1.1.10"
  }

  @entity_sensor_oids %{
    # ENTITY-SENSOR-MIB - Generic sensor support
    ent_phys_sensor_type: "1.3.6.1.2.1.99.1.1.1.1",
    ent_phys_sensor_scale: "1.3.6.1.2.1.99.1.1.1.2",
    ent_phys_sensor_precision: "1.3.6.1.2.1.99.1.1.1.3",
    ent_phys_sensor_value: "1.3.6.1.2.1.99.1.1.1.4",
    ent_phys_sensor_oper_status: "1.3.6.1.2.1.99.1.1.1.5",
    # Sensor thresholds (from ENTITY-SENSOR-MIB)
    ent_phys_sensor_units_display: "1.3.6.1.2.1.99.1.1.1.6",
    ent_phys_sensor_value_timestamp: "1.3.6.1.2.1.99.1.1.1.7"
  }

  @entity_mib_oids %{
    # ENTITY-MIB - Physical entity information (for sensor descriptions)
    ent_phys_descr: "1.3.6.1.2.1.47.1.1.1.1.2",
    ent_phys_vendor_type: "1.3.6.1.2.1.47.1.1.1.1.3",
    ent_phys_contained_in: "1.3.6.1.2.1.47.1.1.1.1.4",
    ent_phys_class: "1.3.6.1.2.1.47.1.1.1.1.5",
    ent_phys_name: "1.3.6.1.2.1.47.1.1.1.1.7",
    ent_phys_hardware_rev: "1.3.6.1.2.1.47.1.1.1.1.8",
    ent_phys_firmware_rev: "1.3.6.1.2.1.47.1.1.1.1.9",
    ent_phys_software_rev: "1.3.6.1.2.1.47.1.1.1.1.10",
    ent_phys_serial_num: "1.3.6.1.2.1.47.1.1.1.1.11",
    ent_phys_mfg_name: "1.3.6.1.2.1.47.1.1.1.1.12",
    ent_phys_model_name: "1.3.6.1.2.1.47.1.1.1.1.13"
  }

  @entity_state_oids %{
    # ENTITY-STATE-MIB - Operational state monitoring
    ent_state_oper: "1.3.6.1.2.1.131.1.1.1.1"
  }

  @q_bridge_oids %{
    # Q-BRIDGE-MIB - IEEE 802.1Q VLAN discovery
    dot1q_vlan_static_name: "1.3.6.1.2.1.17.7.1.4.3.1.1",
    dot1q_vlan_static_row_status: "1.3.6.1.2.1.17.7.1.4.3.1.5"
  }

  @ip_mib_oids %{
    # IP-MIB - IPv4 address discovery (ipAddrTable, deprecated but widely supported)
    ip_ad_ent_if_index: "1.3.6.1.2.1.4.20.1.2",
    ip_ad_ent_net_mask: "1.3.6.1.2.1.4.20.1.3"
  }

  @ipv6_mib_oids %{
    # IPV6-MIB - IPv6 address discovery (RFC 2465)
    # ipv6AddrTable indexed by: ifIndex.ipv6Address (16 octets)
    ipv6_addr_pfx_length: "1.3.6.1.2.1.55.1.8.1.2",
    ipv6_addr_type: "1.3.6.1.2.1.55.1.8.1.3"
  }

  @ip_address_mib_oids %{
    # IP-MIB ipAddressTable (RFC 4293) - unified IPv4/IPv6 table
    # Indexed by: addressType.address (1=ipv4, 2=ipv6)
    ip_address_if_index: "1.3.6.1.2.1.4.34.1.3",
    ip_address_prefix: "1.3.6.1.2.1.4.34.1.5",
    ip_address_type: "1.3.6.1.2.1.4.34.1.4"
  }

  @host_resources_oids %{
    # HOST-RESOURCES-MIB - hrStorageTable for memory and storage
    hr_storage_type: "1.3.6.1.2.1.25.2.3.1.2",
    hr_storage_descr: "1.3.6.1.2.1.25.2.3.1.3",
    hr_storage_allocation_units: "1.3.6.1.2.1.25.2.3.1.4",
    hr_storage_size: "1.3.6.1.2.1.25.2.3.1.5",
    hr_storage_used: "1.3.6.1.2.1.25.2.3.1.6"
  }

  @hr_processor_oids %{
    # HOST-RESOURCES-MIB - hrProcessorTable for CPU discovery
    # hrProcessorFrwID: 1.3.6.1.2.1.25.3.3.1.1 (reference to hrDeviceEntry)
    hr_processor_load: "1.3.6.1.2.1.25.3.3.1.2"
  }

  @hr_device_oids %{
    # HOST-RESOURCES-MIB - hrDeviceTable for device descriptions
    hr_device_descr: "1.3.6.1.2.1.25.3.2.1.3"
  }

  @cisco_cpu_oids %{
    # CISCO-PROCESS-MIB - cpmCPUTotalTable
    cpm_cpu_total_5sec: "1.3.6.1.4.1.9.9.109.1.1.1.1.3",
    cpm_cpu_total_1min: "1.3.6.1.4.1.9.9.109.1.1.1.1.4",
    cpm_cpu_total_5min: "1.3.6.1.4.1.9.9.109.1.1.1.1.5"
  }

  @ucd_cpu_oids %{
    # UCD-SNMP-MIB - System CPU statistics
    ss_cpu_user: "1.3.6.1.4.1.2021.11.9.0",
    ss_cpu_system: "1.3.6.1.4.1.2021.11.10.0",
    ss_cpu_idle: "1.3.6.1.4.1.2021.11.11.0"
  }

  # HOST-RESOURCES-MIB storage type OIDs mapping
  # Memory types (for discover_memory_pools)
  @memory_type_oids %{
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 2] => "ram",
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 3] => "swap"
  }

  # Disk/storage types (for discover_storage) - excludes "other" (1) which is too generic
  @storage_type_oids %{
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 4] => "fixed_disk",
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 5] => "removable_disk",
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 6] => "floppy_disk",
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 7] => "compact_disc",
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 8] => "ram_disk",
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 9] => "flash_memory",
    [1, 3, 6, 1, 2, 1, 25, 2, 1, 10] => "network_disk"
  }

  # ENTITY-MIB physical class values to track for state sensors
  # 6 = powerSupply, 7 = fan
  @state_sensor_classes [6, 7]

  @doc """
  Discovers system information from SNMPv2-MIB.
  Returns a map with device metadata.
  """
  @spec discover_system_info(Client.connection_opts()) ::
          {:ok, Discovery.system_info()} | {:error, term()}
  def discover_system_info(client_opts) do
    oids = Map.values(@system_oids)

    case Client.get_multiple(client_opts, oids) do
      {:ok, values} ->
        system_info =
          @system_oids
          |> Map.keys()
          |> Enum.zip(values)
          |> Map.new()
          |> parse_system_info()

        {:ok, system_info}

      {:error, reason} = error ->
        Logger.error("Failed to discover system info: #{inspect(reason)}")
        error
    end
  end

  @doc """
  Discovers network interfaces from IF-MIB.
  Returns a list of interface maps.
  """
  @spec discover_interfaces(Client.connection_opts()) ::
          {:ok, [Discovery.interface_data()]} | {:error, term()}
  def discover_interfaces(client_opts) do
    with {:ok, if_indices} <- Client.walk(client_opts, @interface_oids.if_index) do
      # Extract interface indices
      indices = if_indices |> Map.values() |> Enum.map(&parse_integer/1)

      # Fetch all interface data in parallel
      # Timeout must be longer than SNMP timeout (30s) to allow slow devices
      interface_data =
        indices
        |> Task.async_stream(
          fn index -> fetch_interface_data(client_opts, index) end,
          max_concurrency: 10,
          timeout: 40_000
        )
        |> Enum.map(fn
          {:ok, {:ok, interface}} -> interface
          _ -> nil
        end)
        |> Enum.reject(&is_nil/1)

      {:ok, interface_data}
    end
  end

  @doc """
  Discovers sensors from ENTITY-SENSOR-MIB with descriptions from ENTITY-MIB.
  This is the universal baseline sensor discovery that works across many vendors.
  Returns a list of sensor maps.
  """
  @spec discover_sensors(Client.connection_opts()) ::
          {:ok, [Discovery.sensor_data()]} | {:error, term()}
  def discover_sensors(client_opts) do
    # Try to walk the sensor type OID to see if device supports ENTITY-SENSOR-MIB
    case Client.walk(client_opts, @entity_sensor_oids.ent_phys_sensor_type) do
      {:ok, sensor_types} when map_size(sensor_types) > 0 ->
        # Device supports ENTITY-SENSOR-MIB
        sensor_indices = extract_indices_from_oids(sensor_types)

        # Pre-fetch entity descriptions for all sensors (more efficient than per-sensor queries)
        entity_descriptions = fetch_entity_descriptions(client_opts, sensor_indices)

        sensors =
          sensor_indices
          |> Enum.map(fn index ->
            build_sensor_from_entity_mib(client_opts, index, entity_descriptions)
          end)
          |> Enum.reject(&is_nil/1)

        Logger.debug("Discovered #{length(sensors)} sensors from ENTITY-SENSOR-MIB")
        {:ok, sensors}

      _ ->
        # Device doesn't support ENTITY-SENSOR-MIB, return empty list
        Logger.debug("Device does not support ENTITY-SENSOR-MIB")
        {:ok, []}
    end
  end

  @doc """
  Fetches physical entity descriptions from ENTITY-MIB for sensor naming.
  Returns a map of index => %{descr: "...", name: "...", class: ...}
  """
  @spec fetch_entity_descriptions(Client.connection_opts(), [integer()]) :: map()
  def fetch_entity_descriptions(client_opts, indices) do
    # Walk entity physical table once to get all descriptions
    descr_results =
      case Client.walk(client_opts, @entity_mib_oids.ent_phys_descr) do
        {:ok, results} -> results
        _ -> %{}
      end

    name_results =
      case Client.walk(client_opts, @entity_mib_oids.ent_phys_name) do
        {:ok, results} -> results
        _ -> %{}
      end

    class_results =
      case Client.walk(client_opts, @entity_mib_oids.ent_phys_class) do
        {:ok, results} -> results
        _ -> %{}
      end

    # Build a map of index => entity info
    Map.new(indices, fn index ->
      descr_oid = @entity_mib_oids.ent_phys_descr <> ".#{index}"
      name_oid = @entity_mib_oids.ent_phys_name <> ".#{index}"
      class_oid = @entity_mib_oids.ent_phys_class <> ".#{index}"

      {index,
       %{
         descr: Map.get(descr_results, descr_oid),
         name: Map.get(name_results, name_oid),
         class: Map.get(class_results, class_oid)
       }}
    end)
  end

  @doc """
  Discovers state sensors from ENTITY-MIB and ENTITY-STATE-MIB.
  State sensors track discrete status values for power supplies, fans, and modules.
  Returns a list of state sensor maps.
  """
  @spec discover_state_sensors(Client.connection_opts()) :: {:ok, [map()]}
  def discover_state_sensors(client_opts) do
    # Walk entity physical class to find entities with state
    # Client.walk returns {:ok, %{oid => value}} map
    case Client.walk(client_opts, @entity_mib_oids.ent_phys_class) do
      {:ok, class_results} when is_map(class_results) and map_size(class_results) > 0 ->
        do_discover_state_sensors(client_opts, class_results)

      {:ok, _} ->
        Logger.debug("No ENTITY-MIB physical entities found")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("ENTITY-MIB walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_state_sensors(client_opts, class_results) do
    # Build a map of index => class (Client.walk already extracts values)
    class_map = build_index_value_map(class_results)

    # Filter to only power supplies and fans
    state_indices =
      class_map
      |> Enum.filter(fn {_index, class} -> class in @state_sensor_classes end)
      |> Enum.map(fn {index, _class} -> index end)

    if Enum.empty?(state_indices) do
      Logger.debug("No power supply or fan entities found")
      {:ok, []}
    else
      build_state_sensors_from_indices(client_opts, state_indices, class_map)
    end
  end

  defp build_state_sensors_from_indices(client_opts, state_indices, class_map) do
    # Fetch descriptions and operational states
    descr_map = fetch_entity_descr_map(client_opts)
    state_map = fetch_entity_state_map(client_opts)

    state_sensors =
      state_indices
      |> Enum.map(fn index ->
        build_state_sensor(index, class_map, descr_map, state_map)
      end)
      |> Enum.reject(&is_nil/1)

    Logger.debug("Discovered #{length(state_sensors)} state sensors")
    {:ok, state_sensors}
  end

  @doc """
  Discovers VLANs from Q-BRIDGE-MIB.
  Returns a list of VLAN maps with vlan_id, vlan_name, and status.
  """
  @spec discover_vlans(Client.connection_opts()) :: {:ok, [map()]}
  def discover_vlans(client_opts) do
    # Walk VLAN names from Q-BRIDGE-MIB
    case Client.walk(client_opts, @q_bridge_oids.dot1q_vlan_static_name) do
      {:ok, name_results} when is_map(name_results) and map_size(name_results) > 0 ->
        do_discover_vlans(client_opts, name_results)

      {:ok, _} ->
        # No VLANs found via name walk, try status walk
        try_vlan_status_walk(client_opts)

      {:error, reason} ->
        Logger.debug("Q-BRIDGE-MIB VLAN name walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_vlans(client_opts, name_results) do
    # Fetch VLAN status
    status_map =
      case Client.walk(client_opts, @q_bridge_oids.dot1q_vlan_static_row_status) do
        {:ok, results} when is_map(results) -> build_vlan_index_map(results)
        _ -> %{}
      end

    # Build VLAN list from names
    vlans =
      name_results
      |> Enum.map(fn {oid, name} ->
        vlan_id = extract_vlan_id_from_oid(oid)
        status = Map.get(status_map, vlan_id, 1)

        if valid_vlan_id?(vlan_id) do
          %{
            vlan_id: vlan_id,
            vlan_name: name,
            vlan_type: "ethernet",
            status: vlan_row_status_to_status(status),
            last_checked_at: DateTime.utc_now()
          }
        end
      end)
      |> Enum.reject(&is_nil/1)

    Logger.debug("Discovered #{length(vlans)} VLANs from Q-BRIDGE-MIB")
    {:ok, vlans}
  end

  defp try_vlan_status_walk(client_opts) do
    # Try to discover VLANs from status table (some devices only have this)
    case Client.walk(client_opts, @q_bridge_oids.dot1q_vlan_static_row_status) do
      {:ok, status_results} when is_map(status_results) and map_size(status_results) > 0 ->
        vlans = build_vlans_from_status(status_results)
        Logger.debug("Discovered #{length(vlans)} VLANs from status walk")
        {:ok, vlans}

      _ ->
        Logger.debug("Device does not support Q-BRIDGE-MIB")
        {:ok, []}
    end
  end

  defp build_vlans_from_status(status_results) do
    status_results
    |> Enum.map(&build_vlan_from_status/1)
    |> Enum.reject(&is_nil/1)
  end

  defp build_vlan_from_status({oid, status}) do
    vlan_id = extract_vlan_id_from_oid(oid)

    if valid_vlan_id?(vlan_id) do
      %{
        vlan_id: vlan_id,
        vlan_name: "VLAN #{vlan_id}",
        vlan_type: "ethernet",
        status: vlan_row_status_to_status(status),
        last_checked_at: DateTime.utc_now()
      }
    end
  end

  @doc """
  Discovers IP addresses from IP-MIB.
  Returns a list of IP address maps with ip_address, if_index, subnet_mask, and prefix_length.
  """
  @spec discover_ip_addresses(Client.connection_opts()) :: {:ok, [map()]}
  def discover_ip_addresses(client_opts) do
    # Walk IP address to interface mapping
    case Client.walk(client_opts, @ip_mib_oids.ip_ad_ent_if_index) do
      {:ok, if_index_results} when is_map(if_index_results) and map_size(if_index_results) > 0 ->
        do_discover_ip_addresses(client_opts, if_index_results)

      {:ok, _} ->
        Logger.debug("No IP addresses found in IP-MIB")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("IP-MIB walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_ip_addresses(client_opts, if_index_results) do
    # Fetch subnet masks
    mask_map =
      case Client.walk(client_opts, @ip_mib_oids.ip_ad_ent_net_mask) do
        {:ok, results} when is_map(results) -> build_ip_mask_map(results)
        _ -> %{}
      end

    # Build IP address list
    ip_addresses =
      if_index_results
      |> Enum.map(fn {oid, if_index} ->
        ip_address = extract_ip_from_oid(oid)
        subnet_mask = Map.get(mask_map, ip_address)
        prefix_length = subnet_mask_to_prefix(subnet_mask)

        %{
          ip_address: ip_address,
          if_index: if_index,
          subnet_mask: subnet_mask,
          prefix_length: prefix_length,
          ip_type: "ipv4",
          last_checked_at: DateTime.utc_now()
        }
      end)
      |> Enum.reject(fn ip -> ip.ip_address == nil end)

    Logger.debug("Discovered #{length(ip_addresses)} IP addresses from IP-MIB")
    {:ok, ip_addresses}
  end

  @doc """
  Discovers IPv6 addresses from IPV6-MIB or IP-MIB ipAddressTable.
  Returns a list of IP address maps with ip_address, if_index, prefix_length, and ip_type.
  """
  @spec discover_ipv6_addresses(Client.connection_opts()) :: {:ok, [map()]}
  def discover_ipv6_addresses(client_opts) do
    # Try modern IP-MIB ipAddressTable first (RFC 4293, supports both IPv4/IPv6)
    case discover_from_ip_address_table(client_opts) do
      {:ok, [_ | _] = addresses} ->
        {:ok, addresses}

      _ ->
        # Fall back to IPV6-MIB ipv6AddrTable (RFC 2465)
        discover_from_ipv6_addr_table(client_opts)
    end
  end

  # Discover from RFC 4293 ipAddressTable (unified IPv4/IPv6)
  defp discover_from_ip_address_table(client_opts) do
    case Client.walk(client_opts, @ip_address_mib_oids.ip_address_if_index) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        do_discover_from_ip_address_table(client_opts, results)

      {:ok, _} ->
        Logger.debug("No addresses found in ipAddressTable")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("ipAddressTable walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_from_ip_address_table(client_opts, if_index_results) do
    # Fetch prefix lengths
    prefix_map =
      case Client.walk(client_opts, @ip_address_mib_oids.ip_address_prefix) do
        {:ok, results} when is_map(results) -> results
        _ -> %{}
      end

    # Build address list - only IPv6 (type 2)
    addresses =
      if_index_results
      |> Enum.map(fn {oid, if_index} ->
        {ip_type, ip_address} = parse_ip_address_table_oid(oid)

        if ip_type == "ipv6" do
          prefix_length = extract_prefix_from_map(prefix_map, oid)

          %{
            ip_address: ip_address,
            if_index: if_index,
            subnet_mask: nil,
            prefix_length: prefix_length,
            ip_type: "ipv6",
            last_checked_at: DateTime.utc_now()
          }
        end
      end)
      |> Enum.reject(&is_nil/1)

    Logger.debug("Discovered #{length(addresses)} IPv6 addresses from ipAddressTable")
    {:ok, addresses}
  end

  # Discover from RFC 2465 IPV6-MIB ipv6AddrTable
  defp discover_from_ipv6_addr_table(client_opts) do
    case Client.walk(client_opts, @ipv6_mib_oids.ipv6_addr_pfx_length) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        do_discover_from_ipv6_addr_table(results)

      {:ok, _} ->
        Logger.debug("No IPv6 addresses found in IPV6-MIB")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("IPV6-MIB walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_from_ipv6_addr_table(prefix_results) do
    # ipv6AddrTable is indexed by ifIndex.ipv6Address (16 octets)
    addresses =
      prefix_results
      |> Enum.map(fn {oid, prefix_length} ->
        {if_index, ipv6_address} = parse_ipv6_addr_table_oid(oid)

        if ipv6_address do
          %{
            ip_address: ipv6_address,
            if_index: if_index,
            subnet_mask: nil,
            prefix_length: prefix_length,
            ip_type: "ipv6",
            last_checked_at: DateTime.utc_now()
          }
        end
      end)
      |> Enum.reject(&is_nil/1)

    Logger.debug("Discovered #{length(addresses)} IPv6 addresses from IPV6-MIB")
    {:ok, addresses}
  end

  # Parse ipAddressTable OID to extract address type and address
  # OID format: base.addressType.addressLength.address...
  # addressType: 1=ipv4, 2=ipv6
  defp parse_ip_address_table_oid(oid) when is_binary(oid) do
    parts = String.split(oid, ".")

    # Find where the address starts (after the base OID)
    # The format is: base.type.length.addr1.addr2...
    case find_address_in_oid(parts) do
      {:ipv4, octets} when length(octets) == 4 ->
        {"ipv4", Enum.join(octets, ".")}

      {:ipv6, octets} when length(octets) == 16 ->
        {"ipv6", format_ipv6_address(octets)}

      _ ->
        {nil, nil}
    end
  end

  defp parse_ip_address_table_oid(_), do: {nil, nil}

  defp find_address_in_oid(parts) do
    # Look for type indicator (1 or 2) followed by length
    # Pattern: ...1.4.a.b.c.d (IPv4) or ...2.16.a.b.c... (IPv6)
    parts
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.find_value(fn
      ["1", "4" | _] ->
        idx = Enum.find_index(parts, &(&1 == "1"))

        if idx do
          octets = Enum.slice(parts, idx + 2, 4)
          {:ipv4, octets}
        end

      ["2", "16" | _] ->
        idx = Enum.find_index(parts, &(&1 == "2"))

        if idx do
          octets = Enum.slice(parts, idx + 2, 16)
          {:ipv6, octets}
        end

      _ ->
        nil
    end)
  end

  # Parse ipv6AddrTable OID to extract ifIndex and IPv6 address
  # OID format: base.ifIndex.ipv6Addr (16 octets)
  defp parse_ipv6_addr_table_oid(oid) when is_binary(oid) do
    parts = String.split(oid, ".")

    # Last 17 parts: ifIndex + 16 address octets
    if length(parts) >= 17 do
      suffix = Enum.take(parts, -17)
      [if_index_str | addr_octets] = suffix

      if_index = String.to_integer(if_index_str)
      ipv6_address = format_ipv6_address(addr_octets)

      {if_index, ipv6_address}
    else
      {nil, nil}
    end
  rescue
    _ -> {nil, nil}
  end

  defp parse_ipv6_addr_table_oid(_), do: {nil, nil}

  # Format IPv6 address from decimal octets to standard notation
  defp format_ipv6_address(octets) when is_list(octets) and length(octets) == 16 do
    octets
    |> Enum.map(&String.to_integer/1)
    |> Enum.chunk_every(2)
    |> Enum.map_join(":", fn [hi, lo] ->
      (hi * 256 + lo)
      |> Integer.to_string(16)
      |> String.downcase()
    end)
  rescue
    _ -> nil
  end

  defp format_ipv6_address(_), do: nil

  defp extract_prefix_from_map(prefix_map, oid) do
    # Try to find matching prefix entry
    # The OID suffix should match
    Enum.find_value(prefix_map, fn {prefix_oid, value} ->
      if String.ends_with?(prefix_oid, String.slice(oid, -20..-1)) do
        value
      end
    end)
  end

  @doc """
  Discovers all IP addresses (both IPv4 and IPv6) from SNMP.
  Combines results from discover_ip_addresses/1 and discover_ipv6_addresses/1.
  """
  @spec discover_all_ip_addresses(Client.connection_opts()) :: {:ok, [map()]}
  def discover_all_ip_addresses(client_opts) do
    ipv4_result = discover_ip_addresses(client_opts)
    ipv6_result = discover_ipv6_addresses(client_opts)

    ipv4_addresses = elem(ipv4_result, 1)
    ipv6_addresses = elem(ipv6_result, 1)

    all_addresses = ipv4_addresses ++ ipv6_addresses

    Logger.debug(
      "Discovered #{length(all_addresses)} total IP addresses (#{length(ipv4_addresses)} IPv4, #{length(ipv6_addresses)} IPv6)"
    )

    {:ok, all_addresses}
  end

  @doc """
  Discovers memory pools from HOST-RESOURCES-MIB hrStorageTable.
  Returns a list of memory pool maps (RAM, swap/virtual memory).
  """
  @spec discover_memory_pools(Client.connection_opts()) :: {:ok, [map()]}
  def discover_memory_pools(client_opts) do
    # Walk storage type to identify memory entries
    case Client.walk(client_opts, @host_resources_oids.hr_storage_type) do
      {:ok, type_results} when is_map(type_results) and map_size(type_results) > 0 ->
        do_discover_memory_pools(client_opts, type_results)

      {:ok, _} ->
        Logger.debug("No storage entries found in HOST-RESOURCES-MIB")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("HOST-RESOURCES-MIB walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_memory_pools(client_opts, type_results) do
    # Fetch all storage attributes
    descr_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_descr)
    alloc_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_allocation_units)
    size_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_size)
    used_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_used)

    # Build memory pool list (only RAM and virtual memory)
    memory_pools =
      type_results
      |> Enum.map(fn {oid, type_oid} ->
        index = extract_hr_storage_index(oid)
        pool_type = hr_storage_type_to_memory_type(type_oid)

        if pool_type do
          allocation_units = Map.get(alloc_map, index, 1)
          size_units = Map.get(size_map, index, 0)
          used_units = Map.get(used_map, index, 0)

          %{
            pool_index: to_string(index),
            pool_name: Map.get(descr_map, index),
            pool_type: pool_type,
            total_bytes: size_units * allocation_units,
            used_bytes: used_units * allocation_units,
            last_checked_at: DateTime.utc_now()
          }
        end
      end)
      |> Enum.reject(&is_nil/1)

    Logger.debug("Discovered #{length(memory_pools)} memory pools from HOST-RESOURCES-MIB")
    {:ok, memory_pools}
  end

  @doc """
  Discovers storage from HOST-RESOURCES-MIB hrStorageTable.
  Returns a list of storage maps (fixed disks, removable disks, network disks, etc.).
  """
  @spec discover_storage(Client.connection_opts()) :: {:ok, [map()]}
  def discover_storage(client_opts) do
    # Walk storage type to identify storage entries
    case Client.walk(client_opts, @host_resources_oids.hr_storage_type) do
      {:ok, type_results} when is_map(type_results) and map_size(type_results) > 0 ->
        do_discover_storage(client_opts, type_results)

      {:ok, _} ->
        Logger.debug("No storage entries found in HOST-RESOURCES-MIB")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("HOST-RESOURCES-MIB walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_storage(client_opts, type_results) do
    # Fetch all storage attributes
    descr_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_descr)
    alloc_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_allocation_units)
    size_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_size)
    used_map = walk_hr_storage_attribute(client_opts, @host_resources_oids.hr_storage_used)

    # Build storage list (only disk types, not RAM/virtual memory)
    storage =
      type_results
      |> Enum.map(fn {oid, type_oid} ->
        index = extract_hr_storage_index(oid)
        storage_type = hr_storage_type_to_disk_type(type_oid)

        if storage_type do
          allocation_units = Map.get(alloc_map, index, 1)
          size_units = Map.get(size_map, index, 0)
          used_units = Map.get(used_map, index, 0)
          raw_description = Map.get(descr_map, index)

          %{
            storage_index: index,
            description: Sanitizer.sanitize_string(raw_description),
            storage_type: storage_type,
            allocation_units: allocation_units,
            total_bytes: size_units * allocation_units,
            used_bytes: used_units * allocation_units,
            last_checked_at: DateTime.utc_now()
          }
        end
      end)
      |> Enum.reject(&is_nil/1)

    Logger.debug("Discovered #{length(storage)} storage entries from HOST-RESOURCES-MIB")
    {:ok, storage}
  end

  defp walk_hr_storage_attribute(client_opts, oid) do
    case Client.walk(client_opts, oid) do
      {:ok, results} when is_map(results) ->
        Map.new(results, fn {result_oid, value} ->
          {extract_hr_storage_index(result_oid), value}
        end)

      _ ->
        %{}
    end
  end

  defp extract_hr_storage_index(oid) do
    oid
    |> String.split(".")
    |> List.last()
    |> String.to_integer()
  rescue
    _ -> 0
  end

  # Map hrStorageType OID to memory pool type (RAM, swap)
  defp hr_storage_type_to_memory_type(type_oid) when is_list(type_oid) do
    Map.get(@memory_type_oids, type_oid)
  end

  defp hr_storage_type_to_memory_type(_), do: nil

  # Map hrStorageType OID to storage type (disk types only)
  defp hr_storage_type_to_disk_type(type_oid) when is_list(type_oid) do
    Map.get(@storage_type_oids, type_oid)
  end

  defp hr_storage_type_to_disk_type(_), do: nil

  @doc """
  Discovers physical entities from ENTITY-MIB.
  Returns a list of physical entity maps including chassis, modules, PSUs, fans, etc.
  """
  @spec discover_physical_entities(Client.connection_opts()) :: {:ok, [map()]}
  def discover_physical_entities(client_opts) do
    # Walk physical entity descriptions to get entity indices
    case Client.walk(client_opts, @entity_mib_oids.ent_phys_descr) do
      {:ok, descr_results} when is_map(descr_results) and map_size(descr_results) > 0 ->
        do_discover_physical_entities(client_opts, descr_results)

      {:ok, _} ->
        Logger.debug("No physical entities found in ENTITY-MIB")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("ENTITY-MIB walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_physical_entities(client_opts, descr_results) do
    # Fetch all entity attributes in parallel-ish manner
    contained_in_map = walk_entity_attribute(client_opts, @entity_mib_oids.ent_phys_contained_in)
    class_map = walk_entity_attribute(client_opts, @entity_mib_oids.ent_phys_class)
    serial_map = walk_entity_attribute(client_opts, @entity_mib_oids.ent_phys_serial_num)
    model_map = walk_entity_attribute(client_opts, @entity_mib_oids.ent_phys_model_name)
    mfg_map = walk_entity_attribute(client_opts, @entity_mib_oids.ent_phys_mfg_name)
    hw_rev_map = walk_entity_attribute(client_opts, @entity_mib_oids.ent_phys_hardware_rev)
    fw_rev_map = walk_entity_attribute(client_opts, @entity_mib_oids.ent_phys_firmware_rev)

    # Build entity list
    entities =
      descr_results
      |> Enum.map(fn {oid, description} ->
        entity_index = extract_entity_index(oid)

        %{
          entity_index: entity_index,
          description: to_string_or_nil(description),
          entity_class: entity_class_to_string(Map.get(class_map, entity_index)),
          contained_in: Map.get(contained_in_map, entity_index, 0),
          serial_number: to_string_or_nil(Map.get(serial_map, entity_index)),
          model_name: to_string_or_nil(Map.get(model_map, entity_index)),
          manufacturer_name: to_string_or_nil(Map.get(mfg_map, entity_index)),
          hardware_revision: to_string_or_nil(Map.get(hw_rev_map, entity_index)),
          firmware_revision: to_string_or_nil(Map.get(fw_rev_map, entity_index)),
          last_checked_at: DateTime.utc_now()
        }
      end)
      |> Enum.sort_by(& &1.entity_index)

    Logger.debug("Discovered #{length(entities)} physical entities from ENTITY-MIB")
    {:ok, entities}
  end

  defp walk_entity_attribute(client_opts, oid) do
    case Client.walk(client_opts, oid) do
      {:ok, results} when is_map(results) ->
        Map.new(results, fn {result_oid, value} ->
          {extract_entity_index(result_oid), value}
        end)

      _ ->
        %{}
    end
  end

  defp extract_entity_index(oid) do
    oid
    |> String.split(".")
    |> List.last()
    |> String.to_integer()
  rescue
    _ -> 0
  end

  # ENTITY-MIB PhysicalClass values mapping
  @entity_class_map %{
    1 => "other",
    2 => "unknown",
    3 => "chassis",
    4 => "backplane",
    5 => "container",
    6 => "power",
    7 => "fan",
    8 => "sensor",
    9 => "module",
    10 => "port",
    11 => "stack",
    12 => "cpu",
    13 => "memory"
  }

  defp entity_class_to_string(class) when is_integer(class) do
    Map.get(@entity_class_map, class, "unknown")
  end

  defp entity_class_to_string(_), do: "unknown"

  defp to_string_or_nil(nil), do: nil
  defp to_string_or_nil(""), do: nil
  defp to_string_or_nil(value) when is_binary(value), do: value
  defp to_string_or_nil(value), do: to_string(value)

  @doc """
  Discovers processor/CPU information from multiple MIBs.

  Tries in order:
  1. HOST-RESOURCES-MIB hrProcessorTable (standard, most compatible)
  2. CISCO-PROCESS-MIB cpmCPUTotalTable (Cisco-specific)
  3. UCD-SNMP-MIB CPU statistics (Linux/Unix systems)

  Returns a list of processor maps with processor_index, description, processor_type, and load_percent.
  """
  @spec discover_processors(Client.connection_opts()) :: {:ok, [map()]}
  def discover_processors(client_opts) do
    # Try HOST-RESOURCES-MIB first (most widely supported)
    case discover_hr_processors(client_opts) do
      {:ok, [_ | _] = processors} ->
        {:ok, processors}

      _ ->
        # Try Cisco-specific OIDs
        case discover_cisco_processors(client_opts) do
          {:ok, [_ | _] = processors} ->
            {:ok, processors}

          _ ->
            # Try UCD-SNMP-MIB
            discover_ucd_cpu(client_opts)
        end
    end
  end

  # Discover processors from HOST-RESOURCES-MIB hrProcessorTable
  defp discover_hr_processors(client_opts) do
    case Client.walk(client_opts, @hr_processor_oids.hr_processor_load) do
      {:ok, results} when map_size(results) > 0 ->
        do_discover_hr_processors(client_opts, results)

      {:ok, _} ->
        Logger.debug("No processors found in HOST-RESOURCES-MIB")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("hrProcessorTable walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_hr_processors(client_opts, load_results) do
    # Get device descriptions for processors
    descr_map = walk_hr_device_descr(client_opts)

    processors =
      load_results
      |> Enum.map(fn {oid, load_value} ->
        index = extract_processor_index(oid)
        raw_description = Map.get(descr_map, index, "CPU #{index}")
        description = Sanitizer.sanitize_string(raw_description)
        load_percent = parse_integer(load_value)

        %{
          processor_index: "hr_#{index}",
          description: description,
          processor_type: "hr_processor",
          load_percent: load_percent && load_percent * 1.0,
          last_checked_at: DateTime.utc_now()
        }
      end)
      |> Enum.reject(fn p -> is_nil(p.processor_index) end)

    Logger.debug("Discovered #{length(processors)} processors from HOST-RESOURCES-MIB")
    {:ok, processors}
  end

  defp walk_hr_device_descr(client_opts) do
    case Client.walk(client_opts, @hr_device_oids.hr_device_descr) do
      {:ok, results} ->
        Map.new(results, fn {oid, value} ->
          index = extract_processor_index(oid)
          {index, extract_string_value(value)}
        end)

      _ ->
        %{}
    end
  end

  # Discover processors from CISCO-PROCESS-MIB cpmCPUTotalTable
  defp discover_cisco_processors(client_opts) do
    case Client.walk(client_opts, @cisco_cpu_oids.cpm_cpu_total_5min) do
      {:ok, results} when map_size(results) > 0 ->
        do_discover_cisco_processors(results)

      {:ok, _} ->
        Logger.debug("No Cisco CPU data found")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("CISCO-PROCESS-MIB walk failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp do_discover_cisco_processors(cpu_results) do
    processors =
      Enum.map(cpu_results, fn {oid, load_value} ->
        index = extract_processor_index(oid)
        load_percent = parse_integer(load_value)

        %{
          processor_index: "cisco_#{index}",
          description: "Cisco CPU #{index}",
          processor_type: "cisco_cpu",
          load_percent: load_percent && load_percent * 1.0,
          last_checked_at: DateTime.utc_now()
        }
      end)

    Logger.debug("Discovered #{length(processors)} Cisco CPUs")
    {:ok, processors}
  end

  # Discover CPU stats from UCD-SNMP-MIB
  defp discover_ucd_cpu(client_opts) do
    # Get user, system, and idle percentages
    oids = [@ucd_cpu_oids.ss_cpu_user, @ucd_cpu_oids.ss_cpu_system, @ucd_cpu_oids.ss_cpu_idle]

    case Client.get_multiple(client_opts, oids) do
      {:ok, values} when is_list(values) and length(values) == 3 ->
        [user, system, _idle] = values
        user_pct = parse_integer(user) || 0
        system_pct = parse_integer(system) || 0
        total_load = user_pct + system_pct

        processors = [
          %{
            processor_index: "ucd_0",
            description: "System CPU",
            processor_type: "ucd_cpu",
            load_percent: total_load * 1.0,
            last_checked_at: DateTime.utc_now(),
            metadata: %{
              user_percent: user_pct,
              system_percent: system_pct
            }
          }
        ]

        Logger.debug("Discovered UCD-SNMP CPU with #{total_load}% load")
        {:ok, processors}

      {:ok, _} ->
        Logger.debug("No UCD-SNMP CPU data found")
        {:ok, []}

      {:error, reason} ->
        Logger.debug("UCD-SNMP-MIB get failed: #{inspect(reason)}")
        {:ok, []}
    end
  end

  defp extract_processor_index(oid) when is_binary(oid) do
    oid
    |> String.split(".")
    |> List.last()
    |> String.to_integer()
  rescue
    _ -> nil
  end

  defp extract_processor_index(_), do: nil

  # Extract string value from SNMP result (could be binary, charlist, or other)
  defp extract_string_value(value) when is_binary(value), do: value
  defp extract_string_value(value) when is_list(value), do: List.to_string(value)
  defp extract_string_value(value), do: to_string(value)

  @doc """
  Identifies device manufacturer and model from sysDescr and sysObjectID.
  Can be overridden by vendor-specific profiles.
  """
  @spec identify_device(Discovery.system_info()) :: Discovery.device_info()
  def identify_device(system_info) do
    sys_descr = Map.get(system_info, :sys_descr, "")
    sys_object_id = Map.get(system_info, :sys_object_id, "")

    {manufacturer, model} = parse_sys_descr(sys_descr, sys_object_id)

    Map.merge(system_info, %{
      manufacturer: manufacturer,
      model: model
    })
  end

  # Private functions

  defp parse_system_info(raw_info) do
    %{
      sys_descr: raw_info |> Map.get(:sys_descr) |> Sanitizer.sanitize_string(),
      sys_object_id: parse_oid(Map.get(raw_info, :sys_object_id)),
      sys_name: raw_info |> Map.get(:sys_name) |> Sanitizer.sanitize_string(),
      sys_uptime: parse_integer(Map.get(raw_info, :sys_uptime)),
      sys_contact: raw_info |> Map.get(:sys_contact) |> Sanitizer.sanitize_string(),
      sys_location: raw_info |> Map.get(:sys_location) |> Sanitizer.sanitize_string()
    }
  end

  defp parse_oid(oid) when is_list(oid) do
    Enum.map_join(oid, ".", &to_string/1)
  end

  defp parse_oid(oid) when is_binary(oid), do: oid
  defp parse_oid(_), do: ""

  defp fetch_interface_data(client_opts, index) do
    # Fetch required IF-MIB fields (all devices should support these)
    required_oids = [
      @interface_oids.if_descr <> ".#{index}",
      @interface_oids.if_type <> ".#{index}",
      @interface_oids.if_speed <> ".#{index}",
      @interface_oids.if_phys_address <> ".#{index}",
      @interface_oids.if_admin_status <> ".#{index}",
      @interface_oids.if_oper_status <> ".#{index}"
    ]

    with {:ok, [if_descr, if_type, if_speed, if_phys_addr, if_admin, if_oper]} <-
           Client.get_multiple(client_opts, required_oids) do
      # Try to fetch optional IF-MIB extension fields (ifName, ifAlias, ifHighSpeed)
      # These are from RFC 2863 and not all devices support them
      if_name = fetch_optional_field(client_opts, @interface_oids.if_name <> ".#{index}")
      if_alias = fetch_optional_field(client_opts, @interface_oids.if_alias <> ".#{index}")
      if_high_speed = fetch_optional_field(client_opts, @interface_oids.if_high_speed <> ".#{index}")

      # Use ifHighSpeed (Mbps) for 10G+ interfaces where ifSpeed (bps) maxes out at ~4.3Gbps
      # ifHighSpeed is in Mbps, convert to bps for consistency
      actual_speed = calculate_interface_speed(if_speed, if_high_speed)

      {:ok,
       %{
         if_index: index,
         if_descr: Sanitizer.sanitize_string(if_descr),
         if_name: Sanitizer.sanitize_string(if_name),
         if_alias: Sanitizer.sanitize_string(if_alias),
         if_type: parse_integer(if_type),
         if_speed: actual_speed,
         if_phys_address: Sanitizer.sanitize_mac(if_phys_addr) || format_mac_address(if_phys_addr),
         if_admin_status: parse_if_status(if_admin),
         if_oper_status: parse_if_status(if_oper)
       }}
    end
  end

  # Calculate actual interface speed, preferring ifHighSpeed for 10G+ interfaces
  # ifSpeed maxes out at 4,294,967,295 bps (~4.3 Gbps) due to 32-bit gauge
  # ifHighSpeed is in Mbps and supports up to 4.3 Tbps
  defp calculate_interface_speed(if_speed, if_high_speed) do
    speed_bps = parse_integer(if_speed)
    high_speed_mbps = parse_integer(if_high_speed)

    cond do
      # If ifHighSpeed is available and indicates 10G+ (or ifSpeed maxed out)
      high_speed_mbps != nil and high_speed_mbps >= 10_000 ->
        # Convert Mbps to bps
        high_speed_mbps * 1_000_000

      # If ifSpeed is at or near max (4.3 Gbps), use ifHighSpeed if available
      speed_bps != nil and speed_bps >= 4_000_000_000 and high_speed_mbps != nil ->
        high_speed_mbps * 1_000_000

      # Otherwise use standard ifSpeed
      true ->
        speed_bps
    end
  end

  # Fetch an optional SNMP field - returns nil if not supported
  defp fetch_optional_field(client_opts, oid) do
    case Client.get(client_opts, oid) do
      {:ok, value} -> value
      {:error, _} -> nil
    end
  end

  defp build_sensor_from_entity_mib(client_opts, index, entity_descriptions) do
    oids = [
      @entity_sensor_oids.ent_phys_sensor_type <> ".#{index}",
      @entity_sensor_oids.ent_phys_sensor_scale <> ".#{index}",
      @entity_sensor_oids.ent_phys_sensor_precision <> ".#{index}",
      @entity_sensor_oids.ent_phys_sensor_value <> ".#{index}",
      @entity_sensor_oids.ent_phys_sensor_oper_status <> ".#{index}"
    ]

    case Client.get_multiple(client_opts, oids) do
      {:ok, [type, scale, precision, value, status]} ->
        sensor_type = parse_sensor_type(type)
        entity_info = Map.get(entity_descriptions, index, %{})

        # Use ENTITY-MIB description/name for better sensor names
        sensor_descr = get_sensor_description(entity_info, index)

        %{
          sensor_type: sensor_type,
          sensor_index: "#{index}",
          sensor_oid: @entity_sensor_oids.ent_phys_sensor_value <> ".#{index}",
          sensor_descr: sensor_descr,
          sensor_unit: sensor_type_to_unit(sensor_type),
          sensor_divisor: calculate_divisor(scale, precision),
          last_value: parse_float(value),
          status: parse_sensor_status(status),
          # Include entity class for filtering (sensor=8)
          entity_class: Map.get(entity_info, :class)
        }

      _ ->
        nil
    end
  end

  # Get best available description from ENTITY-MIB (sanitized)
  defp get_sensor_description(%{name: name}, _index) when is_binary(name) and byte_size(name) > 0 do
    Sanitizer.sanitize_string(name)
  end

  defp get_sensor_description(%{descr: descr}, _index) when is_binary(descr) and byte_size(descr) > 0 do
    Sanitizer.sanitize_string(descr)
  end

  defp get_sensor_description(_, index), do: "Sensor #{index}"

  # Calculate divisor from scale and precision (like LibreNMS)
  # Scale: 10^scale exponent (negative = small units like milli-, positive = large units like kilo-)
  # Precision: decimal places
  # Always returns an integer for compatibility with Ecto schema
  #
  # Formula: divisor = 10^(precision - scale)
  # For scale=-3 (milli), precision=0: divisor = 10^(0-(-3)) = 10^3 = 1000
  # For scale=3 (kilo), precision=0: divisor = 10^(0-3) = 10^-3 = 1/1000 (use fallback)
  defp calculate_divisor(scale, precision) when is_integer(scale) and is_integer(precision) do
    exponent = precision - scale

    # Positive exponent: can represent as integer divisor
    if exponent >= 0 do
      Integer.pow(10, exponent)
    else
      # Negative exponent: would need fractional divisor, use scale_to_divisor fallback
      # This handles kilo-, mega- units where raw value needs to be multiplied, not divided
      scale_to_divisor(scale)
    end
  end

  defp calculate_divisor(scale, _) when is_integer(scale), do: scale_to_divisor(scale)
  defp calculate_divisor(_, _), do: 1

  defp extract_indices_from_oids(oid_map) do
    oid_map
    |> Map.keys()
    |> Enum.map(fn oid_string ->
      # Extract the last part of the OID which is the index
      oid_string
      |> String.split(".")
      |> List.last()
      |> String.to_integer()
    end)
  end

  # Vendor detection patterns: {descr_patterns, oid_patterns, manufacturer, model_extractor}
  @vendor_patterns [
    {["Cisco", "IOS"], [], "Cisco", :cisco},
    {["Cambium", "ePMP", "PMP", "PTP", "cnPilot"], ["17713"], "Cambium Networks", :cambium},
    {["Ubiquiti", "airOS", "AirFiber", "airMAX", "EdgeOS"], ["10002", "41112"], "Ubiquiti", :ubiquiti},
    {["MikroTik", "RouterOS"], ["14988"], "MikroTik", :mikrotik},
    {["Linux"], [], "Linux", :linux},
    {["Windows"], [], "Microsoft", :windows}
  ]

  defp parse_sys_descr(sys_descr, sys_object_id) do
    case find_vendor_match(sys_descr, sys_object_id) do
      {manufacturer, extractor} -> {manufacturer, extract_model(extractor, sys_descr, sys_object_id)}
      nil -> {"Unknown", "Generic Device"}
    end
  end

  defp find_vendor_match(sys_descr, sys_object_id) do
    sys_descr = sys_descr || ""
    sys_object_id = sys_object_id || ""

    Enum.find_value(@vendor_patterns, fn {descr_patterns, oid_patterns, manufacturer, extractor} ->
      descr_match = descr_patterns != [] and String.contains?(sys_descr, descr_patterns)
      oid_match = oid_patterns != [] and String.contains?(sys_object_id, oid_patterns)

      if descr_match or oid_match do
        {manufacturer, extractor}
      end
    end)
  end

  defp extract_model(:cisco, sys_descr, _sys_object_id), do: extract_cisco_model(sys_descr)
  defp extract_model(:cambium, sys_descr, sys_object_id), do: extract_cambium_model(sys_descr, sys_object_id)
  defp extract_model(:ubiquiti, sys_descr, _sys_object_id), do: extract_ubiquiti_model(sys_descr)
  defp extract_model(:mikrotik, sys_descr, _sys_object_id), do: extract_mikrotik_model(sys_descr)
  defp extract_model(:linux, sys_descr, _sys_object_id), do: sys_descr
  defp extract_model(:windows, sys_descr, _sys_object_id), do: sys_descr

  # Cambium sysObjectID product line mapping
  # Format: .1.3.6.1.4.1.17713.X.Y.Z where X indicates product line
  # Order matters - more specific patterns first (21 before 2)
  @cambium_product_lines [
    {~r/\.17713\.21\./, "ePMP"},
    {~r/\.17713\.21$/, "ePMP"},
    {~r/\.17713\.[12]\./, "PMP"},
    {~r/\.17713\.[12]$/, "PMP"},
    {~r/\.17713\.[678]\./, "PTP"},
    {~r/\.17713\.[678]$/, "PTP"},
    {~r/\.17713\.9\./, "cnPilot"},
    {~r/\.17713\.9$/, "cnPilot"}
  ]

  defp extract_cambium_model(sys_descr, sys_object_id) do
    # First try to extract from sysDescr, then fall back to sysObjectID product line
    extract_cambium_model_from_descr(sys_descr) ||
      extract_cambium_product_line(sys_object_id) ||
      "Wireless"
  end

  defp extract_cambium_model_from_descr(sys_descr) do
    cond do
      match = Regex.run(~r/ePMP\s*(\d+)/i, sys_descr) -> "ePMP #{Enum.at(match, 1)}"
      Regex.match?(~r/ePMP/i, sys_descr) -> "ePMP"
      match = Regex.run(~r/PMP\s*(\d+)/i, sys_descr) -> "PMP #{Enum.at(match, 1)}"
      match = Regex.run(~r/PTP\s*(\d+)/i, sys_descr) -> "PTP #{Enum.at(match, 1)}"
      match = Regex.run(~r/cnPilot\s*(\w+)/i, sys_descr) -> "cnPilot #{Enum.at(match, 1)}"
      Regex.match?(~r/cnPilot/i, sys_descr) -> "cnPilot"
      true -> nil
    end
  end

  defp extract_cambium_product_line(sys_object_id) when is_binary(sys_object_id) do
    Enum.find_value(@cambium_product_lines, fn {regex, product} ->
      if Regex.match?(regex, sys_object_id), do: product
    end)
  end

  defp extract_cambium_product_line(_), do: nil

  defp extract_ubiquiti_model(sys_descr) do
    cond do
      match = Regex.run(~r/(AF-?\w+)/i, sys_descr) -> hd(match)
      match = Regex.run(~r/(LBE-?\w+|NBE-?\w+|NSM\d+|PBE-?\w+)/i, sys_descr) -> hd(match)
      Regex.match?(~r/EdgeOS/i, sys_descr) -> "EdgeRouter"
      true -> "Wireless"
    end
  end

  defp extract_mikrotik_model(sys_descr) do
    case Regex.run(~r/(RB\w+|CCR\w+|CRS\w+|hAP\w*|hEX\w*)/i, sys_descr) do
      [model | _] -> model
      _ -> "RouterOS"
    end
  end

  defp extract_cisco_model(sys_descr) do
    # Try to extract model from sysDescr
    # Example: "Cisco IOS Software, C2960 Software..."
    case Regex.run(~r/\b(C\d+\w*|WS-C\d+\w*)\b/, sys_descr) do
      [_, model] -> model
      _ -> "Unknown Model"
    end
  end

  defp parse_integer(value) when is_integer(value), do: value
  defp parse_integer(""), do: nil
  defp parse_integer("null"), do: nil
  defp parse_integer(value) when is_binary(value), do: String.to_integer(value)
  defp parse_integer(_), do: nil

  defp parse_float(value) when is_float(value), do: value
  defp parse_float(value) when is_integer(value), do: value / 1.0
  defp parse_float(""), do: nil
  defp parse_float("null"), do: nil
  defp parse_float(value) when is_binary(value), do: String.to_float(value)
  defp parse_float(_), do: nil

  defp parse_if_status(1), do: "up"
  defp parse_if_status(2), do: "down"
  defp parse_if_status(3), do: "testing"
  defp parse_if_status(_), do: "unknown"

  defp format_mac_address(<<>>) when is_binary(<<>>), do: nil
  defp format_mac_address(nil), do: nil

  defp format_mac_address(mac) when is_binary(mac) do
    mac
    |> :binary.bin_to_list()
    |> Enum.map_join(":", &String.pad_leading(Integer.to_string(&1, 16), 2, "0"))
    |> String.downcase()
  end

  defp format_mac_address(_), do: nil

  # ENTITY-SENSOR-MIB sensor types
  defp parse_sensor_type(1), do: "other"
  defp parse_sensor_type(2), do: "unknown"
  defp parse_sensor_type(3), do: "volts"
  defp parse_sensor_type(4), do: "volts"
  defp parse_sensor_type(5), do: "amperes"
  defp parse_sensor_type(6), do: "watts"
  defp parse_sensor_type(7), do: "hertz"
  defp parse_sensor_type(8), do: "celsius"
  defp parse_sensor_type(9), do: "percent"
  defp parse_sensor_type(10), do: "rpm"
  defp parse_sensor_type(11), do: "cmm"
  defp parse_sensor_type(_), do: "unknown"

  defp sensor_type_to_unit("celsius"), do: "°C"
  defp sensor_type_to_unit("volts"), do: "V"
  defp sensor_type_to_unit("amperes"), do: "A"
  defp sensor_type_to_unit("watts"), do: "W"
  defp sensor_type_to_unit("hertz"), do: "Hz"
  defp sensor_type_to_unit("percent"), do: "%"
  defp sensor_type_to_unit("rpm"), do: "RPM"
  defp sensor_type_to_unit(_), do: ""

  # ENTITY-SENSOR-MIB scale values (10^scale)
  defp scale_to_divisor(-24), do: 1_000_000_000_000_000_000_000_000
  defp scale_to_divisor(-9), do: 1_000_000_000
  defp scale_to_divisor(-3), do: 1_000
  defp scale_to_divisor(0), do: 1
  defp scale_to_divisor(_), do: 1

  defp parse_sensor_status(1), do: "ok"
  defp parse_sensor_status(2), do: "unavailable"
  defp parse_sensor_status(3), do: "nonoperational"
  defp parse_sensor_status(_), do: "unknown"

  # State sensor helper functions

  # Build a map of index => value from Client.walk results (already a Map)
  # Client.walk returns %{"full.oid.string" => extracted_value}
  defp build_index_value_map(walk_map) when is_map(walk_map) do
    Map.new(walk_map, fn {oid, value} ->
      index = extract_index_from_oid(oid)
      {index, value}
    end)
  end

  defp extract_index_from_oid(oid) when is_binary(oid) do
    oid
    |> String.split(".")
    |> List.last()
  end

  defp fetch_entity_descr_map(client_opts) do
    case Client.walk(client_opts, @entity_mib_oids.ent_phys_descr) do
      {:ok, results} when is_map(results) -> build_index_value_map(results)
      _ -> %{}
    end
  end

  defp fetch_entity_state_map(client_opts) do
    case Client.walk(client_opts, @entity_state_oids.ent_state_oper) do
      {:ok, results} when is_map(results) -> build_index_value_map(results)
      _ -> %{}
    end
  end

  defp build_state_sensor(index, class_map, descr_map, state_map) do
    class = Map.get(class_map, index)
    descr = Map.get(descr_map, index, "Entity #{index}")
    state_value = Map.get(state_map, index)

    entity_type = physical_class_to_type(class)
    status = entity_state_to_status(state_value)

    %{
      sensor_index: index,
      sensor_oid: @entity_state_oids.ent_state_oper <> ".#{index}",
      sensor_descr: descr,
      entity_type: entity_type,
      state_value: state_value,
      state_descr: entity_state_to_descr(state_value),
      status: status,
      last_checked_at: DateTime.utc_now()
    }
  end

  # ENTITY-MIB physical class to type mapping
  defp physical_class_to_type(6), do: "power_supply"
  defp physical_class_to_type(7), do: "fan"
  defp physical_class_to_type(9), do: "module"
  defp physical_class_to_type(3), do: "chassis"
  defp physical_class_to_type(5), do: "container"
  defp physical_class_to_type(8), do: "sensor"
  defp physical_class_to_type(_), do: "unknown"

  # ENTITY-STATE-MIB operational status mapping
  # 1=unknown, 2=disabled, 3=enabled, 4=testing
  # We map to: ok, warning, critical, unknown
  defp entity_state_to_status(1), do: "unknown"
  defp entity_state_to_status(2), do: "ok"
  defp entity_state_to_status(3), do: "warning"
  defp entity_state_to_status(4), do: "unknown"
  defp entity_state_to_status(_), do: "unknown"

  defp entity_state_to_descr(1), do: "unknown"
  defp entity_state_to_descr(2), do: "enabled"
  defp entity_state_to_descr(3), do: "disabled"
  defp entity_state_to_descr(4), do: "testing"
  defp entity_state_to_descr(_), do: "unknown"

  # VLAN helper functions

  defp build_vlan_index_map(walk_map) when is_map(walk_map) do
    Map.new(walk_map, fn {oid, value} ->
      vlan_id = extract_vlan_id_from_oid(oid)
      {vlan_id, value}
    end)
  end

  defp extract_vlan_id_from_oid(oid) when is_binary(oid) do
    oid
    |> String.split(".")
    |> List.last()
    |> String.to_integer()
  rescue
    _ -> 0
  end

  defp extract_vlan_id_from_oid(_), do: 0

  defp valid_vlan_id?(vlan_id) when is_integer(vlan_id) do
    vlan_id >= 1 and vlan_id <= 4094
  end

  # Q-BRIDGE-MIB RowStatus values:
  # 1=active, 2=notInService, 3=notReady, 4=createAndGo, 5=createAndWait, 6=destroy
  defp vlan_row_status_to_status(1), do: "active"
  defp vlan_row_status_to_status(2), do: "suspended"
  defp vlan_row_status_to_status(3), do: "suspended"
  defp vlan_row_status_to_status(_), do: "active"

  # IP address helper functions

  defp build_ip_mask_map(walk_map) when is_map(walk_map) do
    Map.new(walk_map, fn {oid, value} ->
      ip_address = extract_ip_from_oid(oid)
      mask = format_ip_address(value)
      {ip_address, mask}
    end)
  end

  defp extract_ip_from_oid(oid) when is_binary(oid) do
    # OID format: base_oid.ip1.ip2.ip3.ip4
    # Extract last 4 octets as IP address
    parts = String.split(oid, ".")

    if length(parts) >= 4 do
      parts
      |> Enum.take(-4)
      |> Enum.join(".")
    end
  rescue
    _ -> nil
  end

  defp extract_ip_from_oid(_), do: nil

  defp format_ip_address({a, b, c, d}) when is_integer(a) do
    "#{a}.#{b}.#{c}.#{d}"
  end

  # Handle raw 4-byte binary (IPv4 address bytes) - common from SNMP
  defp format_ip_address(<<a, b, c, d>>) do
    "#{a}.#{b}.#{c}.#{d}"
  end

  # Handle already-formatted string, but sanitize non-printable characters
  defp format_ip_address(value) when is_binary(value) do
    if String.printable?(value) do
      value
    else
      # Try to convert as raw bytes
      bytes = :binary.bin_to_list(value)

      if length(bytes) == 4 and Enum.all?(bytes, &(&1 >= 0 and &1 <= 255)) do
        Enum.join(bytes, ".")
      end
    end
  end

  defp format_ip_address(_), do: nil

  defp subnet_mask_to_prefix(nil), do: nil

  defp subnet_mask_to_prefix(mask) when is_binary(mask) do
    case String.split(mask, ".") do
      [a, b, c, d] ->
        [a, b, c, d]
        |> Enum.map(&String.to_integer/1)
        |> Enum.map(&count_bits/1)
        |> Enum.sum()

      _ ->
        nil
    end
  rescue
    _ -> nil
  end

  defp subnet_mask_to_prefix(_), do: nil

  defp count_bits(octet) when is_integer(octet) do
    # Count the number of 1 bits in the octet
    octet
    |> Integer.digits(2)
    |> Enum.count(&(&1 == 1))
  end

  @doc """
  Collects raw debug data for troubleshooting.
  Includes system OIDs and interface/sensor tables.
  """
  def collect_raw_debug_data(client_opts) do
    # Collect system OID values
    system_oids =
      Map.new(@system_oids, fn {key, oid} ->
        value =
          case Client.get(client_opts, oid) do
            {:ok, val} -> val
            {:error, reason} -> "ERROR: #{inspect(reason)}"
          end

        {key, %{oid: oid, value: value}}
      end)

    # Walk interface tables
    if_table =
      case Client.walk(client_opts, "1.3.6.1.2.1.2.2") do
        {:ok, results} -> results
        {:error, _} -> %{}
      end

    ifx_table =
      case Client.walk(client_opts, "1.3.6.1.2.1.31.1.1") do
        {:ok, results} -> results
        {:error, _} -> %{}
      end

    # Walk entity sensor table
    entity_sensor_table =
      case Client.walk(client_opts, "1.3.6.1.2.1.99.1.1") do
        {:ok, results} -> results
        {:error, _} -> %{}
      end

    %{
      timestamp: DateTime.utc_now(),
      system_oids: system_oids,
      if_table: if_table,
      ifx_table: ifx_table,
      entity_sensor_table: entity_sensor_table
    }
  end
end
