"""
Ansible action plugin to ensure inventory variables are set
appropriately and no conflicting options have been provided.
"""
import fnmatch
import json
import re

from ansible.plugins.action import ActionBase
from ansible import errors
# pylint: disable=import-error,no-name-in-module
from ansible.module_utils.six.moves.urllib.parse import urlparse


# Valid values for openshift_deployment_type
VALID_DEPLOYMENT_TYPES = ('origin', 'openshift-enterprise')

# Tuple of variable names and default values if undefined.
NET_PLUGIN_LIST = (('openshift_use_openshift_sdn', True),
                   ('openshift_use_flannel', False),
                   ('openshift_use_nuage', False),
                   ('openshift_use_contiv', False),
                   ('openshift_use_calico', False),
                   ('openshift_use_kuryr', False),
                   ('openshift_use_nsx', False))

ENTERPRISE_TAG_REGEX_ERROR = """openshift_image_tag must be in the format
v#.#[.#[.#]]. Examples: v1.2, v3.4.1, v3.5.1.3,
v3.5.1.3.4, v1.2-1, v1.2.3-4, v1.2.3-4.5, v1.2.3-4.5.6
You specified openshift_image_tag={}"""

ORIGIN_TAG_REGEX_ERROR = """openshift_image_tag must be in the format
v#.#[.#-optional.#]. Examples: v1.2.3, v3.5.1-alpha.1
You specified openshift_image_tag={}"""

ORIGIN_TAG_REGEX = {'re': '(^v?\\d+\\.\\d+.*)',
                    'error_msg': ORIGIN_TAG_REGEX_ERROR}
ENTERPRISE_TAG_REGEX = {'re': '(^v\\d+\\.\\d+(\\.\\d+)*(-\\d+(\\.\\d+)*)?$)',
                        'error_msg': ENTERPRISE_TAG_REGEX_ERROR}
IMAGE_TAG_REGEX = {'origin': ORIGIN_TAG_REGEX,
                   'openshift-enterprise': ENTERPRISE_TAG_REGEX}

PKG_VERSION_REGEX_ERROR = """openshift_pkg_version must be in the format
-[optional.release]. Examples: -3.6.0, -3.7.0-0.126.0.git.0.9351aae.el7 -3.11*
You specified openshift_pkg_version={}"""
PKG_VERSION_REGEX = {'re': '(^-.*)',
                     'error_msg': PKG_VERSION_REGEX_ERROR}

RELEASE_REGEX_ERROR = """openshift_release must be in the format
v#[.#[.#]]. Examples: v3.9, v3.10.0
You specified openshift_release={}"""
RELEASE_REGEX = {'re': '(^v?\\d+(\\.\\d+(\\.\\d+)?)?$)',
                 'error_msg': RELEASE_REGEX_ERROR}

STORAGE_KIND_TUPLE = (
    'openshift_loggingops_storage_kind',
    'openshift_logging_storage_kind',
    'openshift_metrics_storage_kind')

IMAGE_POLICY_CONFIG_VAR = "openshift_master_image_policy_config"
ALLOWED_REGISTRIES_VAR = "openshift_master_image_policy_allowed_registries_for_import"

