defmodule Towerops.Monitoring.Executors.SnmpInterfaceExecutor do
  @moduledoc """
  Executes SNMP interface checks (operational status, bandwidth utilization).

  Polls interface status and traffic counters from IF-MIB.
  """

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

  require Logger

  # IF-MIB OIDs
  # ifOperStatus
  @if_oper_status_oid "1.3.6.1.2.1.2.2.1.8"

  @doc """
  Executes an SNMP interface check.

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

      # Map operational status to check status
      status = determine_status(oper_status, interface)

      # Format output with interface name and status
      output = format_output(interface, oper_status)

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

  defp get_interface(interface_id) do
    case Snmp.get_interface(interface_id) do
      nil -> {:error, "Interface not found: #{interface_id}"}
      interface -> {:ok, interface}
    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_oper_status(snmp_opts, if_index) do
    oid = "#{@if_oper_status_oid}.#{if_index}"

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

  defp determine_status(oper_status, interface) do
    # ifOperStatus values from IF-MIB:
    # 1 = up, 2 = down, 3 = testing, 4 = unknown, 5 = dormant, 6 = notPresent, 7 = lowerLayerDown

    cond do
      # Interface is up - OK
      oper_status == 1 -> 0
      # Interface is down - check if this is expected
      oper_status == 2 && interface.if_admin_status == "down" -> 0
      # Interface is down but should be up - CRITICAL
      oper_status == 2 -> 2
      # Testing, dormant - WARNING
      oper_status in [3, 5] -> 1
      # Unknown, notPresent, lowerLayerDown - CRITICAL
      true -> 2
    end
  end

  defp format_output(interface, oper_status) do
    status_str = oper_status_to_string(oper_status)
    interface_name = interface.if_alias || interface.if_name || interface.if_descr || "Interface #{interface.if_index}"
    "#{interface_name}: #{status_str}"
  end

  defp oper_status_to_string(1), do: "Up"
  defp oper_status_to_string(2), do: "Down"
  defp oper_status_to_string(3), do: "Testing"
  defp oper_status_to_string(4), do: "Unknown"
  defp oper_status_to_string(5), do: "Dormant"
  defp oper_status_to_string(6), do: "Not Present"
  defp oper_status_to_string(7), do: "Lower Layer Down"
  defp oper_status_to_string(_), do: "Unknown Status"
end
