defmodule Towerops.Profiles.MibCache do
  @moduledoc """
  GenServer that manages a cache of MIB names to numeric OIDs.

  Pre-resolves MIB names from device profiles at startup (asynchronously)
  and provides fast lookup with runtime fallback for cache misses.

  This prevents blocking application startup while still providing fast
  MIB name resolution during profile matching.
  """

  use GenServer

  require Logger

  @table :mib_cache

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc """
  Looks up a MIB name in the cache and returns the numeric OID.

  If the name is not in cache, it will be resolved at runtime and cached
  for future lookups.

  Returns the numeric OID string, or the original input if resolution fails.
  """
  def lookup(mib_name) do
    case :ets.lookup(@table, mib_name) do
      [{^mib_name, oid}] ->
        # Cache hit - return immediately
        oid

      [] ->
        # Cache miss - resolve at runtime and cache result
        resolve_and_cache(mib_name)
    end
  end

  @doc """
  Pre-resolves a list of MIB names asynchronously.

  This is called by YamlProfiles after loading profiles to warm the cache.
  """
  def pre_resolve(mib_names) when is_list(mib_names) do
    GenServer.cast(__MODULE__, {:pre_resolve, mib_names})
  end

  @doc """
  Returns the current cache size (number of cached MIB names).
  """
  def size do
    :ets.info(@table, :size)
  end

  # GenServer callbacks

  @impl true
  def init(_opts) do
    # Create ETS table for MIB cache
    _ = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true])

    Logger.info("MIB cache initialized (empty)")

    {:ok, %{}}
  end

  @impl true
  def handle_cast({:pre_resolve, mib_names}, state) do
    # Pre-resolve MIB names in the background
    # This happens asynchronously and doesn't block callers
    Logger.info("Pre-resolving #{length(mib_names)} unique MIB names from profiles...")

    resolved_count =
      Enum.reduce(mib_names, 0, fn mib_name, acc ->
        case resolve_oid_with_timeout(mib_name) do
          {:ok, oid} when is_binary(oid) ->
            :ets.insert(@table, {mib_name, oid})
            acc + 1

          {:error, _reason} ->
            Logger.warning("Failed to resolve MIB name: #{mib_name}")
            acc
        end
      end)

    Logger.info("Successfully pre-resolved #{resolved_count}/#{length(mib_names)} MIB names")

    {:noreply, state}
  end

  # Private helpers

  defp resolve_and_cache(mib_name) do
    Logger.info("Cache miss for MIB name #{mib_name}, resolving at runtime (fast)")

    case resolve_oid_with_timeout(mib_name) do
      {:ok, oid} when is_binary(oid) ->
        :ets.insert(@table, {mib_name, oid})
        oid

      {:error, _reason} ->
        # Resolution failed or timeout - at minimum, strip module prefix
        # Handle MODULE-NAME::objectName format by returning just objectName
        stripped =
          if String.contains?(mib_name, "::") do
            mib_name |> String.split("::") |> List.last()
          else
            mib_name
          end

        stripped
    end
  end

  defp resolve_oid_with_timeout(mib_name, timeout \\ 5000) do
    # In test environment, skip resolution to avoid hanging
    # Tests should stub ToweropsNative if they need specific behavior
    if Application.get_env(:towerops, :env) == :test do
      {:error, :test_environment}
    else
      # Add timeout to prevent hanging when snmptranslate is unavailable
      task =
        Task.async(fn ->
          ToweropsNative.resolve_oid(mib_name)
        end)

      case Task.yield(task, timeout) || Task.shutdown(task) do
        {:ok, oid} when is_binary(oid) ->
          {:ok, oid}

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

        nil ->
          # Timeout
          Logger.warning("MIB name resolution timeout for #{mib_name}")
          {:error, :timeout}
      end
    end
  end
end
