defmodule Towerops.Monitoring.Executors.SnmpSensorExecutor do
  @moduledoc """
  Executes SNMP sensor checks (temperature, voltage, power, fan speed, etc.).

  Polls a sensor's OID and returns the current value with status determination.
  """

  alias Towerops.Devices
  alias Towerops.Repo
  alias Towerops.Snmp
  alias Towerops.Snmp.Client

  require Logger

  @doc """
  Executes an SNMP sensor check.

  Takes a check struct with:
  - source_id: UUID of the sensor to poll
  - device_id: UUID of the device
  - config: Map with sensor configuration

  Returns:
  - {:ok, %{value:, status:, output:, response_time_ms:}} on success
  - {:error, reason} on failure
  """
  def execute(check) do
    start_time = System.monotonic_time(:millisecond)

    with {:ok, sensor} <- get_sensor(check.source_id),
         {:ok, device} <- get_device_with_snmp(check.device_id),
         {:ok, snmp_opts} <- build_snmp_opts(device),
         {:ok, raw_value} <- poll_sensor(snmp_opts, sensor.sensor_oid) do
      response_time = System.monotonic_time(:millisecond) - start_time

      # Apply divisor to get actual value
      value = raw_value / sensor.sensor_divisor

      # Determine status based on sensor limits (if configured)
      status = determine_status(value, sensor)

      # Format output with sensor description and value
      output = format_output(sensor, value)

      {:ok,
       %{
         value: value,
         status: status,
         output: output,
         response_time_ms: response_time
       }}
    else
      {:error, reason} = error ->
        Logger.warning("SNMP sensor check failed: #{inspect(reason)}")
        error
    end
  rescue
    e ->
      Logger.error("SNMP sensor check exception: #{inspect(e)}")
      {:error, "Exception: #{Exception.message(e)}"}
  end

  defp get_sensor(sensor_id) do
    case Snmp.get_sensor(sensor_id) do
      nil -> {:error, "Sensor not found: #{sensor_id}"}
      sensor -> {:ok, sensor}
    end
  end

  defp get_device_with_snmp(device_id) do
    device = device_id |> Devices.get_device!() |> Repo.preload(:snmp_device)

    if device.snmp_device do
      {:ok, device}
    else
      {:error, "Device #{device_id} does not have SNMP configured"}
    end
  end

  defp build_snmp_opts(device) do
    opts = [
      ip: device.ip_address,
      version: device.snmp_version || "2c",
      port: device.snmp_port || 161,
      timeout: 5000
    ]

    opts = add_snmp_credentials(opts, device)
    {:ok, opts}
  end

  defp add_snmp_credentials(opts, %{snmp_version: "3"} = device) do
    opts
    |> Keyword.put(:security_name, device.snmpv3_username || "")
    |> Keyword.put(:security_level, device.snmpv3_security_level || "noAuthNoPriv")
    |> Keyword.put(:auth_protocol, device.snmpv3_auth_protocol || "MD5")
    |> Keyword.put(:auth_password, device.snmpv3_auth_password || "")
    |> Keyword.put(:priv_protocol, device.snmpv3_priv_protocol || "DES")
    |> Keyword.put(:priv_password, device.snmpv3_priv_password || "")
  end

  defp add_snmp_credentials(opts, device) do
    Keyword.put(opts, :community, device.snmp_community || "public")
  end

  defp poll_sensor(snmp_opts, oid) do
    case Client.get(snmp_opts, oid) do
      {:ok, value} when is_number(value) -> {:ok, value}
      {:ok, value} -> {:error, "Unexpected value type: #{inspect(value)}"}
      {:error, _} = error -> error
    end
  end

  defp determine_status(value, sensor) do
    cond do
      critical_high?(value, sensor) -> 2
      critical_low?(value, sensor) -> 2
      warning_high?(value, sensor) -> 1
      warning_low?(value, sensor) -> 1
      true -> 0
    end
  end

  defp critical_high?(value, sensor) do
    limit = get_in(sensor.metadata, ["limit_high"])
    limit && value >= limit
  end

  defp critical_low?(value, sensor) do
    limit = get_in(sensor.metadata, ["limit_low"])
    limit && value <= limit
  end

  defp warning_high?(value, sensor) do
    limit = get_in(sensor.metadata, ["limit_warn_high"])
    limit && value >= limit
  end

  defp warning_low?(value, sensor) do
    limit = get_in(sensor.metadata, ["limit_warn_low"])
    limit && value <= limit
  end

  defp format_output(sensor, value) do
    formatted_value = format_value(value, sensor.sensor_type, sensor.sensor_unit)
    "#{sensor.sensor_descr}: #{formatted_value}"
  end

  @sensor_formats %{
    "temperature" => {1, "°C"},
    "voltage" => {2, "V"},
    "current" => {2, "A"},
    "power" => {1, "W"},
    "frequency" => {0, "Hz"},
    "humidity" => {1, "%"}
  }

  defp format_value(value, "fanspeed", unit) do
    "#{round(value)}#{unit || " RPM"}"
  end

  defp format_value(value, sensor_type, unit) do
    {precision, default_unit} = Map.get(@sensor_formats, sensor_type, {2, ""})
    "#{Float.round(value, precision)}#{unit || default_unit}"
  end
end
