defmodule SnmpKit.SnmpLib.MIB.Parser do
  @moduledoc """
  Pure native SNMP MIB parser using custom Elixir grammar.

  This module provides complete native parsing of SNMP MIB files with 100%
  compatibility. All MIBs are parsed natively ensuring complete data integrity.
  """

  alias SnmpKit.SnmpLib.MIB.SnmpTokenizer

  require Logger

  @doc """
  Initialize the parser by compiling the grammar file.
  This creates a proper yacc-generated parser for native MIB parsing.
  """
  def init_parser do
    module_name = :mib_grammar_elixir

    # Check if the parser module is already loaded
    case Code.ensure_loaded(module_name) do
      {:module, ^module_name} ->
        # Module is already loaded, no need to recompile
        {:ok, module_name}

      {:error, :nofile} ->
        # Module not found, need to compile
        compile_grammar()
    end
  end

  defp compile_grammar do
    # Get the path to our Elixir-compatible grammar file
    grammar_file = Path.join([__DIR__, "..", "..", "..", "src", "mib_grammar_elixir.yrl"])

    # Ensure the output directory exists
    output_dir = Path.join([__DIR__, "..", "..", "..", "src"])
    File.mkdir_p!(output_dir)

    # Compile the grammar using Erlang's yecc (if available)
    if Code.ensure_loaded?(:yecc) and function_exported?(:yecc, :file, 1) do
      # credo:disable-for-next-line Credo.Check.Refactor.Apply
      result = apply(:yecc, :file, [to_charlist(grammar_file)])

      case result do
        {:ok, _generated_file} ->
          module_name = :mib_grammar_elixir

          Logger.debug("Successfully compiled MIB grammar. Generated module: #{inspect(module_name)}")

          {:ok, module_name}

        {:error, reason} ->
          Logger.error("Failed to compile MIB grammar: #{inspect(reason)}")
          {:error, {:grammar_compilation_failed, reason}}

        :error ->
          Logger.error("Grammar compilation failed with error")
          {:error, :compilation_failed}
      end
    else
      Logger.warning("yecc module not available, MIB grammar compilation disabled")
      {:error, :yecc_not_available}
    end
  end

  @doc """
  Parse all MIB files in a list of directories using pure native parsing.

  Returns a map with directory paths as keys and results as values.
  Each result contains successful compilations and failures.

  ## Examples

      # Parse MIBs in multiple directories
      dirs = [
        "/path/to/mibs/working",
        "/path/to/mibs/docsis"
      ]
      results = SnmpKit.SnmpLib.MIB.Parser.mibdirs(dirs)

      # Access results by directory
      working_results = results["/path/to/mibs/working"]
      IO.puts("Success: \#{length(working_results.success)}/\#{working_results.total}")

      # Get all successful MIBs across directories
      all_mibs = Enum.flat_map(results, fn {_dir, result} -> result.success end)
  """
  def mibdirs(directories) when is_list(directories) do
    Logger.info("Compiling MIBs in #{length(directories)} directories")

    results =
      Map.new(directories, fn dir ->
        {dir, compile_directory(dir)}
      end)

    # Log summary
    total_success =
      Enum.reduce(results, 0, fn {_dir, result}, acc ->
        acc + length(result.success)
      end)

    total_files =
      Enum.reduce(results, 0, fn {_dir, result}, acc ->
        acc + result.total
      end)

    Logger.info(
      "OVERALL RESULTS: Total MIBs compiled: #{total_success}/#{total_files} (#{Float.round(total_success / max(total_files, 1) * 100, 1)}%)"
    )

    Enum.each(results, fn {dir, result} ->
      dir_name = Path.basename(dir)
      success_rate = Float.round(length(result.success) / max(result.total, 1) * 100, 1)
      Logger.info("  #{dir_name}: #{length(result.success)}/#{result.total} (#{success_rate}%)")
    end)

    results
  end

  @doc """
  Parse a MIB file using pure native grammar parsing.
  This is the production MIB parser with 100% native compatibility.
  """
  def parse(mib_content) when is_binary(mib_content) do
    with {:ok, parser_module} <- init_parser(),
         {:ok, tokens} <- tokenize(mib_content),
         {:ok, parse_tree} <- parse_with_module(parser_module, tokens) do
      {:ok, convert_to_elixir_format(parse_tree)}
    else
      {:error, {:tokenize, reason}} ->
        Logger.debug("Tokenize failed: #{inspect(reason)}")
        {:error, reason}

      {:error, {:parse, reason}} ->
        Logger.debug("Parse failed: #{inspect(reason)}")
        {:error, convert_error_to_string(reason)}

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

  defp parse_with_module(parser_module, tokens) do
    # credo:disable-for-next-line Credo.Check.Refactor.Apply
    case apply(parser_module, :parse, [tokens]) do
      {:ok, parse_tree} -> {:ok, parse_tree}
      {:error, reason} -> {:error, {:parse, reason}}
    end
  end

  @doc """
  Parse pre-tokenized MIB tokens using native grammar parsing.
  This function takes tokens directly without tokenizing.
  """
  def parse_tokens(tokens) when is_list(tokens) do
    with {:ok, parser_module} <- init_parser(),
         {:ok, parse_tree} <- parse_with_module(parser_module, tokens) do
      {:ok, convert_to_elixir_format(parse_tree)}
    else
      {:error, {:parse, reason}} ->
        Logger.debug("Parse failed: #{inspect(reason)}")
        {:error, convert_error_to_string(reason)}

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

  @doc """
  Tokenize MIB content using the native SNMP tokenizer.

  Uses the SnmpKit.SnmpLib.MIB.SnmpTokenizer module for complete MIB tokenization.
  """
  def tokenize(mib_content) when is_binary(mib_content) do
    # Convert to charlist for Erlang compatibility
    char_content = to_charlist(mib_content)

    # Use the native SNMP tokenizer
    case SnmpTokenizer.tokenize(
           char_content,
           &SnmpTokenizer.null_get_line/0
         ) do
      {:ok, tokens} ->
        Logger.debug("Tokenized MIB content successfully")
        # Apply hex atom conversion to tokens from the tokenizer
        converted_tokens = apply_hex_conversion(tokens)
        {:ok, converted_tokens}

      {:error, reason} ->
        Logger.error("Tokenization failed: #{inspect(reason)}")
        {:error, reason}
    end
  end

  # Apply hex atom conversion to tokens from the tokenizer.
  # Converts long hex atoms like :"07fffffff" to integers for grammar compatibility.
  defp apply_hex_conversion(tokens) do
    Enum.map(tokens, &convert_hex_atom/1)
  end

  # Convert hex atoms that look like integers to actual integers
  defp convert_hex_atom({:atom, line, atom_value}) when is_atom(atom_value) do
    atom_string = Atom.to_string(atom_value)

    # Check if it looks like a hex number (only convert long hex strings, not short identifiers like d1, d2)
    if String.match?(atom_string, ~r/^[0-9a-fA-F]{8,}$/) do
      try do
        # Try to convert from hex to integer
        hex_value = String.to_integer(atom_string, 16)
        {:integer, line, hex_value}
      rescue
        _ ->
          # If conversion fails, keep as atom
          {:atom, line, atom_value}
      end
    else
      # Not a hex pattern, keep as atom
      {:atom, line, atom_value}
    end
  end

  # Pass through all other tokens unchanged
  defp convert_hex_atom(token), do: token

  # Convert the Erlang parse tree to Elixir-friendly format.
  defp convert_to_elixir_format(result) do
    case result do
      {:pdata, version, mib_name, exports, imports, definitions} ->
        %{
          __type__: :mib,
          name: to_string(mib_name),
          version: version,
          exports: convert_exports(exports),
          imports: convert_imports(imports),
          definitions: convert_definitions(definitions)
        }

      {:pdata, version, mib_name, imports, definitions} ->
        %{
          __type__: :mib,
          name: to_string(mib_name),
          version: version,
          exports: [],
          imports: convert_imports(imports),
          definitions: convert_definitions(definitions)
        }

      other ->
        %{__type__: :unknown, raw: other}
    end
  end

  defp convert_exports(exports) when is_list(exports) do
    Enum.map(exports, fn export ->
      case export do
        {type, name} -> %{type: type, name: to_string(name)}
        name -> %{name: to_string(name)}
      end
    end)
  end

  defp convert_imports(imports) when is_list(imports) do
    Enum.map(imports, fn
      {{module_name, symbols}, _line} ->
        %{
          __type__: :import,
          from_module: to_string(module_name),
          symbols:
            Enum.map(symbols, fn
              {:builtin, symbol} -> to_string(symbol)
              {:node, symbol} -> to_string(symbol)
              {:type, symbol} -> to_string(symbol)
              symbol -> to_string(symbol)
            end)
        }

      {{module_name, symbols}} ->
        %{
          __type__: :import,
          from_module: to_string(module_name),
          symbols:
            Enum.map(symbols, fn
              {:builtin, symbol} -> to_string(symbol)
              {:node, symbol} -> to_string(symbol)
              {:type, symbol} -> to_string(symbol)
              symbol -> to_string(symbol)
            end)
        }

      other ->
        %{__type__: :import, raw: other}
    end)
  end

  defp convert_imports(_imports) do
    []
  end

  defp convert_definitions(definitions) when is_list(definitions) do
    Enum.map(definitions, &convert_definition/1)
  end

  # Handle the actual Erlang SNMP record format from the grammar
  defp convert_definition({{record_type, name, macro, parent, sub_index}, line}) when record_type == :mc_internal do
    %{
      __type__: :object_identifier,
      name: to_string(name),
      macro: macro,
      parent: if(is_binary(parent), do: parent, else: to_string(parent)),
      sub_index: convert_sub_index(sub_index),
      line: line
    }
  end

  defp convert_definition({{record_type, name, syntax, units, max_acc, status, desc, ref, kind, oid}, line})
       when record_type == :mc_object_type do
    %{
      __type__: :object_type,
      name: to_string(name),
      syntax: syntax,
      units: units,
      max_access: max_acc,
      status: status,
      description: clean_description(desc),
      reference: ref,
      kind: kind,
      oid: convert_oid(oid),
      line: line
    }
  end

  # Handle module identity record
  defp convert_definition(
         {{:mc_module_identity, name, last_updated, organization, contact_info, description, revisions, oid}, line}
       ) do
    %{
      __type__: :module_identity,
      name: to_string(name),
      last_updated: to_string(last_updated),
      organization: clean_description(to_string(organization)),
      contact_info: clean_description(to_string(contact_info)),
      description: clean_description(to_string(description)),
      revisions: convert_revisions(revisions),
      oid: convert_oid(oid),
      line: line
    }
  end

  # Handle textual convention record
  defp convert_definition({{:mc_new_type, name, macro, status, description, reference, display_hint, syntax}, line}) do
    %{
      __type__: :textual_convention,
      name: to_string(name),
      macro: macro,
      status: status,
      description: clean_description(to_string(description)),
      reference: if(reference == :undefined, do: nil, else: to_string(reference)),
      display_hint: if(display_hint == :undefined, do: nil, else: to_string(display_hint)),
      syntax: syntax,
      line: line
    }
  end

  # Handle legacy format first (more specific pattern)
  defp convert_definition({:ok, {type, name, rest}}) do
    base = %{
      __type__: type,
      name: to_string(name)
    }

    case type do
      :objectidentifier ->
        Map.put(base, :oid, convert_oid(rest))

      :objectType ->
        convert_object_type(base, rest)

      :moduleIdentity ->
        convert_module_identity(base, rest)

      :textualConvention ->
        convert_textual_convention(base, rest)

      :objectGroup ->
        convert_object_group(base, rest)

      _ ->
        Map.put(base, :data, rest)
    end
  end

  # Handle other record types as catch-all
  defp convert_definition({record_tuple, line}) do
    %{
      __type__: :unknown,
      record: record_tuple,
      line: line
    }
  end

  defp convert_object_type(base, {syntax, access, status, description, reference, index, defval, oid}) do
    base
    |> Map.put(:syntax, convert_syntax(syntax))
    |> Map.put(:max_access, convert_atom(access))
    |> Map.put(:status, convert_atom(status))
    |> Map.put(:description, clean_description(to_string(description)))
    |> Map.put(:reference, if(reference == :undefined, do: nil, else: to_string(reference)))
    |> Map.put(:index, convert_index(index))
    |> Map.put(:defval, convert_defval(defval))
    |> Map.put(:oid, convert_oid(oid))
  end

  defp convert_module_identity(base, {last_updated, organization, contact_info, description, revisions, oid}) do
    base
    |> Map.put(:last_updated, to_string(last_updated))
    |> Map.put(:organization, to_string(organization))
    |> Map.put(:contact_info, to_string(contact_info))
    |> Map.put(:description, clean_description(to_string(description)))
    |> Map.put(:revisions, convert_revisions(revisions))
    |> Map.put(:oid, convert_oid(oid))
  end

  defp convert_textual_convention(base, {display_hint, status, description, reference, syntax}) do
    base
    |> Map.put(
      :display_hint,
      if(display_hint == :undefined, do: nil, else: to_string(display_hint))
    )
    |> Map.put(:status, convert_atom(status))
    |> Map.put(:description, clean_description(to_string(description)))
    |> Map.put(:reference, if(reference == :undefined, do: nil, else: to_string(reference)))
    |> Map.put(:syntax, convert_syntax(syntax))
  end

  defp convert_object_group(base, {objects, status, description, reference, oid}) do
    base
    |> Map.put(:objects, Enum.map(objects, &to_string/1))
    |> Map.put(:status, convert_atom(status))
    |> Map.put(:description, clean_description(to_string(description)))
    |> Map.put(:reference, if(reference == :undefined, do: nil, else: to_string(reference)))
    |> Map.put(:oid, convert_oid(oid))
  end

  defp convert_oid(oid_list) when is_list(oid_list) do
    Enum.map(oid_list, fn
      {name, value} when is_atom(name) and is_integer(value) ->
        %{name: to_string(name), value: value}

      {name, value} when is_atom(name) and is_list(value) ->
        # Handle charlists in tuple values
        %{name: to_string(name), value: convert_oid_value(value)}

      value when is_integer(value) ->
        %{value: value}

      value when is_list(value) ->
        # Handle charlists
        %{value: convert_oid_value(value)}

      name when is_atom(name) ->
        %{name: to_string(name)}
    end)
  end

  # Handle tuple OIDs like {:"mib-2", ~c"4"}
  defp convert_oid({name, value}) when is_atom(name) do
    {name, convert_oid_value(value)}
  end

  # Handle other OID formats
  defp convert_oid(oid), do: oid

  # Convert OID values, handling charlists
  defp convert_oid_value(value) when is_list(value) do
    # Check if it's a charlist that can be converted to string
    if Enum.all?(value, fn
         i when is_integer(i) -> i >= 0 and i <= 1_114_111
         _ -> false
       end) do
      # Convert charlist to string, then try to parse as integer if possible
      str_value = List.to_string(value)

      case Integer.parse(str_value) do
        # Pure integer string
        {int_value, ""} -> int_value
        # Keep as string if not pure integer
        _ -> str_value
      end
    else
      # Not a charlist, return as-is
      value
    end
  rescue
    # If conversion fails, return original
    _ -> value
  end

  defp convert_oid_value(value), do: value

  defp convert_syntax(:integer), do: :integer
  defp convert_syntax({:integer, constraints}), do: {:integer, convert_constraints(constraints)}
  defp convert_syntax(:"octet string"), do: :octet_string
  defp convert_syntax({:"octet string", size}), do: {:octet_string, convert_constraints(size)}
  defp convert_syntax(:"object identifier"), do: :object_identifier
  defp convert_syntax(atom) when is_atom(atom), do: atom

  defp convert_constraints(constraints), do: constraints

  defp convert_index(:undefined), do: nil

  defp convert_index(index_list) when is_list(index_list) do
    Enum.map(index_list, fn
      {:implied, name} -> {:implied, to_string(name)}
      name when is_atom(name) -> to_string(name)
    end)
  end

  defp convert_defval(:undefined), do: nil
  defp convert_defval(value), do: value

  defp convert_revisions(revisions) when is_list(revisions) do
    Enum.map(revisions, fn
      {:mc_revision, date, description} ->
        %{date: to_string(date), description: clean_description(to_string(description))}

      {date, description} ->
        %{date: to_string(date), description: clean_description(to_string(description))}
    end)
  end

  defp convert_revisions(_revisions), do: []

  defp convert_atom(atom) when is_atom(atom) do
    atom |> to_string() |> String.replace("-", "_") |> String.to_atom()
  end

  # Helper function to compile all MIB files in a directory
  defp compile_directory(directory) do
    case File.ls(directory) do
      {:ok, files} ->
        mib_files =
          files
          |> Enum.filter(&mib_file?/1)
          |> Enum.sort()

        Logger.debug("Processing #{Path.basename(directory)}: #{length(mib_files)} files")

        results = Enum.map(mib_files, &process_mib_file(directory, &1))

        successes = Enum.filter(results, &(elem(&1, 0) == :success))
        failures = Enum.filter(results, &(elem(&1, 0) == :error))

        # Extract MIB data from successes
        success_mibs =
          Enum.map(successes, fn {:success, file, mib_data} ->
            Map.put(mib_data, :source_file, file)
          end)

        # Extract error info from failures
        error_info =
          Enum.map(failures, fn {:error, file, reason} ->
            %{file: file, error: reason}
          end)

        %{
          directory: directory,
          total: length(mib_files),
          success: success_mibs,
          failures: error_info,
          success_count: length(successes),
          failure_count: length(failures)
        }

      {:error, reason} ->
        Logger.error("Cannot read directory #{directory}: #{inspect(reason)}")

        %{
          directory: directory,
          total: 0,
          success: [],
          failures: [%{file: "directory", error: reason}],
          success_count: 0,
          failure_count: 1
        }
    end
  end

  # Process a single MIB file
  defp process_mib_file(directory, file) do
    file_path = Path.join(directory, file)

    with {:ok, content} <- File.read(file_path),
         {:ok, mib_data} <- parse(content) do
      {:success, file, mib_data}
    else
      {:error, {:file_read_error, _}} = error ->
        error

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

  # Helper to identify MIB files
  defp mib_file?(filename) do
    String.ends_with?(filename, ".mib") or
      (not String.contains?(filename, ".") and not String.ends_with?(filename, ".bin"))
  end

  # Clean up description strings by trimming lines and removing excessive whitespace
  defp clean_description(desc) when is_binary(desc) do
    # Early exit for very large descriptions to prevent timeout
    if byte_size(desc) > 50_000 do
      # For very large descriptions, just do basic cleanup
      desc
      |> String.trim()
      # Truncate to reasonable size
      |> String.slice(0, 1000)
      |> Kernel.<>(" [truncated]")
    else
      desc
      |> String.split("\n")
      |> Enum.map(&String.trim/1)
      |> Enum.reject(&(&1 == ""))
      |> Enum.join(" ")
      |> collapse_whitespace()
      |> String.trim()
    end
  end

  defp clean_description(desc), do: desc

  # Efficiently collapse multiple whitespace characters into single spaces
  defp collapse_whitespace(text) do
    text
    |> String.split()
    |> Enum.join(" ")
  end

  # Convert charlist error messages to binary strings
  defp convert_error_to_string({mib_name, module, message}) when is_binary(mib_name) and is_atom(module) do
    # Handle Erlang parser errors like {"MIB-NAME", :parser_module, "error message"}
    message_str =
      if is_list(message), do: convert_deep_charlist(message), else: to_string(message)

    "#{mib_name}: #{message_str}"
  end

  defp convert_error_to_string({line, module, message}) when is_list(message) do
    {line, module, convert_deep_charlist(message)}
  end

  defp convert_error_to_string(message), do: message

  defp convert_deep_charlist(list) when is_list(list) do
    # Handle lists of charlists like [[115, 121, 110, ...], [39, 84, 69, ...]]
    if list_of_charlists?(list) do
      Enum.map_join(list, "", &charlist_to_string/1)
    else
      # Try to convert as a single charlist
      charlist_to_string(list)
    end
  rescue
    # If conversion fails, return original
    _ -> list
  end

  defp convert_deep_charlist(other), do: other

  defp list_of_charlists?(list) do
    Enum.all?(list, fn
      sublist when is_list(sublist) -> charlist?(sublist)
      _ -> false
    end)
  end

  defp charlist?(list) do
    Enum.all?(list, fn
      i when is_integer(i) -> i >= 0 and i <= 1_114_111
      # Allow mixed content for improper lists like [115, 121 | ""]
      _ -> true
    end)
  rescue
    _ -> false
  end

  defp charlist_to_string(charlist) when is_list(charlist) do
    # Handle improper lists like [115, 121, 110 | ""]
    case charlist do
      [] ->
        ""

      [head | _tail] when is_integer(head) and head >= 0 and head <= 1_114_111 ->
        try do
          # Try to convert the whole thing, handling improper lists
          convert_improper_charlist(charlist, [])
        rescue
          # Fallback to inspect if conversion fails
          _ -> inspect(charlist)
        end

      _ ->
        inspect(charlist)
    end
  rescue
    _ -> inspect(charlist)
  end

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

  defp convert_improper_charlist([], acc) do
    acc |> Enum.reverse() |> List.to_string()
  end

  defp convert_improper_charlist([head | tail], acc) when is_integer(head) do
    convert_improper_charlist(tail, [head | acc])
  end

  defp convert_improper_charlist(other, acc) when is_binary(other) do
    # Handle case where tail is a string (like in [115, 121 | ""])
    (acc |> Enum.reverse() |> List.to_string()) <> other
  end

  defp convert_improper_charlist(_, acc) do
    # For any other tail, just convert what we have
    acc |> Enum.reverse() |> List.to_string()
  end

  # Convert sub_index from charlist to appropriate format
  defp convert_sub_index(sub_index) when is_list(sub_index) do
    # Check if it's a charlist that can be converted to string
    if Enum.all?(sub_index, fn
         i when is_integer(i) -> i >= 0 and i <= 1_114_111
         _ -> false
       end) do
      case List.to_string(sub_index) do
        # Handle common cases
        # Convert newline to nil (often used as placeholder)
        "\n" -> nil
        # Convert empty string to nil
        "" -> nil
        # Keep as string
        str -> str
      end
    else
      # If not a pure charlist, return as-is (might be list of integers)
      sub_index
    end
  rescue
    # If conversion fails, return original
    _ -> sub_index
  end

  defp convert_sub_index(sub_index), do: sub_index
end
