defmodule Towerops.Snmp.MibParser do
  @moduledoc """
  Simple MIB file parser for extracting OID definitions.

  This parser extracts OID assignments from MIB files for validation purposes.
  It's not a full ASN.1 parser - just enough to validate our hardcoded OIDs
  against the official MIB definitions.

  ## Usage

      iex> MibParser.parse_mib_file("priv/mibs/standard/IF-MIB")
      {:ok, %{
        "ifIndex" => "1.3.6.1.2.1.2.2.1.1",
        "ifDescr" => "1.3.6.1.2.1.2.2.1.2",
        ...
      }}
  """

  require Logger

  @doc """
  Parse a MIB file and return a map of object names to OID strings.
  """
  @spec parse_mib_file(String.t()) :: {:ok, %{String.t() => String.t()}} | {:error, term()}
  def parse_mib_file(path) do
    case File.read(path) do
      {:ok, content} ->
        oids = parse_mib_content(content)
        {:ok, oids}

      {:error, reason} = error ->
        Logger.error("Failed to read MIB file #{path}: #{inspect(reason)}")
        error
    end
  end

  @doc """
  Parse MIB content and extract OID definitions.
  """
  @spec parse_mib_content(String.t()) :: %{String.t() => String.t()}
  def parse_mib_content(content) do
    # First, build a map of object assignments (e.g., "ifTable ::= { interfaces 2 }")
    assignments = extract_assignments(content)

    # Then resolve all OIDs by following the assignment chain
    resolve_oids(assignments)
  end

  @doc """
  Validate that a given OID matches the definition in a MIB file.
  """
  @spec validate_oid(String.t(), String.t(), String.t()) :: :ok | {:error, String.t()}
  def validate_oid(mib_file, object_name, expected_oid) do
    case parse_mib_file(mib_file) do
      {:ok, oids} ->
        case Map.get(oids, object_name) do
          ^expected_oid ->
            :ok

          actual_oid when is_binary(actual_oid) ->
            {:error, "OID mismatch for #{object_name}: expected #{expected_oid}, got #{actual_oid}"}

          nil ->
            {:error, "Object #{object_name} not found in MIB"}
        end

      {:error, reason} ->
        {:error, "Failed to parse MIB: #{inspect(reason)}"}
    end
  end

  # Private functions

  defp extract_assignments(content) do
    # Match patterns like:
    # objectName OBJECT-TYPE ::= { parent index }
    # objectName OBJECT IDENTIFIER ::= { parent index }
    # We'll extract: objectName, parent, index

    # Remove comments (lines starting with --)
    content = Regex.replace(~r/--.*$/m, content, "")

    # Match assignment patterns
    # This regex captures: name, parent, and index
    ~r/(\w+)\s+(?:OBJECT-TYPE|OBJECT\s+IDENTIFIER)[^:]*::=\s*\{\s*(\w+)\s+(\d+)\s*\}/
    |> Regex.scan(content)
    |> Map.new(fn [_, name, parent, index] ->
      {name, {parent, index}}
    end)
  end

  defp resolve_oids(assignments) do
    # Well-known root OIDs
    roots = %{
      "iso" => "1",
      "org" => "1.3",
      "dod" => "1.3.6",
      "internet" => "1.3.6.1",
      "directory" => "1.3.6.1.1",
      "mgmt" => "1.3.6.1.2",
      "mib-2" => "1.3.6.1.2.1",
      "experimental" => "1.3.6.1.3",
      "private" => "1.3.6.1.4",
      "enterprises" => "1.3.6.1.4.1",
      "security" => "1.3.6.1.5",
      "snmpV2" => "1.3.6.1.6",
      "snmpModules" => "1.3.6.1.6.3"
    }

    # Build OID map by recursively resolving assignments
    Enum.reduce(assignments, roots, fn {name, {parent, index}}, acc ->
      case resolve_oid(parent, index, acc, assignments) do
        {:ok, oid} -> Map.put(acc, name, oid)
        :error -> acc
      end
    end)
  end

  defp resolve_oid(parent, index, resolved, assignments) do
    cond do
      # Parent already resolved
      Map.has_key?(resolved, parent) ->
        {:ok, "#{resolved[parent]}.#{index}"}

      # Parent needs to be resolved from assignments
      Map.has_key?(assignments, parent) ->
        {grandparent, parent_index} = assignments[parent]

        case resolve_oid(grandparent, parent_index, resolved, assignments) do
          {:ok, parent_oid} ->
            {:ok, "#{parent_oid}.#{index}"}

          :error ->
            :error
        end

      # Can't resolve
      true ->
        :error
    end
  end

  @doc """
  List all MIB files in the priv/mibs directory.
  """
  @spec list_mib_files() :: [String.t()]
  def list_mib_files do
    mibs_dir = Application.app_dir(:towerops, "priv/mibs")

    case File.ls(mibs_dir) do
      {:ok, subdirs} ->
        Enum.flat_map(subdirs, &list_mib_files_in_subdir(mibs_dir, &1))

      {:error, _} ->
        []
    end
  end

  defp list_mib_files_in_subdir(mibs_dir, subdir) do
    subdir_path = Path.join(mibs_dir, subdir)

    case File.ls(subdir_path) do
      {:ok, files} ->
        files
        |> Enum.filter(&String.ends_with?(&1, "MIB"))
        |> Enum.map(&Path.join([mibs_dir, subdir, &1]))

      {:error, _} ->
        []
    end
  end
end
