defmodule Towerops.Snmp.Sanitizer do
  @moduledoc """
  Sanitizes SNMP data to ensure it's safe to store in the database.

  SNMP devices may return:
  - Non-UTF8 binary data
  - Non-printable characters
  - Malformed strings
  - Raw binary values (MAC addresses, IP addresses)

  This module provides sanitization functions to convert these to safe,
  printable strings suitable for PostgreSQL text/varchar columns.
  """

  @doc """
  Sanitizes a string field from SNMP data.

  - Returns nil for nil input
  - Returns printable strings as-is (trimmed)
  - Converts non-printable binaries to colon-separated hex format
  - Converts other values to strings

  ## Examples

      iex> Sanitizer.sanitize_string(nil)
      nil

      iex> Sanitizer.sanitize_string("Hello World")
      "Hello World"

      iex> Sanitizer.sanitize_string(<<255, 255, 255, 0>>)
      "ff:ff:ff:00"

  """
  @spec sanitize_string(any()) :: String.t() | nil
  def sanitize_string(nil), do: nil

  def sanitize_string(value) when is_binary(value) do
    if String.valid?(value) and String.printable?(value) do
      String.trim(value)
    else
      # Convert non-printable binary to hex string
      value
      |> :binary.bin_to_list()
      |> Enum.map_join(":", &String.pad_leading(Integer.to_string(&1, 16), 2, "0"))
      |> String.downcase()
    end
  end

  def sanitize_string(value) when is_list(value) do
    # Erlang charlists from SNMP
    value
    |> List.to_string()
    |> sanitize_string()
  rescue
    _ -> inspect(value)
  end

  def sanitize_string(value), do: to_string(value)

  @doc """
  Sanitizes an IPv4 address from various SNMP formats.

  Handles:
  - Tuple format: {a, b, c, d}
  - Raw 4-byte binary: <<a, b, c, d>>
  - String format: "a.b.c.d"
  - Longer binary (takes first 4 bytes as IP)

  Returns dotted-decimal string or nil if invalid.
  """
  @spec sanitize_ipv4(any()) :: String.t() | nil
  def sanitize_ipv4({a, b, c, d}) when is_integer(a) and is_integer(b) and is_integer(c) and is_integer(d) do
    "#{a}.#{b}.#{c}.#{d}"
  end

  def sanitize_ipv4(<<a, b, c, d>>) do
    "#{a}.#{b}.#{c}.#{d}"
  end

  def sanitize_ipv4(value) when is_binary(value) do
    cond do
      # Already formatted string - valid IP
      String.printable?(value) and String.match?(value, ~r/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) ->
        value

      # Printable string that doesn't match IP format - invalid
      String.printable?(value) ->
        nil

      # Raw bytes - try to convert (only for non-printable binary data)
      byte_size(value) >= 4 ->
        <<a, b, c, d, _rest::binary>> = value
        "#{a}.#{b}.#{c}.#{d}"

      true ->
        nil
    end
  end

  def sanitize_ipv4(_), do: nil

  @doc """
  Sanitizes an IPv6 address from various SNMP formats.

  Handles:
  - Raw 16-byte binary
  - String format (already formatted)
  - Tuple format (8 16-bit values)

  Returns formatted IPv6 string or nil if invalid.
  """
  @spec sanitize_ipv6(any()) :: String.t() | nil
  def sanitize_ipv6(value) when is_binary(value) and byte_size(value) == 16 do
    # Raw 16-byte IPv6 address
    bytes = :binary.bin_to_list(value)

    bytes
    |> Enum.chunk_every(2)
    |> Enum.map_join(":", fn [high, low] ->
      (high * 256 + low)
      |> Integer.to_string(16)
      |> String.downcase()
    end)
  end

  def sanitize_ipv6(value) when is_binary(value) do
    # Already formatted - validate and return
    if String.printable?(value) and String.contains?(value, ":") do
      String.trim(value)
    end
  end

  def sanitize_ipv6(_), do: nil

  @doc """
  Sanitizes a MAC address from various SNMP formats.

  Handles:
  - Raw 6-byte binary
  - Colon-separated hex string
  - Dash-separated hex string

  Returns colon-separated lowercase hex format or nil if invalid.
  """
  @spec sanitize_mac(any()) :: String.t() | nil
  def sanitize_mac(value) when is_binary(value) and byte_size(value) == 6 do
    value
    |> :binary.bin_to_list()
    |> Enum.map_join(":", &String.pad_leading(Integer.to_string(&1, 16), 2, "0"))
    |> String.downcase()
  end

  def sanitize_mac(value) when is_binary(value) do
    # Already formatted - validate pattern
    cleaned = String.replace(value, "-", ":")

    if String.match?(cleaned, ~r/^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$/) do
      String.downcase(cleaned)
    end
  end

  def sanitize_mac(_), do: nil

  @doc """
  Sanitizes all string fields in a map recursively.

  Applies sanitize_string/1 to all string values in the map.
  """
  @spec sanitize_map(map()) :: map()
  def sanitize_map(map) when is_map(map) do
    Map.new(map, fn
      {key, value} when is_binary(value) ->
        {key, sanitize_string(value)}

      {key, value} when is_map(value) ->
        {key, sanitize_map(value)}

      {key, value} ->
        {key, value}
    end)
  end
end
