# pylint: disable=bad-continuation,missing-docstring,no-self-use,invalid-name,too-many-instance-attributes,too-few-public-methods

from __future__ import (absolute_import, print_function)

import os
import sys
import logging
import yaml
from pkg_resources import resource_filename


installer_log = logging.getLogger('installer')

CONFIG_PERSIST_SETTINGS = [
    'ansible_ssh_user',
    'ansible_callback_facts_yaml',
    'ansible_inventory_path',
    'ansible_log_path',
    'deployment',
    'version',
    'variant',
    'variant_subtype',
    'variant_version',
]

DEPLOYMENT_VARIABLES_BLACKLIST = [
    'hosts',
    'roles',
]

HOST_VARIABLES_BLACKLIST = [
    'ip',
    'public_ip',
    'hostname',
    'public_hostname',
    'node_labels',
    'containerized',
    'preconfigured',
    'schedulable',
    'other_variables',
    'roles',
]

DEFAULT_REQUIRED_FACTS = ['ip', 'public_ip', 'hostname', 'public_hostname']
PRECONFIGURED_REQUIRED_FACTS = ['hostname', 'public_hostname']


def print_read_config_error(error, path='the configuration file'):
    message = """
Error loading config. {}.

See https://docs.openshift.com/enterprise/latest/install_config/install/quick_install.html#defining-an-installation-configuration-file
for information on creating a configuration file or delete {} and re-run the installer.
"""
    print(message.format(error, path))


class OOConfigFileError(Exception):
    """The provided config file path can't be read/written
    """
    pass


class OOConfigInvalidHostError(Exception):
    """ Host in config is missing both ip and hostname. """
    pass


class Host(object):
    """ A system we will or have installed OpenShift on. """
    def __init__(self, **kwargs):
        self.ip = kwargs.get('ip', None)
        self.hostname = kwargs.get('hostname', None)
        self.public_ip = kwargs.get('public_ip', None)
        self.public_hostname = kwargs.get('public_hostname', None)
        self.connect_to = kwargs.get('connect_to', None)

        self.preconfigured = kwargs.get('preconfigured', None)
        self.schedulable = kwargs.get('schedulable', None)
        self.new_host = kwargs.get('new_host', None)
        self.containerized = kwargs.get('containerized', False)
        self.node_labels = kwargs.get('node_labels', '')

        # allowable roles: master, node, etcd, storage, master_lb
        self.roles = kwargs.get('roles', [])

        self.other_variables = kwargs.get('other_variables', {})

        if self.connect_to is None:
            raise OOConfigInvalidHostError(
                "You must specify either an ip or hostname as 'connect_to'")

    def __str__(self):
        return self.connect_to

    def __repr__(self):
        return self.connect_to

    def to_dict(self):
        """ Used when exporting to yaml. """
        d = {}

        for prop in ['ip', 'hostname', 'public_ip', 'public_hostname', 'connect_to',
                     'preconfigured', 'containerized', 'schedulable', 'roles', 'node_labels', ]:
            # If the property is defined (not None or False), export it:
            if getattr(self, prop):
                d[prop] = getattr(self, prop)
        for variable, value in self.other_variables.items():
            d[variable] = value

        return d

    def is_master(self):
        return 'master' in self.roles

    def is_node(self):
        return 'node' in self.roles

    def is_master_lb(self):
        return 'master_lb' in self.roles

    def is_storage(self):
        return 'storage' in self.roles

    def is_etcd(self):
        """ Does this host have the etcd role """
        return 'etcd' in self.roles

    def is_etcd_member(self, all_hosts):
        """ Will this host be a member of a standalone etcd cluster. """
        if not self.is_master():
            return False
        masters = [host for host in all_hosts if host.is_master()]
        if len(masters) > 1:
            return True
        return False

    def is_dedicated_node(self):
        """ Will this host be a dedicated node. (not a master) """
        return self.is_node() and not self.is_master()

    def is_schedulable_node(self, all_hosts):
        """ Will this host be a node marked as schedulable. """
        if not self.is_node():
            return False
        if not self.is_master():
            return True

        masters = [host for host in all_hosts if host.is_master()]
        nodes = [host for host in all_hosts if host.is_node()]
        if len(masters) == len(nodes):
            return True
        return False


class Role(object):
    """ A role that will be applied to a host. """
    def __init__(self, name, variables):
        self.name = name
        self.variables = variables

    def __str__(self):
        return self.name

    def __repr__(self):
        return self.name

    def to_dict(self):
        """ Used when exporting to yaml. """
        d = {}
        for prop in ['name', 'variables']:
            # If the property is defined (not None or False), export it:
            if getattr(self, prop):
                d[prop] = getattr(self, prop)
        return d


