defmodule SnmpKit.SnmpMgr do
  @moduledoc """
  Lightweight SNMP client library for Elixir.

  This library provides a simple, stateless interface for SNMP operations
  without requiring heavyweight management processes or configurations.

  ## Core Operations

  - `get/3` - Single OID retrieval
  - `get_next/3` - Get next OID in tree
  - `get_bulk/3` - Bulk retrieval (SNMPv2c)
  - `walk/3` - Walk subtree
  - `set/4` - Set OID value
  """

  alias SnmpKit.SnmpLib.OID
  alias SnmpKit.SnmpMgr.Bulk
  alias SnmpKit.SnmpMgr.Config
  alias SnmpKit.SnmpMgr.Core
  alias SnmpKit.SnmpMgr.Format
  alias SnmpKit.SnmpMgr.MIB
  alias SnmpKit.SnmpMgr.Walk

  @type target :: binary() | tuple() | map()
  @type oid :: binary() | list()
  @type opts :: keyword()

  @doc """
  Performs an SNMP GET request.

  ## Parameters
  - `target` - The target device (e.g., "192.168.1.1:161" or "device.local")
  - `oid` - The OID to retrieve (string or list format)
  - `opts` - Options including :community, :timeout, :retries

  ## Examples

      {:ok, value} = SnmpMgr.get("device.local:161", "sysDescr.0", community: "public")
  """
  def get(target, oid, opts \\ []) do
    merged_opts = Config.merge_opts(opts)

    case Core.send_get_request_with_type(target, oid, merged_opts) do
      {:ok, {oid_str, type, value}} ->
        {:ok, Format.enrich_varbind({oid_str, type, value}, merged_opts)}

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

  @doc """
  Performs an SNMP GETNEXT request.

  ## Parameters
  - `target` - The target device
  - `oid` - The starting OID
  - `opts` - Options including :community, :timeout, :retries
  """
  def get_next(target, oid, opts \\ []) do
    merged_opts = Config.merge_opts(opts)

    case Core.send_get_next_request(target, oid, merged_opts) do
      {:ok, {oid_string, type, value}} ->
        {:ok, Format.enrich_varbind({oid_string, type, value}, merged_opts)}

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

  @doc """
  Performs an SNMP SET request.

  ## Parameters
  - `target` - The target device
  - `oid` - The OID to set
  - `value` - The value to set
  - `opts` - Options including :community, :timeout, :retries
  """
  def set(target, oid, value, opts \\ []) do
    merged_opts = Config.merge_opts(opts)
    Core.send_set_request(target, oid, value, merged_opts)
  end

  @doc """
  Performs an asynchronous SNMP GET request.

  Returns immediately with a reference. The caller will receive a message
  with the result.
  """
  def get_async(target, oid, opts \\ []) do
    merged_opts = Config.merge_opts(opts)
    Core.send_get_request_async(target, oid, merged_opts)
  end

  @doc """
  Performs an SNMP GETBULK request (SNMPv2c only).

  GETBULK is more efficient than multiple GETNEXT requests for retrieving
  large amounts of data.

  ## Parameters
  - `target` - The target device
  - `oid` - The starting OID
  - `opts` - Options including :non_repeaters, :max_repetitions, :community, :timeout
  """
  @spec get_bulk(target(), oid(), opts()) :: {:ok, [{list(), atom(), any()}]} | {:error, any()}
  def get_bulk(target, oid, opts \\ []) do
    case Keyword.get(opts, :version) do
      :v1 ->
        {:error, {:unsupported_operation, :get_bulk_requires_v2c}}

      :v3 ->
        {:error, {:unsupported_operation, :get_bulk_requires_v2c}}

      _ ->
        merged_opts =
          opts
          |> Keyword.put(:version, :v2c)
          |> then(&Config.merge_opts/1)

        case Core.send_get_bulk_request(target, oid, merged_opts) do
          {:ok, results} -> {:ok, Format.enrich_varbinds(results, merged_opts)}
          {:error, reason} -> {:error, reason}
        end
    end
  end

  @doc """
  Performs an asynchronous SNMP GETBULK request.

  Returns immediately with a reference. The caller will receive a message
  with the result.
  """
  def get_bulk_async(target, oid, opts \\ []) do
    case Keyword.get(opts, :version) do
      :v1 ->
        {:error, {:unsupported_operation, :get_bulk_requires_v2c}}

      :v3 ->
        {:error, {:unsupported_operation, :get_bulk_requires_v2c}}

      _ ->
        merged_opts =
          opts
          |> Keyword.put(:version, :v2c)
          |> then(&Config.merge_opts/1)

        Core.send_get_bulk_request_async(target, oid, merged_opts)
    end
  end

  @doc """
  Performs an SNMP walk operation using iterative GETNEXT requests.

  Walks the SNMP tree starting from the given OID and returns all OID/value
  pairs found under that subtree.

  ## Parameters
  - `target` - The target device
  - `root_oid` - The starting OID for the walk
  - `opts` - Options including :community, :timeout, :max_repetitions
  """
  @spec walk(target(), oid(), opts()) :: {:ok, [{list(), atom(), any()}]} | {:error, any()}
  def walk(target, root_oid, opts \\ []) do
    merged_opts = Config.merge_opts(opts)

    case Walk.walk(target, root_oid, merged_opts) do
      {:ok, results} -> {:ok, Format.enrich_varbinds(results, merged_opts)}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Walks an SNMP table and returns all entries.

  ## Parameters
  - `target` - The target device
  - `table_oid` - The table OID to walk
  - `opts` - Options including :community, :timeout
  """
  @spec walk_table(target(), oid(), opts()) :: {:ok, [{list(), atom(), any()}]} | {:error, any()}
  def walk_table(target, table_oid, opts \\ []) do
    merged_opts = Config.merge_opts(opts)

    case Walk.walk_table(target, table_oid, merged_opts) do
      {:ok, results} -> {:ok, Format.enrich_varbinds(results, merged_opts)}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Gets a specific column from an SNMP table.

  ## Parameters
  - `target` - The target device
  - `table_oid` - The table OID
  - `column` - The column number or name
  - `opts` - Options including :community, :timeout
  """
  def get_column(target, table_oid, column, opts \\ []) do
    with {:ok, resolved_table_oid} <- resolve_oid_if_needed(table_oid),
         {:ok, column_oid} <- build_column_oid(resolved_table_oid, column) do
      walk(target, column_oid, opts)
    end
  end

  defp build_column_oid(table_oid, column) when is_integer(column) do
    {:ok, table_oid ++ [1, column]}
  end

  defp build_column_oid(_table_oid, column) do
    case MIB.resolve(column) do
      {:ok, oid} -> {:ok, oid}
      error -> error
    end
  end

  @doc """
  Performs an SNMP GET operation and returns a formatted value.
  """
  @spec get_pretty(target(), oid(), opts()) :: {:ok, String.t()} | {:error, any()}
  def get_pretty(target, oid, opts \\ []) do
    merged_opts =
      opts
      |> Keyword.put(:include_formatted, true)
      |> Config.merge_opts()

    case get(target, oid, merged_opts) do
      {:ok, enriched} -> {:ok, enriched}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Performs an SNMP WALK operation and returns formatted results.
  """
  @spec walk_pretty(target(), oid(), opts()) ::
          {:ok, [{String.t(), String.t()}]} | {:error, any()}
  def walk_pretty(target, oid, opts \\ []) do
    merged_opts =
      opts
      |> Keyword.put(:include_formatted, true)
      |> Config.merge_opts()

    case Walk.walk(target, oid, merged_opts) do
      {:ok, results} -> {:ok, Format.enrich_varbinds(results, merged_opts)}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Performs an SNMP BULK operation and returns formatted results.
  """
  @spec bulk_pretty(target(), oid(), opts()) ::
          {:ok, [{String.t(), String.t()}]} | {:error, any()}
  def bulk_pretty(target, oid, opts \\ []) do
    merged_opts =
      opts
      |> Keyword.put(:include_formatted, true)
      |> Config.merge_opts()

    case Bulk.get_bulk(target, oid, merged_opts) do
      {:ok, results} -> {:ok, Format.enrich_varbinds(results, merged_opts)}
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Performs an SNMP BULK WALK operation.

  Uses iterative GETBULK requests to efficiently walk the subtree.
  """
  @spec bulk_walk(target(), oid(), opts()) ::
          {:ok, [{String.t(), atom(), any()}]} | {:error, any()}
  def bulk_walk(target, oid, opts \\ []) do
    merged_opts =
      opts
      |> Keyword.put(:version, :v2c)
      |> Config.merge_opts()

    case Bulk.walk_bulk(target, oid, merged_opts) do
      {:ok, results} ->
        {:ok, results}

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

  @doc """
  Performs an SNMP BULK WALK operation and returns formatted results.
  """
  @spec bulk_walk_pretty(target(), oid(), opts()) ::
          {:ok, [{String.t(), atom(), String.t()}]} | {:error, any()}
  def bulk_walk_pretty(target, oid, opts \\ []) do
    merged_opts =
      opts
      |> Keyword.put(:include_formatted, true)
      |> Config.merge_opts()

    case bulk_walk(target, oid, merged_opts) do
      {:ok, results} -> {:ok, results}
      {:error, reason} -> {:error, reason}
    end
  end

  # Private helper function
  defp resolve_oid_if_needed(oid) when is_binary(oid) do
    case OID.string_to_list(oid) do
      {:ok, oid_list} ->
        {:ok, oid_list}

      {:error, _} ->
        # Try resolving as symbolic name
        MIB.resolve(oid)
    end
  end

  defp resolve_oid_if_needed(oid) when is_list(oid), do: {:ok, oid}
  defp resolve_oid_if_needed(_), do: {:error, :invalid_oid_format}
end
