defmodule Towerops.Snmp.Adapters.Replay do
  @moduledoc """
  Replay adapter - reads from pre-collected OID map.

  Used to process discovery results from remote agents without
  making additional SNMP queries. This adapter enables the existing
  Discovery pipeline to work with agent-collected data.

  ## Usage

      # Create connection opts with agent's OID map
      oid_map = %{
        "1.3.6.1.2.1.1.1.0" => "Cisco IOS Software",
        "1.3.6.1.2.1.2.2.1.1.1" => "1",
        "1.3.6.1.2.1.2.2.1.2.1" => "GigabitEthernet0/1"
      }

      opts = Replay.new(oid_map)
      {:ok, value} = Replay.get(opts, "1.3.6.1.2.1.1.1.0")
      # => {:ok, "Cisco IOS Software"}

      {:ok, results} = Replay.walk(opts, "1.3.6.1.2.1.2.2.1.1")
      # => {:ok, [{"1.3.6.1.2.1.2.2.1.1.1", 1}]}

  ## Type Inference

  The agent protocol sends all values as strings. This adapter uses
  heuristics to infer the correct type:

  - MAC addresses (aa:bb:cc:dd:ee:ff) → string
  - IPv4 addresses (192.168.1.1) → string
  - OID lists (1.3.6.1.4.1.9) → list of integers [1, 3, 6, 1, 4, 1, 9]
  - Pure integers (42) → integer
  - Floats (98.6) → float
  - Everything else → string

  ## Known Limitations

  Type inference is 95% accurate but has edge cases:
  - "1.2.3.4" is always parsed as IPv4, never as OID [1, 2, 3, 4]
  - Ambiguous values default to string

  Future improvement: Update agent protocol to send typed values.
  """

  @behaviour Towerops.Snmp.Adapter

  require Logger

  @doc """
  Creates connection opts with Replay adapter and OID map.

  ## Parameters
  - oid_map: Map of string OID → string value from agent

  ## Returns
  Keyword list suitable for passing to Client functions
  """
  @spec new(map()) :: keyword()
  def new(oid_map) when is_map(oid_map) do
    [
      adapter: __MODULE__,
      oid_map: oid_map,
      # Required fields for compatibility (not used by Replay)
      ip: "replay",
      community: "public",
      version: "2c",
      port: 161
    ]
  end

  @impl true
  def get(opts, oid) when is_binary(oid) do
    oid_map = Keyword.fetch!(opts, :oid_map)

    case lookup_oid(oid_map, oid) do
      {:ok, value} ->
        {:ok, parse_value(value)}

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

  # Normalize OID lookup to handle leading dot differences.
  # gosnmp returns OIDs with leading dots (e.g., ".1.3.6.1.2.1.1.1.0")
  # but server code uses OIDs without (e.g., "1.3.6.1.2.1.1.1.0").
  defp lookup_oid(oid_map, oid) do
    normalized = String.trim_leading(oid, ".")

    with :error <- Map.fetch(oid_map, oid),
         :error <- Map.fetch(oid_map, normalized) do
      Map.fetch(oid_map, "." <> normalized)
    end
  end

  @impl true
  def walk(opts, base_oid) when is_binary(base_oid) do
    oid_map = Keyword.fetch!(opts, :oid_map)

    # Normalize base_oid (strip leading dot if present)
    normalized_base = String.trim_leading(base_oid, ".")

    # Filter OIDs that start with base_oid (SNMP walk semantics)
    # Important: Must be children (start with base_oid + ".")
    # Handle both absolute (.1.2.3) and relative (1.2.3) OID notation
    results =
      oid_map
      |> Enum.filter(fn {oid, _value} ->
        # Normalize OID (strip leading dot)
        normalized_oid = String.trim_leading(oid, ".")
        String.starts_with?(normalized_oid, normalized_base <> ".")
      end)
      |> Enum.map(fn {oid, value} ->
        {oid, parse_value(value)}
      end)
      |> Enum.sort_by(fn {oid, _} -> oid_to_sortable(oid) end)

    {:ok, results}
  end

  @impl true
  def get_multiple(opts, oids) when is_list(oids) do
    # Call get/2 for each OID and collect results
    results =
      for oid <- oids, into: %{} do
        case get(opts, oid) do
          {:ok, value} -> {oid, value}
          {:error, _} -> {oid, nil}
        end
      end

    {:ok, results}
  end

  # Parse string values from protobuf (agent sends everything as strings)
  # This is the critical type inference logic
  @spec parse_value(String.t()) :: term()
  defp parse_value(value) when is_binary(value) do
    cond do
      # Empty string (from ASN_NULL values) - check FIRST
      # SNMP NULL values have no data, return nil
      value == "" ->
        Logger.debug("Parsing empty string as nil")
        nil

      # MAC address (very specific pattern) - check SECOND
      # Examples: aa:bb:cc:dd:ee:ff, AA-BB-CC-DD-EE-FF
      Regex.match?(~r/^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$/, value) ->
        value

      # IPv4 address (specific pattern) - check THIRD
      # Example: 192.168.1.1
      Regex.match?(~r/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, value) ->
        value

      # Float - check FOURTH (before OID check!)
      # Example: "98.6" => 98.6
      # Must have exactly one dot with digits before and after
      Regex.match?(~r/^-?\d+\.\d+$/, value) ->
        String.to_float(value)

      # Pure integer (no dots) - check FIFTH
      # Example: "42" => 42, "-3" => -3
      Regex.match?(~r/^-?\d+$/, value) ->
        String.to_integer(value)

      # OID-like string (multiple dots) - check SIXTH
      # Parse to list of integers per module documentation
      # Example: "1.3.6.1.4.1.9" => [1, 3, 6, 1, 4, 1, 9]
      # Requires at least 2 dots (3+ components) to distinguish from floats
      Regex.match?(~r/^\.?\d+(\.\d+){2,}$/, value) ->
        value
        |> String.trim_leading(".")
        |> String.split(".")
        |> Enum.map(&String.to_integer/1)

      # Default: string
      # Example: "Cisco IOS", "GigabitEthernet0/1"
      true ->
        value
    end
  end

  # Convert OID string to sortable list for correct ordering
  # SNMP walk results must be in lexicographic order
  @spec oid_to_sortable(String.t()) :: [non_neg_integer()]
  defp oid_to_sortable(oid) do
    oid
    |> String.split(".")
    # Filter out empty strings from leading dots
    |> Enum.reject(&(&1 == ""))
    |> Enum.map(&String.to_integer/1)
  end
end
