defmodule Towerops.Workers.PollWorker do
  @moduledoc """
  Background worker for SNMP polling.

  Enqueued when:
  - trigger_poll is called but the PollerWorker GenServer doesn't exist
  - Manual poll trigger from UI

  Queue: polling
  """

  alias Towerops.Devices
  alias Towerops.Snmp
  alias Towerops.Snmp.Client
  alias Towerops.Snmp.NeighborDiscovery

  require Logger

  @doc """
  Performs SNMP polling for a device.

  ## Parameters
  - device_id: The UUID of the device to poll

  ## Returns
  - :ok on success
  - {:error, reason} on failure
  """
  def perform(device_id) do
    Logger.info("Starting SNMP poll for device #{device_id}")

    device = Devices.get_device!(device_id)

    if device.snmp_enabled do
      poll_device(device_id, device)
      :ok
    else
      Logger.debug("Device #{device_id} does not have SNMP enabled, skipping poll")
      :ok
    end
  rescue
    Ecto.NoResultsError ->
      Logger.error("Device #{device_id} not found")
      {:error, :device_not_found}

    error ->
      Logger.error("SNMP poll failed for device #{device_id}: #{inspect(error)}")
      {:error, error}
  end

  defp poll_device(device_id, device) do
    snmp_device = Snmp.get_device_with_associations(device_id)

    if snmp_device do
      perform_device_data_collection(device, snmp_device)
    else
      Logger.debug("No SNMP device found for #{device_id}, skipping poll")
    end
  end

  defp perform_device_data_collection(device, snmp_device) do
    Devices.update_snmp_poll_time(device)

    client_opts = build_client_opts(device)
    now = DateTime.truncate(DateTime.utc_now(), :second)

    poll_sensors(snmp_device.sensors, client_opts, now)
    poll_interfaces(snmp_device.interfaces, client_opts, now)
    poll_neighbors(device, snmp_device, client_opts)

    Logger.info("SNMP poll completed for device #{device.id}")
  end

  defp poll_sensors(sensors, client_opts, timestamp) do
    Enum.each(sensors, fn sensor ->
      case poll_sensor(sensor, client_opts) do
        {:ok, value} ->
          Snmp.create_sensor_reading(%{
            sensor_id: sensor.id,
            value: value,
            status: "ok",
            checked_at: timestamp
          })

          Snmp.update_sensor(sensor, %{
            last_value: value,
            last_checked_at: timestamp
          })

        {:error, _reason} ->
          Snmp.create_sensor_reading(%{
            sensor_id: sensor.id,
            value: nil,
            status: "error",
            checked_at: timestamp
          })
      end
    end)
  end

  defp poll_sensor(sensor, client_opts) do
    case Client.get(client_opts, sensor.sensor_oid) do
      {:ok, raw_value} when is_number(raw_value) ->
        value = raw_value / sensor.sensor_divisor
        {:ok, value}

      {:error, reason} ->
        {:error, reason}

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

  defp poll_interfaces(interfaces, client_opts, timestamp) do
    Enum.each(interfaces, fn interface ->
      {:ok, stat_data} = get_interface_stats(client_opts, interface.if_index)

      stats =
        Map.merge(stat_data, %{
          interface_id: interface.id,
          checked_at: timestamp
        })

      Snmp.create_interface_stat(stats)
    end)
  end

  defp get_interface_stats(client_opts, if_index) do
    oids = [
      if_in_octets: "1.3.6.1.2.1.2.2.1.10.#{if_index}",
      if_out_octets: "1.3.6.1.2.1.2.2.1.16.#{if_index}",
      if_in_errors: "1.3.6.1.2.1.2.2.1.14.#{if_index}",
      if_out_errors: "1.3.6.1.2.1.2.2.1.20.#{if_index}",
      if_in_discards: "1.3.6.1.2.1.2.2.1.13.#{if_index}",
      if_out_discards: "1.3.6.1.2.1.2.2.1.19.#{if_index}"
    ]

    results =
      Enum.map(oids, fn {key, oid} ->
        case Client.get(client_opts, oid) do
          {:ok, value} when is_number(value) -> {key, value}
          _ -> {key, nil}
        end
      end)

    {:ok, Map.new(results)}
  end

  defp poll_neighbors(device, snmp_device, client_opts) do
    interfaces_with_device = Enum.map(snmp_device.interfaces, &Map.put(&1, :device_id, snmp_device.id))

    {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces_with_device)

    cutoff = DateTime.add(DateTime.utc_now(), -5, :minute)
    Snmp.delete_stale_neighbors(device.id, cutoff)

    Enum.each(neighbors, fn neighbor_data ->
      Snmp.upsert_neighbor(neighbor_data)
    end)
  rescue
    error ->
      Logger.error("Error polling neighbors: #{inspect(error)}")
  end

  defp build_client_opts(device) do
    snmp_config = Devices.get_snmp_config(device)

    [
      ip: device.ip_address,
      community: snmp_config.community,
      version: snmp_config.version,
      port: device.snmp_port || 161,
      timeout: 5000
    ]
  end
end
