defmodule Towerops.Snmp.Profiles.NetSnmp do
  @moduledoc """
  Net-SNMP device profile for Linux/Unix servers.
  Extends Base profile with Net-SNMP specific sensor discovery from:
  - LM-SENSORS-MIB (lm-sensors hardware monitoring)
  - UCD-SNMP-MIB (system statistics)

  Discovers:
  - CPU temperature, fan speed, voltage from lm-sensors
  - System load, memory, disk usage from UCD-SNMP-MIB
  """

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

  require Logger

  @lm_sensors_oids %{
    # LM-SENSORS-MIB
    lm_temp_sensors_device: "1.3.6.1.4.1.2021.13.16.2.1.2",
    lm_temp_sensors_value: "1.3.6.1.4.1.2021.13.16.2.1.3",
    lm_fans_device: "1.3.6.1.4.1.2021.13.16.3.1.2",
    lm_fans_value: "1.3.6.1.4.1.2021.13.16.3.1.3",
    lm_voltage_device: "1.3.6.1.4.1.2021.13.16.4.1.2",
    lm_voltage_value: "1.3.6.1.4.1.2021.13.16.4.1.3"
  }

  @ucd_snmp_oids %{
    # UCD-SNMP-MIB system statistics
    load_average_1: "1.3.6.1.4.1.2021.10.1.3.1",
    load_average_5: "1.3.6.1.4.1.2021.10.1.3.2",
    load_average_15: "1.3.6.1.4.1.2021.10.1.3.3",
    mem_total: "1.3.6.1.4.1.2021.4.5.0",
    mem_available: "1.3.6.1.4.1.2021.4.6.0",
    mem_buffer: "1.3.6.1.4.1.2021.4.14.0",
    mem_cached: "1.3.6.1.4.1.2021.4.15.0"
  }

  @doc """
  Discovers system information using Base profile.
  """
  @spec discover_system_info(Client.connection_opts()) ::
          {:ok, Discovery.system_info()} | {:error, term()}
  def discover_system_info(client_opts) do
    Base.discover_system_info(client_opts)
  end

  @doc """
  Discovers network interfaces using Base profile.
  """
  @spec discover_interfaces(Client.connection_opts()) ::
          {:ok, [Discovery.interface_data()]} | {:error, term()}
  def discover_interfaces(client_opts) do
    Base.discover_interfaces(client_opts)
  end

  @doc """
  Discovers sensors from LM-SENSORS-MIB and UCD-SNMP-MIB.
  Combines hardware sensors (temperature, fans, voltage) with system stats (load, memory).
  """
  def discover_sensors(client_opts) do
    lm_sensors = discover_lm_sensors(client_opts)
    ucd_sensors = discover_ucd_sensors(client_opts)

    all_sensors = lm_sensors ++ ucd_sensors

    if all_sensors == [] do
      # Fall back to standard ENTITY-SENSOR-MIB
      Logger.debug("Net-SNMP specific sensors not available, using standard discovery")
      Base.discover_sensors(client_opts)
    else
      {:ok, all_sensors}
    end
  end

  @doc """
  Identifies Linux/Unix device from sysDescr.
  """
  def identify_device(system_info) do
    sys_descr = Map.get(system_info, :sys_descr, "")

    {os, version} = extract_linux_info(sys_descr)

    Map.merge(system_info, %{
      manufacturer: "Linux",
      model: os,
      firmware_version: version
    })
  end

  # Private functions

  defp discover_lm_sensors(client_opts) do
    temp_sensors = discover_temperature_sensors(client_opts)
    fan_sensors = discover_fan_sensors(client_opts)
    voltage_sensors = discover_voltage_sensors(client_opts)

    temp_sensors ++ fan_sensors ++ voltage_sensors
  end

  defp discover_temperature_sensors(client_opts) do
    with {:ok, devices} <- Client.walk(client_opts, @lm_sensors_oids.lm_temp_sensors_device),
         {:ok, values} <- Client.walk(client_opts, @lm_sensors_oids.lm_temp_sensors_value) do
      build_lm_sensors(devices, values, "celsius", "°C")
    else
      _ -> []
    end
  end

  defp discover_fan_sensors(client_opts) do
    with {:ok, devices} <- Client.walk(client_opts, @lm_sensors_oids.lm_fans_device),
         {:ok, values} <- Client.walk(client_opts, @lm_sensors_oids.lm_fans_value) do
      build_lm_sensors(devices, values, "rpm", "RPM")
    else
      _ -> []
    end
  end

  defp discover_voltage_sensors(client_opts) do
    with {:ok, devices} <- Client.walk(client_opts, @lm_sensors_oids.lm_voltage_device),
         {:ok, values} <- Client.walk(client_opts, @lm_sensors_oids.lm_voltage_value) do
      build_lm_sensors(devices, values, "volts", "V")
    else
      _ -> []
    end
  end

  defp build_lm_sensors(devices, values, sensor_type, unit) do
    Enum.map(devices, fn {oid, device_name} ->
      index = extract_last_oid_part(oid)
      value_oid = find_matching_oid(values, index)

      %{
        sensor_type: sensor_type,
        sensor_index: index,
        sensor_oid: value_oid,
        sensor_descr: device_name,
        sensor_unit: unit,
        sensor_divisor: divisor_for_type(sensor_type),
        last_value: get_sensor_value(values, index),
        status: "ok"
      }
    end)
  end

  defp discover_ucd_sensors(client_opts) do
    load_sensors = discover_load_sensors(client_opts)
    memory_sensors = discover_memory_sensors(client_opts)

    load_sensors ++ memory_sensors
  end

  defp discover_load_sensors(client_opts) do
    case Client.get_multiple(client_opts, [
           @ucd_snmp_oids.load_average_1,
           @ucd_snmp_oids.load_average_5,
           @ucd_snmp_oids.load_average_15
         ]) do
      {:ok, [load1, load5, load15]} ->
        [
          %{
            sensor_type: "load",
            sensor_index: "1min",
            sensor_oid: @ucd_snmp_oids.load_average_1,
            sensor_descr: "System Load (1 min)",
            sensor_unit: "",
            sensor_divisor: 100,
            last_value: parse_float(load1),
            status: "ok"
          },
          %{
            sensor_type: "load",
            sensor_index: "5min",
            sensor_oid: @ucd_snmp_oids.load_average_5,
            sensor_descr: "System Load (5 min)",
            sensor_unit: "",
            sensor_divisor: 100,
            last_value: parse_float(load5),
            status: "ok"
          },
          %{
            sensor_type: "load",
            sensor_index: "15min",
            sensor_oid: @ucd_snmp_oids.load_average_15,
            sensor_descr: "System Load (15 min)",
            sensor_unit: "",
            sensor_divisor: 100,
            last_value: parse_float(load15),
            status: "ok"
          }
        ]

      _ ->
        []
    end
  end

  defp discover_memory_sensors(client_opts) do
    case Client.get_multiple(client_opts, [
           @ucd_snmp_oids.mem_total,
           @ucd_snmp_oids.mem_available
         ]) do
      {:ok, [total, available]} ->
        total_kb = parse_integer(total)
        available_kb = parse_integer(available)
        used_kb = calculate_used_memory(total_kb, available_kb)
        used_percent = calculate_memory_percentage(total_kb, used_kb)

        [
          %{
            sensor_type: "memory",
            sensor_index: "usage",
            sensor_oid: @ucd_snmp_oids.mem_available,
            sensor_descr: "Memory Usage",
            sensor_unit: "%",
            sensor_divisor: 1,
            last_value: used_percent,
            status: "ok"
          }
        ]

      _ ->
        []
    end
  end

  defp calculate_used_memory(nil, _available_kb), do: nil
  defp calculate_used_memory(_total_kb, nil), do: nil
  defp calculate_used_memory(total_kb, available_kb), do: total_kb - available_kb

  defp calculate_memory_percentage(total_kb, used_kb) when is_number(total_kb) and total_kb > 0 and is_number(used_kb) do
    percent = used_kb / total_kb * 100
    cap_memory_percentage(percent, used_kb, total_kb)
  end

  defp calculate_memory_percentage(_total_kb, _used_kb), do: nil

  defp cap_memory_percentage(percent, _used_kb, _total_kb) when percent <= 100, do: percent

  defp cap_memory_percentage(percent, used_kb, total_kb) do
    Logger.warning(
      "Memory usage exceeded 100%: #{Float.round(percent, 2)}% " <>
        "(used=#{used_kb} KB, total=#{total_kb} KB). " <>
        "Capping at 100%. This may indicate a device firmware bug."
    )

    100.0
  end

  defp extract_linux_info(sys_descr) do
    cond do
      String.contains?(sys_descr, "Ubuntu") ->
        version = extract_version(sys_descr, ~r/Ubuntu[\/\s]+([\d.]+)/)
        {"Ubuntu", version}

      String.contains?(sys_descr, "Debian") ->
        version = extract_version(sys_descr, ~r/Debian[\/\s]+([\d.]+)/)
        {"Debian", version}

      String.contains?(sys_descr, "CentOS") ->
        version = extract_version(sys_descr, ~r/CentOS[\/\s]+([\d.]+)/)
        {"CentOS", version}

      String.contains?(sys_descr, "Red Hat") ->
        version = extract_version(sys_descr, ~r/Red Hat[\/\s]+([\d.]+)/)
        {"Red Hat Enterprise Linux", version}

      String.contains?(sys_descr, "Linux") ->
        version = extract_version(sys_descr, ~r/Linux[\/\s]+([\d.]+)/)
        {"Linux", version}

      true ->
        {"Unix", nil}
    end
  end

  defp extract_version(sys_descr, regex) do
    case Regex.run(regex, sys_descr) do
      [_, version] -> version
      _ -> nil
    end
  end

  defp extract_last_oid_part(oid_string) do
    oid_string
    |> String.split(".")
    |> List.last()
  end

  defp find_matching_oid(values, index) do
    values
    |> Map.keys()
    |> Enum.find(fn oid -> String.ends_with?(oid, ".#{index}") end)
  end

  defp get_sensor_value(values, index) do
    oid = find_matching_oid(values, index)
    value = Map.get(values, oid)
    parse_float(value)
  end

  defp divisor_for_type("celsius"), do: 1000
  defp divisor_for_type("volts"), do: 1000
  defp divisor_for_type("rpm"), do: 1
  defp divisor_for_type(_), do: 1

  defp parse_integer(value) when is_integer(value), do: value
  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
end
