#!/usr/bin/env python2


import ConfigParser
import argparse
import getpass
import ldap
import logging
import os
import os.path
import re
import sys


# Get logger
log = logging.getLogger(__name__)

# Shared config data structure
conf = {}


def parse_arguments():
    log.debug('Parsing command line arguments')

    description = 'LDAP phone book.'

    parser = argparse.ArgumentParser(description=description)
    parser.add_argument(
        '--template', '-t',
        metavar='TEMPLATE',
        help='template used for printing the output')
    parser.add_argument(
        '--search-template', '-s',
        dest='search_template',
        metavar='EXPR',
        help='search filter template')
    parser.add_argument(
        '--search-filter', '-S',
        dest='search_filter',
        metavar='EXPR',
        help='search filter expression')
    parser.add_argument(
        '--config', '-c',
        metavar='FILE',
        help='configuration file location')
    parser.add_argument(
        '--verbose', '-v',
        action='store_true',
        help='show verbose output')
    parser.add_argument(
        '--decoration', '-d',
        action='store_true',
        help='output LDAP value decoration')
    parser.add_argument(
        'SEARCH_STR',
        nargs='?',
        help='value to search for')

    return (parser.parse_args(), parser)


def get_config(f, parser):
    log.debug('Parsing config file: %s' % f)

    if f and os.path.isfile(f):
        config_file = f
    elif os.path.isfile('/etc/pbook.conf'):
        config_file = '/etc/pbook.conf'
    elif os.path.isfile(os.environ['HOME'] + '/.pbook'):
        config_file = os.environ['HOME'] + '/.pbook'
    else:
        log.error('No config file found!')
        parser.print_help()
        sys.exit(0)

    config = ConfigParser.RawConfigParser()

    # Preserve case
    config.optionxform = str
    config.read(config_file)

    for section in config.sections():
        tmp = {}

        for option in config.options(section):
            tmp[option] = config.get(section, option)

        conf[section] = tmp

    return conf


def get_template(args_template, args_decoration):
    # Decide which template to use
    template_name = conf['template']['_default']
    if args_template and args_template in conf['template']:
        template_name = args_template

    log.debug('Using template: %s' % (template_name))
    template = conf['template'][template_name]

    # Create template string if necessary
    simple = False
    key_max_len = 0
    if template.startswith('('):
        simple = True
        ldap_fields = template.lstrip('(').rstrip(')').split(',')

        # Get lentgh of the longest label in the template
        for key in ldap_fields:
            l = len(key)
            if not args_decoration:
                l = len(conf['label'][key])

            if l > key_max_len:
                key_max_len = l

        template = ''.join(map(lambda x: (
            "%%(__%(x)s)%(l)ds: %%(%(x)s)s\n" %
            {'x': x, 'l': key_max_len}
        ), ldap_fields))
    else:
        # Unique list of fields to query from LDAP
        ldap_fields = list(set(filter(
            lambda x: not x.startswith('__'), re.findall(
                '%\((.[^)]*)\)(?:\d+(?:\.\d+|)|)[diouxXeEfFgGaAcCsSpnm]',
                template))))
        ldap_fields.reverse()

    log.debug('Template string: %s' % (template))
    log.debug('LDAP fields: %s' % (ldap_fields))

    return (template, template_name, key_max_len, simple, ldap_fields)


def get_searchfilter(args_search_filter, args_search_template, search_str):
    search_filter = ''

    # Get the correct search filter
    if args_search_filter:
        search_filter = (args_search_filter.replace(
            '%s', '%(str)s') %
            {'str': search_str})
        log.debug('Using provided search filter: %s' % search_filter)
    elif args_search_template:
        search_filter = (
            conf['search_template'][args_search_template].replace(
                '%s', '%(str)s') %
            {'str': search_str})
        log.debug('Using provided search template: %s' % search_filter)
    else:
        # Try to match search string with search patterns
        for r in conf['search_pattern']:
            if r != '_default' and re.match(r, search_str):
                log.debug(
                    'Using search pattern: %s -> %s' %
                    (r, conf['search_pattern'][r]))
                search_filter = re.sub(
                    r, conf['search_pattern'][r], search_str)
                break

        # If no matching expression found, use the default one
        if not search_filter:
            default_name = conf['search_template']['_default']
            log.debug(
                'Using default filter (%s): %s' % (
                    conf['search_template']['_default'],
                    conf['search_template'][default_name]))
            search_filter = (conf['search_template'][default_name].replace(
                '%s', '%(str)s') %
                {'str': search_str})

    return search_filter


def get_result(search_filter, ldap_fields):
    # Define LDAP connection
    conn = ldap.initialize(conf['connection']['uri'])

    # Get password
    password = conf['connection']['password']
    if password == '-1':
        password = getpass.getpass('LDAP password: ')

    # Get results
    results = None
    try:
        # Bind LDAP connection
        conn.bind_s(conf['connection']['bind_dn'], password)
        # Perform LDAP search
        results = conn.search_s(
            conf['connection']['base_dn'], ldap.SCOPE_SUBTREE, search_filter,
            ldap_fields)
    except ldap.LDAPError, e:
        log.error(e.message['desc'])
        sys.exit(0)
    finally:
        try:
            conn.unbind()
        except ldap.LDAPError, e:
            pass

    # Number of results
    log.debug('Number of results: %d' % len(results))

    return results


def print_results(
        results, template, template_name, key_max_len, simple,
        args_decoration):

    # Create headers dictionary
    headers = {}
    for k in conf['label']:
        val = conf['label'][k]
        if args_decoration:
            val = k
        headers['__' + k] = val

    # Print header if any
    if len(results) and template_name + '_header' in conf['template']:
        print(
            conf['template'][template_name + '_header'].decode('string-escape')
            % headers)

    # Print out formated results
    #for r in sorted(result, key=lambda x: x[1]['sn'], reverse=False):
    for r in results:
        print_template(
            r[1], template, key_max_len, headers, simple, args_decoration)


def print_template(item, template, key_max_len, data, simple, decoration):
    for key in conf['label']:
        # Output replacement
        if 'output' in conf and key in conf['output'] and key in item:
            i = 0
            for v in item[key]:
                (m, r) = conf['output'][key].split(';')
                item[key][i] = re.compile(m).sub(r, v)
                i += 1

        if key in item:
            fields = filter(lambda x: x != '' and x is not None, item[key])
            if simple:
                data[key] = ('\n' + ' ' * (key_max_len+2)).join(fields)
            else:
                data[key] = ', '.join(fields)
        else:
            data[key] = '-'

    print template.decode('string-escape') % data


def main():
    # Parse command line arguments
    (args, parser) = parse_arguments()

    # Set logging level
    level = logging.WARNING
    if args.verbose:
        level = logging.DEBUG
    logging.basicConfig(format='%(levelname)s: %(message)s', level=level)

    # Check for input
    if not args.SEARCH_STR and not args.search_filter:
        log.error('No search string or filter defined!')
        parser.print_help()
        sys.exit(1)

    # Parse config file
    get_config(args.config, parser)

    # Get search string
    search_filter = get_searchfilter(
        args.search_filter, args.search_template, args.SEARCH_STR)

    # Get template
    (template, template_name, key_max_len, simple, ldap_fields) = get_template(
        args.template, args.decoration)

    # Get search result
    results = get_result(search_filter, ldap_fields)

    # Print results
    print_results(
        results, template, template_name, key_max_len, simple, args.decoration)


if __name__ == '__main__':
    main()
