defmodule Towerops.Snmp.Profiles.Vendors.Routeros do
  @moduledoc """
  MikroTik RouterOS device-specific SNMP handling.

  Mirrors LibreNMS/OS/Routeros.php for wireless sensor discovery and hardware detection.

  Supported features:
  - Wireless AP/Station sensors (CCQ, clients, frequency, noise floor, rate)
  - 60GHz wireless sensors (RSSI, quality, distance)
  - LTE modem sensors (RSRQ, RSRP, SINR)
  - DHCP leases and PPPoE sessions
  """

  @behaviour Towerops.Snmp.Profiles.Vendors.Vendor

  alias Towerops.Snmp.Client
  alias Towerops.Snmp.Profiles.Vendors.Vendor

  require Logger

  # OID for license level (integer) - MIKROTIK-MIB::mtxrLicLevel.0
  @license_level_oid "1.3.6.1.4.1.14988.1.1.4.3.0"

  # OID for license version (string) - MIKROTIK-MIB::mtxrLicVersion.0
  @version_oid "1.3.6.1.4.1.14988.1.1.4.4.0"

  # OID for serial number (string) - MIKROTIK-MIB::mtxrSerialNumber.0
  @serial_number_oid "1.3.6.1.4.1.14988.1.1.7.3.0"

  # Table OIDs for walking
  @wl_ap_table "1.3.6.1.4.1.14988.1.1.1.3.1"
  @wl_stat_table "1.3.6.1.4.1.14988.1.1.1.1.1"
  @wl_60g_table "1.3.6.1.4.1.14988.1.1.1.8.1"
  @lte_modem_table "1.3.6.1.4.1.14988.1.1.16.1.1"

  # mtxrGaugeTable - System health sensors (temperature, voltage, current, power, fan speed)
  # .2.{idx} = mtxrGaugeName, .3.{idx} = mtxrGaugeValue, .4.{idx} = mtxrGaugeUnit
  @gauge_table "1.3.6.1.4.1.14988.1.1.3.100.1"

  # mtxrOpticalTable - SFP/optical transceiver sensors
  # .2.{idx} = mtxrOpticalName, .6.{idx} = Temperature, .7.{idx} = Supply Voltage
  # .8.{idx} = Tx Bias Current, .9.{idx} = Tx Power, .10.{idx} = Rx Power
  @optical_table "1.3.6.1.4.1.14988.1.1.19.1.1"

  # mtxrGaugeUnit values
  @gauge_unit_celsius 1
  @gauge_unit_rpm 2
  @gauge_unit_dv 3
  @gauge_unit_da 4
  @gauge_unit_dw 5
  @gauge_unit_status 6

  @impl true
  def profile_names, do: ["routeros"]

  @impl true
  def detect_hardware(client_opts) do
    # MikroTik hardware is typically identified by sysDescr
    # Use the license level OID to get the integer license level
    case Client.get(client_opts, @license_level_oid) do
      {:ok, license} when is_integer(license) ->
        "RouterOS Level #{license}"

      _ ->
        nil
    end
  end

  @doc """
  Get RouterOS firmware version from MIKROTIK-MIB.
  Returns version string with "RouterOS" prefix and license level in parentheses.
  Example: "RouterOS 7.14.1 (Level 6)"
  """
  def detect_version(client_opts) do
    # Query the version OID which returns the version string
    version_result = Client.get(client_opts, @version_oid)

    case version_result do
      {:ok, version} when is_binary(version) and version != "" ->
        # Got version string, now try to get license level
        # The license level can be derived from the version or queried separately
        license_level = detect_license_level(client_opts)

        formatted_version =
          case license_level do
            level when is_integer(level) ->
              "RouterOS #{version} (Level #{level})"

            _ ->
              "RouterOS #{version}"
          end

        Logger.debug("RouterOS: Detected version: #{formatted_version}")
        formatted_version

      {:ok, version} when is_list(version) ->
        # Version came back as list of integers like [7, 20, 6]
        # Convert to dotted string format
        version_string = Enum.join(version, ".")
        license_level = detect_license_level(client_opts)

        formatted_version =
          case license_level do
            level when is_integer(level) ->
              "RouterOS #{version_string} (Level #{level})"

            _ ->
              "RouterOS #{version_string}"
          end

        Logger.debug("RouterOS: Detected version: #{formatted_version}")
        formatted_version

      other ->
        Logger.warning("RouterOS: Failed to detect version: #{inspect(other)}")
        nil
    end
  end

  @doc """
  Get RouterOS serial number from MIKROTIK-MIB.
  Returns the RouterBOARD serial number string.
  Example: "HC907K5P55K"
  """
  def detect_serial_number(client_opts) do
    case Client.get(client_opts, @serial_number_oid) do
      {:ok, serial} when is_binary(serial) and serial != "" ->
        Logger.debug("RouterOS: Detected serial number: #{serial}")
        serial

      other ->
        Logger.debug("RouterOS: Failed to detect serial number: #{inspect(other)}")
        nil
    end
  end

  # Try to detect the license level as an integer from mtxrLicLevel OID
  defp detect_license_level(client_opts) do
    case Client.get(client_opts, @license_level_oid) do
      {:ok, level} when is_integer(level) ->
        level

      _ ->
        # Couldn't get license level, return nil
        nil
    end
  end

  def discover_sensors(client_opts) do
    # RouterOS uses discover_wireless_sensors for all custom sensors
    {:ok, discover_wireless_sensors(client_opts)}
  end

  @impl true
  def discover_wireless_sensors(client_opts) do
    # Discover from multiple tables like LibreNMS does
    sensors =
      discover_ap_sensors(client_opts) ++
        discover_station_sensors(client_opts) ++
        discover_60g_sensors(client_opts) ++
        discover_lte_sensors(client_opts) ++
        discover_gauge_sensors(client_opts) ++
        discover_optical_sensors(client_opts) ++
        discover_system_sensors(client_opts)

    Logger.debug("RouterOS: Discovered #{length(sensors)} wireless sensors")
    sensors
  end

  @impl true
  def wireless_oid_defs do
    # Scalar OIDs for basic discovery (tables are walked separately)
    # Note: DHCP Leases (mtxrDHCPLeaseCount) is defined in the routeros.yaml
    # YAML profile's count_sensor_oids section - not duplicated here.
    [
      # PPPoE Sessions (Cisco OID that RouterOS also supports)
      %{
        oid: "1.3.6.1.4.1.9.9.150.1.1.1.0",
        sensor_type: "count",
        sensor_descr: "PPPoE Sessions",
        sensor_unit: "",
        sensor_divisor: 1
      }
    ]
  end

  # Discover sensors from mtxrWlApTable (AP mode)
  defp discover_ap_sensors(client_opts) do
    case Client.walk(client_opts, @wl_ap_table) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        parse_ap_table(results)

      _ ->
        []
    end
  end

  defp parse_ap_table(results) do
    # Group by index (last OID component)
    grouped = group_by_index(results)

    Enum.flat_map(grouped, fn {idx, values} ->
      ssid = get_string_value(values, 5) || "wlan#{idx}"
      freq = get_int_value(values, 7)
      freq_band = if freq && freq > 0, do: "#{div(freq, 1000)}G", else: ""
      prefix = if freq_band == "", do: ssid, else: "#{freq_band}: #{ssid}"

      build_ap_sensors(idx, prefix, values)
    end)
  end

  defp build_ap_sensors(idx, prefix, values) do
    sensors = []

    # Client count (.6)
    sensors =
      if client_count = get_int_value(values, 6) do
        [
          %{
            sensor_type: "clients",
            sensor_index: "mikrotik_ap_clients_#{idx}",
            sensor_oid: "#{@wl_ap_table}.6.#{idx}",
            sensor_descr: "#{prefix} Clients",
            sensor_unit: "",
            sensor_divisor: 1,
            last_value: client_count / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Frequency (.7)
    sensors =
      if freq = get_int_value(values, 7) do
        [
          %{
            sensor_type: "frequency",
            sensor_index: "mikrotik_ap_freq_#{idx}",
            sensor_oid: "#{@wl_ap_table}.7.#{idx}",
            sensor_descr: "#{prefix} Frequency",
            sensor_unit: "MHz",
            sensor_divisor: 1,
            last_value: freq / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Noise floor (.9)
    sensors =
      if noise = get_int_value(values, 9) do
        [
          %{
            sensor_type: "noise-floor",
            sensor_index: "mikrotik_ap_noise_#{idx}",
            sensor_oid: "#{@wl_ap_table}.9.#{idx}",
            sensor_descr: "#{prefix} Noise Floor",
            sensor_unit: "dBm",
            sensor_divisor: 1,
            last_value: noise / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # CCQ (.10)
    sensors =
      if ccq = get_int_value(values, 10) do
        [
          %{
            sensor_type: "ccq",
            sensor_index: "mikrotik_ap_ccq_#{idx}",
            sensor_oid: "#{@wl_ap_table}.10.#{idx}",
            sensor_descr: "#{prefix} CCQ",
            sensor_unit: "%",
            sensor_divisor: 1,
            last_value: ccq / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Tx Rate (.2)
    sensors =
      if tx_rate = get_int_value(values, 2) do
        [
          %{
            sensor_type: "rate",
            sensor_index: "mikrotik_ap_tx_rate_#{idx}",
            sensor_oid: "#{@wl_ap_table}.2.#{idx}",
            sensor_descr: "#{prefix} Tx Rate",
            sensor_unit: "bps",
            sensor_divisor: 1,
            last_value: tx_rate / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Rx Rate (.3)
    sensors =
      if rx_rate = get_int_value(values, 3) do
        [
          %{
            sensor_type: "rate",
            sensor_index: "mikrotik_ap_rx_rate_#{idx}",
            sensor_oid: "#{@wl_ap_table}.3.#{idx}",
            sensor_descr: "#{prefix} Rx Rate",
            sensor_unit: "bps",
            sensor_divisor: 1,
            last_value: rx_rate / 1
          }
          | sensors
        ]
      else
        sensors
      end

    sensors
  end

  # Discover sensors from mtxrWlStatTable (Station mode)
  defp discover_station_sensors(client_opts) do
    case Client.walk(client_opts, @wl_stat_table) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        parse_station_table(results)

      _ ->
        []
    end
  end

  defp parse_station_table(results) do
    grouped = group_by_index(results)

    Enum.flat_map(grouped, fn {idx, values} ->
      ssid = get_string_value(values, 5) || "wlan#{idx}"
      freq = get_int_value(values, 7)
      freq_band = if freq && freq > 0, do: "#{div(freq, 1000)}G", else: ""
      prefix = if freq_band == "", do: ssid, else: "#{freq_band}: #{ssid}"

      build_station_sensors(idx, prefix, values)
    end)
  end

  defp build_station_sensors(idx, prefix, values) do
    sensors = []

    # Frequency (.7)
    sensors =
      if freq = get_int_value(values, 7) do
        [
          %{
            sensor_type: "frequency",
            sensor_index: "mikrotik_sta_freq_#{idx}",
            sensor_oid: "#{@wl_stat_table}.7.#{idx}",
            sensor_descr: "#{prefix} Frequency",
            sensor_unit: "MHz",
            sensor_divisor: 1,
            last_value: freq / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Tx CCQ (.9)
    sensors =
      if tx_ccq = get_int_value(values, 9) do
        [
          %{
            sensor_type: "ccq",
            sensor_index: "mikrotik_sta_tx_ccq_#{idx}",
            sensor_oid: "#{@wl_stat_table}.9.#{idx}",
            sensor_descr: "#{prefix} Tx CCQ",
            sensor_unit: "%",
            sensor_divisor: 1,
            last_value: tx_ccq / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Rx CCQ (.10)
    sensors =
      if rx_ccq = get_int_value(values, 10) do
        [
          %{
            sensor_type: "ccq",
            sensor_index: "mikrotik_sta_rx_ccq_#{idx}",
            sensor_oid: "#{@wl_stat_table}.10.#{idx}",
            sensor_descr: "#{prefix} Rx CCQ",
            sensor_unit: "%",
            sensor_divisor: 1,
            last_value: rx_ccq / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Tx Rate (.2)
    sensors =
      if tx_rate = get_int_value(values, 2) do
        [
          %{
            sensor_type: "rate",
            sensor_index: "mikrotik_sta_tx_rate_#{idx}",
            sensor_oid: "#{@wl_stat_table}.2.#{idx}",
            sensor_descr: "#{prefix} Tx Rate",
            sensor_unit: "bps",
            sensor_divisor: 1,
            last_value: tx_rate / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Rx Rate (.3)
    sensors =
      if rx_rate = get_int_value(values, 3) do
        [
          %{
            sensor_type: "rate",
            sensor_index: "mikrotik_sta_rx_rate_#{idx}",
            sensor_oid: "#{@wl_stat_table}.3.#{idx}",
            sensor_descr: "#{prefix} Rx Rate",
            sensor_unit: "bps",
            sensor_divisor: 1,
            last_value: rx_rate / 1
          }
          | sensors
        ]
      else
        sensors
      end

    sensors
  end

  # Discover sensors from mtxrWl60GTable (60GHz wireless)
  defp discover_60g_sensors(client_opts) do
    case Client.walk(client_opts, @wl_60g_table) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        parse_60g_table(results)

      _ ->
        []
    end
  end

  defp parse_60g_table(results) do
    grouped = group_by_index(results)

    Enum.flat_map(grouped, fn {idx, values} ->
      ssid = get_string_value(values, 2) || "60G-#{idx}"
      prefix = "60G: #{ssid}"

      build_60g_sensors(idx, prefix, values)
    end)
  end

  defp build_60g_sensors(idx, prefix, values) do
    sensors = []

    # Frequency (.6)
    sensors =
      if freq = get_int_value(values, 6) do
        [
          %{
            sensor_type: "frequency",
            sensor_index: "mikrotik_60g_freq_#{idx}",
            sensor_oid: "#{@wl_60g_table}.6.#{idx}",
            sensor_descr: "#{prefix} Frequency",
            sensor_unit: "MHz",
            sensor_divisor: 1,
            last_value: freq / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Signal Quality (.8)
    sensors =
      if quality = get_int_value(values, 8) do
        [
          %{
            sensor_type: "quality",
            sensor_index: "mikrotik_60g_quality_#{idx}",
            sensor_oid: "#{@wl_60g_table}.8.#{idx}",
            sensor_descr: "#{prefix} Signal Quality",
            sensor_unit: "%",
            sensor_divisor: 1,
            last_value: quality / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # RSSI (.12)
    sensors =
      if rssi = get_int_value(values, 12) do
        [
          %{
            sensor_type: "rssi",
            sensor_index: "mikrotik_60g_rssi_#{idx}",
            sensor_oid: "#{@wl_60g_table}.12.#{idx}",
            sensor_descr: "#{prefix} RSSI",
            sensor_unit: "dBm",
            sensor_divisor: 1,
            last_value: rssi / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Phy Rate (.13)
    sensors =
      if phy_rate = get_int_value(values, 13) do
        [
          %{
            sensor_type: "rate",
            sensor_index: "mikrotik_60g_phy_rate_#{idx}",
            sensor_oid: "#{@wl_60g_table}.13.#{idx}",
            sensor_descr: "#{prefix} Phy Rate",
            sensor_unit: "Mbps",
            sensor_divisor: 1,
            last_value: phy_rate / 1
          }
          | sensors
        ]
      else
        sensors
      end

    sensors
  end

  # Discover sensors from mtxrLTEModemTable
  defp discover_lte_sensors(client_opts) do
    case Client.walk(client_opts, @lte_modem_table) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        parse_lte_table(results)

      _ ->
        []
    end
  end

  defp parse_lte_table(results) do
    grouped = group_by_index(results)

    Enum.flat_map(grouped, fn {idx, values} ->
      prefix = "LTE #{idx}"
      build_lte_sensors(idx, prefix, values)
    end)
  end

  defp build_lte_sensors(idx, prefix, values) do
    sensors = []

    # RSRQ (.3)
    sensors =
      if rsrq = get_int_value(values, 3) do
        [
          %{
            sensor_type: "rsrq",
            sensor_index: "mikrotik_lte_rsrq_#{idx}",
            sensor_oid: "#{@lte_modem_table}.3.#{idx}",
            sensor_descr: "#{prefix} RSRQ",
            sensor_unit: "dB",
            sensor_divisor: 1,
            last_value: rsrq / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # RSRP (.4)
    sensors =
      if rsrp = get_int_value(values, 4) do
        [
          %{
            sensor_type: "rsrp",
            sensor_index: "mikrotik_lte_rsrp_#{idx}",
            sensor_oid: "#{@lte_modem_table}.4.#{idx}",
            sensor_descr: "#{prefix} RSRP",
            sensor_unit: "dBm",
            sensor_divisor: 1,
            last_value: rsrp / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # SINR (.7)
    sensors =
      if sinr = get_int_value(values, 7) do
        [
          %{
            sensor_type: "sinr",
            sensor_index: "mikrotik_lte_sinr_#{idx}",
            sensor_oid: "#{@lte_modem_table}.7.#{idx}",
            sensor_descr: "#{prefix} SINR",
            sensor_unit: "dB",
            sensor_divisor: 1,
            last_value: sinr / 1
          }
          | sensors
        ]
      else
        sensors
      end

    sensors
  end

  # Discover sensors from mtxrGaugeTable (system health: temperature, voltage, current, power, fan speed)
  defp discover_gauge_sensors(client_opts) do
    case Client.walk(client_opts, @gauge_table) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        parse_gauge_table(results)

      _ ->
        []
    end
  end

  defp parse_gauge_table(results) do
    # Group by index (last OID component)
    grouped = group_by_index(results)

    grouped
    |> Enum.flat_map(fn {idx, values} ->
      build_gauge_sensor(idx, values)
    end)
    |> Enum.reject(&is_nil/1)
  end

  defp build_gauge_sensor(idx, values) do
    # .2 = mtxrGaugeName, .3 = mtxrGaugeValue, .4 = mtxrGaugeUnit
    name = get_string_value(values, 2)
    value = get_int_value(values, 3)
    unit_type = get_int_value(values, 4)

    if name && value do
      # Get base type from unit, then check if name suggests different type
      base_type = gauge_unit_to_sensor_type(unit_type)
      {sensor_type, sensor_unit, divisor} = maybe_override_sensor_type(base_type, name)

      # Make sensor description more descriptive
      # Add "System" prefix for generic names to distinguish from transceiver sensors
      sensor_descr = format_gauge_description(name, sensor_type)

      [
        %{
          sensor_type: sensor_type,
          sensor_index: "mikrotik_gauge_#{idx}",
          sensor_oid: "#{@gauge_table}.3.#{idx}",
          sensor_descr: sensor_descr,
          sensor_unit: sensor_unit,
          sensor_divisor: divisor,
          last_value: value / divisor
        }
      ]
    else
      []
    end
  end

  # Format gauge sensor descriptions to be more descriptive
  defp format_gauge_description(name, _sensor_type) when is_binary(name) do
    name_lower = String.downcase(name)

    cond do
      # Generic "temperature" -> "System Temperature"
      name_lower in ["temperature", "temp"] ->
        "System Temperature"

      # Generic "voltage" -> "System Voltage"
      name_lower in ["voltage", "volt"] ->
        "System Voltage"

      # Generic "current" -> "System Current"
      name_lower in ["current", "amp", "ampere"] ->
        "System Current"

      # Generic "fan" or "fan speed" -> "System Fan"
      name_lower in ["fan", "fan speed", "fanspeed"] ->
        "System Fan"

      # Generic "power" -> "System Power"
      name_lower in ["power", "watt"] ->
        "System Power"

      # Already descriptive (contains more than one word or specific details)
      String.contains?(name, " ") or String.length(name) > 15 ->
        name

      # Default: prefix with "System" for generic single-word names
      true ->
        "System #{name}"
    end
  end

  defp format_gauge_description(name, _sensor_type), do: name

  # Map mtxrGaugeUnit value to sensor type, unit string, and divisor
  # Mikrotik reports temperature in tenths of degrees (e.g., 230 = 23.0°C)
  defp gauge_unit_to_sensor_type(@gauge_unit_celsius), do: {"temperature", "°C", 10}
  defp gauge_unit_to_sensor_type(@gauge_unit_rpm), do: {"fanspeed", "RPM", 1}
  defp gauge_unit_to_sensor_type(@gauge_unit_dv), do: {"voltage", "V", 10}
  defp gauge_unit_to_sensor_type(@gauge_unit_da), do: {"current", "A", 10}
  defp gauge_unit_to_sensor_type(@gauge_unit_dw), do: {"power", "W", 10}
  defp gauge_unit_to_sensor_type(@gauge_unit_status), do: {"state", "", 1}
  defp gauge_unit_to_sensor_type(_), do: {"unknown", "", 1}

  # Override sensor type based on name when the device reports incorrect unit type
  # This handles cases where MikroTik devices report wrong mtxrGaugeUnit values
  defp maybe_override_sensor_type({sensor_type, _unit, _divisor} = type_info, name) when is_binary(name) do
    name_lower = String.downcase(name)
    inferred_type = infer_type_from_name(name_lower)

    case {sensor_type, inferred_type} do
      # Type matches or no override needed
      {type, type} -> type_info
      {_, nil} -> type_info
      # Override with inferred type
      {_, inferred} -> sensor_type_config(inferred)
    end
  end

  defp maybe_override_sensor_type(type_info, _name), do: type_info

  # Infer sensor type from name keywords
  defp infer_type_from_name(name_lower) do
    cond do
      String.contains?(name_lower, "voltage") -> "voltage"
      String.contains?(name_lower, ["temperature", "temp"]) -> "temperature"
      String.contains?(name_lower, ["current", "ampere"]) -> "current"
      String.contains?(name_lower, ["power", "watt"]) -> "power"
      String.contains?(name_lower, ["fan", "rpm"]) -> "fanspeed"
      true -> nil
    end
  end

  # Sensor type configurations for overrides (must match gauge_unit_to_sensor_type divisors)
  defp sensor_type_config("voltage"), do: {"voltage", "V", 10}
  defp sensor_type_config("temperature"), do: {"temperature", "°C", 10}
  defp sensor_type_config("current"), do: {"current", "A", 10}
  defp sensor_type_config("power"), do: {"power", "W", 10}
  defp sensor_type_config("fanspeed"), do: {"fanspeed", "RPM", 1}

  # Discover sensors from mtxrOpticalTable (SFP/optical transceiver sensors)
  defp discover_optical_sensors(client_opts) do
    case Client.walk(client_opts, @optical_table) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        parse_optical_table(results)

      _ ->
        []
    end
  end

  defp parse_optical_table(results) do
    # Group by index (last OID component)
    grouped = group_by_index(results)

    Enum.flat_map(grouped, fn {idx, values} ->
      build_optical_sensors(idx, values)
    end)
  end

  defp build_optical_sensors(idx, values) do
    # .2 = mtxrOpticalName, .6 = Temperature, .7 = Supply Voltage
    # .8 = Tx Bias Current, .9 = Tx Power, .10 = Rx Power
    name = get_string_value(values, 2) || "SFP #{idx}"

    sensors = []

    # Temperature (.6)
    sensors =
      if temp = get_int_value(values, 6) do
        [
          %{
            sensor_type: "temperature",
            sensor_index: "mikrotik_optical_temp_#{idx}",
            sensor_oid: "#{@optical_table}.6.#{idx}",
            sensor_descr: "#{name} Temperature",
            sensor_unit: "°C",
            sensor_divisor: 1,
            last_value: temp / 1
          }
          | sensors
        ]
      else
        sensors
      end

    # Supply Voltage (.7) - in mV, convert to V
    sensors =
      if voltage = get_int_value(values, 7) do
        [
          %{
            sensor_type: "voltage",
            sensor_index: "mikrotik_optical_voltage_#{idx}",
            sensor_oid: "#{@optical_table}.7.#{idx}",
            sensor_descr: "#{name} Supply Voltage",
            sensor_unit: "V",
            sensor_divisor: 1000,
            last_value: voltage / 1000
          }
          | sensors
        ]
      else
        sensors
      end

    # Tx Bias Current (.8) - in uA, convert to mA
    sensors =
      if bias = get_int_value(values, 8) do
        [
          %{
            sensor_type: "current",
            sensor_index: "mikrotik_optical_tx_bias_#{idx}",
            sensor_oid: "#{@optical_table}.8.#{idx}",
            sensor_descr: "#{name} Tx Bias",
            sensor_unit: "mA",
            sensor_divisor: 1000,
            last_value: bias / 1000
          }
          | sensors
        ]
      else
        sensors
      end

    # Tx Power (.9) - in thousandths of dBm
    sensors =
      if tx_power = get_int_value(values, 9) do
        [
          %{
            sensor_type: "dbm",
            sensor_index: "mikrotik_optical_tx_power_#{idx}",
            sensor_oid: "#{@optical_table}.9.#{idx}",
            sensor_descr: "#{name} Tx Power",
            sensor_unit: "dBm",
            sensor_divisor: 1000,
            last_value: tx_power / 1000
          }
          | sensors
        ]
      else
        sensors
      end

    # Rx Power (.10) - in thousandths of dBm
    sensors =
      if rx_power = get_int_value(values, 10) do
        [
          %{
            sensor_type: "dbm",
            sensor_index: "mikrotik_optical_rx_power_#{idx}",
            sensor_oid: "#{@optical_table}.10.#{idx}",
            sensor_descr: "#{name} Rx Power",
            sensor_unit: "dBm",
            sensor_divisor: 1000,
            last_value: rx_power / 1000
          }
          | sensors
        ]
      else
        sensors
      end

    sensors
  end

  # Discover system-level sensors (DHCP leases, PPPoE sessions)
  defp discover_system_sensors(client_opts) do
    Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
  end

  # Helper: Group OID results by their index (last component)
  defp group_by_index(results) do
    Enum.reduce(results, %{}, fn {oid, value}, acc ->
      parts = String.split(oid, ".")
      # Get the sub-OID (second to last) and index (last)
      {sub_oid, idx} =
        case Enum.take(parts, -2) do
          [sub, index] -> {String.to_integer(sub), index}
          _ -> {0, "0"}
        end

      Map.update(acc, idx, %{sub_oid => value}, fn existing ->
        Map.put(existing, sub_oid, value)
      end)
    end)
  end

  # Helper: Get integer value from grouped results
  defp get_int_value(values, sub_oid) do
    case Map.get(values, sub_oid) do
      value when is_integer(value) -> value
      _ -> nil
    end
  end

  # Helper: Get string value from grouped results
  defp get_string_value(values, sub_oid) do
    case Map.get(values, sub_oid) do
      value when is_binary(value) and value != "" -> value
      _ -> nil
    end
  end
end
