import copy
import os

from cli.src.Config import Config
from cli.src.helpers.build_io import (get_ansible_path,
                                      get_ansible_path_for_build,
                                      get_ansible_vault_path)
from cli.src.helpers.data_loader import (load_all_schema_objs_from_directory,
                                         load_schema_obj, types)
from cli.src.helpers.doc_list_helpers import (ExpectedSingleResultException,
                                              select_first, select_single)
from cli.src.helpers.naming_helpers import to_feature_name, to_role_name
from cli.src.helpers.ObjDict import ObjDict
from cli.src.helpers.yaml_helpers import dump
from cli.src.schema.DefaultMerger import DefaultMerger
from cli.src.Step import Step
from cli.version import VERSION


class AnsibleVarsGenerator(Step):

    def __init__(self, inventory_creator=None, inventory_upgrade=None):
        super().__init__(__name__)

        self.inventory_creator = inventory_creator
        self.inventory_upgrade = inventory_upgrade
        self.roles_with_generated_vars = []
        self.manifest_docs = []

        if inventory_creator is not None and inventory_upgrade is None:
            self.cluster_model = inventory_creator.cluster_model
            self.config_docs = [self.cluster_model] + inventory_creator.config_docs
        elif inventory_upgrade is not None and inventory_creator is None:
            self.cluster_model = inventory_upgrade.cluster_model
            self.config_docs = []
            defaults = load_all_schema_objs_from_directory(types.DEFAULT, 'common', 'configuration')
            for default in defaults:
                config_doc = select_first(inventory_upgrade.config_docs, lambda x: x.kind == default.kind)
                if config_doc is None:
                    self.config_docs.append(default)
                else:
                    self.config_docs.append(config_doc)
            self.manifest_docs = inventory_upgrade.manifest_docs
        else:
            raise Exception('Invalid AnsibleVarsGenerator configuration')

    def __enter__(self):
        super().__enter__()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def generate(self):
        self.logger.info('Generate Ansible vars')
        self.is_upgrade_run = self.inventory_creator is None
        if self.is_upgrade_run:
            ansible_dir = get_ansible_path_for_build(self.inventory_upgrade.build_dir)
        else:
            ansible_dir = get_ansible_path(self.cluster_model.specification.name)

        cluster_config_file_path = os.path.join(ansible_dir, 'roles', 'common', 'vars', 'main.yml')
        clean_cluster_model = self.get_clean_cluster_model()
        with open(cluster_config_file_path, 'w') as stream:
            if 'name' in clean_cluster_model:
                del clean_cluster_model['name']  # reserved word in ansible!
            dump(clean_cluster_model, stream)

        if self.is_upgrade_run:
            # For upgrade we always need common, repository, image_registry, node_exporter and postgresql. Common is
            # already provisioned from the cluster model constructed from the inventory. As PostgreSQL configuration
            # is changed between versions (e.g. wal_keep_segments -> wal_keep_size) and sometimes previous parameters
            # are not compatible with the new ones, defaults are used for template processing
            roles_with_defaults = [
                'haproxy', 'image_registry', 'jmx_exporter', 'kafka_exporter',
                'node_exporter', 'postgres_exporter', 'postgresql', 'repository'
                ]
            # now lets add any external configs we want to load
            roles_with_defaults = [*roles_with_defaults, *self.inventory_upgrade.get_new_config_roles()]
            # In special cases (like haproxy), where user specifies majority of the config, it's easier (and less
            # awkward) to re-render config templates instead of modifying (for example with regular expressions)
            # no-longer-compatible config files.
            roles_with_manifest = ['filebeat', 'postgresql', 'repository']
        else:
            roles_with_defaults = self.inventory_creator.get_enabled_roles()
            roles_with_manifest = []  # applies only to upgrades

        for role in roles_with_defaults:
            kind = 'configuration/' + to_feature_name(role)

            document = select_first(self.config_docs, lambda x: x.kind == kind)
            if document is None:
                self.logger.warn('No config document for enabled role: ' + role)
                continue
            document.specification['provider'] = self.cluster_model.provider

            self.write_role_vars(ansible_dir, role, document)

        for role in roles_with_manifest:
            kind = 'configuration/' + to_feature_name(role)

            self.write_role_manifest_vars(ansible_dir, role, kind)

        self.populate_group_vars(ansible_dir)

    def write_role_vars(self, ansible_dir, role, document, vars_file_name='main.yml'):
        vars_dir = os.path.join(ansible_dir, 'roles', to_role_name(role), 'vars')
        if not os.path.exists(vars_dir):
            os.makedirs(vars_dir)

        vars_file_path = os.path.join(vars_dir, vars_file_name)

        with open(vars_file_path, 'w') as stream:
            if 'name' in document:
                del document['name']  # reserved word in ansible!
            dump(document, stream)

        if vars_file_name == 'main.yml':
            self.roles_with_generated_vars.append(to_role_name(role))

    def write_role_manifest_vars(self, ansible_dir, role, kind):
        try:
            cluster_model = select_single(self.manifest_docs, lambda x: x.kind == 'epiphany-cluster')
        except ExpectedSingleResultException:
            return  # skip

        document = select_first(self.manifest_docs, lambda x: x.kind == kind)
        if document is None:
            # If there is no document provided by the user, then fallback to defaults
            document = load_schema_obj(types.DEFAULT, 'common', kind)
            # Inject the required "version" attribute
            document['version'] = VERSION

        # Copy the "provider" value from the cluster model
        document['provider'] = cluster_model['provider']

        # Merge the document with defaults
        with DefaultMerger([document]) as doc_merger:
            document = doc_merger.run()[0]

        self.write_role_vars(ansible_dir, role, document, vars_file_name='manifest.yml')

    def populate_group_vars(self, ansible_dir):
        main_vars = ObjDict()
        main_vars['admin_user'] = self.cluster_model.specification.admin_user
        main_vars['validate_certs'] = Config().validate_certs
        main_vars['offline_requirements'] = Config().offline_requirements
        main_vars['wait_for_pods'] = Config().wait_for_pods
        main_vars['is_upgrade_run'] = self.is_upgrade_run
        main_vars['roles_with_generated_vars'] = sorted(self.roles_with_generated_vars)
        main_vars['upgrade_components'] = Config().upgrade_components
        main_vars['epiphany_version'] = VERSION

        # Consider to move this to the provider level.
        if self.cluster_model.provider != 'any':
            main_vars['k8s_as_cloud_service'] = self.cluster_model.specification.cloud.k8s_as_cloud_service
        else:
            main_vars['k8s_as_cloud_service'] = False

        if self.is_upgrade_run:
            shared_config_doc = self.get_shared_config_from_manifest()
        else:
            shared_config_doc = select_first(self.config_docs, lambda x: x.kind == 'configuration/shared-config')

        # Fallback if there is completely no trace of the shared-config doc
        if shared_config_doc is None:
            shared_config_doc = load_schema_obj(types.DEFAULT, 'common', 'configuration/shared-config')

        self.set_vault_path(shared_config_doc)
        main_vars.update(shared_config_doc.specification)

        vars_dir = os.path.join(ansible_dir, 'group_vars')
        if not os.path.exists(vars_dir):
            os.makedirs(vars_dir)

        vars_file_name = 'all.yml'
        vars_file_path = os.path.join(vars_dir, vars_file_name)

        with open(vars_file_path, 'a') as stream:
            dump(main_vars, stream)

    def set_vault_path(self, shared_config):
        if shared_config.specification.vault_location == '':
            shared_config.specification.vault_tmp_file_location = Config().vault_password_location
            cluster_name = self.get_cluster_name()
            shared_config.specification.vault_location = get_ansible_vault_path(cluster_name)

    def get_cluster_name(self):
        if 'name' in self.cluster_model.specification.keys():
            return self.cluster_model.specification.name
        elif self.inventory_upgrade is not None:
            return os.path.basename(self.inventory_upgrade.build_dir)
        return 'default'

    def get_clean_cluster_model(self):
        cluster_model = copy.copy(self.cluster_model)
        self.clear_object(cluster_model, 'credentials')
        return cluster_model

    def get_shared_config_from_manifest(self):
        # Reuse shared config from existing manifest
        # Shared config contains the use_ha_control_plane flag which is required during upgrades

        cluster_model = select_single(self.manifest_docs, lambda x: x.kind == 'epiphany-cluster')

        try:
            shared_config_doc = select_single(self.manifest_docs, lambda x: x.kind == 'configuration/shared-config')
            shared_config_doc['provider'] = cluster_model['provider']
        except ExpectedSingleResultException:
            # If there is no shared-config doc inside the manifest file, this is probably a v0.3 cluster
            # Returning None here (there is nothing to merge at this point) and
            # hoping that the shared-config doc from defaults will be enough
            return None

        # Remove un-used supported_os list if present from shared/config from manifest so we avoid namedlist merging errors.
        # This has been refactored in from Epicli 1.0.x and no longer needed at this stage.
        if hasattr(shared_config_doc.specification, 'supported_os'):
            del shared_config_doc.specification['supported_os']

        # Merge the shared config doc with defaults
        with DefaultMerger([shared_config_doc]) as doc_merger:
            shared_config_doc = doc_merger.run()[0]
            del shared_config_doc['provider']

        return shared_config_doc

    def clear_object(self, obj_to_clean, key_to_clean):
        for key, val in obj_to_clean.items():
            if key == key_to_clean:
                obj_to_clean[key] = ''
                continue
            if isinstance(obj_to_clean[key], ObjDict):
                self.clear_object(obj_to_clean[key], key_to_clean)

