#
#    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 heat.common import exception
from heat.common.i18n import _
from heat.engine import constraints
from heat.engine import properties
from heat.engine.resources.openstack.neutron import neutron
from heat.engine import support


class SecurityGroup(neutron.NeutronResource):

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

    PROPERTIES = (
        NAME, DESCRIPTION, RULES,
    ) = (
        'name', 'description', 'rules',
    )

    _RULE_KEYS = (
        RULE_DIRECTION, RULE_ETHERTYPE, RULE_PORT_RANGE_MIN,
        RULE_PORT_RANGE_MAX, RULE_PROTOCOL, RULE_REMOTE_MODE,
        RULE_REMOTE_GROUP_ID, RULE_REMOTE_IP_PREFIX,
    ) = (
        'direction', 'ethertype', 'port_range_min',
        'port_range_max', 'protocol', 'remote_mode',
        'remote_group_id', 'remote_ip_prefix',
    )

    _rule_schema = {
        RULE_DIRECTION: properties.Schema(
            properties.Schema.STRING,
            _('The direction in which the security group rule is applied. '
              'For a compute instance, an ingress security group rule '
              'matches traffic that is incoming (ingress) for that '
              'instance. An egress rule is applied to traffic leaving '
              'the instance.'),
            default='ingress',
            constraints=[
                constraints.AllowedValues(['ingress', 'egress']),
            ]
        ),
        RULE_ETHERTYPE: properties.Schema(
            properties.Schema.STRING,
            _('Ethertype of the traffic.'),
            default='IPv4',
            constraints=[
                constraints.AllowedValues(['IPv4', 'IPv6']),
            ]
        ),
        RULE_PORT_RANGE_MIN: properties.Schema(
            properties.Schema.INTEGER,
            _('The minimum port number in the range that is matched by the '
              'security group rule. If the protocol is TCP or UDP, this '
              'value must be less than or equal to the value of the '
              'port_range_max attribute. If the protocol is ICMP, this '
              'value must be an ICMP type.')
        ),
        RULE_PORT_RANGE_MAX: properties.Schema(
            properties.Schema.INTEGER,
            _('The maximum port number in the range that is matched by the '
              'security group rule. The port_range_min attribute constrains '
              'the port_range_max attribute. If the protocol is ICMP, this '
              'value must be an ICMP type.')
        ),
        RULE_PROTOCOL: properties.Schema(
            properties.Schema.STRING,
            _('The protocol that is matched by the security group rule. '
              'Valid values include tcp, udp, and icmp.')
        ),
        RULE_REMOTE_MODE: properties.Schema(
            properties.Schema.STRING,
            _('Whether to specify a remote group or a remote IP prefix.'),
            default='remote_ip_prefix',
            constraints=[
                constraints.AllowedValues(['remote_ip_prefix',
                                           'remote_group_id']),
            ]
        ),
        RULE_REMOTE_GROUP_ID: properties.Schema(
            properties.Schema.STRING,
            _('The remote group ID to be associated with this security group '
              'rule. If no value is specified then this rule will use this '
              'security group for the remote_group_id. The remote mode '
              'parameter must be set to "remote_group_id".')
        ),
        RULE_REMOTE_IP_PREFIX: properties.Schema(
            properties.Schema.STRING,
            _('The remote IP prefix (CIDR) to be associated with this '
              'security group rule.')
        ),
    }

    properties_schema = {
        NAME: properties.Schema(
            properties.Schema.STRING,
            _('A string specifying a symbolic name for the security group, '
              'which is not required to be unique.'),
            update_allowed=True
        ),
        DESCRIPTION: properties.Schema(
            properties.Schema.STRING,
            _('Description of the security group.'),
            update_allowed=True
        ),
        RULES: properties.Schema(
            properties.Schema.LIST,
            _('List of security group rules.'),
            default=[],
            schema=properties.Schema(
                properties.Schema.MAP,
                schema=_rule_schema
            ),
            update_allowed=True
        ),
    }

    default_egress_rules = [
        {"direction": "egress", "ethertype": "IPv4"},
        {"direction": "egress", "ethertype": "IPv6"}
    ]

    def validate(self):
        super(SecurityGroup, self).validate()
        if self.properties.get(self.NAME) == 'default':
            msg = _('Security groups cannot be assigned the name "default".')
            raise exception.StackValidationFailed(message=msg)

    def handle_create(self):
        props = self.prepare_properties(
            self.properties,
            self.physical_resource_name())
        rules = props.pop(self.RULES, [])

        sec = self.neutron().create_security_group(
            {'security_group': props})['security_group']

        self.resource_id_set(sec['id'])
        self._create_rules(rules)

    def _format_rule(self, r):
        rule = dict(r)
        rule['security_group_id'] = self.resource_id

        if 'remote_mode' in rule:
            remote_mode = rule.get(self.RULE_REMOTE_MODE)
            del(rule[self.RULE_REMOTE_MODE])

            if remote_mode == self.RULE_REMOTE_GROUP_ID:
                rule[self.RULE_REMOTE_IP_PREFIX] = None
                if not rule.get(self.RULE_REMOTE_GROUP_ID):
                    # if remote group is not specified then make this
                    # a self-referencing rule
                    rule[self.RULE_REMOTE_GROUP_ID] = self.resource_id
            else:
                rule[self.RULE_REMOTE_GROUP_ID] = None

        for key in (self.RULE_PORT_RANGE_MIN, self.RULE_PORT_RANGE_MAX):
            if rule.get(key) is not None:
                rule[key] = str(rule[key])
        return rule

    def _create_rules(self, rules):
        egress_deleted = False

        for i in rules:
            if i[self.RULE_DIRECTION] == 'egress' and not egress_deleted:
                # There is at least one egress rule, so delete the default
                # rules which allow all egress traffic
                egress_deleted = True

                def is_egress(rule):
                    return rule[self.RULE_DIRECTION] == 'egress'

                self._delete_rules(is_egress)

            rule = self._format_rule(i)

            try:
                self.neutron().create_security_group_rule(
                    {'security_group_rule': rule})
            except Exception as ex:
                if not self.client_plugin().is_conflict(ex):
                    raise

    def _delete_rules(self, to_delete=None):
        try:
            sec = self.neutron().show_security_group(
                self.resource_id)['security_group']
        except Exception as ex:
            self.client_plugin().ignore_not_found(ex)
        else:
            for rule in sec['security_group_rules']:
                if to_delete is None or to_delete(rule):
                    try:
                        self.neutron().delete_security_group_rule(rule['id'])
                    except Exception as ex:
                        self.client_plugin().ignore_not_found(ex)

    def handle_delete(self):

        if self.resource_id is None:
            return

        self._delete_rules()
        try:
            self.neutron().delete_security_group(self.resource_id)
        except Exception as ex:
            self.client_plugin().ignore_not_found(ex)

    def handle_update(self, json_snippet, tmpl_diff, prop_diff):
        props = self.prepare_update_properties(json_snippet)
        rules = props.pop(self.RULES, [])

        self.neutron().update_security_group(
            self.resource_id, {'security_group': props})

        # handle rules changes by:
        # * deleting all rules
        # * restoring the default egress rules
        # * creating new rules
        self._delete_rules()
        self._create_rules(self.default_egress_rules)
        if rules:
            self._create_rules(rules)


def resource_mapping():
    return {
        'OS::Neutron::SecurityGroup': SecurityGroup,
    }
