# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from sahara import exceptions as ex
from sahara.i18n import _
from sahara.plugins import base as plugins_base
from sahara.utils import resources


class ProvisioningPluginBase(plugins_base.PluginInterface):
    @plugins_base.required
    def get_versions(self):
        pass

    @plugins_base.required
    def get_configs(self, hadoop_version):
        pass

    @plugins_base.required
    def get_node_processes(self, hadoop_version):
        pass

    @plugins_base.required_with_default
    def get_required_image_tags(self, hadoop_version):
        return [self.name, hadoop_version]

    @plugins_base.required_with_default
    def validate(self, cluster):
        pass

    @plugins_base.required_with_default
    def validate_scaling(self, cluster, existing, additional):
        pass

    @plugins_base.required_with_default
    def update_infra(self, cluster):
        pass

    @plugins_base.required
    def configure_cluster(self, cluster):
        pass

    @plugins_base.required
    def start_cluster(self, cluster):
        pass

    @plugins_base.optional
    def scale_cluster(self, cluster, instances):
        pass

    @plugins_base.optional
    def get_edp_engine(self, cluster, job_type):
        pass

    @plugins_base.optional
    def get_edp_job_types(self, versions=None):
        return {}

    @plugins_base.optional
    def get_edp_config_hints(self, job_type, version):
        return {}

    @plugins_base.required_with_default
    def get_open_ports(self, node_group):
        return []

    @plugins_base.required_with_default
    def decommission_nodes(self, cluster, instances):
        pass

    @plugins_base.optional
    def convert(self, config, plugin_name, version, template_name,
                cluster_template_create):
        pass

    @plugins_base.required_with_default
    def on_terminate_cluster(self, cluster):
        pass

    @plugins_base.optional
    def recommend_configs(self, cluster, scaling=False):
        pass

    @plugins_base.required_with_default
    def get_health_checks(self, cluster):
        return []

    def get_all_configs(self, hadoop_version):
        common = list_of_common_configs()
        plugin_specific_configs = self.get_configs(hadoop_version)
        if plugin_specific_configs:
            common.extend(plugin_specific_configs)
        return common

    def to_dict(self):
        res = super(ProvisioningPluginBase, self).to_dict()
        res['versions'] = self.get_versions()
        return res

    # Some helpers for plugins

    def _map_to_user_inputs(self, hadoop_version, configs):
        config_objs = self.get_all_configs(hadoop_version)

        # convert config objects to applicable_target -> config_name -> obj
        config_objs_map = {}
        for config_obj in config_objs:
            applicable_target = config_obj.applicable_target
            confs = config_objs_map.get(applicable_target, {})
            confs[config_obj.name] = config_obj
            config_objs_map[applicable_target] = confs

        # iterate over all configs and append UserInputs to result list
        result = []
        for applicable_target in configs:
            for config_name in configs[applicable_target]:
                confs = config_objs_map.get(applicable_target)
                if not confs:
                    raise ex.ConfigurationError(
                        _("Can't find applicable target "
                          "'%(applicable_target)s' for '%(config_name)s'")
                        % {"applicable_target": applicable_target,
                           "config_name": config_name})
                conf = confs.get(config_name)
                if not conf:
                    raise ex.ConfigurationError(
                        _("Can't find config '%(config_name)s' "
                          "in '%(applicable_target)s'")
                        % {"config_name": config_name,
                           "applicable_target": applicable_target})
                result.append(UserInput(
                    conf, configs[applicable_target][config_name]))

        return sorted(result)


class Config(resources.BaseResource):
    """Describes a single config parameter.

    Config type - could be 'str', 'integer', 'boolean', 'enum'.
    If config type is 'enum' then list of valid values should be specified in
    config_values property.

    Priority - integer parameter which helps to differentiate all
    configurations in the UI. Priority decreases from the lower values to
    higher values.

    For example:

        "some_conf", "map_reduce", "node", is_optional=True
    """

    def __init__(self, name, applicable_target, scope, config_type="string",
                 config_values=None, default_value=None, is_optional=False,
                 description=None, priority=2):
        self.name = name
        self.description = description
        self.config_type = config_type
        self.config_values = config_values
        self.default_value = default_value
        self.applicable_target = applicable_target
        self.scope = scope
        self.is_optional = is_optional
        self.priority = priority

    def to_dict(self):
        res = super(Config, self).to_dict()
        # TODO(slukjanov): all custom fields from res
        return res

    def __lt__(self, other):
        return self.name < other.name

    def __repr__(self):
        return '<Config %s in %s>' % (self.name, self.applicable_target)


class UserInput(object):
    """Value provided by the user for a specific config entry."""

    def __init__(self, config, value):
        self.config = config
        self.value = value

    def __eq__(self, other):
        return self.config == other.config and self.value == other.value

    def __lt__(self, other):
        return (self.config, self.value) < (other.config, other.value)

    def __repr__(self):
        return '<UserInput %s = %s>' % (self.config.name, self.value)


class ValidationError(object):
    """Describes what is wrong with one of the values provided by user."""

    def __init__(self, config, message):
        self.config = config
        self.message = message

    def __repr__(self):
        return "<ValidationError %s>" % self.config.name

# COMMON FOR ALL PLUGINS CONFIGS

XFS_ENABLED = Config(
    "Enable XFS", 'general', 'cluster', priority=1,
    default_value=True, config_type="bool", is_optional=True,
    description='Enables XFS for formatting'
)

DISKS_PREPARING_TIMEOUT = Config(
    "Timeout for disk preparing", 'general', 'cluster', priority=1,
    default_value=300, config_type="int", is_optional=True,
    description='Timeout for preparing disks, formatting and mounting'
)


NTP_URL = Config(
    "URL of NTP server", 'general', 'cluster', priority=1,
    default_value='', is_optional=True,
    description='URL of the NTP server for synchronization time on cluster'
                ' instances'
)

NTP_ENABLED = Config(
    "Enable NTP service", 'general', 'cluster', priority=1, default_value=True,
    config_type="bool",
    description='Enables NTP service for synchronization time on cluster '
                'instances'
)

HEAT_WAIT_CONDITION_TIMEOUT = Config(
    "Heat Wait Condition timeout", "general", "cluster", priority=1,
    config_type="int", default_value=3600, is_optional=True,
    description="The number of seconds to wait for the instance to boot")


def list_of_common_configs():
    return [DISKS_PREPARING_TIMEOUT, NTP_ENABLED, NTP_URL,
            HEAT_WAIT_CONDITION_TIMEOUT, XFS_ENABLED]
