# Copyright (c) 2017 Ansible by Red Hat
# All Rights Reserved.

# Python
import re
import logging
from collections import OrderedDict


# Logger is used for any data-related messages so that the log level
# can be adjusted on command invocation
logger = logging.getLogger('awx.main.commands.inventory_import')


__all__ = ['MemHost', 'MemGroup', 'MemInventory',
           'mem_data_to_dict', 'dict_to_mem_data']


ipv6_port_re = re.compile(r'^\[([A-Fa-f0-9:]{3,})\]:(\d+?)$')


# Models for in-memory objects that represent an inventory


class MemObject(object):
    '''
    Common code shared between in-memory groups and hosts.
    '''

    def __init__(self, name):
        assert name, 'no name'
        self.name = name


class MemGroup(MemObject):
    '''
    In-memory representation of an inventory group.
    '''

    def __init__(self, name):
        super(MemGroup, self).__init__(name)
        self.children = []
        self.hosts = []
        self.variables = {}
        self.parents = []
        # Used on the "all" group in place of previous global variables.
        # maps host and group names to hosts to prevent redudant additions
        self.all_hosts = {}
        self.all_groups = {}
        self.variables = {}
        logger.debug('Loaded group: %s', self.name)

    def __repr__(self):
        return '<_in-memory-group_ `{}`>'.format(self.name)

    def add_child_group(self, group):
        assert group.name is not 'all', 'group name is all'
        assert isinstance(group, MemGroup), 'not MemGroup instance'
        logger.debug('Adding child group %s to parent %s', group.name, self.name)
        if group not in self.children:
            self.children.append(group)
        if self not in group.parents:
            group.parents.append(self)

    def add_host(self, host):
        assert isinstance(host, MemHost), 'not MemHost instance'
        logger.debug('Adding host %s to group %s', host.name, self.name)
        if host not in self.hosts:
            self.hosts.append(host)

    def debug_tree(self, group_names=None):
        group_names = group_names or set()
        if self.name in group_names:
            return
        logger.debug('Dumping tree for group "%s":', self.name)
        logger.debug('- Vars: %r', self.variables)
        for h in self.hosts:
            logger.debug('- Host: %s, %r',  h.name, h.variables)
        for g in self.children:
            logger.debug('- Child: %s', g.name)
        logger.debug('----')
        group_names.add(self.name)
        for g in self.children:
            g.debug_tree(group_names)


class MemHost(MemObject):
    '''
    In-memory representation of an inventory host.
    '''

    def __init__(self, name, port=None):
        super(MemHost, self).__init__(name)
        self.variables = {}
        self.instance_id = None
        self.name = name
        if port:
            # was `ansible_ssh_port` in older Ansible versions
            self.variables['ansible_port'] = port
        logger.debug('Loaded host: %s', self.name)

    def __repr__(self):
        return '<_in-memory-host_ `{}`>'.format(self.name)


class MemInventory(object):
    '''
    Common functions for an inventory loader from a given source.
    '''
    def __init__(self, all_group=None, group_filter_re=None, host_filter_re=None):
        if all_group:
            assert isinstance(all_group, MemGroup), '{} is not MemGroup instance'.format(all_group)
            self.all_group = all_group
        else:
            self.all_group = self.create_group('all')
        self.group_filter_re = group_filter_re
        self.host_filter_re = host_filter_re

    def create_host(self, host_name, port):
        host = MemHost(host_name, port)
        self.all_group.all_hosts[host_name] = host
        return host

    def get_host(self, name):
        '''
        Return a MemHost instance from host name, creating if needed.  If name
        contains brackets, they will NOT be interpreted as a host pattern.
        '''
        m = ipv6_port_re.match(name)
        if m:
            host_name = m.groups()[0]
            port = int(m.groups()[1])
        elif name.count(':') == 1:
            host_name = name.split(':')[0]
            try:
                port = int(name.split(':')[1])
            except (ValueError, UnicodeDecodeError):
                logger.warning(u'Invalid port "%s" for host "%s"',
                               name.split(':')[1], host_name)
                port = None
        else:
            host_name = name
            port = None
        if self.host_filter_re and not self.host_filter_re.match(host_name):
            logger.debug('Filtering host %s', host_name)
            return None
        if host_name not in self.all_group.all_hosts:
            self.create_host(host_name, port)
        return self.all_group.all_hosts[host_name]

    def create_group(self, group_name):
        group = MemGroup(group_name)
        if group_name not in ['all', 'ungrouped']:
            self.all_group.all_groups[group_name] = group
        return group

    def get_group(self, name, all_group=None, child=False):
        '''
        Return a MemGroup instance from group name, creating if needed.
        '''
        all_group = all_group or self.all_group
        if name in ['all', 'ungrouped']:
            return all_group
        if self.group_filter_re and not self.group_filter_re.match(name):
            logger.debug('Filtering group %s', name)
            return None
        if name not in self.all_group.all_groups:
            group = self.create_group(name)
            if not child:
                all_group.add_child_group(group)
        return self.all_group.all_groups[name]

    def delete_empty_groups(self):
        for name, group in self.all_group.all_groups.items():
            if not group.children and not group.hosts and not group.variables:
                logger.debug('Removing empty group %s', name)
                for parent in group.parents:
                    if group in parent.children:
                        parent.children.remove(group)
                del self.all_group.all_groups[name]


