#
#    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 oslo_log import log as logging

from heat.common import exception
from heat.common import grouputils
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine.hot import template
from heat.engine import output
from heat.engine import properties
from heat.engine.resources.aws.autoscaling import autoscaling_group as aws_asg
from heat.engine import rsrc_defn
from heat.engine import support

LOG = logging.getLogger(__name__)


class HOTInterpreter(template.HOTemplate20150430):
    def __new__(cls):
        return object.__new__(cls)

    def __init__(self):
        version = {'heat_template_version': '2015-04-30'}
        super(HOTInterpreter, self).__init__(version)

    def parse(self, stack, snippet, path=''):
        return snippet

    def parse_conditions(self, stack, snippet, path=''):
        return snippet


class AutoScalingResourceGroup(aws_asg.AutoScalingGroup):
    """An autoscaling group that can scale arbitrary resources.

    An autoscaling group allows the creation of a desired count of similar
    resources, which are defined with the resource property in HOT format.
    If there is a need to create many of the same resources (e.g. one
    hundred sets of Server, WaitCondition and WaitConditionHandle or even
    Neutron Nets), AutoScalingGroup is a convenient and easy way to do that.
    """

    PROPERTIES = (
        RESOURCE, MAX_SIZE, MIN_SIZE, COOLDOWN, DESIRED_CAPACITY,
        ROLLING_UPDATES,
    ) = (
        'resource', 'max_size', 'min_size', 'cooldown', 'desired_capacity',
        'rolling_updates',
    )

    _ROLLING_UPDATES_SCHEMA = (
        MIN_IN_SERVICE, MAX_BATCH_SIZE, PAUSE_TIME,
    ) = (
        'min_in_service', 'max_batch_size', 'pause_time',
    )

    ATTRIBUTES = (
        OUTPUTS, OUTPUTS_LIST, CURRENT_SIZE, REFS, REFS_MAP,
    ) = (
        'outputs', 'outputs_list', 'current_size', 'refs', 'refs_map',
    )

    (OUTPUT_MEMBER_IDS,) = (REFS_MAP,)

    properties_schema = {
        RESOURCE: properties.Schema(
            properties.Schema.MAP,
            _('Resource definition for the resources in the group, in HOT '
              'format. The value of this property is the definition of a '
              'resource just as if it had been declared in the template '
              'itself.'),
            required=True,
            update_allowed=True,
        ),
        MAX_SIZE: properties.Schema(
            properties.Schema.INTEGER,
            _('Maximum number of resources in the group.'),
            required=True,
            update_allowed=True,
            constraints=[constraints.Range(min=0)],
        ),
        MIN_SIZE: properties.Schema(
            properties.Schema.INTEGER,
            _('Minimum number of resources in the group.'),
            required=True,
            update_allowed=True,
            constraints=[constraints.Range(min=0)]
        ),
        COOLDOWN: properties.Schema(
            properties.Schema.INTEGER,
            _('Cooldown period, in seconds.'),
            update_allowed=True
        ),
        DESIRED_CAPACITY: properties.Schema(
            properties.Schema.INTEGER,
            _('Desired initial number of resources.'),
            update_allowed=True
        ),
        ROLLING_UPDATES: properties.Schema(
            properties.Schema.MAP,
            _('Policy for rolling updates for this scaling group.'),
            update_allowed=True,
            schema={
                MIN_IN_SERVICE: properties.Schema(
                    properties.Schema.INTEGER,
                    _('The minimum number of resources in service while '
                      'rolling updates are being executed.'),
                    constraints=[constraints.Range(min=0)],
                    default=0),
                MAX_BATCH_SIZE: properties.Schema(
                    properties.Schema.INTEGER,
                    _('The maximum number of resources to replace at once.'),
                    constraints=[constraints.Range(min=1)],
                    default=1),
                PAUSE_TIME: properties.Schema(
                    properties.Schema.NUMBER,
                    _('The number of seconds to wait between batches of '
                      'updates.'),
                    constraints=[constraints.Range(min=0)],
                    default=0),
            },
            # A default policy has all fields with their own default values.
            default={
                MIN_IN_SERVICE: 0,
                MAX_BATCH_SIZE: 1,
                PAUSE_TIME: 0,
            },
        ),
    }

    attributes_schema = {
        OUTPUTS: attributes.Schema(
            _("A map of resource names to the specified attribute of each "
              "individual resource that is part of the AutoScalingGroup. "
              "This map specifies output parameters that are available "
              "once the AutoScalingGroup has been instantiated."),
            support_status=support.SupportStatus(version='2014.2'),
            type=attributes.Schema.MAP
        ),
        OUTPUTS_LIST: attributes.Schema(
            _("A list of the specified attribute of each individual resource "
              "that is part of the AutoScalingGroup. This list of attributes "
              "is available as an output once the AutoScalingGroup has been "
              "instantiated."),
            support_status=support.SupportStatus(version='2014.2'),
            type=attributes.Schema.LIST
        ),
        CURRENT_SIZE: attributes.Schema(
            _("The current size of AutoscalingResourceGroup."),
            support_status=support.SupportStatus(version='2015.1'),
            type=attributes.Schema.INTEGER
        ),
        REFS: attributes.Schema(
            _("A list of resource IDs for the resources in the group."),
            type=attributes.Schema.LIST,
            support_status=support.SupportStatus(version='7.0.0'),
        ),
        REFS_MAP: attributes.Schema(
            _("A map of resource names to IDs for the resources in "
              "the group."),
            type=attributes.Schema.MAP,
            support_status=support.SupportStatus(version='7.0.0'),
        ),

    }
    update_policy_schema = {}

    def _get_resource_definition(self):
        resource_def = self.properties[self.RESOURCE]
        defn_data = dict(HOTInterpreter()._rsrc_defn_args(None, 'member',
                                                          resource_def))
        return rsrc_defn.ResourceDefinition(None, **defn_data)

    def child_template_files(self, child_env):
        is_update = self.action == self.UPDATE
        return grouputils.get_child_template_files(self.context, self.stack,
                                                   is_update,
                                                   self.old_template_id)

    def _try_rolling_update(self, prop_diff):
        if self.RESOURCE in prop_diff:
            policy = self.properties[self.ROLLING_UPDATES]
            self._replace(policy[self.MIN_IN_SERVICE],
                          policy[self.MAX_BATCH_SIZE],
                          policy[self.PAUSE_TIME])

    def _create_template(self, num_instances, num_replace=0,
                         template_version=('heat_template_version',
                                           '2015-04-30')):
        """Create a template in the HOT format for the nested stack."""
        return super(AutoScalingResourceGroup,
                     self)._create_template(num_instances, num_replace,
                                            template_version=template_version)

    def _attribute_output_name(self, *attr_path):
        return ', '.join(str(a) for a in attr_path)

    def get_attribute(self, key, *path):  # noqa: C901
        if key == self.CURRENT_SIZE:
            return grouputils.get_size(self)

        op_key = key
        op_path = path
        keycomponents = None
        if key == self.OUTPUTS_LIST:
            op_key = self.OUTPUTS
        elif key == self.REFS:
            op_key = self.REFS_MAP
        elif key.startswith("resource."):
            keycomponents = key.split('.', 2)
            if len(keycomponents) > 2:
                op_path = (keycomponents[2],) + path
            op_key = self.OUTPUTS if op_path else self.REFS_MAP
        try:
            output = self.get_output(self._attribute_output_name(op_key,
                                                                 *op_path))
        except (exception.NotFound,
                exception.TemplateOutputError) as op_err:
            LOG.debug('Falling back to grouputils due to %s', op_err)

            if key == self.REFS:
                return grouputils.get_member_refids(self)
            if key == self.REFS_MAP:
                members = grouputils.get_members(self)
                return {m.name: m.resource_id for m in members}
            if path and key in {self.OUTPUTS, self.OUTPUTS_LIST}:
                members = grouputils.get_members(self)
                attrs = ((rsrc.name,
                          rsrc.FnGetAtt(*path)) for rsrc in members)
                if key == self.OUTPUTS:
                    return dict(attrs)
                if key == self.OUTPUTS_LIST:
                    return [value for name, value in attrs]
            if keycomponents is not None:
                return grouputils.get_nested_attrs(self, key, True, *path)
        else:
            if key in {self.REFS, self.REFS_MAP}:
                names = self._group_data().member_names(False)
                if key == self.REFS:
                    return [output[n] for n in names if n in output]
                else:
                    return {n: output[n] for n in names if n in output}

            if path and key in {self.OUTPUTS_LIST, self.OUTPUTS}:
                names = self._group_data().member_names(False)
                if key == self.OUTPUTS_LIST:
                    return [output[n] for n in names if n in output]
                else:
                    return {n: output[n] for n in names if n in output}

            if keycomponents is not None:
                names = list(self._group_data().member_names(False))
                index = keycomponents[1]
                try:
                    resource_name = names[int(index)]
                    return output[resource_name]
                except (IndexError, KeyError):
                    raise exception.NotFound(_("Member '%(mem)s' not found "
                                               "in group resource '%(grp)s'.")
                                             % {'mem': index,
                                                'grp': self.name})

        raise exception.InvalidTemplateAttribute(resource=self.name,
                                                 key=key)

    def _nested_output_defns(self, resource_names, get_attr_fn, get_res_fn):
        for attr in self.referenced_attrs():
            if isinstance(attr, str):
                key, path = attr, []
            else:
                key, path = attr[0], list(attr[1:])
            # Always use map types, as list order is not defined at
            # template generation time.
            if key == self.OUTPUTS_LIST:
                key = self.OUTPUTS
            if key.startswith("resource."):
                keycomponents = key.split('.', 2)
                path = keycomponents[2:] + path
                if path:
                    key = self.OUTPUTS
            output_name = self._attribute_output_name(key, *path)

            if key == self.OUTPUTS and path:
                value = {r: get_attr_fn([r] + path) for r in resource_names}
                yield output.OutputDefinition(output_name, value)

        # Always define an output for the member IDs, which also doubles as the
        # output used by the REFS and REFS_MAP attributes.
        member_ids_value = {r: get_res_fn(r) for r in resource_names}
        yield output.OutputDefinition(self.OUTPUT_MEMBER_IDS,
                                      member_ids_value)


def resource_mapping():
    return {
        'OS::Heat::AutoScalingGroup': AutoScalingResourceGroup,
    }
