# Copyright (c) 2012-2016 Hewlett Packard Enterprise Development LP
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

from __future__ import absolute_import

import itertools
import logging

import six.moves


class ConfigView(object):
    def __init__(self, data, region=None, user=None):
        self.log = data.log
        self.__data = data
        self.__region = None
        self.__user = None
        if region:
            self.region = region
        if user:
            self.user = user

    @property
    def region(self):
        return self.__region

    @region.setter
    def region(self, value):
        self.__region = value
        self.log.info('set region to %s', value)

    @property
    def user(self):
        return self.__user

    @user.setter
    def user(self, value):
        self.__user = value
        self.log.info('set user to %s', value)

    def clone(self, region=None, user=None):
        region = region or self.region
        user = user or self.user
        return ConfigView(self.__data, region=region, user=user)

    def get_global_option(self, option, redact=False):
        return self.__data.lookup_global(option, redact=redact)

    def get_region_option(self, option, region=None, redact=False):
        return self.get_region_option2(option, region=region,
                                       redact=redact)[0]

    def get_region_option2(self, option, region=None, redact=False):
        region = region or self.region
        if region:
            return self.__data.lookup(self.__data.regions, region, option,
                                      redact=redact,
                                      confdict_log_name='region')
        return None, None

    def get_user_option(self, option, user=None, redact=False):
        return self.get_user_option2(option, user=user, redact=redact)[0]

    def get_user_option2(self, option, user=None, redact=False):
        user = user or self.user
        if user:
            return self.__data.lookup(self.__data.users, user, option,
                                      redact=redact, confdict_log_name='user')
        return None, None

    def get_all_region_options(self, option):
        return self.__get_all_options(self.__data.regions, option)

    def get_all_user_options(self, option):
        return self.__get_all_options(self.__data.users, option)

    @staticmethod
    def __get_all_options(confdict, option):
        matches = {}
        for section, options in six.iteritems(confdict):
            if '*' not in section and option in options:
                matches[section] = options[option]
        return matches

    @staticmethod
    def convert_to_bool(value, default=None):
        if value is None:
            return default
        elif str(value).lower() in ('true', '1', 'yes', 'on'):
            return True
        elif str(value).lower() in ('false', '0', 'no', 'off'):
            return False
        else:
            raise ValueError('value {0} is not boolean'.format(repr(value)))


