defmodule Towerops.HoneybadgerFilter do
  @moduledoc """
  Custom Honeybadger filter to exclude noisy errors during deployments
  and filter sensitive SNMP credentials from error reports.

  Filters out errors that occur during normal application shutdown/restart:
  - OTP supervisor/gen_server termination errors
  - Redix connection errors (tcp_closed, econnrefused)
  - DBConnection errors during shutdown
  - Normal process exits (:normal, :shutdown, {:shutdown, _})

  Also filters sensitive data from error reports (passwords, tokens, SNMP communities).
  """

  use Honeybadger.Filter.Mixin

  # Named supervisors/processes that generate errors during normal shutdown
  @ignored_registered_names [
    "memsup",
    "cpu_sup",
    "Elixir.PollerSupervisor"
  ]

  # Sensitive keys that should be filtered from error reports
  @sensitive_keys [
    "password",
    "snmp_community",
    "community",
    "secret",
    "token",
    "api_key",
    "auth_password",
    "priv_password",
    "auth_pass",
    "priv_pass"
  ]

  @impl Honeybadger.Filter
  def filter_context(context) do
    cond do
      # Named process in our ignore list
      get_in(context, [:registered_name]) in @ignored_registered_names ->
        nil

      # Anonymous gen_server shutdown errors (OTP domain, gen_server error_info)
      otp_gen_server_shutdown?(context) ->
        nil

      # Redix connection errors during shutdown/restart
      redix_connection_error?(context) ->
        nil

      # DBConnection errors during shutdown/restart
      db_connection_error?(context) ->
        nil

      # Normal process shutdown (exit :normal, :shutdown, {:shutdown, _})
      normal_shutdown?(context) ->
        nil

      true ->
        context
        |> super()
        |> filter_sensitive_data()
    end
  end

  @impl Honeybadger.Filter
  def filter_params(params) when is_map(params) do
    Map.new(params, fn {key, value} ->
      if should_filter?(key) do
        {key, "[FILTERED]"}
      else
        {key, filter_params(value)}
      end
    end)
  end

  def filter_params(params), do: params

  # Detect anonymous gen_server termination during shutdown
  defp otp_gen_server_shutdown?(context) do
    domain = get_in(context, [:domain])
    mfa = get_in(context, [:mfa])

    domain == ["otp"] and match?(["gen_server", "error_info", _], mfa)
  end

  # Detect Redix connection errors during shutdown/restart
  defp redix_connection_error?(context) do
    reason = get_in(context, [:reason])
    message = get_in(context, [:message])

    cond do
      # Redix connection closed errors
      is_tuple(reason) and elem(reason, 0) == :tcp_closed ->
        true

      # Redix connection refused (during restart)
      is_tuple(reason) and elem(reason, 0) == :econnrefused ->
        true

      # String message check for Redix errors
      is_binary(message) and String.contains?(message, "Redix") ->
        true

      true ->
        false
    end
  end

  # Detect DBConnection errors during shutdown/restart
  defp db_connection_error?(context) do
    reason = get_in(context, [:reason])
    message = get_in(context, [:message])

    cond do
      # DBConnection.ConnectionError
      is_exception(reason) and reason.__struct__ == DBConnection.ConnectionError ->
        true

      # String message check
      is_binary(message) and String.contains?(message, "DBConnection") ->
        true

      true ->
        false
    end
  end

  # Detect normal process shutdown
  defp normal_shutdown?(context) do
    reason = get_in(context, [:reason])

    case reason do
      :normal -> true
      :shutdown -> true
      {:shutdown, _} -> true
      {:port_died, :normal} -> true
      {:port_died, :shutdown} -> true
      _ -> false
    end
  end

  # Filter sensitive data from context
  defp filter_sensitive_data(nil), do: nil

  defp filter_sensitive_data(context) when is_map(context) do
    context
    |> Map.update(:cgi_data, %{}, &filter_params/1)
    |> Map.update(:params, %{}, &filter_params/1)
    |> Map.update(:session, %{}, &filter_params/1)
    |> Map.update(:context, %{}, &filter_params/1)
  end

  defp filter_sensitive_data(context), do: context

  # Check if a key should be filtered
  defp should_filter?(key) when is_binary(key) do
    key_lower = String.downcase(key)
    Enum.any?(@sensitive_keys, &String.contains?(key_lower, &1))
  end

  defp should_filter?(key) when is_atom(key) do
    should_filter?(Atom.to_string(key))
  end

  defp should_filter?(_), do: false
end