REMOVED_VARIABLES = (
    ('openshift_set_node_ip', 'Deprecated: see documentation.'),
    ('openshift_hostname', 'Removed: See documentation'),
    # TODO(michaelgugino): Remove in 3.12
    ('oreg_auth_credentials_replace', 'Removed: Credentials are now always updated'),
    ('oreg_url_master', 'oreg_url'),
    ('oreg_url_node', 'oreg_url'),
    ('openshift_cockpit_deployer_prefix', 'openshift_cockpit_deployer_image'),
    ('openshift_cockpit_deployer_basename', 'openshift_cockpit_deployer_image'),
    ('openshift_cockpit_deployer_version', 'openshift_cockpit_deployer_image'),
    ('openshift_hosted_logging_elasticsearch_pvc_prefix', 'openshift_logging_es_pvc_prefix'),
    ('logging_ops_hostname', 'openshift_logging_kibana_ops_hostname'),
    ('openshift_hosted_logging_ops_hostname', 'openshift_logging_kibana_ops_hostname'),
    ('openshift_hosted_logging_elasticsearch_cluster_size', 'logging_elasticsearch_cluster_size'),
    ('openshift_hosted_logging_elasticsearch_ops_cluster_size', 'logging_elasticsearch_ops_cluster_size'),
    ('openshift_hosted_logging_storage_kind', 'openshift_logging_storage_kind'),
    ('openshift_hosted_logging_storage_host', 'openshift_logging_storage_host'),
    ('openshift_hosted_logging_storage_labels', 'openshift_logging_storage_labels'),
    ('openshift_hosted_logging_storage_volume_size', 'openshift_logging_storage_volume_size'),
    ('openshift_hosted_loggingops_storage_kind', 'openshift_loggingops_storage_kind'),
    ('openshift_hosted_loggingops_storage_host', 'openshift_loggingops_storage_host'),
    ('openshift_hosted_loggingops_storage_labels', 'openshift_loggingops_storage_labels'),
    ('openshift_hosted_loggingops_storage_volume_size', 'openshift_loggingops_storage_volume_size'),
    ('openshift_hosted_logging_enable_ops_cluster', 'openshift_logging_use_ops'),
    ('openshift_hosted_logging_image_pull_secret', 'openshift_logging_image_pull_secret'),
    ('openshift_hosted_logging_hostname', 'openshift_logging_kibana_hostname'),
    ('openshift_hosted_logging_kibana_nodeselector', 'openshift_logging_kibana_nodeselector'),
    ('openshift_hosted_logging_kibana_ops_nodeselector', 'openshift_logging_kibana_ops_nodeselector'),
    ('openshift_hosted_logging_journal_source', 'openshift_logging_fluentd_journal_source'),
    ('openshift_hosted_logging_journal_read_from_head', 'openshift_logging_fluentd_journal_read_from_head'),
    ('openshift_hosted_logging_fluentd_nodeselector_label', 'openshift_logging_fluentd_nodeselector'),
    ('openshift_hosted_logging_elasticsearch_instance_ram', 'openshift_logging_es_memory_limit'),
    ('openshift_hosted_logging_elasticsearch_nodeselector', 'openshift_logging_es_nodeselector'),
    ('openshift_hosted_logging_elasticsearch_ops_nodeselector', 'openshift_logging_es_ops_nodeselector'),
    ('openshift_hosted_logging_elasticsearch_ops_instance_ram', 'openshift_logging_es_ops_memory_limit'),
    ('openshift_hosted_logging_storage_access_modes', 'openshift_logging_storage_access_modes'),
    ('openshift_hosted_logging_master_public_url', 'openshift_logging_master_public_url'),
    ('openshift_hosted_logging_deployer_prefix', 'openshift_logging_image_prefix'),
    ('openshift_hosted_logging_deployer_version', 'openshift_logging_image_version'),
    ('openshift_hosted_logging_deploy', 'openshift_logging_install_logging'),
    ('openshift_hosted_logging_curator_nodeselector', 'openshift_logging_curator_nodeselector'),
    ('openshift_hosted_logging_curator_ops_nodeselector', 'openshift_logging_curator_ops_nodeselector'),
    ('openshift_hosted_metrics_storage_access_modes', 'openshift_metrics_storage_access_modes'),
    ('openshift_hosted_metrics_storage_host', 'openshift_metrics_storage_host'),
    ('openshift_hosted_metrics_storage_nfs_directory', 'openshift_metrics_storage_nfs_directory'),
    ('openshift_hosted_metrics_storage_volume_name', 'openshift_metrics_storage_volume_name'),
    ('openshift_hosted_metrics_storage_volume_size', 'openshift_metrics_storage_volume_size'),
    ('openshift_hosted_metrics_storage_labels', 'openshift_metrics_storage_labels'),
    ('openshift_hosted_metrics_deployer_prefix', 'openshift_metrics_image_prefix'),
    ('openshift_hosted_metrics_deployer_version', 'openshift_metrics_image_version'),
    ('openshift_hosted_metrics_deploy', 'openshift_metrics_install_metrics'),
    ('openshift_hosted_metrics_storage_kind', 'openshift_metrics_storage_kind'),
    ('openshift_hosted_metrics_public_url', 'openshift_metrics_hawkular_hostname'),
    ('openshift_node_labels', 'openshift_node_groups[<item>].labels'),
    ('openshift_node_kubelet_args', 'openshift_node_groups[<item>].edits'),
)