[((62, 35, 62, 99), 'os.path.join', 'os.path.join', ({(62, 48, 62, 59): 'ansible_dir', (62, 61, 62, 68): '"""roles"""', (62, 70, 62, 78): '"""common"""', (62, 80, 62, 86): '"""vars"""', (62, 88, 62, 98): '"""main.yml"""'}, {}), "(ansible_dir, 'roles', 'common', 'vars', 'main.yml')", False, 'import os\n'), ((111, 25, 111, 63), 'os.path.join', 'os.path.join', ({(111, 38, 111, 46): 'vars_dir', (111, 48, 111, 62): 'vars_file_name'}, {}), '(vars_dir, vars_file_name)', False, 'import os\n'), ((127, 19, 127, 77), 'cli.src.helpers.doc_list_helpers.select_first', 'select_first', ({(127, 32, 127, 50): 'self.manifest_docs', (127, 52, 127, 76): 'lambda x: x.kind == kind'}, {}), '(self.manifest_docs, lambda x: x.kind == kind)', False, 'from cli.src.helpers.doc_list_helpers import ExpectedSingleResultException, select_first, select_single\n'), ((144, 20, 144, 29), 'cli.src.helpers.ObjDict.ObjDict', 'ObjDict', ({}, {}), '()', False, 'from cli.src.helpers.ObjDict import ObjDict\n'), ((172, 19, 172, 58), 'os.path.join', 'os.path.join', ({(172, 32, 172, 43): 'ansible_dir', (172, 45, 172, 57): '"""group_vars"""'}, {}), "(ansible_dir, 'group_vars')", False, 'import os\n'), ((177, 25, 177, 63), 'os.path.join', 'os.path.join', ({(177, 38, 177, 46): 'vars_dir', (177, 48, 177, 62): 'vars_file_name'}, {}), '(vars_dir, vars_file_name)', False, 'import os\n'), ((196, 24, 196, 53), 'copy.copy', 'copy.copy', ({(196, 34, 196, 52): 'self.cluster_model'}, {}), '(self.cluster_model)', False, 'import copy\n'), ((204, 24, 204, 97), 'cli.src.helpers.doc_list_helpers.select_single', 'select_single', ({(204, 38, 204, 56): 'self.manifest_docs', (204, 58, 204, 96): "lambda x: x.kind == 'epiphany-cluster'"}, {}), "(self.manifest_docs, lambda x: x.kind == 'epiphany-cluster')", False, 'from cli.src.helpers.doc_list_helpers import ExpectedSingleResultException, select_first, select_single\n'), ((58, 26, 58, 86), 'cli.src.helpers.build_io.get_ansible_path_for_build', 'get_ansible_path_for_build', ({(58, 53, 58, 85): 'self.inventory_upgrade.build_dir'}, {}), '(self.inventory_upgrade.build_dir)', False, 'from cli.src.helpers.build_io import get_ansible_path, get_ansible_path_for_build, get_ansible_vault_path\n'), ((60, 26, 60, 81), 'cli.src.helpers.build_io.get_ansible_path', 'get_ansible_path', ({(60, 43, 60, 80): 'self.cluster_model.specification.name'}, {}), '(self.cluster_model.specification.name)', False, 'from cli.src.helpers.build_io import get_ansible_path, get_ansible_path_for_build, get_ansible_vault_path\n'), ((67, 12, 67, 45), 'cli.src.helpers.yaml_helpers.dump', 'dump', ({(67, 17, 67, 36): 'clean_cluster_model', (67, 38, 67, 44): 'stream'}, {}), '(clean_cluster_model, stream)', False, 'from cli.src.helpers.yaml_helpers import dump\n'), ((91, 23, 91, 79), 'cli.src.helpers.doc_list_helpers.select_first', 'select_first', ({(91, 36, 91, 52): 'self.config_docs', (91, 54, 91, 78): 'lambda x: x.kind == kind'}, {}), '(self.config_docs, lambda x: x.kind == kind)', False, 'from cli.src.helpers.doc_list_helpers import ExpectedSingleResultException, select_first, select_single\n'), ((107, 54, 107, 72), 'cli.src.helpers.naming_helpers.to_role_name', 'to_role_name', ({(107, 67, 107, 71): 'role'}, {}), '(role)', False, 'from cli.src.helpers.naming_helpers import to_feature_name, to_role_name\n'), ((108, 15, 108, 39), 'os.path.exists', 'os.path.exists', ({(108, 30, 108, 38): 'vars_dir'}, {}), '(vars_dir)', False, 'import os\n'), ((109, 12, 109, 33), 'os.makedirs', 'os.makedirs', ({(109, 24, 109, 32): 'vars_dir'}, {}), '(vars_dir)', False, 'import os\n'), ((116, 12, 116, 34), 'cli.src.helpers.yaml_helpers.dump', 'dump', ({(116, 17, 116, 25): 'document', (116, 27, 116, 33): 'stream'}, {}), '(document, stream)', False, 'from cli.src.helpers.yaml_helpers import dump\n'), ((123, 28, 123, 101), 'cli.src.helpers.doc_list_helpers.select_single', 'select_single', ({(123, 42, 123, 60): 'self.manifest_docs', (123, 62, 123, 100): "lambda x: x.kind == 'epiphany-cluster'"}, {}), "(self.manifest_docs, lambda x: x.kind == 'epiphany-cluster')", False, 'from cli.src.helpers.doc_list_helpers import ExpectedSingleResultException, select_first, select_single\n'), ((130, 23, 130, 69), 'cli.src.helpers.data_loader.load_schema_obj', 'load_schema_obj', ({(130, 39, 130, 52): 'types.DEFAULT', (130, 54, 130, 62): '"""common"""', (130, 64, 130, 68): 'kind'}, {}), "(types.DEFAULT, 'common', kind)", False, 'from cli.src.helpers.data_loader import load_all_schema_objs_from_directory, load_schema_obj, types\n'), ((138, 13, 138, 38), 'cli.src.schema.DefaultMerger.DefaultMerger', 'DefaultMerger', ({(138, 27, 138, 37): '[document]'}, {}), '([document])', False, 'from cli.src.schema.DefaultMerger import DefaultMerger\n'), ((146, 38, 146, 46), 'cli.src.Config.Config', 'Config', ({}, {}), '()', False, 'from cli.src.Config import Config\n'), ((147, 44, 147, 52), 'cli.src.Config.Config', 'Config', ({}, {}), '()', False, 'from cli.src.Config import Config\n'), ((148, 37, 148, 45), 'cli.src.Config.Config', 'Config', ({}, {}), '()', False, 'from cli.src.Config import Config\n'), ((151, 42, 151, 50), 'cli.src.Config.Config', 'Config', ({}, {}), '()', False, 'from cli.src.Config import Config\n'), ((163, 32, 163, 113), 'cli.src.helpers.doc_list_helpers.select_first', 'select_first', ({(163, 45, 163, 61): 'self.config_docs', (163, 63, 163, 112): "lambda x: x.kind == 'configuration/shared-config'"}, {}), "(self.config_docs, lambda x: x.kind ==\n    'configuration/shared-config')", False, 'from cli.src.helpers.doc_list_helpers import ExpectedSingleResultException, select_first, select_single\n'), ((167, 32, 167, 103), 'cli.src.helpers.data_loader.load_schema_obj', 'load_schema_obj', ({(167, 48, 167, 61): 'types.DEFAULT', (167, 63, 167, 71): '"""common"""', (167, 73, 167, 102): '"""configuration/shared-config"""'}, {}), "(types.DEFAULT, 'common', 'configuration/shared-config')", False, 'from cli.src.helpers.data_loader import load_all_schema_objs_from_directory, load_schema_obj, types\n'), ((173, 15, 173, 39), 'os.path.exists', 'os.path.exists', ({(173, 30, 173, 38): 'vars_dir'}, {}), '(vars_dir)', False, 'import os\n'), ((174, 12, 174, 33), 'os.makedirs', 'os.makedirs', ({(174, 24, 174, 32): 'vars_dir'}, {}), '(vars_dir)', False, 'import os\n'), ((180, 12, 180, 35), 'cli.src.helpers.yaml_helpers.dump', 'dump', ({(180, 17, 180, 26): 'main_vars', (180, 28, 180, 34): 'stream'}, {}), '(main_vars, stream)', False, 'from cli.src.helpers.yaml_helpers import dump\n'), ((186, 57, 186, 93), 'cli.src.helpers.build_io.get_ansible_vault_path', 'get_ansible_vault_path', ({(186, 80, 186, 92): 'cluster_name'}, {}), '(cluster_name)', False, 'from cli.src.helpers.build_io import get_ansible_path, get_ansible_path_for_build, get_ansible_vault_path\n'), ((207, 32, 207, 116), 'cli.src.helpers.doc_list_helpers.select_single', 'select_single', ({(207, 46, 207, 64): 'self.manifest_docs', (207, 66, 207, 115): "lambda x: x.kind == 'configuration/shared-config'"}, {}), "(self.manifest_docs, lambda x: x.kind ==\n    'configuration/shared-config')", False, 'from cli.src.helpers.doc_list_helpers import ExpectedSingleResultException, select_first, select_single\n'), ((221, 13, 221, 47), 'cli.src.schema.DefaultMerger.DefaultMerger', 'DefaultMerger', ({(221, 27, 221, 46): '[shared_config_doc]'}, {}), '([shared_config_doc])', False, 'from cli.src.schema.DefaultMerger import DefaultMerger\n'), ((36, 23, 36, 100), 'cli.src.helpers.data_loader.load_all_schema_objs_from_directory', 'load_all_schema_objs_from_directory', ({(36, 59, 36, 72): 'types.DEFAULT', (36, 74, 36, 82): '"""common"""', (36, 84, 36, 99): '"""configuration"""'}, {}), "(types.DEFAULT, 'common', 'configuration')", False, 'from cli.src.helpers.data_loader import load_all_schema_objs_from_directory, load_schema_obj, types\n'), ((89, 38, 89, 59), 'cli.src.helpers.naming_helpers.to_feature_name', 'to_feature_name', ({(89, 54, 89, 58): 'role'}, {}), '(role)', False, 'from cli.src.helpers.naming_helpers import to_feature_name, to_role_name\n'), ((100, 38, 100, 59), 'cli.src.helpers.naming_helpers.to_feature_name', 'to_feature_name', ({(100, 54, 100, 58): 'role'}, {}), '(role)', False, 'from cli.src.helpers.naming_helpers import to_feature_name, to_role_name\n'), ((119, 50, 119, 68), 'cli.src.helpers.naming_helpers.to_role_name', 'to_role_name', ({(119, 63, 119, 67): 'role'}, {}), '(role)', False, 'from cli.src.helpers.naming_helpers import to_feature_name, to_role_name\n'), ((184, 66, 184, 74), 'cli.src.Config.Config', 'Config', ({}, {}), '()', False, 'from cli.src.Config import Config\n'), ((192, 19, 192, 69), 'os.path.basename', 'os.path.basename', ({(192, 36, 192, 68): 'self.inventory_upgrade.build_dir'}, {}), '(self.inventory_upgrade.build_dir)', False, 'import os\n'), ((38, 29, 38, 106), 'cli.src.helpers.doc_list_helpers.select_first', 'select_first', ({(38, 42, 38, 71): 'inventory_upgrade.config_docs', (38, 73, 38, 105): 'lambda x: x.kind == default.kind'}, {}), '(inventory_upgrade.config_docs, lambda x: x.kind == default.kind)', False, 'from cli.src.helpers.doc_list_helpers import ExpectedSingleResultException, select_first, select_single\n')]