defmodule Towerops.Monitoring.Executors.SnmpProcessorExecutor do
  @moduledoc """
  Executes SNMP processor/CPU checks.

  Polls CPU load from various MIBs:
  - HOST-RESOURCES-MIB hrProcessorLoad
  - CISCO-PROCESS-MIB cpmCPUTotal
  - UCD-SNMP-MIB system CPU statistics
  """

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

  require Logger

  # OID base paths for different processor types
  @hr_processor_load_oid "1.3.6.1.2.1.25.3.3.1.2"
  @cisco_cpu_5min_oid "1.3.6.1.4.1.9.9.109.1.1.1.1.5"

  @doc """
  Executes an SNMP processor check.

  Takes a check struct with:
  - source_id: UUID of the processor to poll
  - device_id: UUID of the device
  - config: Map with processor 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, processor} <- get_processor(check.source_id),
         {:ok, device} <- get_device_with_snmp(check.device_id),
         {:ok, snmp_opts} <- build_snmp_opts(device),
         {:ok, load_percent} <- poll_processor_load(snmp_opts, processor) do
      response_time = System.monotonic_time(:millisecond) - start_time

      # Determine status based on load thresholds
      status = determine_status(load_percent)

      # Format output with processor description and load
      output = format_output(processor, load_percent)

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

  defp get_processor(processor_id) do
    case Snmp.get_processor(processor_id) do
      nil -> {:error, "Processor not found: #{processor_id}"}
      processor -> {:ok, processor}
    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_processor_load(snmp_opts, processor) do
    oid = build_processor_oid(processor)

    case Client.get(snmp_opts, oid) do
      {:ok, value} when is_integer(value) -> {:ok, value * 1.0}
      {:ok, value} when is_float(value) -> {:ok, value}
      {:ok, value} -> {:error, "Unexpected value type: #{inspect(value)}"}
      {:error, _} = error -> error
    end
  end

  defp build_processor_oid(processor) do
    # Extract numeric index from processor_index string
    # Examples: "1" -> "1", "cisco_1" -> "1", "hr_1" -> "1"
    index = String.replace(processor.processor_index, ~r/[^\d]/, "")

    case processor.processor_type do
      "hr_processor" -> "#{@hr_processor_load_oid}.#{index}"
      "cisco_cpu" -> "#{@cisco_cpu_5min_oid}.#{index}"
      _ -> "#{@hr_processor_load_oid}.#{index}"
    end
  end

  defp determine_status(load_percent) do
    cond do
      # CRITICAL
      load_percent >= 90 -> 2
      # WARNING
      load_percent >= 80 -> 1
      # OK
      true -> 0
    end
  end

  defp format_output(processor, load_percent) do
    description = processor.description || "Processor #{processor.processor_index}"
    "#{description}: #{Float.round(load_percent, 1)}% load"
  end
end
