require 'net/ldap'
require 'slf4r'
require 'ldap/conditions_2_filter'

module Ldap
  class NetLdapFacade

    # @param config Hash for the ldap connection
    def self.open(config)
      Net::LDAP.open( config ) do |ldap|
        yield ldap
      end
    end

    include ::Slf4r::Logger

    # @param config Hash for the ldap connection
    def initialize(config)
      @ldap_config = config
      if config.is_a? Hash
        @ldap = Net::LDAP.new( config )
      else
        @ldap = config
      end
    end

    def retrieve_next_id(treebase, key_field)
      id_sym = key_field.downcase.to_sym
      max = 0
      @ldap.search( :base => base(treebase),
                    :attributes => [key_field],
                    :return_result => false ) do |entry|
        n = entry[id_sym].first.to_i
        max = n if max < n
      end
      max + 1
    end

    # @param dn_prefix String the prefix of the dn
    # @param treebase the treebase of the dn or any search
    # @param key_field field which carries the integer unique id of the entity
    # @param props Hash of the ldap attributes of the new ldap object
    # @return nil in case of an error or the new id of the created object
    def create_object(dn_prefix, treebase, key_field, props, silence = false)
      if @ldap.add( :dn => dn(dn_prefix, treebase),
                    :attributes => props) || @ldap.get_operation_result.code.to_s == "0"
        props[key_field.to_sym]
      else
        unless silence
          msg = ldap_error("create",
                             dn(dn_prefix, treebase)) + "\n\t#{props.inspect}"
          # TODO maybe raise always an error
          if @ldap.get_operation_result.code.to_s == "68"
            raise ::DataMapper::PersistenceError.new(msg)
          else
            logger.warn(msg)
          end
        end
        nil
      end
    end

    # @param treebase the treebase of the search
    # @param key_fields Array of fields which carries the integer unique id(s) of the entity
    # @param Array of conditions for the search
    # @return Array of Hashes with a name/values pair for each attribute
    def read_objects(treebase, key_fields, conditions, field_names, order_field = nil)

      # If there is a :dn in the filter skip everything and look it up
      if dn = conditions.detect { |c| c[1] == "dn" } then
        searchbase = dn[2]
        filter = nil
      else
        searchbase = base(treebase)
        filter = Conditions2Filter.convert(conditions)
      end

      result = []
      @ldap.search( :base => searchbase,
                    :attributes => field_names,
                    :filter => filter ) do |res|
        mapp = to_map(field_names, res)

        #puts map[key_field.to_sym]
        # TODO maybe make filter which removes this unless
        # TODO move this into the ldap_Adapter to make it more general, so that
        # all field with Integer gets converted, etc
        result << mapp if key_fields.all? do |key_field|
          mapp.keys.detect {|k| k.to_s.downcase == key_field.downcase }
        end
      end
      result
    end


    # @param dn_prefix String the prefix of the dn
    # @param treebase the treebase of the dn or any search
    # @param actions the add/replace/delete actions on the attributes
    # @return nil in case of an error or true
    def update_object(dn_prefix, treebase, actions)
      if @ldap.modify( :dn => dn(dn_prefix, treebase),
                       :operations => actions ) || @ldap.get_operation_result.code.to_s == "0"
        true
      else
        puts caller.join("\n")
        logger.warn(ldap_error("update",
                               dn(dn_prefix, treebase) + "\n\t#{actions.inspect}"))
        nil
      end
    end

    # @param dn_prefix String the prefix of the dn
    # @param treebase the treebase of the dn or any search
    # @return nil in case of an error or true
    def delete_object(dn_prefix, treebase)
      if @ldap.delete( :dn => dn(dn_prefix, treebase) )
        true
      else
        logger.warn(ldap_error("delete",
                               dn(dn_prefix, treebase)))

        nil
      end
    end


    # @param dn String for identifying the ldap object
    # @param password String to be used for authenticate to the dn
    def authenticate(dn, password)
      config = @ldap_config.merge(:auth => {
                                    :method => :simple,
                                    :username => dn,
                                    :password => password
                                  })
      Net::LDAP.new(config).bind
    end

    # helper to concat the dn from the various parts
    # @param dn_prefix String the prefix of the dn
    # @param treebase the treebase of the dn or any search
    # @return the complete dn String
    def dn(dn_prefix, treebase)
      [ dn_prefix, base(treebase) ].compact.join(",")
    end

    # helper to concat the base from the various parts
    # @param treebase
    # @param ldap_base the ldap_base defaulting to connection ldap_base
    # @return the complete base String
    def base(treebase = nil, ldap_base = @ldap.base)
      [ treebase, ldap_base ].compact.join(",")
    end

    private

    # helper to extract the Hash from the ldap search result
    # @param Entry from the ldap_search
    # @return Hash with name/value pairs of the entry
    def to_map(field_names, entry)
      fields = {:dn => :dn}
      field_names.each { |f| fields[f.downcase.to_sym] = f.to_sym }
      def entry.map
        @myhash
      end
      result = {}
      entry.map.each do |k,v|
        result[fields[k]] = v
      end
      result
    end
    
    def ldap_error(method, dn)
      "#{method} error: (#{@ldap.get_operation_result.code}) #{@ldap.get_operation_result.message}\n\tDN: #{dn}"
    end
  end
end
