"""
This module makes simple LDAP queries simple.
"""

import ldap

from simpleldap.cidict import cidict


#
# Exceptions.
#

class SimpleLDAPException(Exception):
    """Base class for all simpleldap exceptions."""


class ObjectNotFound(SimpleLDAPException):
    """
    Exception when no objects were returned, but was expecting a single item.
    """


class MultipleObjectsFound(SimpleLDAPException):
    """
    Exception for when multiple objects were returned, but was expecting only
    a single item.
    """


class ConnectionException(Exception):
    """Base class for all Connection object exceptions."""


class InvalidEncryptionProtocol(ConnectionException):
    """Exception when given an unsupported encryption protocol."""


#
# Classes.
#

class LDAPItem(cidict):
    """
    A convenience class for wrapping standard LDAPResult objects.
    """

    def __init__(self, result):
        super(LDAPItem, self).__init__()
        self.dn, self.attributes = result
        # XXX: quick and dirty, should really proxy straight to the existing
        # self.attributes dict.
        for attribute, values in self.attributes.iteritems():
            # Make the entire list of values for each LDAP attribute
            # accessible through a dictionary mapping.
            self[attribute] = values

    def first(self, attribute):
        """
        Return the first value for the given LDAP attribute.
        """
        return self[attribute][0]

    def value_contains(self, value, attribute):
        """
        Determine if any of the items in the value list for the given
        attribute contain value.
        """
        for item in self[attribute]:
            if value in item:
                return True
        return False

    def __str__(self):
        """
        Print attribute names and values, one per line, in alphabetical order.

        Attribute names are displayed right-aligned to the length of the
        longest attribute name.
        """
        attributes = self.keys()
        longestKeyLength = max([len(attr) for attr in attributes])
        output = []
        for attr in sorted(attributes):
            values = ("\n%*s  " % (longestKeyLength, ' ')).join(self[attr])
            output.append("%*s: %s" % (longestKeyLength, attr, values))
        return "\n".join(output)

    def __eq__(self, other):
        return self.dn == other.dn


class Connection(object):
    """
    A connection to an LDAP server.
    """

    # The class to use for items returned in results.  Subclasses can change
    # this to a class of their liking.
    result_item_class = LDAPItem

    # List of exceptions to treat as a failed bind operation in the
    # authenticate method.
    failed_authentication_exceptions = [
        ldap.NO_SUCH_OBJECT,  # e.g. dn matches no objects.
        ldap.UNWILLING_TO_PERFORM,  # e.g. dn with no password.
        ldap.INVALID_CREDENTIALS,  # e.g. wrong password.
    ]

    def __init__(self, hostname='localhost', port=None, dn='', password='',
                 encryption=None, require_cert=None, debug=False,
                 initialize_kwargs=None, options=None, search_defaults=None):
        """
        Bind to hostname:port using the passed distinguished name (DN), as
        ``dn``, and password.

        If ``hostname`` is not given, default to ``'localhost'``.

        If no user and password is given, try to connect anonymously with a
        blank DN and password.

        ``encryption`` should be one of ``'tls'``, ``'ssl'``, or ``None``.
        If ``'tls'``, then the standard port 389 is used by default and after
        binding, tls is started.  If ``'ssl'``, then port 636 is used by
        default.  ``port`` can optionally be given for connecting to a
        non-default port.

        ``require_cert`` is None by default.  Set this to ``True`` or
        ``False`` to set the ``OPT_X_TLS_REQUIRE_CERT`` ldap option.

        If ``debug`` is ``True``, debug options are turned on within ldap and
        statements are ouput to standard error.  Default is ``False``.

        If given, ``options`` should be a dictionary of any additional
        connection-specific ldap  options to set, e.g.:
        ``{'OPT_TIMELIMIT': 3}``.

        If given, ``search_defaults`` should be a dictionary of default
        parameters to be passed to the search method.
        """
        if search_defaults is None:
            self._search_defaults = {}
        else:
            self._search_defaults = search_defaults

        if not encryption or encryption == 'tls':
            protocol = 'ldap'
            if not port:
                port = 389
        elif encryption == 'ssl':
            protocol = 'ldaps'
            if not port:
                port = 636
        else:
            raise InvalidEncryptionProtocol(
                "Invalid encryption protocol, must be one of: 'tls' or 'ssl'.")

        if require_cert is not None:
            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, require_cert)
        if debug:
            ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
        else:
            ldap.set_option(ldap.OPT_DEBUG_LEVEL, 0)

        uri = '%s://%s:%s' % (protocol, hostname, port)
        if initialize_kwargs:
            self.connection = ldap.initialize(uri, **initialize_kwargs)
        else:
            self.connection = ldap.initialize(uri)
        if options:
            for name, value in options.iteritems():
                self.connection.set_option(getattr(ldap, name), value)
        if encryption == 'tls':
            self.connection.start_tls_s()
        self.connection.simple_bind_s(dn, password)

    def set_search_defaults(self, **kwargs):
        """
        Set defaults for search.

        Examples::

            conn.set_search_defaults(base_dn='dc=example,dc=com', timeout=100)
            conn.set_search_defaults(attrs=['cn'], scope=ldap.SCOPE_BASE)