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

  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_alias: "1.3.6.1.2.1.31.1.1.1.18"
  }

  @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_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"
  }

  @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.
  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)

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

        {: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 """
  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: Map.get(raw_info, :sys_descr),
      sys_object_id: parse_oid(Map.get(raw_info, :sys_object_id)),
      sys_name: Map.get(raw_info, :sys_name),
      sys_uptime: parse_integer(Map.get(raw_info, :sys_uptime)),
      sys_contact: Map.get(raw_info, :sys_contact),
      sys_location: Map.get(raw_info, :sys_location)
    }
  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)
      # 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}")

      {:ok,
       %{
         if_index: index,
         if_descr: if_descr,
         if_name: if_name,
         if_alias: if_alias,
         if_type: parse_integer(if_type),
         if_speed: parse_integer(if_speed),
         if_phys_address: format_mac_address(if_phys_addr),
         if_admin_status: parse_if_status(if_admin),
         if_oper_status: parse_if_status(if_oper)
       }}
    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) do
    oids = [
      @entity_sensor_oids.ent_phys_sensor_type <> ".#{index}",
      @entity_sensor_oids.ent_phys_sensor_scale <> ".#{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, value, status]} ->
        %{
          sensor_type: parse_sensor_type(type),
          sensor_index: "#{index}",
          sensor_oid: @entity_sensor_oids.ent_phys_sensor_value <> ".#{index}",
          sensor_descr: "Sensor #{index}",
          sensor_unit: sensor_type_to_unit(parse_sensor_type(type)),
          sensor_divisor: scale_to_divisor(scale),
          last_value: parse_float(value),
          status: parse_sensor_status(status)
        }

      _ ->
        nil
    end
  end

  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

  defp parse_sys_descr(sys_descr, _sys_object_id) do
    cond do
      String.contains?(sys_descr, ["Cisco", "IOS"]) ->
        {"Cisco", extract_cisco_model(sys_descr)}

      String.contains?(sys_descr, "Linux") ->
        {"Linux", "Server"}

      String.contains?(sys_descr, "Windows") ->
        {"Microsoft", "Windows Server"}

      true ->
        {"Unknown", "Generic Device"}
    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(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(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"
end
