defmodule SnmpKit.SnmpMgr.Format do
  @moduledoc """
  SNMP data formatting and presentation utilities.

  This module provides user-friendly formatting functions for SNMP data types,
  delegating to the underlying SnmpKit.SnmpLib.Types functions while maintaining a
  clean SnmpMgr API surface.

  All functions work with the 3-tuple format `{oid_string, type, value}` that
  SnmpMgr uses throughout the library.

  Note: As of 1.1, enrichment helpers will also include `oid_list` alongside
  `oid` (string) and guarantee `formatted` is a String.t when requested.

  ## Examples

      # Format uptime from SNMP result
      {:ok, {_oid, :timeticks, ticks}} = SnmpKit.SnmpMgr.get("router.local", "sysUpTime.0")
      SnmpKit.SnmpMgr.Format.uptime(ticks)
      # => "5 days, 12 hours, 34 minutes, 56 seconds"

      # Format IP address
      SnmpKit.SnmpMgr.Format.ip_address(<<192, 168, 1, 1>>)
      # => "192.168.1.1"

      # Pretty print any SNMP result
      {:ok, result} = SnmpKit.SnmpMgr.get("router.local", "sysDescr.0")
      SnmpKit.SnmpMgr.Format.pretty_print(result)
      # => {"1.3.6.1.2.1.1.1.0", :octet_string, "Cisco IOS Router"}
  """

  alias SnmpKit.SnmpLib.OID
  alias SnmpKit.SnmpLib.Types
  alias SnmpKit.SnmpMgr.MIB

  # Delegate core formatting functions to SnmpKit.SnmpLib.Types
  # These have negligible performance overhead (~1-2ns per call)

  @doc """
  Formats timeticks (hundredths of seconds) into human-readable uptime.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.uptime(12345678)
      "1 day, 10 hours, 17 minutes, 36 seconds"

      iex> SnmpKit.SnmpMgr.Format.uptime(4200)
      "42 seconds"
  """
  defdelegate uptime(ticks), to: Types, as: :format_timeticks_uptime

  @doc """
  Formats IP address bytes into dotted decimal notation.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.ip_address(<<192, 168, 1, 1>>)
      "192.168.1.1"

      iex> SnmpKit.SnmpMgr.Format.ip_address({10, 0, 0, 1})
      "10.0.0.1"
  """
  def ip_address(ip_bytes) when is_binary(ip_bytes) do
    Types.format_ip_address(ip_bytes)
  end

  def ip_address({a, b, c, d}) when a in 0..255 and b in 0..255 and c in 0..255 and d in 0..255 do
    Types.format_ip_address(<<a, b, c, d>>)
  end

  def ip_address(other), do: inspect(other)

  @doc """
  Pretty prints an SNMP result with type-aware formatting.

  Takes a 3-tuple `{oid_string, type, value}` and returns a formatted version
  with human-readable values based on the SNMP type.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.pretty_print({"1.3.6.1.2.1.1.3.0", :timeticks, 12345678})
      {"1.3.6.1.2.1.1.3.0", :timeticks, "1 day, 10 hours, 17 minutes, 36 seconds"}

      iex> SnmpKit.SnmpMgr.Format.pretty_print({"1.3.6.1.2.1.4.20.1.1.192.168.1.1", :ip_address, <<192, 168, 1, 1>>})
      {"1.3.6.1.2.1.4.20.1.1.192.168.1.1", :ip_address, "192.168.1.1"}
  """
  def pretty_print({oid, type, value}) do
    {oid, type, format_by_type(type, value)}
  end

  @doc """
  Pretty prints a list of SNMP results.

  ## Examples

      iex> results = [
      ...>   {"1.3.6.1.2.1.1.3.0", :timeticks, 12345678},
      ...>   {"1.3.6.1.2.1.1.1.0", :octet_string, "Router"}
      ...> ]
      iex> SnmpKit.SnmpMgr.Format.pretty_print_all(results)
      [
        {"1.3.6.1.2.1.1.3.0", :timeticks, "1 day, 10 hours, 17 minutes, 36 seconds"},
        {"1.3.6.1.2.1.1.1.0", :octet_string, "\"Router\""}
      ]
  """
  def pretty_print_all(results) when is_list(results) do
    Enum.map(results, &pretty_print/1)
  end

  @doc """
  Automatically formats a value based on its SNMP type.

  This function provides a single entry point for type-aware formatting,
  automatically choosing the appropriate formatting function based on the type.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.format_by_type(:timeticks, 126691300)
      "14 days 15 hours 55 minutes 13 seconds"

      iex> SnmpKit.SnmpMgr.Format.format_by_type(:gauge32, 1000000000)
      "1 GB"

      iex> SnmpKit.SnmpMgr.Format.format_by_type(:octet_string, "Hello")
      "Hello"

  """
  @spec format_by_type(atom(), any()) :: String.t()
  def format_by_type(:timeticks, value), do: uptime(value)

  def format_by_type(:gauge32, value) when is_integer(value) and value > 1_000_000, do: bytes(value)
  def format_by_type(:gauge32, value) when is_integer(value), do: Integer.to_string(value)
  # Handle case where value is already a formatted string with type label
  def format_by_type(:gauge32, value) when is_binary(value), do: String.replace(value, ~r/ \(Gauge32\)$/, "")

  def format_by_type(:counter32, value) when is_integer(value) and value > 1_000_000, do: speed(value)
  def format_by_type(:counter32, value) when is_integer(value), do: Integer.to_string(value)
  # Handle case where value is already a formatted string with type label
  def format_by_type(:counter32, value) when is_binary(value), do: String.replace(value, ~r/ \(Counter32\)$/, "")

  def format_by_type(:counter64, value) when is_integer(value) and value > 1_000_000, do: speed(value)
  def format_by_type(:counter64, value) when is_integer(value), do: Integer.to_string(value)

  def format_by_type(:integer, 1), do: interface_status(1)
  def format_by_type(:integer, 2), do: interface_status(2)

  def format_by_type(:integer, value) when is_integer(value) and value in 1..200, do: interface_type(value)

  def format_by_type(:object_identifier, value) when is_list(value), do: Enum.join(value, ".")
  def format_by_type(:object_identifier, value) when is_binary(value), do: value
  def format_by_type(:ip_address, value), do: ip_address(value)
  def format_by_type(:mac_address, value), do: mac_address(value)

  def format_by_type(:octet_string, value) when is_binary(value), do: format_octet_string(value)

  def format_by_type(:octet_string, value) when is_list(value) do
    if Enum.all?(value, &is_integer/1) and Enum.all?(value, &(&1 >= 0 and &1 <= 255)) do
      bin = :erlang.list_to_binary(value)
      format_octet_string(bin)
    else
      inspect(value)
    end
  end

  def format_by_type(:octet_string, value), do: inspect(value)

  def format_by_type(_type, value) when is_binary(value) do
    # If the caller provided a binary (not necessarily UTF-8), attempt to
    # convert to printable string; otherwise fallback to hex representation.
    if String.valid?(value) and String.printable?(value) do
      value
    else
      "hex:" <> hex_pairs(value)
    end
  end

  def format_by_type(_type, value) when is_integer(value), do: Integer.to_string(value)
  def format_by_type(_type, value) when is_atom(value), do: Atom.to_string(value)
  def format_by_type(_type, value), do: inspect(value)

  # Enrichment helpers
  @doc """
  Enrich a single varbind tuple into a standardized map.

  Accepts {oid, type, value} where oid may be a list or dotted string.
  Options:
  - include_names (default true)
  - include_formatted (default true)
  """
  # Provide default opts once, then pattern match in subsequent clauses
  def enrich_varbind(varbind, opts \\ [])
  # Idempotent: if a map already looks enriched, return it unchanged.
  def enrich_varbind(%{oid: _oid, type: _type, value: _value} = already_enriched, _opts) do
    already_enriched
  end

  def enrich_varbind({oid_any, type, value}, opts) do
    include_names = Keyword.get(opts, :include_names, true)
    include_formatted = Keyword.get(opts, :include_formatted, true)

    {oid_string, oid_list} = normalize_oid(oid_any)
    name = get_mib_name(oid_string, include_names)

    %{oid: oid_string, oid_list: oid_list, type: type, value: value}
    |> maybe_add_name(name, include_names)
    |> maybe_add_formatted(type, value, include_formatted)
  end

  defp normalize_oid(oid) when is_list(oid), do: {Enum.join(oid, "."), oid}

  defp normalize_oid(oid) when is_binary(oid) do
    case OID.string_to_list(oid) do
      {:ok, list} -> {oid, list}
      _ -> {oid, []}
    end
  end

  defp normalize_oid(oid) do
    str = to_string(oid)

    case OID.string_to_list(str) do
      {:ok, list} -> {str, list}
      _ -> {str, []}
    end
  end

  defp get_mib_name(_oid_string, false), do: nil

  defp get_mib_name(oid_string, true) do
    case MIB.reverse_lookup(oid_string) do
      {:ok, mib_name} -> mib_name
      _ -> nil
    end
  catch
    :exit, _ -> nil
  end

  defp maybe_add_name(enriched, _name, false), do: enriched
  defp maybe_add_name(enriched, name, true), do: Map.put(enriched, :name, name)

  defp maybe_add_formatted(enriched, _type, _value, false), do: enriched

  defp maybe_add_formatted(enriched, type, value, true) do
    Map.put(enriched, :formatted, format_by_type(type, value))
  end

  @doc """
  Enrich a list of varbind tuples into standardized maps.
  Idempotent with respect to already-enriched maps.
  """
  def enrich_varbinds(list, opts \\ []) when is_list(list) do
    Enum.map(list, fn
      %{oid: _oid, type: _type, value: _value} = m -> enrich_varbind(m, opts)
      other -> enrich_varbind(other, opts)
    end)
  end

  @doc """
  Formats byte counts into human-readable sizes.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.bytes(1024)
      "1.0 KB"

      iex> SnmpKit.SnmpMgr.Format.bytes(1073741824)
      "1.0 GB"
  """
  def bytes(byte_count) when is_integer(byte_count) and byte_count >= 0 do
    cond do
      byte_count >= 1_099_511_627_776 -> "#{Float.round(byte_count / 1_099_511_627_776, 1)} TB"
      byte_count >= 1_073_741_824 -> "#{Float.round(byte_count / 1_073_741_824, 1)} GB"
      byte_count >= 1_048_576 -> "#{Float.round(byte_count / 1_048_576, 1)} MB"
      byte_count >= 1_024 -> "#{Float.round(byte_count / 1_024, 1)} KB"
      true -> "#{byte_count} bytes"
    end
  end

  def bytes(other), do: inspect(other)

  # Internal helpers
  defp printable_utf8?(bin) when is_binary(bin) do
    String.valid?(bin) and String.printable?(bin)
  end

  defp hex_pairs(bin) when is_binary(bin) do
    bin
    |> :binary.bin_to_list()
    |> Enum.map(&Integer.to_string(&1, 16))
    |> Enum.map(&String.upcase/1)
    |> Enum.map_join(" ", &String.pad_leading(&1, 2, "0"))
  end

  defp format_octet_string(value) when is_binary(value) do
    if printable_utf8?(value) do
      value
    else
      "hex:" <> hex_pairs(value)
    end
  end

  defp format_octet_string(value) when is_list(value) do
    if Enum.all?(value, &is_integer/1) and Enum.all?(value, &(&1 >= 0 and &1 <= 255)) do
      value
      |> :erlang.list_to_binary()
      |> format_octet_string()
    else
      inspect(value)
    end
  end

  defp format_octet_string(other), do: inspect(other)

  @doc """
  Formats network speeds (bits per second) into human-readable rates.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.speed(100_000_000)
      "100.0 Mbps"

      iex> SnmpKit.SnmpMgr.Format.speed(1_000_000_000)
      "1.0 Gbps"
  """
  def speed(bps) when is_integer(bps) and bps >= 0 do
    cond do
      bps >= 1_000_000_000_000 -> "#{Float.round(bps / 1_000_000_000_000, 1)} Tbps"
      bps >= 1_000_000_000 -> "#{Float.round(bps / 1_000_000_000, 1)} Gbps"
      bps >= 1_000_000 -> "#{Float.round(bps / 1_000_000, 1)} Mbps"
      bps >= 1_000 -> "#{Float.round(bps / 1_000, 1)} Kbps"
      true -> "#{bps} bps"
    end
  end

  def speed(other), do: inspect(other)

  @doc """
  Formats SNMP interface status values into readable strings.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.interface_status(1)
      "up"

      iex> SnmpKit.SnmpMgr.Format.interface_status(2)
      "down"
  """
  def interface_status(1), do: "up"
  def interface_status(2), do: "down"
  def interface_status(3), do: "testing"
  def interface_status(4), do: "unknown"
  def interface_status(5), do: "dormant"
  def interface_status(6), do: "notPresent"
  def interface_status(7), do: "lowerLayerDown"
  def interface_status(other), do: "unknown(#{other})"

  @doc """
  Formats SNMP interface types into readable strings.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.interface_type(6)
      "ethernetCsmacd"

      iex> SnmpKit.SnmpMgr.Format.interface_type(24)
      "softwareLoopback"
  """
  def interface_type(1), do: "other"
  def interface_type(6), do: "ethernetCsmacd"
  def interface_type(24), do: "softwareLoopback"
  def interface_type(131), do: "tunnel"
  def interface_type(161), do: "ieee80211"
  def interface_type(other), do: "type#{other}"

  @doc """
  Formats MAC addresses into standard colon-separated hex format.

  Handles both binary and list representations of MAC addresses commonly
  found in SNMP responses.

  ## Examples

      iex> SnmpKit.SnmpMgr.Format.mac_address(<<0x00, 0x1B, 0x21, 0x3C, 0x4D, 0x5E>>)
      "00:1b:21:3c:4d:5e"

      iex> SnmpKit.SnmpMgr.Format.mac_address([0, 27, 33, 60, 77, 94])
      "00:1b:21:3c:4d:5e"

      iex> SnmpKit.SnmpMgr.Format.mac_address("\\x00\\x1B\\x21\\x3C\\x4D\\x5E")
      "00:1b:21:3c:4d:5e"

  """
  @spec mac_address(binary() | list() | String.t()) :: String.t()
  def mac_address(mac) when is_binary(mac) and byte_size(mac) == 6 do
    mac
    |> :binary.bin_to_list()
    |> Enum.map(&Integer.to_string(&1, 16))
    |> Enum.map(&String.downcase/1)
    |> Enum.map_join(":", &String.pad_leading(&1, 2, "0"))
  end

  def mac_address(mac) when is_list(mac) and length(mac) == 6 do
    mac
    |> Enum.map(&Integer.to_string(&1, 16))
    |> Enum.map(&String.downcase/1)
    |> Enum.map_join(":", &String.pad_leading(&1, 2, "0"))
  end

  def mac_address(mac) when is_binary(mac) do
    # Handle string representation with escape sequences
    case String.length(mac) do
      6 ->
        mac
        |> String.to_charlist()
        |> Enum.map(&Integer.to_string(&1, 16))
        |> Enum.map(&String.downcase/1)
        |> Enum.map_join(":", &String.pad_leading(&1, 2, "0"))

      _ ->
        # If it's already formatted or unknown format, return as-is
        to_string(mac)
    end
  end

  def mac_address(mac), do: inspect(mac)
end
