require "English"
require "cgi"
require 'uri'
begin
  require 'uri/ldaps'
rescue LoadError
  module URI
    class LDAPS < LDAP
      DEFAULT_PORT = 636
    end
    @@schemes['LDAPS'] = LDAPS
  end
end

module ActiveLdap
  # Configuration
  #
  # Configuration provides the default settings required for
  # ActiveLdap to work with your LDAP server. All of these
  # settings can be passed in at initialization time.
  module Configuration
    def self.included(base)
      base.extend(ClassMethods)
    end

    DEFAULT_CONFIG = {}
    DEFAULT_CONFIG[:host] = '127.0.0.1'
    DEFAULT_CONFIG[:port] = nil
    DEFAULT_CONFIG[:method] = :plain  # :ssl, :tls, :plain allowed
    DEFAULT_CONFIG[:tls_options] = nil

    DEFAULT_CONFIG[:bind_dn] = nil
    DEFAULT_CONFIG[:password_block] = nil
    DEFAULT_CONFIG[:password] = nil
    DEFAULT_CONFIG[:store_password] = true
    DEFAULT_CONFIG[:allow_anonymous] = true
    DEFAULT_CONFIG[:sasl_quiet] = true
    DEFAULT_CONFIG[:try_sasl] = false
    DEFAULT_CONFIG[:sasl_options] = nil
    # See http://www.iana.org/assignments/sasl-mechanisms
    DEFAULT_CONFIG[:sasl_mechanisms] = ["GSSAPI", "DIGEST-MD5",
                                        "CRAM-MD5", "EXTERNAL"]

    DEFAULT_CONFIG[:retry_limit] = 1
    DEFAULT_CONFIG[:retry_wait] = 1
    DEFAULT_CONFIG[:timeout] = 0 # in seconds; 0 <= Never timeout
    # Whether or not to retry on timeouts
    DEFAULT_CONFIG[:retry_on_timeout] = true
    DEFAULT_CONFIG[:follow_referrals] = true

    # 500 is the default size limit value of OpenLDAP 2.4:
    #   https://openldap.org/doc/admin24/limits.html#Global%20Limits
    #
    # We may change this when we find LDAP server that its the default
    # size limit is smaller than 500.
    DEFAULT_CONFIG[:page_size] = 500
    # Whether using paged results if available.
    DEFAULT_CONFIG[:use_paged_results] = true

    DEFAULT_CONFIG[:logger] = nil

    module ClassMethods
      @@defined_configurations = {}

      def default_configuration
        DEFAULT_CONFIG.dup
      end

      def ensure_configuration(config=nil)
        if config.nil?
          if defined?(LDAP_ENV)
            config = LDAP_ENV
          elsif defined?(Rails)
            config = Rails.env
          else
            config = configurations
          end
        end

        if config.is_a?(Symbol) or config.is_a?(String)
          _config = configurations[config.to_s]
          unless _config
            raise ConnectionError,
                  _("%s connection is not configured") % config
          end
          config = _config
        end

        config
      end

      def configuration(key=nil)
        @@defined_configurations[key || active_connection_name]
      end

      def define_configuration(key, config)
        @@defined_configurations[key] = config
      end

      def defined_configurations
        @@defined_configurations
      end

      def remove_configuration_by_key(key)
        @@defined_configurations.delete(key)
      end

      CONNECTION_CONFIGURATION_KEYS = [:uri, :base, :adapter]
      def remove_connection_related_configuration(config)
        config.reject do |key, value|
          CONNECTION_CONFIGURATION_KEYS.include?(key)
        end
      end

      def parent_configuration(target)
        if target.is_a?(Base)
          target = target.class
        else
          target = target.superclass
        end
        while target <= Base
          config = defined_configurations[target.active_connection_key]
          return config if config
          target = target.superclass
        end
        default_configuration
      end

      def merge_configuration(user_configuration, target=self)
        configuration = parent_configuration(target).dup
        prepare_configuration(user_configuration).each do |key, value|
          case key
          when :base
            # Scrub before inserting
            target.base = value.gsub(/['}{#]/, '')
          when :scope
            target.scope = value
            configuration[:scope] = value
          else
            configuration[key] = value
          end
        end
        configuration
      end

      def prepare_configuration(configuration)
        configuration = configuration.symbolize_keys
        uri = configuration.delete(:uri)
        return configuration unless uri

        begin
          uri = URI.parse(uri)
        rescue URI::InvalidURIError
          raise ConfigurationError.new(_("invalid URI: %s") % uri)
        end
        unless uri.is_a?(URI::LDAP)
          raise ConfigurationError.new(_("not a LDAP URI: %s") % uri.to_s)
        end

        merger = URIConfigurationMerger.new(uri)
        merger.merge(configuration)
      end

      class URIConfigurationMerger
        def initialize(uri)
          @uri = uri
        end

        def merge(configuration)
          uri_configuration = {:port => @uri.port}
          uri_configuration[:host] = @uri.host if @uri.host
          uri_configuration[:base] = @uri.dn if @uri.dn
          extensions = parse_extensions
          bindname_extension = extensions["bindname"]
          if bindname_extension
            uri_configuration[:bind_dn] = bindname_extension[:value]
            uri_configuration[:allow_anonymous] = !bindname_extension[:critical]
          end
          uri_configuration[:scope] = @uri.scope if @uri.scope
          uri_configuration[:method] = :ssl if @uri.is_a?(URI::LDAPS)
          uri_configuration.merge(configuration)
        end

        private
        def parse_extensions
          extensions = {}
          (@uri.extensions || "").split(",").collect do |extension|
            name, value = extension.split("=", 2)
            case name
            when /\A!/
              critical = true
              name = $POSTMATCH
            else
              critical = false
            end
            extensions[name] = {
              :critical => critical,
              :value => CGI.unescape(value || ""),
            }
          end
          extensions
        end
      end
    end
  end
end
