defmodule Towerops.Snmp.Client do
  @moduledoc """
  SNMP client wrapper around snmpkit for device communication.
  Provides clean interface for SNMP get, walk, and get_bulk operations.
  """

  require Logger

  @type connection_opts :: [
          ip: String.t(),
          community: String.t(),
          version: String.t(),
          port: non_neg_integer(),
          timeout: non_neg_integer()
        ]

  @type oid :: String.t() | [non_neg_integer()]
  @type snmp_value :: term()
  @type snmp_result :: {:ok, snmp_value()} | {:error, term()}

  @default_timeout 5000
  @default_retries 2

  @doc """
  Performs an SNMP GET operation for a single OID.

  ## Examples

      iex> get([ip: "192.168.1.1", community: "public", version: "2c"], "1.3.6.1.2.1.1.1.0")
      {:ok, "Cisco IOS Software..."}

      iex> get([ip: "192.168.1.1", community: "public", version: "2c"], [1, 3, 6, 1, 2, 1, 1, 1, 0])
      {:ok, "Cisco IOS Software..."}
  """
  @spec get(connection_opts(), oid()) :: snmp_result()
  def get(opts, oid) do
    target = build_target(opts)
    snmp_opts = build_snmp_opts(opts)

    case snmp_adapter().get(target, oid, snmp_opts) do
      {:ok, value} ->
        {:ok, extract_snmp_value(value)}

      {:error, reason} = error ->
        # Only log warnings for actual communication errors, not missing OIDs
        case reason do
          :no_such_object ->
            Logger.debug("SNMP object not found for #{target}: #{inspect(oid)}")

          :no_such_instance ->
            Logger.debug("SNMP instance not found for #{target}: #{inspect(oid)}")

          :end_of_mib_view ->
            Logger.debug("End of MIB view reached for #{target}: #{inspect(oid)}")

          _ ->
            # Actual errors like timeout, network issues, etc.
            timeout = Keyword.get(opts, :timeout, @default_timeout)
            version = Keyword.fetch!(opts, :version)

            Logger.warning(
              "SNMP GET failed for #{target} (v#{version}, timeout: #{timeout}ms) OID #{inspect(oid)}: #{inspect(reason)}"
            )
        end

        error
    end
  end

  @doc """
  Performs an SNMP GET operation for multiple OIDs.

  ## Examples

      iex> get_multiple([ip: "192.168.1.1", community: "public", version: "2c"],
      ...>   ["1.3.6.1.2.1.1.1.0", "1.3.6.1.2.1.1.3.0"])
      {:ok, ["Cisco IOS Software...", 12345]}
  """
  @spec get_multiple(connection_opts(), [oid()]) :: {:ok, [snmp_value()]} | {:error, term()}
  def get_multiple(opts, oids) when is_list(oids) do
    target = build_target(opts)
    snmp_opts = build_snmp_opts(opts)

    # Get each OID and collect results
    results =
      Enum.map(oids, fn oid ->
        case snmp_adapter().get(target, oid, snmp_opts) do
          {:ok, value} -> {:ok, extract_snmp_value(value)}
          error -> error
        end
      end)

    # Check if any failed
    if Enum.any?(results, fn result -> match?({:error, _}, result) end) do
      {:error, :partial_failure}
    else
      values = Enum.map(results, fn {:ok, value} -> value end)
      {:ok, values}
    end
  end

  @doc """
  Performs an SNMP WALK operation starting from the given OID.
  Returns all OIDs and values under the specified subtree.

  ## Examples

      iex> walk([ip: "192.168.1.1", community: "public", version: "2c"], "1.3.6.1.2.1.2.2.1.2")
      {:ok, %{
        "1.3.6.1.2.1.2.2.1.2.1" => "Ethernet0",
        "1.3.6.1.2.1.2.2.1.2.2" => "Ethernet1"
      }}
  """
  @spec walk(connection_opts(), oid()) :: {:ok, %{String.t() => snmp_value()}} | {:error, term()}
  def walk(opts, start_oid) do
    target = build_target(opts)
    snmp_opts = build_snmp_opts(opts)

    case snmp_adapter().walk(target, start_oid, snmp_opts) do
      {:ok, results} when is_list(results) ->
        # snmpkit returns a list of maps, convert to OID -> value map
        walked_data =
          Map.new(results, fn %{oid: oid, value: value} -> {oid, extract_snmp_value(value)} end)

        {:ok, walked_data}

      {:error, reason} = error ->
        # Only log warnings for actual communication errors
        case reason do
          :no_such_object ->
            Logger.debug("SNMP WALK: object not found for #{target} at #{inspect(start_oid)}")

          :no_such_instance ->
            Logger.debug("SNMP WALK: instance not found for #{target} at #{inspect(start_oid)}")

          :end_of_mib_view ->
            Logger.debug("SNMP WALK: end of MIB view for #{target} at #{inspect(start_oid)}")

          _ ->
            timeout = Keyword.get(opts, :timeout, @default_timeout)
            version = Keyword.fetch!(opts, :version)

            Logger.warning(
              "SNMP WALK failed for #{target} (v#{version}, timeout: #{timeout}ms) OID #{inspect(start_oid)}: #{inspect(reason)}"
            )
        end

        error
    end
  end

  @doc """
  Performs an SNMP GET-BULK operation for efficient retrieval of multiple values.
  Useful for retrieving table data.

  ## Examples

      iex> get_bulk([ip: "192.168.1.1", community: "public", version: "2c"],
      ...>   "1.3.6.1.2.1.2.2.1.2", max_repetitions: 10)
      {:ok, %{"1.3.6.1.2.1.2.2.1.2.1" => "Ethernet0", ...}}
  """
  @spec get_bulk(connection_opts(), oid(), keyword()) ::
          {:ok, %{String.t() => snmp_value()}} | {:error, term()}
  def get_bulk(opts, start_oid, bulk_opts \\ []) do
    target = build_target(opts)
    snmp_opts = build_snmp_opts(opts)
    max_repetitions = Keyword.get(bulk_opts, :max_repetitions, 10)

    snmp_opts = Keyword.put(snmp_opts, :max_repetitions, max_repetitions)

    case snmp_adapter().get_bulk(target, start_oid, snmp_opts) do
      {:ok, results} when is_list(results) ->
        # snmpkit returns a list of maps, convert to OID -> value map
        bulk_data =
          Map.new(results, fn %{oid: oid, value: value} -> {oid, extract_snmp_value(value)} end)

        {:ok, bulk_data}

      {:error, reason} = error ->
        # Only log warnings for actual communication errors
        case reason do
          :no_such_object ->
            Logger.debug("SNMP GET-BULK: object not found for #{target} at #{inspect(start_oid)}")

          :no_such_instance ->
            Logger.debug("SNMP GET-BULK: instance not found for #{target} at #{inspect(start_oid)}")

          :end_of_mib_view ->
            Logger.debug("SNMP GET-BULK: end of MIB view for #{target} at #{inspect(start_oid)}")

          _ ->
            timeout = Keyword.get(opts, :timeout, @default_timeout)
            version = Keyword.fetch!(opts, :version)

            Logger.warning(
              "SNMP GET-BULK failed for #{target} (v#{version}, timeout: #{timeout}ms) OID #{inspect(start_oid)}: #{inspect(reason)}"
            )
        end

        error
    end
  end

  @doc """
  Tests connectivity to an SNMP agent by retrieving sysUpTime.

  ## Examples

      iex> test_connection([ip: "192.168.1.1", community: "public", version: "2c"])
      {:ok, "Connection successful"}

      iex> test_connection([ip: "192.168.1.99", community: "wrong", version: "2c"])
      {:error, :timeout}
  """
  @spec test_connection(connection_opts()) :: {:ok, String.t()} | {:error, term()}
  def test_connection(opts) do
    # Try to get sysUpTime (1.3.6.1.2.1.1.3.0) as a connectivity test
    case get(opts, "1.3.6.1.2.1.1.3.0") do
      {:ok, _uptime} ->
        {:ok, "Connection successful"}

      {:error, reason} = error ->
        Logger.warning("SNMP connection test failed: #{inspect(reason)}")
        error
    end
  end

  # Private functions

  defp snmp_adapter do
    Application.get_env(:towerops, :snmp_adapter, SnmpKit)
  end

  defp build_target(opts) do
    Keyword.fetch!(opts, :ip)
  end

  defp build_snmp_opts(opts) do
    version = parse_version(Keyword.fetch!(opts, :version))
    community = Keyword.fetch!(opts, :community)
    port = Keyword.get(opts, :port, 161)
    timeout = Keyword.get(opts, :timeout, @default_timeout)

    [
      community: community,
      version: version,
      port: port,
      timeout: timeout,
      retries: @default_retries
    ]
  end

  defp parse_version("1"), do: :v1
  defp parse_version("2c"), do: :v2c
  defp parse_version(:v1), do: :v1
  defp parse_version(:v2c), do: :v2c
  defp parse_version(_other), do: :v2c

  # Extract value from snmpkit's typed responses
  defp extract_snmp_value({:octet_string, value}), do: value
  defp extract_snmp_value({:integer, value}), do: value
  defp extract_snmp_value({:counter32, value}), do: value
  defp extract_snmp_value({:counter64, value}), do: value
  defp extract_snmp_value({:gauge32, value}), do: value
  defp extract_snmp_value({:timeticks, value}), do: value
  defp extract_snmp_value({:ip_address, value}), do: value
  defp extract_snmp_value({:object_identifier, value}), do: value
  defp extract_snmp_value({:opaque, value}), do: value
  # Handle map responses from snmpkit (with :value key)
  defp extract_snmp_value(%{value: value}), do: value
  # Fallback for unknown types
  defp extract_snmp_value(value), do: value
end