class ConfigData(object):
    def __init__(self, filenames):
        self.log = logging.getLogger('Config')
        self.log.addHandler(_NullLogHandler())  # cheap warning silencing
        self.globals = {}
        self.regions = {}
        self.users = {}
        self._memo = {}
        self._parse_config(filenames)

    def _parse_config(self, filenames):
        parser = six.moves.configparser.SafeConfigParser()
        parser.read(filenames)
        for section in parser.sections():
            if section == 'global':
                self.globals = dict(parser.items(section))
            elif section.startswith('region '):
                region = section.split()[1]
                if any(chunk == '' for chunk in region.split(':')):
                    raise ValueError(
                        'configuration file region {0} must not contain '
                        'an empty namespace'.format(repr(region)))
                if '@' in region:
                    raise ValueError(
                        'configuration file region {0} must not contain '
                        '@ characters'.format(repr(region)))
                self.regions[region] = dict(parser.items(section))
                self.regions[region].setdefault('name', region.rsplit(':')[-1])
            elif section.startswith('user '):
                user = section.split()[1]
                if any(chunk == '' for chunk in user.split(':')):
                    raise ValueError(
                        'configuration file user {0} must not contain '
                        'an empty namespace'.format(repr(user)))
                if '@' in user:
                    raise ValueError(
                        'configuration file user {0} must not contain '
                        '@ characters'.format(repr(user)))
                self.users[user] = dict(parser.items(section))
            # Ignore unrecognized sections for forward compatibility

    def lookup_global(self, option, redact=False):
        self._memo.setdefault(id(self.globals), {})
        if option in self._memo[id(self.globals)]:
            return self._memo[id(self.globals)][option]
        else:
            self.log.info('finding global option %s', option)
            value = self.globals.get(option)
            self._memo[id(self.globals)][option] = value
            if value and redact:
                self.log.info('  found   %s = <redacted>', option)
            elif value:
                self.log.info('  found   %s = %s', option, value)
            else:
                self.log.info('  novalue for %s', option)
            return value

    def lookup(self, confdict, section, option, redact=False, seen=None,
               confdict_log_name=None):
        self._memo.setdefault(id(confdict), {})
        if (section, option) in self._memo[id(confdict)]:
            return self._memo[id(confdict)][(section, option)]
        else:
            if confdict_log_name is not None:
                self.log.info('finding %s option %s', confdict_log_name,
                              option)
            values = self.__lookup(confdict, section, option, redact=redact,
                                   seen=seen)
            self._memo[id(confdict)][(section, option)] = values
            return values

    def __lookup(self, confdict, section, option, redact=False, seen=None):
        def redact_if_necessary(value):
            if redact:
                return '<redacted>'
            else:
                return value

        if seen is None:
            seen = []

        # Try wildcard combinations, from most specific to least specific,
        # except for the '*' section, which we handle later on because it is
        # also the global default.
        section_bits = section.split(':')
        for prd in itertools.product((True, False), repeat=len(section_bits)):
            if prd == (False,):
                continue  # We'll handle '*' later.
            s_section = ':'.join(section_bits[i] if prd[i] else '*'
                                 for i in range(len(section_bits)))
            self.log.debug('  section %s', s_section)
            if s_section in confdict and option in confdict[s_section]:
                value = confdict[s_section][option]
                # Check if we're redirected to another section.
                if value.startswith('use '):
                    value_bits = value.split()
                    if len(value_bits) == 1:
                        raise ValueError("something must follow 'use' in {0}"
                                         .format(value))
                    new_section = value_bits[1]
                    if len(value_bits) > 2:
                        new_option = value_bits[2]
                    else:
                        new_option = option
                    self.log.debug('  jump-to %s::%s (deferred)', new_section,
                                   new_option)
                    if new_section not in seen:
                        return self.lookup(confdict, new_section, new_option,
                                           redact=redact,
                                           seen=(seen + [section]))
                    else:
                        self.log.warning('  aborting jump due to a loop')
                # We're done!
                self.log.info('  found   %s::%s = %s', s_section, option,
                              redact_if_necessary(repr(value)))
                return value, s_section
        # Then see if we can find an exact match with a prefix in front of it.
        # Example:  'us-east-1' -> 'aws:us-east-1'
        prefixed_counts = {}
        for s_section in confdict:
            s_section_bits = s_section.split(':')
            if (len(s_section_bits) > len(section_bits) and
                    s_section_bits[-len(section_bits):] == section_bits):
                # It is longer and its last bits are the same as the entirety
                # of section_bits
                prefixed_counts.setdefault(len(s_section_bits), [])
                prefixed_counts[len(s_section_bits)].append(s_section)
        if prefixed_counts:
            shortest_prefixed = prefixed_counts[min(prefixed_counts)]
            if len(shortest_prefixed) == 1:
                self.log.debug('  jump-to %s::%s (from %s)',
                               shortest_prefixed[0], option, section)
                if shortest_prefixed[0] not in seen:
                    return self.lookup(confdict, shortest_prefixed[0], option,
                                       redact=redact, seen=(seen + [section]))
                else:
                    self.log.warning('  aborting jump due to a loop')
            else:
                raise ValueError(
                    '{0} is ambiguous; closest matches are {1}'.format(
                        repr(section), ', '.join(shortest_prefixed)))
        # Finally, try the global default, '*'.
        self.log.debug('  section *')
        if '*' in confdict and option in confdict['*']:
            self.log.info('  found   *::%s = %s', option,
                          redact_if_necessary(repr(value)))
            return value, '*'
        self.log.info('  novalue for %s', option)
        return None, None


class _NullLogHandler(logging.Handler):
    def handle(self, record):
        pass

    def emit(self, record):
        pass

    def createLock(self):
        self.lock = None
