"""Functions to read and process data from a LittleChef repository"""
import os
import simplejson as json

from littlechef import runner, lib, chef
from logbook import Logger

from kitchen.settings import REPO, REPO_BASE_PATH
from kitchen.backends.plugins import plugins

log = Logger(__name__)

KITCHEN_DIR = os.path.join(
    REPO_BASE_PATH, REPO['NAME'], REPO['KITCHEN_SUBDIR'])
DATA_BAG_PATH = os.path.join(KITCHEN_DIR, "data_bags", "node")


class RepoError(Exception):
    """An error related to repository validity"""
    pass


def _check_kitchen():
    """Checks whether there is a valid Chef repository"""
    if not os.path.exists(KITCHEN_DIR):
        raise RepoError("Repo dir doesn't exist at '{0}'".format(KITCHEN_DIR))

    current_dir = os.getcwd()
    os.chdir(KITCHEN_DIR)
    in_a_kitchen, missing = runner._check_appliances()
    os.chdir(current_dir)
    if not in_a_kitchen:
        missing_str = lambda m: ' and '.join(', '.join(m).rsplit(', ', 1))
        raise RepoError("Couldn't find {0}. ".format(missing_str(missing)))
    elif not os.path.exists(DATA_BAG_PATH):
        raise RepoError("The 'node' data bag has not yet been built")
    else:
        return True


def build_node_data_bag():
    """Tells LittleChef to build the node data bag"""
    current_dir = os.getcwd()
    os.chdir(KITCHEN_DIR)
    try:
        lib.get_recipes()  # This builds metadata.json for all recipes
        chef.build_node_data_bag()
    except SystemExit as e:
        log.error(e)
    finally:
        os.chdir(current_dir)
    return True


def get_environments(nodes):
    """Returns an environments set out of chef_environment values found"""
    envs = set()
    counts = {}
    for node in nodes:
        env = node.get('chef_environment', 'none')
        envs.add(env)
        counts.setdefault(env, 0)
        counts[env] += 1
    return [{'name': env, 'counts': counts[env]} for env in sorted(envs)]


def _data_loader(data_type, name=None):
    """Loads data from LittleChef's kitchen"""
    current_dir = os.getcwd()
    os.chdir(KITCHEN_DIR)
    try:
        func = getattr(lib, "get_" + data_type)
        if name:
            data = func(name)
        else:
            data = func()
    except SystemExit as e:
        log.error(e)
        raise RepoError('Error while loading {0} files. Possibly a JSON '
                        'syntax error'.format(data_type))
    else:
        return data
    finally:
        os.chdir(current_dir)


def _load_data(data_type, name=None):
    """Loads the kitchen's node files"""
    _check_kitchen()
    if data_type not in ["node", "nodes", "roles"]:
        log.error("Unsupported data type '{0}'".format(data_type))
        return None
    return _data_loader(data_type, name)


def _load_extended_node_data(nodes):
    """Loads JSON node files from node databag, which has merged attributes"""
    data = []
    for node in nodes:
        # Read corresponding data bag item for each node
        filename = node['name'].replace(".", "_") + ".json"
        filepath = os.path.join(DATA_BAG_PATH, filename)
        if not os.path.exists(filepath):
            error = "'node' data bag was not generated correctly: item "
            error += "'data_bag/node/{0}' is missing".format(filename)
            raise RepoError(error)
        with open(filepath, 'r') as f:
            try:
                data.append(json.loads(f.read()))
            except json.JSONDecodeError as e:
                error = 'LittleChef found the following error in'
                error += ' "{0}":\n {1}'.format(filepath, str(e))
                raise RepoError(error)
    return data


def inject_plugin_data(nodes):
    """Injects kitchen plugin data"""
    for node in nodes:
        for name, plugin in plugins.iteritems():
            try:
                plugin.inject(node)
            except Exception as e:
                log.error("Plugin '{0}' had an error: {1}".format(name, e))
                continue


def group_nodes_by_host(nodes, roles='', env=''):
    """Returns a list of hosts with their virtual machines
    Hosts will only be returned if they or at least one of their guests belong
    to the specified environment and have assigned the specified roles.

    """
    hosts = filter_nodes(nodes, virt_roles='host')
    guests = filter_nodes(nodes, virt_roles='guest')
    filtered_hosts = []

    for host in hosts:
        append = False
        if filter_nodes([host], roles=roles, env=env):
            append = True
        for vm in host['virtualization'].get('guests', []):
            for guest in guests:
                if guest['fqdn'] == vm['fqdn']:
                    vm.update(guest)  # Add guest attributes to the vm
                    if filter_nodes([guest], roles=roles, env=env):
                        append = True
                    break
        if append:
            filtered_hosts.append(host)
    return filtered_hosts


def filter_nodes(nodes, env='', roles=None, virt_roles=''):
    """Returns nodes which fulfill env, roles and virt_roles criteria"""
    retval = []
    if not roles:
        roles = []
    if virt_roles:
        virt_roles = virt_roles.split(',')
    for node in nodes:
        append = True
        if env and node.get('chef_environment', 'none') != env:
            append = False
        if roles:
            if not set.intersection(
                    set(roles),
                    set([role.split("_")[0] for role in node['roles']])):
                append = False
        if virt_roles:
            # Exclude node in two cases:
            #   * the virtualization role is not in the desired virt_roles
            #   * the virtualization role is not defined for the node AND
            #     'guest' is a desired virt_role
            virt_role = node.get('virtualization', {}).get('role')
            if not virt_role in virt_roles and \
                    not ('guest' in virt_roles and not virt_role):
                append = False
        if append:
            retval.append(node)
    return retval


def get_node(name):
    """Returns the given node"""
    node = _load_data("node", name)
    if node == {'name': name, 'run_list': []}:
        # Workaround LittleChef returning an empy node file when not found
        return None
    else:
        return node


def get_nodes():
    """Returns nodes present in the repository's 'nodes' directory"""
    return _load_data("nodes")


def get_nodes_extended(nodes=None):
    """Returns node data from the automatic 'node' data_bag"""
    if nodes is None:
        nodes = _load_data("nodes")
    return _load_extended_node_data(nodes)


def get_roles():
    """Returns roles present in the repository's 'roles' directory"""
    return _load_data("roles")


def get_role_groups(roles):
    """Compiles a set of role prefixes"""
    groups = set()
    for role in roles:
        split = role['name'].split('_')
        if split[0] != REPO['EXCLUDE_ROLE_PREFIX']:
            groups.add(split[0])
    return sorted(groups)