class Deployment(object):
    def __init__(self, **kwargs):
        self.hosts = kwargs.get('hosts', [])
        self.roles = kwargs.get('roles', {})
        self.variables = kwargs.get('variables', {})


class OOConfig(object):
    default_dir = os.path.normpath(
        os.environ.get('XDG_CONFIG_HOME',
                       os.environ['HOME'] + '/.config/') + '/openshift/')
    default_file = '/installer.cfg.yml'

    def __init__(self, config_path):
        if config_path:
            self.config_path = os.path.normpath(config_path)
        else:
            self.config_path = os.path.normpath(self.default_dir +
                                                self.default_file)
        self.deployment = Deployment(hosts=[], roles={}, variables={})
        self.settings = {}
        self._read_config()
        self._set_defaults()

    # pylint: disable=too-many-branches
    #         Lots of different checks ran in a single method, could
    #         use a little refactoring-love some time
    def _read_config(self):
        installer_log.debug("Attempting to read the OO Config")
        try:
            installer_log.debug("Attempting to see if the provided config file exists: %s", self.config_path)
            if os.path.exists(self.config_path):
                installer_log.debug("We think the config file exists: %s", self.config_path)
                with open(self.config_path, 'r') as cfgfile:
                    loaded_config = yaml.safe_load(cfgfile.read())

                if 'version' not in loaded_config:
                    print_read_config_error('Legacy configuration file found', self.config_path)
                    sys.exit(0)

                if loaded_config.get('version', '') == 'v1':
                    loaded_config = self._upgrade_v1_config(loaded_config)

                try:
                    host_list = loaded_config['deployment']['hosts']
                    role_list = loaded_config['deployment']['roles']
                except KeyError as e:
                    print_read_config_error("No such key: {}".format(e), self.config_path)
                    sys.exit(0)

                for setting in CONFIG_PERSIST_SETTINGS:
                    persisted_value = loaded_config.get(setting)
                    if persisted_value is not None:
                        self.settings[setting] = str(persisted_value)

                # We've loaded any persisted configs, let's verify any
                # paths which are required for a correct and complete
                # install

                # - ansible_callback_facts_yaml - Settings from a
                #   pervious run. If the file doesn't exist then we
                #   will just warn about it for now and recollect the
                #   facts.
                if self.settings.get('ansible_callback_facts_yaml', None) is not None:
                    if not os.path.exists(self.settings['ansible_callback_facts_yaml']):
                        # Cached callback facts file does not exist
                        installer_log.warning("The specified 'ansible_callback_facts_yaml'"
                                              "file does not exist (%s)",
                                              self.settings['ansible_callback_facts_yaml'])
                        installer_log.debug("Remote system facts will be collected again later")
                        self.settings.pop('ansible_callback_facts_yaml')

                for setting in loaded_config['deployment']:
                    try:
                        if setting not in DEPLOYMENT_VARIABLES_BLACKLIST:
                            self.deployment.variables[setting] = \
                                str(loaded_config['deployment'][setting])
                    except KeyError:
                        continue

                # Parse the hosts into DTO objects:
                for host in host_list:
                    host['other_variables'] = {}
                    for variable, value in host.items():
                        if variable not in HOST_VARIABLES_BLACKLIST:
                            host['other_variables'][variable] = value
                    self.deployment.hosts.append(Host(**host))

                # Parse the roles into Objects
                for name, variables in role_list.items():
                    self.deployment.roles.update({name: Role(name, variables)})

        except IOError as ferr:
            raise OOConfigFileError('Cannot open config file "{}": {}'.format(ferr.filename,
                                                                              ferr.strerror))
        except yaml.scanner.ScannerError:
            raise OOConfigFileError(
                'Config file "{}" is not a valid YAML document'.format(self.config_path))
        installer_log.debug("Parsed the config file")

    def _upgrade_v1_config(self, config):
        new_config_data = {}
        new_config_data['deployment'] = {}
        new_config_data['deployment']['hosts'] = []
        new_config_data['deployment']['roles'] = {}
        new_config_data['deployment']['variables'] = {}

        role_list = {}

        if config.get('ansible_ssh_user', False):
            new_config_data['deployment']['ansible_ssh_user'] = config['ansible_ssh_user']

        if config.get('variant', False):
            new_config_data['variant'] = config['variant']

        if config.get('variant_version', False):
            new_config_data['variant_version'] = config['variant_version']

        for host in config['hosts']:
            host_props = {}
            host_props['roles'] = []
            host_props['connect_to'] = host['connect_to']

            for prop in ['ip', 'public_ip', 'hostname', 'public_hostname', 'containerized', 'preconfigured']:
                host_props[prop] = host.get(prop, None)

            for role in ['master', 'node', 'master_lb', 'storage', 'etcd']:
                if host.get(role, False):
                    host_props['roles'].append(role)
                    role_list[role] = ''

            new_config_data['deployment']['hosts'].append(host_props)

        new_config_data['deployment']['roles'] = role_list

        return new_config_data

    def _set_defaults(self):
        installer_log.debug("Setting defaults, current OOConfig settings: %s", self.settings)

        if 'ansible_inventory_directory' not in self.settings:
            self.settings['ansible_inventory_directory'] = self._default_ansible_inv_dir()

        if not os.path.exists(self.settings['ansible_inventory_directory']):
            installer_log.debug("'ansible_inventory_directory' does not exist, "
                                "creating it now (%s)",
                                self.settings['ansible_inventory_directory'])
            os.makedirs(self.settings['ansible_inventory_directory'])
        else:
            installer_log.debug("We think this 'ansible_inventory_directory' "
                                "is OK: %s",
                                self.settings['ansible_inventory_directory'])

        if 'ansible_plugins_directory' not in self.settings:
            self.settings['ansible_plugins_directory'] = \
                resource_filename(__name__, 'ansible_plugins')
            installer_log.debug("We think the ansible plugins directory should be: %s (it is not already set)",
                                self.settings['ansible_plugins_directory'])
        else:
            installer_log.debug("The ansible plugins directory is already set: %s",
                                self.settings['ansible_plugins_directory'])

        if 'version' not in self.settings:
            self.settings['version'] = 'v2'

        if 'ansible_callback_facts_yaml' not in self.settings:
            installer_log.debug("No 'ansible_callback_facts_yaml' in self.settings")
            self.settings['ansible_callback_facts_yaml'] = '%s/callback_facts.yaml' % \
                self.settings['ansible_inventory_directory']
            installer_log.debug("Value: %s", self.settings['ansible_callback_facts_yaml'])
        else:
            installer_log.debug("'ansible_callback_facts_yaml' already set "
                                "in self.settings: %s",
                                self.settings['ansible_callback_facts_yaml'])

        if 'ansible_ssh_user' not in self.settings:
            self.settings['ansible_ssh_user'] = ''

        self.settings['ansible_inventory_path'] = \
            '{}/hosts'.format(os.path.dirname(self.config_path))

        # clean up any empty sets
        empty_keys = []
        for setting in self.settings:
            if not self.settings[setting]:
                empty_keys.append(setting)
        for key in empty_keys:
            self.settings.pop(key)

        installer_log.debug("Updated OOConfig settings: %s", self.settings)

    def _default_ansible_inv_dir(self):
        return os.path.normpath(
            os.path.dirname(self.config_path) + "/.ansible")

    def calc_missing_facts(self):
        """
        Determine which host facts are not defined in the config.

        Returns a hash of host to a list of the missing facts.
        """
        result = {}

        for host in self.deployment.hosts:
            missing_facts = []
            if host.preconfigured:
                required_facts = PRECONFIGURED_REQUIRED_FACTS
            else:
                required_facts = DEFAULT_REQUIRED_FACTS

            for required_fact in required_facts:
                if not getattr(host, required_fact):
                    missing_facts.append(required_fact)
            if len(missing_facts) > 0:
                result[host.connect_to] = missing_facts
        return result

    def save_to_disk(self):
        out_file = open(self.config_path, 'w')
        out_file.write(self.yaml())
        out_file.close()

    def persist_settings(self):
        p_settings = {}

        for setting in CONFIG_PERSIST_SETTINGS:
            if setting in self.settings and self.settings[setting]:
                p_settings[setting] = self.settings[setting]

        p_settings['deployment'] = {}
        p_settings['deployment']['hosts'] = []
        p_settings['deployment']['roles'] = {}

        for host in self.deployment.hosts:
            p_settings['deployment']['hosts'].append(host.to_dict())

        for name, role in self.deployment.roles.items():
            p_settings['deployment']['roles'][name] = role.variables

        for setting in self.deployment.variables:
            if setting not in DEPLOYMENT_VARIABLES_BLACKLIST:
                p_settings['deployment'][setting] = self.deployment.variables[setting]

        try:
            p_settings['variant'] = self.settings['variant']
            p_settings['variant_version'] = self.settings['variant_version']

            if self.settings['ansible_inventory_directory'] != self._default_ansible_inv_dir():
                p_settings['ansible_inventory_directory'] = self.settings['ansible_inventory_directory']
        except KeyError as e:
            print("Error persisting settings: {}".format(e))
            sys.exit(0)

        return p_settings

    def yaml(self):
        return yaml.safe_dump(self.persist_settings(), default_flow_style=False)

    def __str__(self):
        return self.yaml()

    def get_host(self, name):
        for host in self.deployment.hosts:
            if host.connect_to == name:
                return host
        return None

    def get_host_roles_set(self):
        roles_set = set()
        for host in self.deployment.hosts:
            for role in host.roles:
                roles_set.add(role)

        return roles_set
