module LogStash
  module PluginMixins
    module Snmp
      # This module provides common configurations and functions to all SNMP plugins
      #
      module Common
        require 'logstash-integration-snmp_jars'

        java_import 'org.logstash.snmp.RubySnmpOidFieldMapper'
        java_import 'org.logstash.snmp.DefaultOidFieldMapper'
        java_import 'org.logstash.snmp.DottedStringOidFieldMapper'
        java_import 'org.logstash.snmp.mib.MibManager'

        OID_MAPPING_FORMAT_DEFAULT = 'default'.freeze
        OID_MAPPING_FORMAT_RUBY_SNMP = 'ruby_snmp'.freeze

        MIB_BASE_PATH = ::File.join(__FILE__, '..', '..', '..', '..', 'mibs')
        MIB_PROVIDED_PATHS = [::File.join(MIB_BASE_PATH, 'logstash'), ::File.join(MIB_BASE_PATH, 'ietf')].map { |path| ::File.expand_path(path) }

        def self.included(base)
          # Common configuration supported by all SNMP plugins

          # This plugin provides sets of MIBs publicly available. The full paths to these provided MIBs paths
          # Will be displayed at plugin startup.
          base.config :use_provided_mibs, :validate => :boolean, :default => true

          # List of paths of MIB (.dic, .yaml) files of dirs. If a dir path is specified, all files with
          # .dic and .yaml extension will be loaded.
          #
          # ATTENTION: a MIB .dic file must be generated using the libsmi library `smidump` command line utility
          # like this for example. Here the `RFC1213-MIB.txt` file is an ASN.1 MIB file.
          #
          # `$ smidump -k -f python RFC1213-MIB.txt > RFC1213-MIB.dic`
          #
          # The OSS libsmi library https://www.ibr.cs.tu-bs.de/projects/libsmi/ is available & installable
          # on most OS.
          #
          # .yaml MIBs files must be on the https://github.com/hallidave/ruby-snmp MIB format.
          base.config :mib_paths, :validate => :array # ["path/to/mib.dic", "path/to/mib/dir"]

          # Defines the OID field format.
          # `ruby_snmp` produces ruby-snmp-like fields, prefixing the module name, followed
          # by :: and resolved identifiers. E.g:
          # 1.3.6.1.2.1.1.2.0 -> SNMPv2-MIB::sysObjectID.0
          # `default` translates every identifier separating them by dots. E.g:
          # 1.3.6.1.2.1.1.2.0 -> iso.org.dod.internet.mgmt.mib-2.system.sysObjectID.0
          # `dotted_string` does not change the OID format and map fields using the dotted string format, E.g:
          # 1.3.6.1.2.1.1.2.0 -> 1.3.6.1.2.1.1.2.0
          base.config :oid_mapping_format, :validate => %w[default ruby_snmp dotted_string], :default => 'default'

          # Defines if the Logstash event field values, which types are `OID`, are mapped using the configured OID textual representation
          # set on the `oid_mapping_format`.
          base.config :oid_map_field_values, :validate => :boolean, :default => false

          # Number of OID root digits to ignore in event field name. For example, in a numeric OID
          # like 1.3.6.1.2.1.1.1.0" the first 5 digits could be ignored by setting oid_root_skip => 5
          # which would result in a field name "1.1.1.0". Similarly when a MIB is used an OID such
          # as "1.3.6.1.2.mib-2.system.sysDescr.0" would become "mib-2.system.sysDescr.0"
          base.config :oid_root_skip, :validate => :number, :default => 0

          # Number of OID tail digits to retain in event field name. For example, in a numeric OID
          # like 1.3.6.1.2.1.1.1.0" the last 2 digits could be retained by setting oid_path_length => 2
          # which would result in a field name "1.0". Similarly, when a MIB is used an OID such as
          # "1.3.6.1.2.mib-2.system.sysDescr.0" would become "sysDescr.0"
          base.config :oid_path_length, :validate => :number, :default => 0

          # Defines a target field for placing fields.
          # If this setting is omitted, data gets stored at the root (top level) of the event.
          # The target is only relevant while decoding data into a new event.
          base.config :target, :validate => :field_reference

          # SNMPv3 Credentials
          #
          # A single user can be configured and will be used for all defined SNMPv3 requests.
          # Multiple snmp input declarations will be needed if multiple SNMPv3 users are required.
          # If not using SNMPv3 simply leave options empty.

          # The SNMPv3 security name or user name
          base.config :security_name, :validate => :string

          # The SNMPv3 authentication protocol or type
          base.config :auth_protocol, :validate => %w[md5 sha sha2 hmac128sha224 hmac192sha256 hmac256sha384 hmac384sha512]

          # The SNMPv3 authentication passphrase or password
          base.config :auth_pass, :validate => :password

          # The SNMPv3 privacy/encryption protocol
          base.config :priv_protocol, :validate => %w[des 3des aes aes128 aes192 aes256 aes256with3desKey]

          # The SNMPv3 encryption password
          base.config :priv_pass, :validate => :password

          # The SNMPv3 security level can be Authentication, No Privacy; Authentication, Privacy; or no Authentication, no Privacy
          base.config :security_level, :validate => %w[noAuthNoPriv authNoPriv authPriv]
        end

        def build_mib_manager!
          mib_manager = new_mib_manager

          if @mib_paths&.any?
            logger.info('Loading user-provided MIB files', :path => @mib_paths)
            @mib_paths.each do |path|
              mib_manager.add(path)
            end
          end

          if @use_provided_mibs
            logger.info('Loading provided MIB files', :path => MIB_PROVIDED_PATHS)
            MIB_PROVIDED_PATHS.each do |path|
              mib_manager.add(path)
            end
          end

          mib_manager
        end

        def build_snmp_client!(client_builder, validate_usm_user: false)
          validate_usm_user! if validate_usm_user

          unless @security_name.nil?
            client_builder.addUsmUser(@security_name, @auth_protocol, @auth_pass&.value, @priv_protocol, @priv_pass&.value, @security_level || 'noAuthNoPriv')
          end

          client_builder.setMapOidVariableValues(@oid_map_field_values)
          client_builder.build
        end

        private

        def new_mib_manager
          MibManager.new(oid_field_mapper!)
        end

        def oid_field_mapper!
          validate_oid_field_mapper_params!

          if @oid_mapping_format == OID_MAPPING_FORMAT_DEFAULT
            DefaultOidFieldMapper.new(@oid_root_skip, @oid_path_length)
          elsif @oid_mapping_format == OID_MAPPING_FORMAT_RUBY_SNMP
            RubySnmpOidFieldMapper.new
          else
            DottedStringOidFieldMapper.new
          end
        end

        def validate_oid_field_mapper_params!
          if @oid_mapping_format == 'default'
            raise(LogStash::ConfigurationError, 'Use either `oid_root_skip` or `oid_path_length`') if @oid_root_skip.positive? && @oid_path_length.positive?
          else
            raise(LogStash::ConfigurationError, 'The `oid_root_skip` and `oid_path_length` requires setting `oid_mapping_format` to `default`') if @oid_root_skip.positive? || @oid_path_length.positive?
          end
        end

        def validate_usm_user!
          errors = []

          errors << '`security_name` is required when SNMP v3 is enabled' if @security_name.nil?
          validate_auth_protocol(errors)
          validate_priv_protocol(errors)

          raise(LogStash::ConfigurationError, errors.join(', ')) if errors.any?
        end

        def validate_auth_protocol(errors)
          validate_protocol_config(errors, 'auth_protocol', 'auth_pass')

          if @auth_protocol.nil? && (@security_level == 'authNoPriv' || @security_level == 'authPriv')
            errors << "Using `security_level` set to `#{@security_level}` requires the configuration of `auth_protocol`"
          end
        end

        def validate_priv_protocol(errors)
          validate_protocol_config(errors, 'priv_protocol', 'priv_pass')

          if @priv_protocol.nil? && @security_level == 'authPriv'
            errors << "Using `security_level` set to `#{@security_level}` requires the configuration of `priv_protocol`"
          end
        end

        def validate_protocol_config(errors, protocol_config_name, pass_config_name)
          protocol_config_value = instance_variable_get("@#{protocol_config_name}".intern)
          pass_config_value = instance_variable_get("@#{pass_config_name}".intern)

          if protocol_config_value && !pass_config_value
            errors << "Using `#{protocol_config_name}` requires the `#{pass_config_name}`"
          elsif pass_config_value && !protocol_config_value
            errors << "`#{protocol_config_name}` is required when using `#{pass_config_name}`"
          end

          if pass_config_value&.value && pass_config_value.value.length < 8
            errors << "`#{pass_config_name}` passphrase must be at least 8 bytes long"
          end
        end
      end
    end
  end
end
