defmodule Towerops.Monitoring.Executors.SnmpStorageExecutor do
  @moduledoc """
  Executes SNMP storage/disk checks.

  Polls storage usage from HOST-RESOURCES-MIB hrStorageTable:
  - Fixed disks (filesystem usage)
  - RAM and virtual memory
  - Network storage
  """

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

  require Logger

  # HOST-RESOURCES-MIB hrStorageTable OIDs
  @hr_storage_allocation_units_oid "1.3.6.1.2.1.25.2.3.1.4"
  @hr_storage_size_oid "1.3.6.1.2.1.25.2.3.1.5"
  @hr_storage_used_oid "1.3.6.1.2.1.25.2.3.1.6"

  @doc """
  Executes an SNMP storage check.

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

      # Calculate usage percentage
      usage_percent = calculate_usage_percent(storage_data)

      # Determine status based on usage thresholds
      status = determine_status(usage_percent)

      # Format output with storage description and usage
      output = format_output(storage, usage_percent, storage_data)

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

  defp get_storage(storage_id) do
    case Snmp.get_storage(storage_id) do
      nil -> {:error, "Storage not found: #{storage_id}"}
      storage -> {:ok, storage}
    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_storage_usage(snmp_opts, storage_index) do
    # Poll allocation units, size, and used from hrStorageTable
    alloc_oid = "#{@hr_storage_allocation_units_oid}.#{storage_index}"
    size_oid = "#{@hr_storage_size_oid}.#{storage_index}"
    used_oid = "#{@hr_storage_used_oid}.#{storage_index}"

    case Client.get_multiple(snmp_opts, [alloc_oid, size_oid, used_oid]) do
      {:ok, [alloc_units, size_units, used_units]} ->
        {:ok,
         %{
           allocation_units: ensure_integer(alloc_units),
           size_units: ensure_integer(size_units),
           used_units: ensure_integer(used_units)
         }}

      {:error, _} = error ->
        error
    end
  end

  defp ensure_integer(value) when is_integer(value), do: value
  defp ensure_integer(value) when is_float(value), do: round(value)
  defp ensure_integer(_), do: 0

  defp calculate_usage_percent(%{size_units: 0}), do: 0.0

  defp calculate_usage_percent(%{size_units: size, used_units: used}) do
    used / size * 100.0
  end

  defp determine_status(usage_percent) do
    cond do
      # CRITICAL
      usage_percent >= 95 -> 2
      # WARNING
      usage_percent >= 85 -> 1
      # OK
      true -> 0
    end
  end

  defp format_output(storage, usage_percent, storage_data) do
    description = storage.description || storage.device_name || "Storage #{storage.storage_index}"
    total_bytes = storage_data.allocation_units * storage_data.size_units
    used_bytes = storage_data.allocation_units * storage_data.used_units

    "#{description}: #{Float.round(usage_percent, 1)}% used (#{format_bytes(used_bytes)} / #{format_bytes(total_bytes)})"
  end

  defp format_bytes(bytes) when bytes >= 1_099_511_627_776 do
    "#{Float.round(bytes / 1_099_511_627_776, 2)} TB"
  end

  defp format_bytes(bytes) when bytes >= 1_073_741_824 do
    "#{Float.round(bytes / 1_073_741_824, 2)} GB"
  end

  defp format_bytes(bytes) when bytes >= 1_048_576 do
    "#{Float.round(bytes / 1_048_576, 2)} MB"
  end

  defp format_bytes(bytes) when bytes >= 1024 do
    "#{Float.round(bytes / 1024, 2)} KB"
  end

  defp format_bytes(bytes), do: "#{bytes} B"
end