# JSON_FORMAT_VARIABLES does not intende to cover all json variables, but
# complicated json variables in hosts.example are covered.
JSON_FORMAT_VARIABLES = (
    'openshift_builddefaults_json',
    'openshift_buildoverrides_json',
    'openshift_master_admission_plugin_config',
    'openshift_master_audit_config',
    'openshift_crio_docker_gc_node_selector',
    'openshift_master_image_policy_allowed_registries_for_import',
    'openshift_master_image_policy_config',
    'openshift_master_oauth_templates',
    'container_runtime_extra_storage',
    'openshift_additional_repos',
    'openshift_master_identity_providers',
    'openshift_master_htpasswd_users',
    'openshift_additional_projects',
    'openshift_hosted_routers',
    'openshift_node_open_ports',
    'openshift_master_open_ports',
)


def to_bool(var_to_check):
    """Determine a boolean value given the multiple
       ways bools can be specified in ansible."""
    # http://yaml.org/type/bool.html
    yes_list = (True, 1, "True", "1", "true", "TRUE",
                "Yes", "yes", "Y", "y", "YES",
                "on", "ON", "On")
    return var_to_check in yes_list


class ActionModule(ActionBase):
    """Action plugin to execute sanity checks."""
    def template_var(self, host, varname):
        """Retrieve a variable from hostvars and template it.
           If undefined, return None type."""
        # We will set the current host and variable checked for easy debugging
        # if there are any unhandled exceptions.
        # pylint: disable=W0201
        self.last_checked_var = varname
        # pylint: disable=W0201
        self.last_checked_host = host
        res = self.current_hostvars.get(varname)
        if res is None:
            return None
        return self._templar.template(res)

    def check_openshift_deployment_type(self, host):
        """Ensure a valid openshift_deployment_type is set"""
        openshift_deployment_type = self.template_var(host, 'openshift_deployment_type')
        if openshift_deployment_type not in VALID_DEPLOYMENT_TYPES:
            type_strings = ", ".join(VALID_DEPLOYMENT_TYPES)
            msg = "openshift_deployment_type must be defined and one of {}".format(type_strings)
            raise errors.AnsibleModuleError(msg)
        return openshift_deployment_type

    def get_allowed_registries(self, host):
        """Returns a list of configured allowedRegistriesForImport as a list of patterns"""
        allowed_registries_for_import = self.template_var(host, ALLOWED_REGISTRIES_VAR)
        if allowed_registries_for_import is None:
            image_policy_config = self.template_var(host, IMAGE_POLICY_CONFIG_VAR)
            if not image_policy_config:
                return image_policy_config

            if isinstance(image_policy_config, str):
                try:
                    image_policy_config = json.loads(image_policy_config)
                except Exception:
                    raise errors.AnsibleModuleError(
                        "{} is not a valid json string".format(IMAGE_POLICY_CONFIG_VAR))

            if not isinstance(image_policy_config, dict):
                raise errors.AnsibleModuleError(
                    "expected dictionary for {}, not {}".format(
                        IMAGE_POLICY_CONFIG_VAR, type(image_policy_config)))

            detailed = image_policy_config.get("allowedRegistriesForImport", None)
            if not detailed:
                return detailed

            if not isinstance(detailed, list):
                raise errors.AnsibleModuleError("expected list for {}['{}'], not {}".format(
                    IMAGE_POLICY_CONFIG_VAR, "allowedRegistriesForImport",
                    type(allowed_registries_for_import)))

            try:
                return [i["domainName"] for i in detailed]
            except Exception:
                raise errors.AnsibleModuleError(
                    "each item of allowedRegistriesForImport must be a dictionary with 'domainName' key")

        if not isinstance(allowed_registries_for_import, list):
            raise errors.AnsibleModuleError("expected list for {}, not {}".format(
                IMAGE_POLICY_CONFIG_VAR, type(allowed_registries_for_import)))

        return allowed_registries_for_import

    def check_whitelisted_registries(self, host):
        """Ensure defined registries are whitelisted"""
        allowed = self.get_allowed_registries(host)
        if allowed is None:
            return

        unmatched_registries = []
        for regvar in (
                "oreg_url"
                "openshift_cockpit_deployer_prefix",
                "openshift_metrics_image_prefix",
                "openshift_logging_image_prefix",
                "openshift_service_catalog_image_prefix",
                "openshift_docker_insecure_registries"):
            value = self.template_var(host, regvar)
            if not value:
                continue
            if isinstance(value, list):
                registries = value
            else:
                registries = [value]

            for reg in registries:
                if not any(is_registry_match(reg, pat) for pat in allowed):
                    unmatched_registries.append((regvar, reg))

        if unmatched_registries:
            registry_list = ", ".join(["{}:{}".format(n, v) for n, v in unmatched_registries])
            raise errors.AnsibleModuleError(
                "registry hostnames of the following image prefixes are not whitelisted by image"
                " policy configuration: {}".format(registry_list))

    def check_python_version(self, host, distro):
        """Ensure python version is 3 for Fedora and python 2 for others"""
        ansible_python = self.template_var(host, 'ansible_python')
        if distro == "Fedora":
            if ansible_python['version']['major'] != 3:
                msg = "openshift-ansible requires Python 3 for {};".format(distro)
                msg += " For information on enabling Python 3 with Ansible,"
                msg += " see https://docs.ansible.com/ansible/python_3_support.html"
                raise errors.AnsibleModuleError(msg)
        else:
            if ansible_python['version']['major'] != 2:
                msg = "openshift-ansible requires Python 2 for {};".format(distro)

    def check_image_tag_format(self, host, openshift_deployment_type):
        """Ensure openshift_image_tag is formatted correctly"""
        openshift_image_tag = self.template_var(host, 'openshift_image_tag')
        if not openshift_image_tag or openshift_image_tag == 'latest':
            return None
        regex_to_match = IMAGE_TAG_REGEX[openshift_deployment_type]['re']
        res = re.match(regex_to_match, str(openshift_image_tag))
        if res is None:
            msg = IMAGE_TAG_REGEX[openshift_deployment_type]['error_msg']
            msg = msg.format(str(openshift_image_tag))
            raise errors.AnsibleModuleError(msg)

    def check_pkg_version_format(self, host):
        """Ensure openshift_pkg_version is formatted correctly"""
        openshift_pkg_version = self.template_var(host, 'openshift_pkg_version')
        if not openshift_pkg_version:
            return None
        regex_to_match = PKG_VERSION_REGEX['re']
        res = re.match(regex_to_match, str(openshift_pkg_version))
        if res is None:
            msg = PKG_VERSION_REGEX['error_msg']
            msg = msg.format(str(openshift_pkg_version))
            raise errors.AnsibleModuleError(msg)

    def check_release_format(self, host):
        """Ensure openshift_release is formatted correctly"""
        openshift_release = self.template_var(host, 'openshift_release')
        if not openshift_release:
            return None
        regex_to_match = RELEASE_REGEX['re']
        res = re.match(regex_to_match, str(openshift_release))
        if res is None:
            msg = RELEASE_REGEX['error_msg']
            msg = msg.format(str(openshift_release))
            raise errors.AnsibleModuleError(msg)

    def network_plugin_check(self, host):
        """Ensure only one type of network plugin is enabled"""
        res = []
        # Loop through each possible network plugin boolean, determine the
        # actual boolean value, and append results into a list.
        for plugin, default_val in NET_PLUGIN_LIST:
            res_temp = self.template_var(host, plugin)
            if res_temp is None:
                res_temp = default_val
            res.append(to_bool(res_temp))

        if sum(res) not in (0, 1):
            plugin_str = list(zip([x[0] for x in NET_PLUGIN_LIST], res))

            msg = "Host Checked: {} Only one of must be true. Found: {}".format(host, plugin_str)
            raise errors.AnsibleModuleError(msg)

    def check_hostname_vars(self, host):
        """Checks to ensure openshift_kubelet_name_override
           and openshift_public_hostname
           conform to the proper length of 63 characters or less"""
        for varname in ('openshift_public_hostname', 'openshift_kubelet_name_override'):
            var_value = self.template_var(host, varname)
            if var_value and len(var_value) > 63:
                msg = '{} must be 63 characters or less'.format(varname)
                raise errors.AnsibleModuleError(msg)

    def check_session_auth_secrets(self, host):
        """Checks session_auth_secrets is correctly formatted"""
        sas = self.template_var(host, 'openshift_master_session_auth_secrets')
        ses = self.template_var(host, 'openshift_master_session_encryption_secrets')
        # This variable isn't mandatory, only check if set.
        if sas is None and ses is None:
            return None

        if not (
                issubclass(type(sas), list) and issubclass(type(ses), list)
        ) or len(sas) != len(ses):
            raise errors.AnsibleModuleError(
                'Expects openshift_master_session_auth_secrets and '
                'openshift_master_session_encryption_secrets are equal length lists')

        for secret in sas:
            if len(secret) < 32:
                raise errors.AnsibleModuleError(
                    'Invalid secret in openshift_master_session_auth_secrets. '
                    'Secrets must be at least 32 characters in length.')

        for secret in ses:
            if len(secret) not in [16, 24, 32]:
                raise errors.AnsibleModuleError(
                    'Invalid secret in openshift_master_session_encryption_secrets. '
                    'Secrets must be 16, 24, or 32 characters in length.')
        return None

    def check_unsupported_nfs_configs(self, host):
        """Fails if nfs storage is in use for any components. This check is
           ignored if openshift_enable_unsupported_configurations=True"""

        enable_unsupported = self.template_var(host, 'openshift_enable_unsupported_configurations')

        if to_bool(enable_unsupported):
            return None

        for storage in STORAGE_KIND_TUPLE:
            kind = self.template_var(host, storage)
            if kind == 'nfs':
                raise errors.AnsibleModuleError(
                    'nfs is an unsupported type for {}. '
                    'openshift_enable_unsupported_configurations=True must '
                    'be specified to continue with this configuration.'
                    ''.format(storage))
        return None

    def check_htpasswd_provider(self, host):
        """Fails if openshift_master_identity_providers contains an entry of
        kind HTPasswdPasswordIdentityProvider and
        openshift_master_manage_htpasswd is False"""

        manage_pass = self.template_var(host, 'openshift_master_manage_htpasswd')
        if to_bool(manage_pass):
            # If we manage the file, we can just generate in the new path.
            return None
        idps = self.template_var(host, 'openshift_master_identity_providers')
        if not idps:
            # If we don't find any identity_providers, nothing for us to do.
            return None
        old_keys = ('file', 'fileName', 'file_name', 'filename')
        if not isinstance(idps, list):
            raise errors.AnsibleModuleError("| not a list")
        for idp in idps:
            if idp['kind'] == 'HTPasswdPasswordIdentityProvider':
                for old_key in old_keys:
                    if old_key in idp is not None:
                        raise errors.AnsibleModuleError(
                            'openshift_master_identity_providers contains a '
                            'provider of kind==HTPasswdPasswordIdentityProvider '
                            'and {} is set.  Please migrate your htpasswd '
                            'files to /etc/origin/master/htpasswd and update your '
                            'existing master configs, and remove the {} key'
                            'before proceeding.'.format(old_key, old_key))

    def validate_json_format_vars(self, host):
        """Fails if invalid json format are found"""
        found_invalid_json = []
        for var in JSON_FORMAT_VARIABLES:
            if var in self.current_hostvars:
                json_var = self.template_var(host, var)
                try:
                    json.loads(json_var)
                except ValueError as json_err:
                    found_invalid_json.append([var, json_var, json_err])
                except BaseException:
                    pass

        if found_invalid_json:
            msg = "Found invalid json format variables:\n"
            for item in found_invalid_json:
                msg += "    {} specified in {} is invalid json format\n    {}".format(item[1], item[0], item[2])
            raise errors.AnsibleModuleError(msg)
        return None

    def check_for_oreg_password(self, host, odt):
        """Ensure oreg_password is defined when using registry.redhat.io"""
        reg_to_check = 'registry.redhat.io'
        err_msg = ("oreg_auth_user and oreg_auth_password must be provided when"
                   "deploying openshift-enterprise")
        err_msg2 = ("oreg_auth_user and oreg_auth_password must be provided when using"
                    "{}".format(reg_to_check))

        oreg_password = self.template_var(host, 'oreg_auth_password')
        if oreg_password is not None:
            # A password is defined, so we're good to go.
            return None

        oreg_url = self.template_var(host, 'oreg_url')

        if oreg_url is not None:
            if reg_to_check in oreg_url:
                raise errors.AnsibleModuleError(err_msg2)

        elif odt == 'openshift-enterprise':
            # We're not using an oreg_url, we're using default enterprise
            # registry.  We require oreg_auth_user and oreg_auth_password
            raise errors.AnsibleModuleError(err_msg)

    def check_for_removed_vars(self):
        """Fails if removed variables are found"""
        found_removed = []
        for item in REMOVED_VARIABLES:
            if item[0] in self.current_hostvars:
                found_removed.append(item)

        if found_removed:
            msg = "Found removed variables: "
            for item in found_removed:
                msg += "{} is replaced by {}; ".format(item[0], item[1])
            raise errors.AnsibleModuleError(msg)
        return None

    def run_checks(self, host):
        """Execute the hostvars validations against host"""
        distro = self.template_var(host, 'ansible_distribution')
        odt = self.check_openshift_deployment_type(host)
        self.check_whitelisted_registries(host)
        self.check_python_version(host, distro)
        self.check_image_tag_format(host, odt)
        self.check_pkg_version_format(host)
        self.check_release_format(host)
        self.network_plugin_check(host)
        self.check_hostname_vars(host)
        self.check_session_auth_secrets(host)
        self.check_unsupported_nfs_configs(host)
        self.check_htpasswd_provider(host)
        self.check_for_removed_vars()
        self.validate_json_format_vars(host)
        self.check_for_oreg_password(host, odt)

    def run(self, tmp=None, task_vars=None):
        result = super(ActionModule, self).run(tmp, task_vars)

        # self.task_vars holds all in-scope variables.
        # Ignore settting self.task_vars outside of init.
        # pylint: disable=W0201
        self.task_vars = task_vars or {}

        # pylint: disable=W0201
        self.last_checked_host = "none"
        # pylint: disable=W0201
        self.last_checked_var = "none"

        # self._task.args holds task parameters.
        # check_hosts is a parameter to this plugin, and should provide
        # a list of hosts.
        check_hosts = self._task.args.get('check_hosts')
        if not check_hosts:
            msg = "check_hosts is required"
            raise errors.AnsibleModuleError(msg)

        # We need to access each host's variables
        hostvars = self.task_vars.get('hostvars')
        if not hostvars:
            msg = hostvars
            raise errors.AnsibleModuleError(msg)

        self.hostvars = hostvars

        # We loop through each host in the provided list check_hosts
        for host in check_hosts:
            try:
                self.current_hostvars = self.hostvars[host]
                self.run_checks(host)
            except Exception as uncaught_e:
                msg = "last_checked_host: {}, last_checked_var: {};"
                msg = msg.format(self.last_checked_host, self.last_checked_var)
                msg += str(uncaught_e)
                raise errors.AnsibleModuleError(msg)

        result["changed"] = False
        result["failed"] = False
        result["msg"] = "Sanity Checks passed"

        return result


def is_registry_match(item, pattern):
    """returns True if the registry matches the given whitelist pattern

    Unlike in OpenShift, the comparison is done solely on hostname part
    (excluding the port part) since the latter is much more difficult due to
    vague definition of port defaulting based on insecure flag. Moreover, most
    of the registries will be listed without the port and insecure flag.
    """
    item = "schema://" + item.split('://', 1)[-1]
    pat = pattern.rsplit(':', 1)[0]
    name = urlparse(item).hostname
    return fnmatch.fnmatch(name, pat)
