#
# Copyright (c) 2014-2017 AT&T Intellectual Property
#
# 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.

"""Group Heat Resource Plugin"""

from heat.common import exception as heat_exception
from heat.common.i18n import _
from heat.engine import attributes
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource
from heat.engine.resources import scheduler_hints as sh
from heat.engine import support
from oslo_log import log as logging

from plugins.common import valet_api
from plugins import exceptions

LOG = logging.getLogger(__name__)


class Group(resource.Resource, sh.SchedulerHintsMixin):
    """Valet Group Resource

    A Group is used to define a particular association amongst
    resources. Groups may be used only by their assigned members,
    currently identified by project (tenant) IDs. If no members are
    assigned, any project (tenant) may assign resources to the group.

    There are three types of groups: affinity, diversity, and exclusivity.
    There are two levels: host and rack.

    All groups must have a unique name, regardless of how they were created
    and regardless of membership.

    There is no lone group owner. Any user with an admin role, regardless
    of project/tenant, can edit or delete the group.
    """

    support_status = support.SupportStatus(version='2015.1')

    _LEVEL_TYPES = (
        HOST, RACK,
    ) = (
        'host', 'rack',
    )

    _RELATIONSHIP_TYPES = (
        AFFINITY, DIVERSITY, EXCLUSIVITY,
    ) = (
        'affinity', 'diversity', 'exclusivity',
    )

    PROPERTIES = (
        DESCRIPTION, LEVEL, MEMBERS, NAME, TYPE,
    ) = (
        'description', 'level', 'members', 'name', 'type',
    )

    ATTRIBUTES = (
        DESCRIPTION_ATTR, LEVEL_ATTR, MEMBERS_ATTR, NAME_ATTR, TYPE_ATTR,
    ) = (
        'description', 'level', 'members', 'name', 'type',
    )

    properties_schema = {
        DESCRIPTION: properties.Schema(
            properties.Schema.STRING,
            _('Description of group.'),
            required=False,
            update_allowed=True
        ),
        LEVEL: properties.Schema(
            properties.Schema.STRING,
            _('Level of relationship between resources.'),
            constraints=[
                constraints.AllowedValues([HOST, RACK])
            ],
            required=True,
            update_allowed=False
        ),
        MEMBERS: properties.Schema(
            properties.Schema.LIST,
            _('List of one or more member IDs allowed to use this group.'),
            required=False,
            update_allowed=True
        ),
        NAME: properties.Schema(
            properties.Schema.STRING,
            _('Name of group.'),
            constraints=[
                constraints.CustomConstraint('valet.group_name'),
            ],
            required=True,
            update_allowed=False
        ),
        TYPE: properties.Schema(
            properties.Schema.STRING,
            _('Type of group.'),
            constraints=[
                constraints.AllowedValues([AFFINITY, DIVERSITY, EXCLUSIVITY])
            ],
            required=True,
            update_allowed=False
        ),
    }

    # To maintain Kilo compatibility, do not use "type" here.
    attributes_schema = {
        DESCRIPTION_ATTR: attributes.Schema(
            _('Description of group.')
        ),
        LEVEL_ATTR: attributes.Schema(
            _('Level of relationship between resources.')
        ),
        MEMBERS_ATTR: attributes.Schema(
            _('List of one or more member IDs allowed to use this group.')
        ),
        NAME_ATTR: attributes.Schema(
            _('Name of group.')
        ),
        TYPE_ATTR: attributes.Schema(
            _('Type of group.')
        ),
    }

    def __init__(self, name, json_snippet, stack):
        """Initialization"""
        super(Group, self).__init__(name, json_snippet, stack)
        self.api = valet_api.ValetAPI()
        self.api.auth_token = self.context.auth_token
        self._group = None

    def _get_resource(self):
        if self._group is None and self.resource_id is not None:
            try:
                groups = self.api.groups_get(
                    self.resource_id)
                if groups:
                    self._group = groups.get('group', {})
            except exceptions.NotFoundError:
                # Ignore Not Found and fall through
                pass

        return self._group

    def _group_name(self):
        """Group Name"""
        name = self.properties.get(self.NAME)
        if name:
            return name

        return self.physical_resource_name()

    def FnGetRefId(self):
        """Get Reference ID"""
        return self.physical_resource_name_or_FnGetRefId()

    def handle_create(self):
        """Create resource"""
        if self.resource_id is not None:
            # TODO(jdandrea): Delete the resource and re-create?
            # I've seen this called if a stack update fails.
            # For now, just leave it be.
            return

        group_type = self.properties.get(self.TYPE)
        level = self.properties.get(self.LEVEL)
        description = self.properties.get(self.DESCRIPTION)
        members = self.properties.get(self.MEMBERS)
        group_args = {
            'name': self._group_name(),
            'type': group_type,
            'level': level,
            'description': description,
        }
        kwargs = {
            'group': group_args,
        }

        # Create the group first. If an exception is
        # thrown by groups_create, let Heat catch it.
        group = self.api.groups_create(**kwargs)
        if group is not None and 'id' in group:
            self.resource_id_set(group.get('id'))
        else:
            raise heat_exception.ResourceNotAvailable(
                resource_name=self._group_name())

        # Now add members to the group
        if members:
            kwargs = {
                'group_id': self.resource_id,
                'members': members,
            }
            err = None
            group = None
            try:
                group = self.api.groups_members_update(**kwargs)
            except exceptions.PythonAPIError as err:
                # Hold on to err. We'll use it in a moment.
                pass
            finally:
                if group is None:
                    # Members couldn't be added.
                    # Delete the group we just created.
                    kwargs = {
                        'group_id': self.resource_id,
                    }
                    try:
                        self.api.groups_delete(**kwargs)
                    except exceptions.PythonAPIError:
                        # Ignore group deletion errors.
                        pass
                    if err:
                        raise err
                    else:
                        raise heat_exception.ResourceNotAvailable(
                            resource_name=self._group_name())

    def handle_update(self, json_snippet, templ_diff, prop_diff):
        """Update resource"""
        if prop_diff:
            if self.DESCRIPTION in prop_diff:
                description = prop_diff.get(
                    self.DESCRIPTION, self.properties.get(self.DESCRIPTION))

                # If an exception is thrown by groups_update,
                # let Heat catch it. Let the state remain as-is.
                kwargs = {
                    'group_id': self.resource_id,
                    'group': {
                        self.DESCRIPTION: description,
                    },
                }
                self.api.groups_update(**kwargs)

            if self.MEMBERS in prop_diff:
                members_update = prop_diff.get(self.MEMBERS, [])
                members = self.properties.get(self.MEMBERS, [])

                # Delete original members not in updated list.
                # If an exception is thrown by groups_member_delete,
                # let Heat catch it. Let the state remain as-is.
                member_deletions = set(members) - set(members_update)
                for member_id in member_deletions:
                    kwargs = {
                        'group_id': self.resource_id,
                        'member_id': member_id,
                    }
                    self.api.groups_member_delete(**kwargs)

                # Add members_update members not in original list.
                # If an exception is thrown by groups_members_update,
                # let Heat catch it. Let the state remain as-is.
                member_additions = set(members_update) - set(members)
                if member_additions:
                    kwargs = {
                        'group_id': self.resource_id,
                        'members': list(member_additions),
                    }
                    self.api.groups_members_update(**kwargs)

            # Clear cached group info
            self._group = None

    def handle_delete(self):
        """Delete resource"""
        if self.resource_id is None:
            return

        kwargs = {
            'group_id': self.resource_id,
        }

        group = self._get_resource()
        if group:
            # First, delete all the members
            members = group.get('members', [])
            if members:
                try:
                    self.api.groups_members_delete(**kwargs)
                except exceptions.NotFoundError:
                    # Ignore Not Found and fall through
                    pass

            # Now delete the group.
            try:
                response = self.api.groups_delete(**kwargs)
                if type(response) is dict and len(response) == 0:
                    self.resource_id_set(None)
                    self._group = None
            except exceptions.NotFoundError:
                # Ignore Not Found and fall through
                pass

    def _resolve_attribute(self, key):
        """Resolve Attributes"""
        if self.resource_id is None:
            return
        group = self._get_resource()
        if group:
            attributes = {
                self.NAME_ATTR: group.get(self.NAME),
                self.TYPE_ATTR: group.get(self.TYPE),
                self.LEVEL_ATTR: group.get(self.LEVEL),
                self.DESCRIPTION_ATTR: group.get(self.DESCRIPTION),
                self.MEMBERS_ATTR: group.get(self.MEMBERS, []),
            }
            return attributes.get(key)


def resource_mapping():
    """Map names to resources."""
    return {
        'OS::Valet::Group': Group,
    }