defmodule Towerops.Snmp.Profiles.Vendors.Arista do
  @moduledoc """
  Arista Networks device-specific SNMP handling.

  Supports Arista EOS switches.

  Supported devices:
  - Arista 7000 series switches
  - Arista 7050 series
  - Arista 7150 series
  - Arista 7280 series
  - Arista 7500 series

  Enterprise OID: 1.3.6.1.4.1.30065
  """

  @behaviour Towerops.Snmp.Profiles.Vendors.Vendor

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

  # Arista model name from sysDescr parsing
  @sysdescr_oid "1.3.6.1.2.1.1.1.0"

  @impl true
  def profile_names, do: ["arista_eos", "arista-mos"]

  @impl true
  def detect_hardware(client_opts) do
    # Arista includes model in sysDescr: "Arista Networks EOS version ... running on an Arista Networks DCS-7050TX-64"
    case Client.get(client_opts, @sysdescr_oid) do
      {:ok, descr} when is_binary(descr) ->
        case Regex.run(~r/running on an Arista Networks (\S+)/i, descr) do
          [_, model] -> model
          _ -> nil
        end

      _ ->
        nil
    end
  end

  @impl true
  def discover_wireless_sensors(client_opts) do
    Vendor.fetch_sensors(wireless_oid_defs(), client_opts)
  end

  @impl true
  def wireless_oid_defs do
    [
      # CPU Load (HOST-RESOURCES-MIB - average of all CPUs)
      %{
        oid: "1.3.6.1.2.1.25.3.3.1.2.1",
        sensor_type: "load",
        sensor_descr: "CPU Load",
        sensor_unit: "%",
        sensor_divisor: 1
      },
      # Memory Storage Size (hrStorageSize for RAM, index 1)
      # Note: hrStorage values need context - this gives total RAM allocation units
      %{
        oid: "1.3.6.1.2.1.25.2.3.1.5.1",
        sensor_type: "count",
        sensor_descr: "Memory Size",
        sensor_unit: "units",
        sensor_divisor: 1
      },
      # Memory Storage Used (hrStorageUsed for RAM, index 1)
      %{
        oid: "1.3.6.1.2.1.25.2.3.1.6.1",
        sensor_type: "count",
        sensor_descr: "Memory Used",
        sensor_unit: "units",
        sensor_divisor: 1
      },
      # System Uptime
      %{
        oid: "1.3.6.1.2.1.1.3.0",
        sensor_type: "count",
        sensor_descr: "System Uptime",
        sensor_unit: "ticks",
        sensor_divisor: 1
      }
    ]
  end

  @doc """
  Post-processes sensors discovered from ENTITY-SENSOR-MIB to apply Arista-specific
  enhancements:

  1. DOM Rx/Tx Power Conversion - Converts optical power from watts to dBm
  2. Arista Threshold Discovery - Fetches warning/critical limits from ARISTA-ENTITY-SENSOR-MIB
  3. Smart Grouping - Organizes sensors by SFPs, PSUs, Platform, System
  4. Description Cleanup - Removes redundant text

  This function should be called after base ENTITY-SENSOR-MIB discovery.
  """
  @spec post_process_sensors([map()], Client.connection_opts()) :: [map()]
  def post_process_sensors(sensors, client_opts) do
    # First, convert DOM power sensors from watts to dBm
    sensors_with_dbm = Enum.map(sensors, &convert_dom_power_to_dbm/1)

    # Then, fetch and apply Arista thresholds
    sensors_with_thresholds = apply_arista_thresholds(sensors_with_dbm, client_opts)

    # Apply smart grouping
    sensors_with_groups = Enum.map(sensors_with_thresholds, &apply_smart_grouping/1)

    # Clean up descriptions
    Enum.map(sensors_with_groups, &cleanup_description/1)
  end

  @doc """
  Converts DOM Rx/Tx power sensors from watts to dBm.

  Arista devices report optical power in watts via ENTITY-SENSOR-MIB, but users
  expect dBm (industry standard for optical power). This function detects DOM
  power sensors and converts them.

  Formula: dBm = 10 * log10(watts / 0.001)
         = 10 * log10(watts / 10^-3)
         = 10 * (log10(watts) + 3)

  Example: 0.0005 watts = -3.010 dBm
  """
  @spec convert_dom_power_to_dbm(map()) :: map()
  def convert_dom_power_to_dbm(%{sensor_descr: descr} = sensor) when is_binary(descr) do
    # Match patterns like "DOM Rx Power", "DOM Tx Power", "Xcvr Rx Power", "Xcvr Tx Power"
    if dom_power_sensor?(descr) do
      do_convert_dom_power(sensor)
    else
      sensor
    end
  end

  def convert_dom_power_to_dbm(sensor), do: sensor

  @spec dom_power_sensor?(String.t()) :: boolean()
  defp dom_power_sensor?(descr), do: Regex.match?(~r/(DOM|Xcvr) (R|T)x Power/i, descr)

  @spec do_convert_dom_power(map()) :: map()
  defp do_convert_dom_power(%{sensor_type: "power", last_value: watts} = sensor) when is_number(watts) and watts > 0 do
    # Convert watts to dBm: dBm = 10 * log10(watts * 1000)
    dbm_value = 10 * :math.log10(watts * 1000)
    dbm_rounded = Float.round(dbm_value, 3)

    # Update sensor type and value
    sensor
    |> Map.put(:sensor_type, "dbm")
    |> Map.put(:sensor_unit, "dBm")
    |> Map.put(:last_value, dbm_rounded)
    |> Map.put(:sensor_divisor, 1)
    |> Map.update(:metadata, %{original_type: "power", original_value: watts}, fn meta ->
      Map.merge(meta, %{original_type: "power", original_value: watts})
    end)
  end

  defp do_convert_dom_power(sensor), do: sensor

  @doc """
  Fetches threshold values from ARISTA-ENTITY-SENSOR-MIB and applies them to sensors.

  Arista extends ENTITY-SENSOR-MIB with aristaEntSensorThresholdTable which provides
  four threshold levels:
  - Type 1: Low Critical  → low_limit
  - Type 2: Low Warning   → low_warn_limit
  - Type 3: High Warning  → warn_limit
  - Type 4: High Critical → high_limit

  OID: 1.3.6.1.4.1.30065.3.3.1.1 (ARISTA-ENTITY-SENSOR-MIB::aristaEntSensorThresholdTable)
  """
  @spec apply_arista_thresholds([map()], Client.connection_opts()) :: [map()]
  def apply_arista_thresholds(sensors, client_opts) do
    # Walk the Arista threshold table
    # OID format: 1.3.6.1.4.1.30065.3.3.1.1.1.X.Y where X = sensor index, Y = threshold type
    threshold_oid = "1.3.6.1.4.1.30065.3.3.1.1.1"

    case Client.walk(client_opts, threshold_oid) do
      {:ok, threshold_data} when map_size(threshold_data) > 0 ->
        # Parse threshold data into a map: sensor_index => %{type => value}
        threshold_map = parse_arista_thresholds(threshold_data)

        # Apply thresholds to each sensor
        Enum.map(sensors, fn sensor ->
          apply_threshold_to_sensor(sensor, threshold_map)
        end)

      _ ->
        # No Arista thresholds available, return sensors unchanged
        sensors
    end
  end

  @spec parse_arista_thresholds(map()) :: map()
  defp parse_arista_thresholds(threshold_data) do
    # Parse OIDs like "1.3.6.1.4.1.30065.3.3.1.1.1.1001.3" where:
    # - 1001 is the sensor index
    # - 3 is the threshold type (1=low_critical, 2=low_warn, 3=high_warn, 4=high_critical)
    # - value is the threshold value

    Enum.reduce(threshold_data, %{}, fn {oid, value}, acc ->
      case extract_sensor_and_threshold_type(oid) do
        {sensor_index, threshold_type} when is_integer(value) or is_float(value) ->
          # Add threshold to sensor's map
          sensor_thresholds = Map.get(acc, sensor_index, %{})
          updated_thresholds = Map.put(sensor_thresholds, threshold_type, value)
          Map.put(acc, sensor_index, updated_thresholds)

        _ ->
          # Invalid OID format or value, skip
          acc
      end
    end)
  end

  @spec extract_sensor_and_threshold_type(String.t()) :: {integer(), integer()} | nil
  defp extract_sensor_and_threshold_type(oid) do
    # Extract last two numbers from OID: sensor_index.threshold_type
    case oid |> String.split(".") |> Enum.take(-2) do
      [sensor_index_str, threshold_type_str] ->
        with {sensor_index, ""} <- Integer.parse(sensor_index_str),
             {threshold_type, ""} <- Integer.parse(threshold_type_str) do
          {sensor_index, threshold_type}
        else
          _ -> nil
        end

      _ ->
        nil
    end
  end

  @spec apply_threshold_to_sensor(map(), map()) :: map()
  defp apply_threshold_to_sensor(%{sensor_index: index} = sensor, threshold_map) when is_binary(index) do
    # Convert sensor_index string to integer for lookup
    case Integer.parse(index) do
      {sensor_idx, ""} ->
        case Map.get(threshold_map, sensor_idx) do
          nil ->
            # No thresholds for this sensor
            sensor

          thresholds ->
            # Apply thresholds based on type
            # If sensor is dBm type, convert threshold from watts to dBm
            apply_thresholds_by_type(sensor, thresholds)
        end

      _ ->
        # Invalid sensor index format
        sensor
    end
  end

  defp apply_threshold_to_sensor(sensor, _threshold_map), do: sensor

  @spec apply_thresholds_by_type(map(), map()) :: map()
  defp apply_thresholds_by_type(sensor, thresholds) do
    is_dbm = sensor[:sensor_type] == "dbm"

    # Convert thresholds if this is a dBm sensor
    converted_thresholds =
      if is_dbm do
        Enum.reduce(thresholds, %{}, fn {type, watts}, acc ->
          dbm = convert_threshold_watts_to_dbm(watts)
          Map.put(acc, type, dbm)
        end)
      else
        thresholds
      end

    # Map threshold types to sensor limit fields
    # Type 1: Low Critical, Type 2: Low Warning, Type 3: High Warning, Type 4: High Critical
    sensor
    |> maybe_put_limit(:low_limit, Map.get(converted_thresholds, 1))
    |> maybe_put_limit(:low_warn_limit, Map.get(converted_thresholds, 2))
    |> maybe_put_limit(:warn_limit, Map.get(converted_thresholds, 3))
    |> maybe_put_limit(:high_limit, Map.get(converted_thresholds, 4))
  end

  @spec convert_threshold_watts_to_dbm(number()) :: float() | nil
  defp convert_threshold_watts_to_dbm(watts) when watts > 0 do
    Float.round(10 * :math.log10(watts * 1000), 2)
  end

  defp convert_threshold_watts_to_dbm(_watts), do: nil

  @spec maybe_put_limit(map(), atom(), number() | nil) :: map()
  defp maybe_put_limit(sensor, _field, nil), do: sensor
  defp maybe_put_limit(sensor, field, value), do: Map.put(sensor, field, value)

  @doc """
  Applies smart grouping to organize sensors by type (SFPs, PSUs, Platform, System).

  This improves UI/UX by grouping related sensors together.
  """
  @spec apply_smart_grouping(map()) :: map()
  def apply_smart_grouping(%{sensor_descr: descr} = sensor) when is_binary(descr) do
    group =
      cond do
        # SFP/transceiver sensors
        String.contains?(descr, "DOM") or String.contains?(descr, "Xcvr") ->
          "SFPs"

        # Power connector sensors
        String.contains?(descr, "PwrCon") ->
          # Extract connector number if present
          case Regex.run(~r/PwrCon(\d+)/, descr) do
            [_, num] -> "PwrCon#{num}"
            _ -> "Power Connectors"
          end

        # Platform chipset sensors (Trident, Jericho, FM6000, etc.)
        String.match?(descr, ~r/^(Trident|Jericho|FM\d+|Arad|Sand)/i) ->
          "Platform"

        # Power supply sensors
        String.match?(descr, ~r/^(Power|PSU)/i) ->
          "PSUs"

        # Everything else
        true ->
          "System"
      end

    Map.put(sensor, :sensor_group, group)
  end

  def apply_smart_grouping(sensor), do: sensor

  @doc """
  Cleans up sensor descriptions by removing redundant text.

  Examples:
  - "Temp Sensor 1 sensor" → "Temp Sensor 1"
  - "DOM Rx Power sensor" → "DOM Rx Power"
  - "PowerSupply1/1 Sensor VoltageSensor1" → "PowerSupply1 Voltage"
  """
  @spec cleanup_description(map()) :: map()
  def cleanup_description(%{sensor_descr: descr} = sensor) when is_binary(descr) do
    cleaned =
      descr
      # Remove trailing " sensor"
      |> String.replace(~r/\s+sensor$/i, "")
      # Remove duplicate "Sensor" strings
      |> String.replace(~r/Sensor\s+(\w+)Sensor/i, "\\1")
      # Simplify PSU naming: "PowerSupply1/1 Sensor VoltageSensor1" → "PowerSupply1 Voltage"
      |> String.replace(~r/PowerSupply(\d+)\/\d+\s+Sensor\s+(\w+)Sensor\d+/i, "PowerSupply\\1 \\2")
      # Remove "hotspot" redundancy
      |> String.replace(~r/\s*hotspot\s*/i, " ")
      # Clean up whitespace
      |> String.replace(~r/\s+/, " ")
      |> String.trim()

    Map.put(sensor, :sensor_descr, cleaned)
  end

  def cleanup_description(sensor), do: sensor
end