# Conversion utilities

def mem_data_to_dict(inventory):
    '''
    Given an in-memory construct of an inventory, returns a dictionary that
    follows Ansible guidelines on the structure of dynamic inventory sources

    May be replaced by removing in-memory constructs within this file later
    '''
    all_group = inventory.all_group
    inventory_data = OrderedDict([])
    # Save hostvars to _meta
    inventory_data['_meta'] = OrderedDict([])
    hostvars = OrderedDict([])
    for name, host_obj in all_group.all_hosts.items():
        hostvars[name] = host_obj.variables
    inventory_data['_meta']['hostvars'] = hostvars
    # Save children of `all` group
    inventory_data['all'] = OrderedDict([])
    if all_group.variables:
        inventory_data['all']['vars'] = all_group.variables
    inventory_data['all']['children'] = [c.name for c in all_group.children]
    inventory_data['all']['children'].append('ungrouped')
    # Save details of declared groups individually
    ungrouped_hosts = set(all_group.all_hosts.keys())
    for name, group_obj in all_group.all_groups.items():
        group_host_names = [h.name for h in group_obj.hosts]
        group_children_names = [c.name for c in group_obj.children]
        group_data = OrderedDict([])
        if group_host_names:
            group_data['hosts'] = group_host_names
            ungrouped_hosts.difference_update(group_host_names)
        if group_children_names:
            group_data['children'] = group_children_names
        if group_obj.variables:
            group_data['vars'] = group_obj.variables
        inventory_data[name] = group_data
    # Save ungrouped hosts
    inventory_data['ungrouped'] = OrderedDict([])
    if ungrouped_hosts:
        inventory_data['ungrouped']['hosts'] = list(ungrouped_hosts)
    return inventory_data


def dict_to_mem_data(data, inventory=None):
    '''
    In-place operation on `inventory`, adds contents from `data` to the
    in-memory representation of memory.
    May be destructive on `data`
    '''
    assert isinstance(data, dict), 'Expected dict, received {}'.format(type(data))
    if inventory is None:
        inventory = MemInventory()

    _meta = data.pop('_meta', {})

    for k,v in data.iteritems():
        group = inventory.get_group(k)
        if not group:
            continue

        # Load group hosts/vars/children from a dictionary.
        if isinstance(v, dict):
            # Process hosts within a group.
            hosts = v.get('hosts', {})
            if isinstance(hosts, dict):
                for hk, hv in hosts.iteritems():
                    host = inventory.get_host(hk)
                    if not host:
                        continue
                    if isinstance(hv, dict):
                        host.variables.update(hv)
                    else:
                        logger.warning('Expected dict of vars for '
                                       'host "%s", got %s instead',
                                       hk, str(type(hv)))
                    group.add_host(host)
            elif isinstance(hosts, (list, tuple)):
                for hk in hosts:
                    host = inventory.get_host(hk)
                    if not host:
                        continue
                    group.add_host(host)
            else:
                logger.warning('Expected dict or list of "hosts" for '
                               'group "%s", got %s instead', k,
                               str(type(hosts)))
            # Process group variables.
            vars = v.get('vars', {})
            if isinstance(vars, dict):
                group.variables.update(vars)
            else:
                logger.warning('Expected dict of vars for '
                               'group "%s", got %s instead',
                               k, str(type(vars)))
            # Process child groups.
            children = v.get('children', [])
            if isinstance(children, (list, tuple)):
                for c in children:
                    child = inventory.get_group(c, inventory.all_group, child=True)
                    if child and c != 'ungrouped':
                        group.add_child_group(child)
            else:
                logger.warning('Expected list of children for '
                               'group "%s", got %s instead',
                               k, str(type(children)))

        # Load host names from a list.
        elif isinstance(v, (list, tuple)):
            for h in v:
                host = inventory.get_host(h)
                if not host:
                    continue
                group.add_host(host)
        else:
            logger.warning('')
            logger.warning('Expected dict or list for group "%s", '
                           'got %s instead', k, str(type(v)))

        if k not in ['all', 'ungrouped']:
            inventory.all_group.add_child_group(group)

    if _meta:
        for k,v in inventory.all_group.all_hosts.iteritems():
            meta_hostvars = _meta['hostvars'].get(k, {})
            if isinstance(meta_hostvars, dict):
                v.variables.update(meta_hostvars)
            else:
                logger.warning('Expected dict of vars for '
                               'host "%s", got %s instead',
                               k, str(type(meta_hostvars)))

    return inventory
