defmodule SnmpKit.SnmpLib.OID do
  @moduledoc """
  Comprehensive OID (Object Identifier) manipulation utilities for SNMP operations.

  Provides string/list conversions, tree operations, table utilities, and validation
  functions needed by both SNMP managers and simulators.

  ## Features

  - String/list format conversions with validation
  - Support for both OID formats: "1.3.6.1.2.1.1" and ".1.3.6.1.2.1.1"
  - OID tree operations (parent/child relationships)
  - SNMP table index parsing and construction
  - OID comparison and sorting
  - Enterprise OID utilities
  - Performance-optimized operations

  ## OID Format Support

  SnmpKit supports both common OID string formats:
  - **Traditional format**: "1.3.6.1.2.1.1.1.0" (Elixir/Erlang style)
  - **Standard format**: ".1.3.6.1.2.1.1.1.0" (RFC standard with leading dot)

  Both formats parse to identical internal representations and can be used
  interchangeably throughout the library.

  ## Examples

      # Basic conversions - both formats supported
      {:ok, oid_list} = SnmpKit.SnmpLib.OID.string_to_list("1.3.6.1.2.1.1.1.0")
      {:ok, same_list} = SnmpKit.SnmpLib.OID.string_to_list(".1.3.6.1.2.1.1.1.0")
      {:ok, oid_string} = SnmpKit.SnmpLib.OID.list_to_string([1, 3, 6, 1, 2, 1, 1, 1, 0])

      # Tree operations
      true = SnmpKit.SnmpLib.OID.child_of?([1, 3, 6, 1, 2, 1, 1, 1, 0], [1, 3, 6, 1, 2, 1])
      {:ok, parent} = SnmpKit.SnmpLib.OID.get_parent([1, 3, 6, 1, 2, 1, 1, 1, 0])

      # Comparison
      :lt = SnmpKit.SnmpLib.OID.compare([1, 3, 6, 1], [1, 3, 6, 2])

      # Table operations
      {:ok, index} = SnmpKit.SnmpLib.OID.extract_table_index([1, 3, 6, 1, 2, 1, 2, 2, 1, 1], [1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1])
  """

  @type oid :: [non_neg_integer()]
  @type oid_string :: String.t()
  @type table_oid :: oid()
  @type index :: [non_neg_integer()]

  # Standard SNMP OID prefixes
  @iso_org_dod_internet [1, 3, 6, 1]
  @mgmt [1, 3, 6, 1, 2]
  @mib_2 [1, 3, 6, 1, 2, 1]
  @enterprises [1, 3, 6, 1, 4, 1]
  @experimental [1, 3, 6, 1, 3]
  @private [1, 3, 6, 1, 4]

  ## String/List Conversions

  @doc """
  Converts an OID string to a list of integers.

  Parses a dot-separated OID string into a list of non-negative integers.
  Validates each component and ensures the OID format is correct.

  ## Parameters

  - `oid_string`: Dot-separated OID string (e.g., "1.3.6.1.2.1.1.1.0" or ".1.3.6.1.2.1.1.1.0")

  ## Returns

  - `{:ok, oid_list}` on success
  - `{:error, reason}` on failure

  ## Examples

      # Standard SNMP OIDs
      iex> SnmpKit.SnmpLib.OID.string_to_list("1.3.6.1.2.1.1.1.0")
      {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]}

      # OIDs with leading dot (standard format)
      iex> SnmpKit.SnmpLib.OID.string_to_list(".1.3.6.1.2.1.1.1.0")
      {:ok, [1, 3, 6, 1, 2, 1, 1, 1, 0]}

      # Short OIDs
      iex> SnmpKit.SnmpLib.OID.string_to_list("1.3.6")
      {:ok, [1, 3, 6]}

      # Short OIDs with leading dot
      iex> SnmpKit.SnmpLib.OID.string_to_list(".1.3.6")
      {:ok, [1, 3, 6]}

      # Error cases
      iex> SnmpKit.SnmpLib.OID.string_to_list("")
      {:error, :empty_oid}

      iex> SnmpKit.SnmpLib.OID.string_to_list("1.3.6.1.a.2")
      {:error, :invalid_oid_string}

      iex> SnmpKit.SnmpLib.OID.string_to_list("1.3.6.1.2.-1")
      {:error, :invalid_oid_string}
  """
  @spec string_to_list(oid_string()) :: {:ok, oid()} | {:error, atom()}
  def string_to_list(oid_string) when is_binary(oid_string) do
    with {:ok, normalized} <- normalize_oid_string(oid_string),
         parts = String.split(normalized, "."),
         {:ok, oid_list} <- parse_oid_components(parts),
         :ok <- validate_oid_list(oid_list) do
      {:ok, oid_list}
    end
  end

  def string_to_list(_), do: {:error, :invalid_input}

  defp normalize_oid_string(oid_string) do
    trimmed = String.trim(oid_string)

    cond do
      trimmed == "" ->
        {:error, :empty_oid}

      String.starts_with?(trimmed, ".") ->
        normalized = String.slice(trimmed, 1..-1//1)
        if normalized == "", do: {:error, :empty_oid}, else: {:ok, normalized}

      true ->
        {:ok, trimmed}
    end
  end

  @doc """
  Converts an OID list to a dot-separated string.

  ## Parameters

  - `oid_list`: List of non-negative integers

  ## Returns

  - `{:ok, oid_string}` on success
  - `{:error, reason}` on failure

  ## Examples

      {:ok, "1.3.6.1.2.1.1.1.0"} = SnmpKit.SnmpLib.OID.list_to_string([1, 3, 6, 1, 2, 1, 1, 1, 0])
      {:error, :invalid_oid_list} = SnmpKit.SnmpLib.OID.list_to_string([1, 3, -1, 4])
      {:error, :empty_oid} = SnmpKit.SnmpLib.OID.list_to_string([])
  """
  @spec list_to_string(oid()) :: {:ok, oid_string()} | {:error, atom()}
  def list_to_string(oid_list) when is_list(oid_list) do
    case validate_oid_list(oid_list) do
      :ok ->
        oid_string = Enum.join(oid_list, ".")
        {:ok, oid_string}

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

  def list_to_string(_), do: {:error, :invalid_input}

  ## Tree Operations

  @doc """
  Checks if one OID is a child of another.

  ## Parameters

  - `child_oid`: Potential child OID
  - `parent_oid`: Potential parent OID

  ## Returns

  - `true` if child_oid is a child of parent_oid
  - `false` otherwise

  ## Examples

      true = SnmpKit.SnmpLib.OID.child_of?([1, 3, 6, 1, 2, 1, 1, 1, 0], [1, 3, 6, 1, 2, 1])
      false = SnmpKit.SnmpLib.OID.child_of?([1, 3, 6, 1], [1, 3, 6, 1, 2, 1])
      false = SnmpKit.SnmpLib.OID.child_of?([1, 3, 6, 2], [1, 3, 6, 1])
  """
  @spec child_of?(oid(), oid()) :: boolean()
  def child_of?(child_oid, parent_oid) when is_list(child_oid) and is_list(parent_oid) do
    child_length = length(child_oid)
    parent_length = length(parent_oid)

    child_length > parent_length and
      Enum.take(child_oid, parent_length) == parent_oid
  end

  def child_of?(_, _), do: false

  @doc """
  Checks if one OID is a parent of another.
  """
  @spec parent_of?(oid(), oid()) :: boolean()
  def parent_of?(parent_oid, child_oid), do: child_of?(child_oid, parent_oid)

  @doc """
  Gets the parent OID by removing the last component.

  ## Examples

      {:ok, [1, 3, 6, 1, 2, 1, 1, 1]} = SnmpKit.SnmpLib.OID.get_parent([1, 3, 6, 1, 2, 1, 1, 1, 0])
      {:error, :root_oid} = SnmpKit.SnmpLib.OID.get_parent([])
      {:error, :root_oid} = SnmpKit.SnmpLib.OID.get_parent([1])
  """
  @spec get_parent(oid()) :: {:ok, oid()} | {:error, atom()}
  def get_parent(oid) when is_list(oid) and length(oid) > 1 do
    parent = Enum.drop(oid, -1)
    {:ok, parent}
  end

  def get_parent(oid) when is_list(oid), do: {:error, :root_oid}
  def get_parent(_), do: {:error, :invalid_input}

  @doc """
  Gets the immediate children prefix for an OID in a given set.

  ## Parameters

  - `parent_oid`: The parent OID
  - `oid_set`: Set of OIDs to search

  ## Returns

  - List of immediate child OIDs

  ## Examples

      children = SnmpKit.SnmpLib.OID.get_children([1, 3, 6, 1], [[1, 3, 6, 1, 2], [1, 3, 6, 1, 4], [1, 3, 6, 1, 2, 1]])
      # Returns [[1, 3, 6, 1, 2], [1, 3, 6, 1, 4]]
  """
  @spec get_children(oid(), [oid()]) :: [oid()]
  def get_children(parent_oid, oid_set) when is_list(parent_oid) and is_list(oid_set) do
    parent_length = length(parent_oid)

    oid_set
    |> Enum.filter(&child_of?(&1, parent_oid))
    |> Enum.map(&Enum.take(&1, parent_length + 1))
    |> Enum.uniq()
  end

  def get_children(_, _), do: []

  @doc """
  Get standard SNMP OID prefixes.

  ## Examples

      iex> SnmpKit.SnmpLib.OID.standard_prefix(:internet)
      [1, 3, 6, 1]

      iex> SnmpKit.SnmpLib.OID.standard_prefix(:mgmt)
      [1, 3, 6, 1, 2]
  """
  @spec standard_prefix(atom()) :: oid() | nil
  def standard_prefix(:internet), do: @iso_org_dod_internet
  def standard_prefix(:mgmt), do: @mgmt
  def standard_prefix(:mib_2), do: @mib_2
  def standard_prefix(:enterprises), do: @enterprises
  def standard_prefix(:experimental), do: @experimental
  def standard_prefix(:private), do: @private
  def standard_prefix(_), do: nil

  @doc """
  Gets the next OID in lexicographic order from a given set.

  ## Parameters

  - `current_oid`: The current OID
  - `oid_set`: Set of OIDs to search (must be sorted)

  ## Returns

  - `{:ok, next_oid}` if found
  - `{:error, :end_of_mib}` if no next OID exists

  ## Examples

      oid_set = [[1, 3, 6, 1, 2, 1, 1, 1, 0], [1, 3, 6, 1, 2, 1, 1, 2, 0], [1, 3, 6, 1, 2, 1, 1, 3, 0]]
      {:ok, [1, 3, 6, 1, 2, 1, 1, 2, 0]} = SnmpKit.SnmpLib.OID.get_next_oid([1, 3, 6, 1, 2, 1, 1, 1, 0], oid_set)
  """
  @spec get_next_oid(oid(), [oid()]) :: {:ok, oid()} | {:error, atom()}
  def get_next_oid(current_oid, oid_set) when is_list(current_oid) and is_list(oid_set) do
    case Enum.find(oid_set, &(compare(&1, current_oid) == :gt)) do
      nil -> {:error, :end_of_mib}
      next_oid -> {:ok, next_oid}
    end
  end

  def get_next_oid(_, _), do: {:error, :invalid_input}

  ## Comparison Operations

  @doc """
  Compares two OIDs lexicographically.

  ## Returns

  - `:lt` if oid1 < oid2
  - `:eq` if oid1 == oid2
  - `:gt` if oid1 > oid2

  ## Examples

      :lt = SnmpKit.SnmpLib.OID.compare([1, 3, 6, 1], [1, 3, 6, 2])
      :eq = SnmpKit.SnmpLib.OID.compare([1, 3, 6, 1], [1, 3, 6, 1])
      :gt = SnmpKit.SnmpLib.OID.compare([1, 3, 6, 2], [1, 3, 6, 1])
  """
  @spec compare(oid(), oid()) :: :lt | :eq | :gt
  def compare(oid1, oid2) when is_list(oid1) and is_list(oid2) do
    compare_components(oid1, oid2)
  end

  @doc """
  Sorts a list of OIDs in lexicographic order.

  ## Examples

      sorted = SnmpKit.SnmpLib.OID.sort([[1, 3, 6, 2], [1, 3, 6, 1, 2], [1, 3, 6, 1]])
      # Returns [[1, 3, 6, 1], [1, 3, 6, 1, 2], [1, 3, 6, 2]]
  """
  @spec sort([oid()]) :: [oid()]
  def sort(oid_list) when is_list(oid_list) do
    Enum.sort(oid_list, &(compare(&1, &2) != :gt))
  end

  def sort(_), do: []

  ## Table Operations

  @doc """
  Extracts table index from an instance OID given the table column OID.

  ## Parameters

  - `table_oid`: Base table column OID
  - `instance_oid`: Full instance OID including index

  ## Returns

  - `{:ok, index}` if successful
  - `{:error, reason}` if extraction fails

  ## Examples

      table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
      instance_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1]
      {:ok, [1]} = SnmpKit.SnmpLib.OID.extract_table_index(table_oid, instance_oid)
  """
  @spec extract_table_index(table_oid(), oid()) :: {:ok, index()} | {:error, atom()}
  def extract_table_index(table_oid, instance_oid) when is_list(table_oid) and is_list(instance_oid) do
    table_length = length(table_oid)
    instance_length = length(instance_oid)

    if instance_length > table_length and
         Enum.take(instance_oid, table_length) == table_oid do
      index = Enum.drop(instance_oid, table_length)
      {:ok, index}
    else
      {:error, :invalid_table_instance}
    end
  end

  def extract_table_index(_, _), do: {:error, :invalid_input}

  @doc """
  Builds a table instance OID from table OID and index.

  ## Parameters

  - `table_oid`: Base table column OID
  - `index`: Table index as list of integers

  ## Returns

  - `{:ok, instance_oid}` if successful
  - `{:error, reason}` if construction fails

  ## Examples

      table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
      index = [1]
      {:ok, [1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1]} = SnmpKit.SnmpLib.OID.build_table_instance(table_oid, index)
  """
  @spec build_table_instance(table_oid(), index()) :: {:ok, oid()} | {:error, atom()}
  def build_table_instance(table_oid, index) when is_list(table_oid) and is_list(index) do
    case {validate_oid_list(table_oid), validate_oid_list(index)} do
      {:ok, :ok} ->
        instance_oid = table_oid ++ index
        {:ok, instance_oid}

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

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

  def build_table_instance(_, _), do: {:error, :invalid_input}

  @doc """
  Parses a table index according to index syntax definition.

  ## Parameters

  - `index`: Raw index from OID
  - `syntax`: Index syntax specification

  ## Returns

  - `{:ok, parsed_index}` if successful
  - `{:error, reason}` if parsing fails

  ## Index Syntax Examples

  - `:integer` - Single integer index
  - `{:string, length}` - Fixed-length string
  - `{:variable_string}` - Length-prefixed string
  - `[:integer, :integer]` - Multiple integer indices

  ## Examples

      {:ok, 42} = SnmpKit.SnmpLib.OID.parse_table_index([42], :integer)
      {:ok, "test"} = SnmpKit.SnmpLib.OID.parse_table_index([4, 116, 101, 115, 116], {:variable_string})
  """
  @spec parse_table_index(index(), term()) :: {:ok, term()} | {:error, atom()}
  def parse_table_index(index, :integer) when is_list(index) and length(index) == 1 do
    {:ok, hd(index)}
  end

  def parse_table_index(index, {:string, length}) when is_list(index) and length(index) == length do
    case build_string_from_bytes(index) do
      {:ok, string} -> {:ok, string}
      {:error, reason} -> {:error, reason}
    end
  end

  def parse_table_index([length | rest], {:variable_string}) when length(rest) == length do
    case build_string_from_bytes(rest) do
      {:ok, string} -> {:ok, string}
      {:error, reason} -> {:error, reason}
    end
  end

  def parse_table_index(index, syntax_list) when is_list(syntax_list) do
    case parse_index_components(index, syntax_list, []) do
      {:ok, {parsed, _remaining}} -> {:ok, parsed}
      {:error, reason} -> {:error, reason}
    end
  end

  def parse_table_index(_, _), do: {:error, :unsupported_syntax}

  @doc """
  Builds a table index from parsed components according to syntax.

  ## Examples

      {:ok, [42]} = SnmpKit.SnmpLib.OID.build_table_index(42, :integer)
      {:ok, [4, 116, 101, 115, 116]} = SnmpKit.SnmpLib.OID.build_table_index("test", {:variable_string})
  """
  @spec build_table_index(term(), term()) :: {:ok, index()} | {:error, atom()}
  def build_table_index(values, syntax_list) when is_list(values) and is_list(syntax_list) do
    if length(values) == length(syntax_list) do
      case build_compound_index(values, syntax_list) do
        {:ok, index} -> {:ok, index}
        {:error, reason} -> {:error, reason}
      end
    else
      {:error, :syntax_value_mismatch}
    end
  end

  def build_table_index(value, :integer) when is_integer(value) and value >= 0 do
    {:ok, [value]}
  end

  def build_table_index(value, {:string, length}) when is_binary(value) do
    if String.length(value) == length do
      index = String.to_charlist(value)
      {:ok, index}
    else
      {:error, :invalid_string_length}
    end
  end

  def build_table_index(value, {:variable_string}) when is_binary(value) do
    char_list = String.to_charlist(value)
    index = [length(char_list) | char_list]
    {:ok, index}
  end

  def build_table_index(_, _), do: {:error, :unsupported_syntax}

  ## Validation

  @doc """
  Validates an OID list for correctness.

  ## Returns

  - `:ok` if valid
  - `{:error, reason}` if invalid

  ## Examples

      :ok = SnmpKit.SnmpLib.OID.valid_oid?([1, 3, 6, 1, 2, 1, 1, 1, 0])
      {:error, :empty_oid} = SnmpKit.SnmpLib.OID.valid_oid?([])
      {:error, :invalid_component} = SnmpKit.SnmpLib.OID.valid_oid?([1, 3, -1, 4])
  """
  @spec valid_oid?(oid()) :: :ok | {:error, atom()}
  def valid_oid?(oid) when is_list(oid), do: validate_oid_list(oid)
  def valid_oid?(_), do: {:error, :invalid_input}

  @doc """
  Normalizes an OID to a consistent format.

  Accepts either string or list format and returns a list.

  ## Examples

      {:ok, [1, 3, 6, 1]} = SnmpKit.SnmpLib.OID.normalize("1.3.6.1")
      {:ok, [1, 3, 6, 1]} = SnmpKit.SnmpLib.OID.normalize([1, 3, 6, 1])
  """
  @spec normalize(oid() | oid_string()) :: {:ok, oid()} | {:error, atom()}
  def normalize(oid) when is_list(oid) do
    case valid_oid?(oid) do
      :ok -> {:ok, oid}
      error -> error
    end
  end

  def normalize(oid) when is_binary(oid), do: string_to_list(oid)
  def normalize(_), do: {:error, :invalid_input}

  ## Utility Functions

  @doc """
  Returns standard SNMP OID prefixes.
  """
  @spec mib_2() :: oid()
  def mib_2, do: @mib_2

  @spec enterprises() :: oid()
  def enterprises, do: @enterprises

  @spec experimental() :: oid()
  def experimental, do: @experimental

  @spec private() :: oid()
  def private, do: @private

  @doc """
  Checks if an OID is under a specific standard tree.

  ## Examples

      true = SnmpKit.SnmpLib.OID.mib_2?([1, 3, 6, 1, 2, 1, 1, 1, 0])
      true = SnmpKit.SnmpLib.OID.enterprise?([1, 3, 6, 1, 4, 1, 9, 1, 1])
  """
  @spec mib_2?(oid()) :: boolean()
  def mib_2?(oid), do: child_of?(oid, @mib_2) or oid == @mib_2

  @spec enterprise?(oid()) :: boolean()
  def enterprise?(oid), do: child_of?(oid, @enterprises)

  @spec experimental?(oid()) :: boolean()
  def experimental?(oid), do: child_of?(oid, @experimental)

  @spec private?(oid()) :: boolean()
  def private?(oid), do: child_of?(oid, @private)

  @doc """
  Gets the enterprise number from an enterprise OID.

  ## Examples

      {:ok, 9} = SnmpKit.SnmpLib.OID.get_enterprise_number([1, 3, 6, 1, 4, 1, 9, 1, 1])
      {:error, :not_enterprise_oid} = SnmpKit.SnmpLib.OID.get_enterprise_number([1, 3, 6, 1, 2, 1])
  """
  @spec get_enterprise_number(oid()) :: {:ok, non_neg_integer()} | {:error, atom()}
  def get_enterprise_number(oid) when is_list(oid) do
    if enterprise?(oid) and length(oid) > length(@enterprises) do
      enterprise_num = Enum.at(oid, length(@enterprises))
      {:ok, enterprise_num}
    else
      {:error, :not_enterprise_oid}
    end
  end

  def get_enterprise_number(_), do: {:error, :invalid_input}

  ## Private Helper Functions

  defp parse_oid_components(parts) do
    oid_list =
      Enum.map(parts, fn part ->
        case parse_oid_component(part) do
          {:error, reason} -> throw(reason)
          num -> num
        end
      end)

    {:ok, oid_list}
  catch
    reason -> {:error, reason}
  end

  defp parse_oid_component(component) when is_binary(component) do
    case Integer.parse(component) do
      {num, ""} when num >= 0 -> num
      _ -> {:error, :invalid_oid_string}
    end
  end

  defp validate_oid_list([]), do: {:error, :empty_oid}

  defp validate_oid_list(oid_list) when is_list(oid_list) do
    if Enum.all?(oid_list, &valid_oid_component?/1) do
      :ok
    else
      {:error, :invalid_component}
    end
  end

  defp validate_oid_list(_), do: {:error, :invalid_input}

  defp valid_oid_component?(component) when is_integer(component) and component >= 0, do: true
  defp valid_oid_component?(_), do: false

  defp compare_components([], []), do: :eq
  defp compare_components([], _), do: :lt
  defp compare_components(_, []), do: :gt

  defp compare_components([h1 | t1], [h2 | t2]) do
    cond do
      h1 < h2 -> :lt
      h1 > h2 -> :gt
      true -> compare_components(t1, t2)
    end
  end

  defp parse_index_components(index, [], acc) do
    {:ok, {Enum.reverse(acc), index}}
  end

  defp parse_index_components(index, [syntax | rest_syntax], acc) do
    case parse_table_index(index, syntax) do
      {:ok, value} ->
        # Calculate how many components were consumed
        case build_table_index(value, syntax) do
          {:ok, consumed_index} ->
            consumed_length = length(consumed_index)
            remaining_index = Enum.drop(index, consumed_length)
            parse_index_components(remaining_index, rest_syntax, [value | acc])

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

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

  defp build_string_from_bytes(bytes) do
    string = Enum.map_join(bytes, "", &<<&1>>)
    {:ok, string}
  rescue
    _ -> {:error, :invalid_string_index}
  end

  defp build_compound_index(values, syntax_list) do
    index_parts =
      values
      |> Enum.zip(syntax_list)
      |> Enum.map(fn {val, syntax} ->
        case build_table_index(val, syntax) do
          {:ok, idx} -> idx
          {:error, reason} -> {:error, reason}
        end
      end)

    case Enum.find(index_parts, &match?({:error, _}, &1)) do
      nil ->
        index = List.flatten(index_parts)
        {:ok, index}

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