defmodule Towerops.Snmp.DeferredDiscovery do
  @moduledoc """
  Handles deferred/slow SNMP discovery checks with timeouts.

  Based on LibreNMS's approach of separating fast checks (string matching on sysDescr/sysObjectID)
  from slow checks (snmpget/snmpwalk for detailed data).

  Fast checks run immediately and are required for basic discovery.
  Slow checks (sensors, detailed interfaces) are deferred and can timeout gracefully
  without failing the entire discovery.

  ## Example

      # Run discovery with deferred checks
      {:ok, results} = DeferredDiscovery.discover_with_deferred(device, [
        fast: [:system_info, :profile_selection],
        slow: [:sensors, :interfaces, :neighbors]
      ])

      # Results include which deferred checks succeeded or timed out
      %{
        system_info: %{...},
        sensors: {:ok, [...]},      # or {:timeout, []} if timed out
        interfaces: {:ok, [...]},
        deferred_status: %{sensors: :ok, interfaces: :ok, neighbors: :timeout}
      }
  """

  alias Towerops.Snmp.Client

  require Logger

  @default_fast_timeout 10_000
  @default_slow_timeout 30_000
  @default_deferred_timeout 60_000

  @type check_type :: :fast | :slow | :deferred
  @type check_result :: {:ok, term()} | {:error, term()} | {:timeout, term()}

  @doc """
  Runs a fast SNMP check with a short timeout.
  Fast checks are for quick string/regex matching operations.
  If they fail, discovery should abort.
  """
  @spec fast_check(Client.connection_opts(), (-> term()), keyword()) :: check_result()
  def fast_check(client_opts, check_fn, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, @default_fast_timeout)
    run_with_timeout(client_opts, check_fn, timeout, :fast)
  end

  @doc """
  Runs a slow SNMP check with a longer timeout.
  Slow checks are for operations that may require multiple SNMP walks.
  If they timeout, discovery can continue with partial data.
  """
  @spec slow_check(Client.connection_opts(), (-> term()), keyword()) :: check_result()
  def slow_check(_client_opts, check_fn, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, @default_slow_timeout)
    default_value = Keyword.get(opts, :default, [])

    task = Task.async(fn -> check_fn.() end)

    case Task.yield(task, timeout) || Task.shutdown(task, :brutal_kill) do
      {:ok, result} ->
        result

      nil ->
        Logger.warning("Slow check timed out after #{timeout}ms, using default value")
        {:ok, default_value}
    end
  end

  @doc """
  Runs a deferred SNMP check that can be retried later.
  Deferred checks are for non-critical data that's nice to have.
  If they fail or timeout, discovery succeeds with a note about what's missing.
  """
  @spec deferred_check(Client.connection_opts(), (-> term()), keyword()) :: check_result()
  def deferred_check(_client_opts, check_fn, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, @default_deferred_timeout)
    default_value = Keyword.get(opts, :default, [])
    check_name = Keyword.get(opts, :name, "unknown")

    task = Task.async(fn -> check_fn.() end)

    case Task.yield(task, timeout) || Task.shutdown(task, :brutal_kill) do
      {:ok, {:ok, result}} ->
        {:ok, result}

      {:ok, {:error, reason}} ->
        Logger.debug("Deferred check '#{check_name}' failed: #{inspect(reason)}")
        {:deferred, default_value, reason}

      nil ->
        Logger.info("Deferred check '#{check_name}' timed out after #{timeout}ms")
        {:timeout, default_value}
    end
  end

  @doc """
  Runs multiple checks in parallel with individual timeouts.
  Returns results for all checks, with status for each.
  """
  @spec parallel_checks([{atom(), (-> term()), keyword()}]) :: %{atom() => check_result()}
  def parallel_checks(checks) do
    checks
    |> Enum.map(fn {name, check_fn, opts} ->
      timeout = Keyword.get(opts, :timeout, @default_slow_timeout)
      default = Keyword.get(opts, :default, [])

      task =
        Task.async(fn ->
          try do
            case check_fn.() do
              {:ok, result} -> {:ok, result}
              {:error, reason} -> {:error, reason}
              other -> {:ok, other}
            end
          rescue
            e -> {:error, Exception.message(e)}
          end
        end)

      {name, task, timeout, default}
    end)
    |> Map.new(fn {name, task, timeout, default} ->
      result =
        case Task.yield(task, timeout) || Task.shutdown(task, :brutal_kill) do
          {:ok, {:ok, value}} -> {:ok, value}
          {:ok, {:error, reason}} -> {:error, reason}
          nil -> {:timeout, default}
        end

      {name, result}
    end)
  end

  @doc """
  Tests if a device is responsive with a quick ping-like SNMP check.
  Uses sysObjectID which all SNMP devices must support.
  """
  @spec device_responsive?(Client.connection_opts(), keyword()) :: boolean()
  def device_responsive?(client_opts, opts \\ []) do
    timeout = Keyword.get(opts, :timeout, 5_000)

    task =
      Task.async(fn ->
        Client.get(client_opts, "1.3.6.1.2.1.1.2.0")
      end)

    case Task.yield(task, timeout) || Task.shutdown(task, :brutal_kill) do
      {:ok, {:ok, _}} -> true
      _ -> false
    end
  end

  @doc """
  Categorizes a device as fast or slow based on initial response time.
  Returns :fast, :slow, or :unresponsive.

  Options:
    * `:fast_threshold` - milliseconds below which device is considered fast (default: 1000)
  """
  @spec categorize_device_speed(Client.connection_opts(), keyword()) ::
          :fast | :slow | :unresponsive
  def categorize_device_speed(client_opts, opts \\ []) do
    fast_threshold = Keyword.get(opts, :fast_threshold, 1_000)
    start_time = System.monotonic_time(:millisecond)

    case Client.get(client_opts, "1.3.6.1.2.1.1.1.0") do
      {:ok, _} ->
        elapsed = System.monotonic_time(:millisecond) - start_time

        if elapsed < fast_threshold, do: :fast, else: :slow

      {:error, reason} ->
        # Log detailed diagnostics to help troubleshoot connectivity issues
        ip = Keyword.get(client_opts, :ip, "unknown")
        community = Keyword.get(client_opts, :community, "unknown")
        version = Keyword.get(client_opts, :version, "unknown")
        port = Keyword.get(client_opts, :port, 161)
        adapter = Keyword.get(client_opts, :adapter, "Real SNMP")

        Logger.warning(
          "Device #{ip} categorized as unresponsive - SNMP GET failed",
          ip: ip,
          port: port,
          version: version,
          community: String.slice(community, 0..3) <> "***",
          adapter: inspect(adapter),
          error: inspect(reason)
        )

        :unresponsive
    end
  end

  @doc """
  Returns recommended timeouts based on device speed category.
  """
  @spec timeouts_for_speed(:fast | :slow | :unresponsive) :: keyword()
  def timeouts_for_speed(:fast) do
    [
      fast: 5_000,
      slow: 15_000,
      deferred: 30_000
    ]
  end

  def timeouts_for_speed(:slow) do
    [
      fast: 15_000,
      slow: 45_000,
      deferred: 90_000
    ]
  end

  def timeouts_for_speed(:unresponsive) do
    [
      fast: 30_000,
      slow: 60_000,
      deferred: 120_000
    ]
  end

  # Private functions

  defp run_with_timeout(_client_opts, check_fn, timeout, check_type) do
    task = Task.async(fn -> check_fn.() end)

    case Task.yield(task, timeout) || Task.shutdown(task, :brutal_kill) do
      {:ok, result} ->
        result

      nil ->
        Logger.warning("#{check_type} check timed out after #{timeout}ms")
        {:error, :timeout}
    end
  end
end
