defmodule Towerops.Snmp.Profiles.Dynamic do
  @moduledoc """
  Dynamic SNMP profile that interprets database-stored device profiles.

  This profile uses the device_profiles table to dynamically discover
  sensors and processors based on imported OID definitions.
  """

  alias Towerops.DeviceProfiles
  alias Towerops.DeviceProfiles.DeviceProfile
  alias Towerops.Snmp.Client
  alias Towerops.Snmp.Discovery
  alias Towerops.Snmp.Profiles.Base

  require Logger

  @doc """
  Identifies device using the database profile information.
  Returns manufacturer, model, and firmware info.

  Polls SNMP OIDs from OS definitions to extract version and serial number.
  """
  @spec identify_device(DeviceProfile.t(), Client.connection_opts(), Discovery.system_info()) ::
          Discovery.device_info()
  def identify_device(profile, client_opts, _system_info) do
    # Load profile with OS definitions
    profile = DeviceProfiles.get_profile_with_associations(profile.os)

    # Poll OS definitions for version, serial, etc.
    os_data = poll_os_definitions(profile, client_opts)

    # Use profile metadata to build device info
    %{
      manufacturer: extract_manufacturer(profile),
      model: profile.text || profile.os,
      firmware_version: os_data[:version] || profile.os,
      serial_number: os_data[:serial]
    }
  end

  @doc """
  Discovers network interfaces using the base profile.
  Dynamic profiles inherit standard IF-MIB interface discovery.
  """
  @spec discover_interfaces(DeviceProfile.t(), Client.connection_opts()) ::
          {:ok, [Discovery.interface_data()]} | {:error, term()}
  def discover_interfaces(_profile, client_opts) do
    # Use base profile for standard interface discovery
    Base.discover_interfaces(client_opts)
  end

  @doc """
  Discovers sensors dynamically using database profile definitions.
  Walks SNMP OIDs defined in the profile's sensor_definitions and processor_definitions.
  """
  @spec discover_sensors(DeviceProfile.t(), Client.connection_opts()) ::
          {:ok, [Discovery.sensor_data()]}
  def discover_sensors(profile, client_opts) do
    # Load profile with sensor and processor definitions
    profile = DeviceProfiles.get_profile_with_associations(profile.os)

    sensor_count = if profile, do: length(profile.sensor_definitions), else: 0
    processor_count = if profile, do: length(profile.processor_definitions), else: 0

    if profile && (sensor_count > 0 || processor_count > 0) do
      Logger.debug(
        "Discovering sensors for #{profile.os} (#{sensor_count} sensor defs, #{processor_count} processor defs)"
      )

      # Discover regular sensors
      sensors =
        Enum.flat_map(profile.sensor_definitions, fn sensor_def ->
          discover_sensor_from_definition(client_opts, sensor_def)
        end)

      # Discover processors as sensors
      cpu_sensors =
        Enum.flat_map(profile.processor_definitions, fn proc_def ->
          discover_processor_as_sensor(client_opts, proc_def)
        end)

      {:ok, sensors ++ cpu_sensors}
    else
      Logger.debug("No sensor or processor definitions found for profile #{profile.os}")
      {:ok, []}
    end
  end

  # Private functions

  defp poll_os_definitions(_profile, _client_opts) do
    # Note: OS definition polling is currently disabled because device profiles
    # use MIB symbolic names (e.g., CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0)
    # which require MIB compilation to resolve to numeric OIDs.
    # The SNMP client only supports numeric OIDs.
    # Future enhancement: Add MIB resolution or import numeric OIDs during profile import
    %{}
  end

  defp extract_manufacturer(%DeviceProfile{group: group}) when is_binary(group) do
    # Capitalize manufacturer from group (e.g., "cambium" -> "Cambium")
    String.capitalize(group)
  end

  defp extract_manufacturer(%DeviceProfile{text: text}) when is_binary(text) do
    # Extract first word from text (e.g., "Ubiquiti AirFiber" -> "Ubiquiti")
    text
    |> String.split(" ")
    |> List.first()
  end

  defp extract_manufacturer(_), do: "Unknown"

  defp discover_processor_as_sensor(client_opts, proc_def) do
    # Try numeric OID first, fall back to named OID
    oid = proc_def.num_oid || proc_def.oid

    if oid do
      # Remove template variables from OID
      walk_oid = clean_oid_template(oid)
      walk_processor_oid(client_opts, walk_oid, proc_def)
    else
      Logger.warning("Processor definition missing OID: #{inspect(proc_def)}")
      []
    end
  end

  defp walk_processor_oid(client_opts, walk_oid, proc_def) do
    case Client.walk(client_opts, walk_oid) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        # Convert walk results (map) to CPU sensor data
        Enum.map(results, fn {result_oid, value} ->
          build_processor_sensor_data(proc_def, %{oid: result_oid, value: value})
        end)

      {:ok, _empty} ->
        Logger.debug("No data found for processor OID: #{walk_oid}")
        []

      {:error, reason} ->
        Logger.warning("Failed to walk processor OID #{walk_oid}: #{inspect(reason)}")
        []
    end
  end

  defp discover_sensor_from_definition(client_opts, sensor_def) do
    # Try numeric OID first, fall back to named OID
    oid = sensor_def.num_oid || sensor_def.oid

    if oid do
      # Remove template variables like ".{{ $index }}" from OID
      # For walks, we need the base OID without the index placeholder
      walk_oid = clean_oid_template(oid)
      walk_sensor_oid(client_opts, walk_oid, sensor_def)
    else
      Logger.warning("Sensor definition missing OID: #{inspect(sensor_def)}")
      []
    end
  end

  defp walk_sensor_oid(client_opts, walk_oid, sensor_def) do
    case Client.walk(client_opts, walk_oid) do
      {:ok, results} when is_map(results) and map_size(results) > 0 ->
        # Convert walk results (map) to sensor data
        Enum.map(results, fn {result_oid, value} ->
          build_sensor_data(sensor_def, %{oid: result_oid, value: value})
        end)

      {:ok, _empty} ->
        Logger.debug("No data found for sensor OID: #{walk_oid}")
        []

      {:error, reason} ->
        Logger.warning("Failed to walk sensor OID #{walk_oid}: #{inspect(reason)}")
        []
    end
  end

  # Remove template variables from OID
  # Examples:
  #   ".1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}" -> ".1.3.6.1.4.1.17713.21.2.1.36"
  #   "sysUptime.{{ $index }}" -> "sysUptime"
  defp clean_oid_template(oid) do
    oid
    |> String.replace(~r/\.?\{\{[^}]+\}\}/, "")
    |> String.trim_trailing(".")
  end

  defp build_sensor_data(sensor_def, %{oid: oid, value: value}) do
    # Build sensor index from OID (extract last component)
    oid_index = extract_index_from_oid(oid, sensor_def.index)

    # Make index unique by combining sensor class with OID index
    # This prevents conflicts when multiple sensors have the same OID index (e.g., ".0")
    sensor_index = build_unique_sensor_index(sensor_def.sensor_class, sensor_def.descr, oid_index)

    # Apply divisor to value if it's numeric
    last_value =
      cond do
        is_number(value) && sensor_def.divisor && sensor_def.divisor > 1 ->
          value / sensor_def.divisor

        is_number(value) ->
          value * 1.0

        true ->
          nil
      end

    %{
      sensor_type: sensor_def.sensor_class,
      sensor_index: sensor_index,
      sensor_oid: oid,
      sensor_descr: sensor_def.descr || "#{sensor_def.sensor_class} #{sensor_index}",
      sensor_unit: determine_unit(sensor_def.sensor_class),
      sensor_divisor: sensor_def.divisor || 1,
      last_value: last_value,
      status: "ok"
    }
  end

  defp build_processor_sensor_data(proc_def, %{oid: oid, value: value}) do
    # Build sensor index from OID (extract last component)
    sensor_index = extract_index_from_oid(oid, proc_def.index)

    # Apply precision (divisor) to value if it's numeric
    last_value = apply_processor_precision(value, proc_def.precision)

    # Determine status based on CPU load percentage
    status = determine_processor_status(last_value)

    %{
      sensor_type: "percent",
      sensor_index: "cpu#{sensor_index}",
      sensor_oid: oid,
      sensor_descr: proc_def.descr || "CPU #{sensor_index}",
      sensor_unit: "%",
      sensor_divisor: proc_def.precision || 1,
      last_value: last_value,
      status: status
    }
  end

  defp apply_processor_precision(value, precision) do
    cond do
      is_number(value) && precision && precision > 1 ->
        value / precision

      is_number(value) ->
        value * 1.0

      true ->
        nil
    end
  end

  defp determine_processor_status(last_value) do
    cond do
      is_nil(last_value) -> "unknown"
      last_value < 70 -> "ok"
      last_value < 90 -> "warning"
      true -> "critical"
    end
  end

  defp extract_index_from_oid(oid, configured_index) when is_binary(configured_index) do
    # If index contains template variable or is empty, extract from OID
    if configured_index == "" or String.contains?(configured_index, "{{") do
      # Extract last component of OID as index
      oid
      |> String.split(".")
      |> List.last()
      |> to_string()
    else
      # Use pre-configured static index
      configured_index
    end
  end

  defp extract_index_from_oid(oid, _) do
    # Extract last component of OID as index
    oid
    |> String.split(".")
    |> List.last()
    |> to_string()
  end

  defp build_unique_sensor_index(sensor_class, descr, oid_index) do
    # For scalar sensors (index 0), use a descriptive name from the description
    # For table sensors, use the OID index
    if oid_index == "0" && descr do
      # Convert description to a safe index string
      # E.g., "DFS Detected Count" -> "dfs_detected_count"
      # E.g., "GPS Status" -> "gps_status"
      descr
      |> String.downcase()
      |> String.replace(~r/[^a-z0-9]+/, "_")
      |> String.trim("_")
    else
      # For table entries, prepend sensor class to OID index
      "#{sensor_class}_#{oid_index}"
    end
  end

  defp determine_unit("temperature"), do: "°C"
  defp determine_unit("voltage"), do: "V"
  defp determine_unit("current"), do: "A"
  defp determine_unit("power"), do: "W"
  defp determine_unit("fanspeed"), do: "RPM"
  defp determine_unit("humidity"), do: "%"
  defp determine_unit("dbm"), do: "dBm"
  defp determine_unit("snr"), do: "dB"
  defp determine_unit("percent"), do: "%"
  defp determine_unit("frequency"), do: "Hz"
  defp determine_unit(_), do: ""
end
