defmodule SnmpKit.SnmpLib.MIB.Utilities do
  @moduledoc """
  Direct port of Erlang snmpc_lib.erl utility functions to Elixir.

  This is a 1:1 port of the utility functions from the official Erlang SNMP compiler
  from OTP lib/snmp/src/compile/snmpc_lib.erl

  Original copyright: Ericsson AB 1996-2025 (Apache License 2.0)
  """

  require Logger

  @type oid() :: [integer()]
  @type oid_status() :: :resolved | :unresolved
  @type verbosity() :: :silent | :warning | :info | :debug

  # OID and Name Resolution

  @doc """
  Register an OID entry in the OID table.
  Port of register_oid/4 from snmpc_lib.erl
  """
  @spec register_oid(binary(), oid(), atom(), map()) :: map()
  def register_oid(name, oid, status, oid_table) do
    entry = %{
      name: name,
      oid: oid,
      status: status,
      parent: get_parent_oid(oid),
      children: []
    }

    Map.put(oid_table, name, entry)
  end

  @doc """
  Resolve symbolic OID references to numeric OIDs.
  Port of resolve_oids/1 from snmpc_lib.erl
  """
  @spec resolve_oids(map()) :: {:ok, map()} | {:error, [binary()]}
  def resolve_oids(oid_table) do
    # Start with root OIDs (those without parents)
    root_oids = find_root_oids(oid_table)

    case resolve_oid_tree(root_oids, oid_table, MapSet.new()) do
      {:ok, resolved_table} -> {:ok, resolved_table}
      {:error, reason} when is_binary(reason) -> {:error, [reason]}
      {:error, unresolved} when is_list(unresolved) -> {:error, unresolved}
    end
  end

  @doc """
  Translate symbolic name to numeric OID.
  Port of tr_oid/2 from snmpc_lib.erl
  """
  @spec tr_oid(binary(), map()) :: {:ok, oid()} | {:error, :not_found}
  def tr_oid(name, oid_table) do
    case Map.get(oid_table, name) do
      %{oid: oid, status: :resolved} -> {:ok, oid}
      %{status: :unresolved} -> {:error, :unresolved}
      nil -> {:error, :not_found}
    end
  end

  @doc """
  Update MIB entries with resolved OIDs.
  Port of update_me_oids/3 from snmpc_lib.erl
  """
  @spec update_me_oids([map()], map(), verbosity()) :: [map()]
  def update_me_oids(mib_entries, oid_table, verbosity) do
    Enum.map(mib_entries, fn entry ->
      update_single_entry_oid(entry, oid_table, verbosity)
    end)
  end

  # Type Checking and Validation

  @doc """
  Validate and transform ASN.1 type definition.
  Port of make_ASN1type/1 from snmpc_lib.erl
  """
  @spec make_asn1_type(term()) :: {:ok, map()} | {:error, binary()}
  def make_asn1_type(type_def) do
    case normalize_type(type_def) do
      {:ok, normalized} -> validate_type_constraints(normalized)
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Validate bit definitions for BITS syntax.
  Port of test_kibbles/2 from snmpc_lib.erl
  """
  @spec test_kibbles([map()], verbosity()) :: :ok | {:error, binary()}
  def test_kibbles(bit_definitions, verbosity) do
    case validate_bit_names(bit_definitions) do
      :ok -> validate_bit_values(bit_definitions, verbosity)
      {:error, reason} -> {:error, reason}
    end
  end

  @doc """
  Check if size constraint is allowed for given type (RFC 1902 compliance).
  Port of allow_size_rfc1902/1 from snmpc_lib.erl
  """
  @spec allow_size_rfc1902(atom()) :: boolean()
  def allow_size_rfc1902(:octet_string), do: true
  def allow_size_rfc1902(_), do: false

  @doc """
  Validate sub-identifier ranges.
  Port of check_sub_ids/3 from snmpc_lib.erl
  """
  @spec check_sub_ids(oid(), integer(), integer()) :: :ok | {:error, binary()}
  def check_sub_ids(oid, min_value, max_value) do
    case Enum.find(oid, &(&1 < min_value or &1 > max_value)) do
      nil ->
        :ok

      invalid_id ->
        {:error, "Sub-identifier #{invalid_id} out of range [#{min_value}..#{max_value}]"}
    end
  end

  # Error Handling and Reporting

  @doc """
  Print error message with formatting.
  Port of print_error/2 and print_error/3 from snmpc_lib.erl
  """
  @spec print_error(binary(), verbosity()) :: :ok
  def print_error(message, verbosity) when verbosity != :silent do
    Logger.error("Error: #{message}")
    :ok
  end

  def print_error(_message, :silent), do: :ok

  @spec print_error(binary(), term(), verbosity()) :: :ok
  def print_error(format, args, verbosity) when verbosity != :silent do
    message = format |> :io_lib.format(args) |> IO.iodata_to_binary()
    print_error(message, verbosity)
  end

  def print_error(_format, _args, :silent), do: :ok

  @doc """
  Terminate compilation with error.
  Port of error/2 and error/3 from snmpc_lib.erl
  """
  @spec compilation_error(binary()) :: no_return()
  def compilation_error(message) do
    throw({:error, message})
  end

  @spec compilation_error(binary(), term()) :: no_return()
  def compilation_error(format, args) do
    message = format |> :io_lib.format(args) |> IO.iodata_to_binary()
    compilation_error(message)
  end

  # Debugging Utilities

  @doc """
  Configurable verbose printing.
  Port of vprint/6 from snmpc_lib.erl
  """
  @spec vprint(verbosity(), verbosity(), binary(), binary(), binary(), [term()]) :: :ok
  def vprint(current_verbosity, required_verbosity, module, function, format, args) do
    if printable?(current_verbosity, required_verbosity) do
      message = format |> :io_lib.format(args) |> IO.iodata_to_binary()
      Logger.debug("[#{module}:#{function}] #{message}")
    end

    :ok
  end

  @doc """
  Determine if message should be printed based on verbosity.
  Port of printable/2 from snmpc_lib.erl
  """
  @spec printable?(verbosity(), verbosity()) :: boolean()
  def printable?(current_verbosity, required_verbosity) do
    verbosity_level(current_verbosity) >= verbosity_level(required_verbosity)
  end

  @doc """
  Validate verbosity level.
  Port of vvalidate/1 from snmpc_lib.erl
  """
  @spec vvalidate(term()) :: {:ok, verbosity()} | {:error, binary()}
  def vvalidate(verbosity) when verbosity in [:silent, :warning, :info, :debug] do
    {:ok, verbosity}
  end

  def vvalidate(invalid) do
    {:error, "Invalid verbosity level: #{inspect(invalid)}"}
  end

  # Miscellaneous Helpers

  @doc """
  Safe key lookup in list of tuples/maps.
  Port of key1search/2 and key1search/3 from snmpc_lib.erl
  """
  @spec key1search(term(), [tuple()]) :: {:value, term()} | false
  def key1search(key, list) do
    case Enum.find(list, &(elem(&1, 0) == key)) do
      nil -> false
      tuple -> {:value, tuple}
    end
  end

  @spec key1search(term(), [tuple()], term()) :: term()
  def key1search(key, list, default) do
    case key1search(key, list) do
      {:value, tuple} -> tuple
      false -> default
    end
  end

  @doc """
  Set directory path helper.
  Port of set_dir/2 from snmpc_lib.erl
  """
  @spec set_dir(binary(), binary()) :: binary()
  def set_dir(filename, directory) do
    case Path.dirname(filename) do
      "." -> Path.join(directory, Path.basename(filename))
      _ -> filename
    end
  end

  @doc """
  Generic lookup function with multiple criteria.
  Port of lookup/2 from snmpc_lib.erl
  """
  @spec lookup(term(), [term()]) :: {:ok, term()} | {:error, :not_found}
  def lookup(key, list) when is_list(list) do
    case Enum.find(list, &match_lookup_criteria(key, &1)) do
      nil -> {:error, :not_found}
      value -> {:ok, value}
    end
  end

  # Private helper functions

  defp get_parent_oid([]), do: nil
  defp get_parent_oid(oid) when length(oid) == 1, do: nil

  defp get_parent_oid(oid) do
    Enum.drop(oid, -1)
  end

  defp find_root_oids(oid_table) do
    oid_table
    |> Enum.filter(fn {_name, entry} -> entry.parent == nil end)
    |> Enum.map(fn {name, _entry} -> name end)
  end

  defp resolve_oid_tree([], oid_table, _visited), do: {:ok, oid_table}

  defp resolve_oid_tree([name | rest], oid_table, visited) do
    if MapSet.member?(visited, name) do
      resolve_oid_tree(rest, oid_table, visited)
    else
      case resolve_single_oid(name, oid_table) do
        {:ok, updated_table} ->
          new_visited = MapSet.put(visited, name)
          resolve_oid_tree(rest, updated_table, new_visited)

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

  defp resolve_single_oid(name, oid_table) do
    case Map.get(oid_table, name) do
      %{status: :resolved} = _entry ->
        {:ok, oid_table}

      %{status: :unresolved, oid: symbolic_oid} = entry ->
        case resolve_symbolic_oid(symbolic_oid, oid_table) do
          {:ok, numeric_oid} ->
            updated_entry = %{entry | oid: numeric_oid, status: :resolved}
            updated_table = Map.put(oid_table, name, updated_entry)
            {:ok, updated_table}

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

      nil ->
        {:error, "OID not found: #{name}"}
    end
  end

  defp resolve_symbolic_oid(oid, oid_table) when is_list(oid) do
    # Convert symbolic OID elements to numeric
    case resolve_oid_elements(oid, oid_table, []) do
      {:ok, numeric_oid} -> {:ok, numeric_oid}
      {:error, reason} -> {:error, reason}
    end
  end

  defp resolve_oid_elements([], _oid_table, acc), do: {:ok, Enum.reverse(acc)}

  defp resolve_oid_elements([element | rest], oid_table, acc) do
    case resolve_oid_element(element, oid_table) do
      {:ok, numeric_value} ->
        resolve_oid_elements(rest, oid_table, [numeric_value | acc])

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

  defp resolve_oid_element(%{name: _name, value: value}, _oid_table) when is_integer(value) do
    {:ok, value}
  end

  defp resolve_oid_element(%{name: name}, oid_table) do
    case tr_oid(name, oid_table) do
      {:ok, [numeric_value]} -> {:ok, numeric_value}
      {:ok, oid} when length(oid) > 1 -> {:error, "Invalid OID reference: #{name}"}
      {:error, reason} -> {:error, "Cannot resolve OID reference: #{name} (#{reason})"}
    end
  end

  defp resolve_oid_element(%{value: value}, _oid_table) when is_integer(value) do
    {:ok, value}
  end

  defp update_single_entry_oid(entry, oid_table, verbosity) do
    case Map.get(entry, :oid) do
      nil ->
        entry

      symbolic_oid ->
        case tr_oid(symbolic_oid, oid_table) do
          {:ok, numeric_oid} ->
            vprint(verbosity, :debug, "Utilities", "update_oid", "Resolved ~s -> ~w", [
              symbolic_oid,
              numeric_oid
            ])

            %{entry | oid: numeric_oid}

          {:error, reason} ->
            print_error("Cannot resolve OID for #{entry.name}: #{reason}", verbosity)
            entry
        end
    end
  end

  defp normalize_type({:integer, constraints}), do: {:ok, %{type: :integer, constraints: constraints}}

  defp normalize_type({:octet_string, constraints}), do: {:ok, %{type: :octet_string, constraints: constraints}}

  defp normalize_type({:object_identifier}), do: {:ok, %{type: :object_identifier, constraints: []}}

  defp normalize_type({:named_type, name}), do: {:ok, %{type: :named_type, name: name, constraints: []}}

  defp normalize_type(type) when is_atom(type), do: {:ok, %{type: type, constraints: []}}
  defp normalize_type(invalid), do: {:error, "Invalid type definition: #{inspect(invalid)}"}

  defp validate_type_constraints(%{type: :integer, constraints: constraints} = type_def) do
    case validate_integer_constraints(constraints) do
      :ok -> {:ok, type_def}
      {:error, reason} -> {:error, reason}
    end
  end

  defp validate_type_constraints(%{type: :octet_string, constraints: constraints} = type_def) do
    case validate_size_constraints(constraints) do
      :ok -> {:ok, type_def}
      {:error, reason} -> {:error, reason}
    end
  end

  defp validate_type_constraints(type_def), do: {:ok, type_def}

  defp validate_integer_constraints([]), do: :ok

  defp validate_integer_constraints([{:range, min, max} | rest])
       when is_integer(min) and is_integer(max) and min <= max do
    validate_integer_constraints(rest)
  end

  defp validate_integer_constraints([{:enum, _values} | rest]) do
    # Note: Enum value validation not yet implemented
    validate_integer_constraints(rest)
  end

  defp validate_integer_constraints([invalid | _]) do
    {:error, "Invalid integer constraint: #{inspect(invalid)}"}
  end

  defp validate_size_constraints([]), do: :ok

  defp validate_size_constraints([size | rest]) when is_integer(size) and size >= 0 do
    validate_size_constraints(rest)
  end

  defp validate_size_constraints([{:range, min, max} | rest])
       when is_integer(min) and is_integer(max) and min <= max and min >= 0 do
    validate_size_constraints(rest)
  end

  defp validate_size_constraints([invalid | _]) do
    {:error, "Invalid size constraint: #{inspect(invalid)}"}
  end

  defp validate_bit_names(bit_definitions) do
    names = Enum.map(bit_definitions, & &1.name)

    if length(names) == length(Enum.uniq(names)) do
      :ok
    else
      {:error, "Duplicate bit names found"}
    end
  end

  defp validate_bit_values(bit_definitions, verbosity) do
    values = Enum.map(bit_definitions, & &1.value)

    if length(values) == length(Enum.uniq(values)) do
      vprint(verbosity, :debug, "Utilities", "validate_bits", "Bit validation successful", [])
      :ok
    else
      {:error, "Duplicate bit values found"}
    end
  end

  defp verbosity_level(:silent), do: 0
  defp verbosity_level(:warning), do: 1
  defp verbosity_level(:info), do: 2
  defp verbosity_level(:debug), do: 3

  defp match_lookup_criteria(key, {key, _value}), do: true
  defp match_lookup_criteria(key, %{name: key}), do: true
  defp match_lookup_criteria(key, %{id: key}), do: true
  defp match_lookup_criteria(_key, _item), do: false
end
