# Copyright 2015 OpenStack Foundation
# All Rights Reserved
#
#    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.


"""
NSX-V3 Distributed Firewall
"""
from oslo_log import log

from vmware_nsx._i18n import _LW
from vmware_nsx.common import utils
from vmware_nsx.nsxlib.v3 import client as nsxclient
from vmware_nsx.nsxlib.v3 import exceptions

LOG = log.getLogger(__name__)

# firewall section types
LAYER3 = 'LAYER3'

INSERT_BEFORE = 'insert_before'
INSERT_BOTTOM = 'insert_bottom'
INSERT_TOP = 'insert_top'

# firewall rule actions
ALLOW = 'ALLOW'
DROP = 'DROP'
REJECT = 'REJECT'

# filtering operators and expressions
EQUALS = 'EQUALS'
NSGROUP_SIMPLE_EXPRESSION = 'NSGroupSimpleExpression'
NSGROUP_TAG_EXPRESSION = 'NSGroupTagExpression'

# nsgroup members update actions
ADD_MEMBERS = 'ADD_MEMBERS'
REMOVE_MEMBERS = 'REMOVE_MEMBERS'

NSGROUP = 'NSGroup'
LOGICAL_SWITCH = 'LogicalSwitch'
LOGICAL_PORT = 'LogicalPort'
IPV4ADDRESS = 'IPv4Address'
IPV6ADDRESS = 'IPv6Address'

IN = 'IN'
OUT = 'OUT'
IN_OUT = 'IN_OUT'

# NSServices resource types
L4_PORT_SET_NSSERVICE = 'L4PortSetNSService'
ICMP_TYPE_NSSERVICE = 'ICMPTypeNSService'
IP_PROTOCOL_NSSERVICE = 'IPProtocolNSService'

TCP = 'TCP'
UDP = 'UDP'
ICMPV4 = 'ICMPv4'
ICMPV6 = 'ICMPv6'

IPV4 = 'IPV4'
IPV6 = 'IPV6'
IPV4_IPV6 = 'IPV4_IPV6'


class DfwApi(object):

    def get_nsservice(self, resource_type, **properties):
        service = {'resource_type': resource_type}
        service.update(properties)
        return {'service': service}

    def get_nsgroup_port_tag_expression(self, scope, tag):
        return {'resource_type': NSGROUP_TAG_EXPRESSION,
                'target_type': LOGICAL_PORT,
                'scope': scope,
                'tag': tag}

    def create_nsgroup(self, display_name, description, tags,
                       membership_criteria=None):
        body = {'display_name': display_name,
                'description': description,
                'tags': tags,
                'members': []}
        if membership_criteria:
            body.update({'membership_criteria': [membership_criteria]})
        return self.client.create('ns-groups', body)

    def list_nsgroups(self):
        return self.client.get(
            'ns-groups?populate_references=false').get('results', [])

    @utils.retry_upon_exception_nsxv3(exceptions.StaleRevision)
    def update_nsgroup(self, nsgroup_id, display_name=None, description=None,
                       membership_criteria=None, members=None):
        nsgroup = self.read_nsgroup(nsgroup_id)
        if display_name is not None:
            nsgroup['display_name'] = display_name
        if description is not None:
            nsgroup['description'] = description
        if members is not None:
            nsgroup['members'] = members
        if membership_criteria is not None:
            nsgroup['membership_criteria'] = [membership_criteria]
        return self.client.update(
            'ns-groups/%s' % nsgroup_id, nsgroup)

    def get_nsgroup_member_expression(self, target_type, target_id):
        return {'resource_type': NSGROUP_SIMPLE_EXPRESSION,
                'target_property': 'id',
                'target_type': target_type,
                'op': EQUALS,
                'value': target_id}

    @utils.retry_upon_exception_nsxv3(exceptions.ManagerError)
    def _update_nsgroup_with_members(self, nsgroup_id, members, action):
        members_update = 'ns-groups/%s?action=%s' % (nsgroup_id, action)
        return self.client.create(members_update, members)

    def add_nsgroup_members(self, nsgroup_id, target_type, target_ids):
        members = []
        for target_id in target_ids:
            member_expr = self.get_nsgroup_member_expression(
                target_type, target_id)
            members.append(member_expr)
        members = {'members': members}
        try:
            return self._update_nsgroup_with_members(
                nsgroup_id, members, ADD_MEMBERS)
        except (exceptions.StaleRevision, exceptions.ResourceNotFound):
            raise
        except exceptions.ManagerError:
            # REVISIT(roeyc): A ManagerError might have been raised for a
            # different reason, e.g - NSGroup does not exists.
            LOG.warning(_LW("Failed to add %(target_type)s resources "
                            "(%(target_ids))s to NSGroup %(nsgroup_id)s"),
                        {'target_type': target_type,
                         'target_ids': target_ids,
                         'nsgroup_id': nsgroup_id})

            raise exceptions.NSGroupIsFull(nsgroup_id=nsgroup_id)

    def remove_nsgroup_member(self, nsgroup_id, target_type,
                              target_id, verify=False):
        member_expr = self.get_nsgroup_member_expression(
            target_type, target_id)
        members = {'members': [member_expr]}
        try:
            return self._update_nsgroup_with_members(
                nsgroup_id, members, REMOVE_MEMBERS)
        except exceptions.ManagerError:
            if verify:
                raise exceptions.NSGroupMemberNotFound(member_id=target_id,
                                                       nsgroup_id=nsgroup_id)

    def read_nsgroup(self, nsgroup_id):
        return self.client.get(
            'ns-groups/%s?populate_references=true' % nsgroup_id)

    def delete_nsgroup(self, nsgroup_id):
        try:
            return self.client.delete(
                'ns-groups/%s?force=true' % nsgroup_id)
        # FIXME(roeyc): Should only except NotFound error.
        except Exception:
            LOG.debug("NSGroup %s does not exists for delete request.",
                      nsgroup_id)

    def _build_section(self, display_name, description, applied_tos, tags):
        return {'display_name': display_name,
                'description': description,
                'stateful': True,
                'section_type': LAYER3,
                'applied_tos': [self.get_nsgroup_reference(t_id)
                                for t_id in applied_tos],
                'tags': tags}

    def create_empty_section(self, display_name, description, applied_tos,
                             tags, operation=INSERT_BOTTOM,
                             other_section=None):
        resource = 'firewall/sections?operation=%s' % operation
        body = self._build_section(display_name, description,
                                   applied_tos, tags)
        if other_section:
            resource += '&id=%s' % other_section
        return self.client.create(resource, body)

    @utils.retry_upon_exception_nsxv3(exceptions.StaleRevision)
    def update_section(self, section_id, display_name=None, description=None,
                       applied_tos=None, rules=None):
        resource = 'firewall/sections/%s' % section_id
        section = self.read_section(section_id)

        if rules is not None:
            resource += '?action=update_with_rules'
            section.update({'rules': rules})
        if display_name is not None:
            section['display_name'] = display_name
        if description is not None:
            section['description'] = description
        if applied_tos is not None:
            section['applied_tos'] = [self.get_nsgroup_reference(nsg_id)
                                      for nsg_id in applied_tos]
        if rules is not None:
            return nsxclient.create_resource(resource, section)
        elif any(p is not None for p in (display_name, description,
                                         applied_tos)):
            return self.client.update(resource, section)

    def read_section(self, section_id):
        resource = 'firewall/sections/%s' % section_id
        return self.client.get(resource)

    def list_sections(self):
        resource = 'firewall/sections'
        return self.client.get(resource).get('results', [])

    def delete_section(self, section_id):
        resource = 'firewall/sections/%s?cascade=true' % section_id
        return self.client.delete(resource)

    def get_nsgroup_reference(self, nsgroup_id):
        return {'target_id': nsgroup_id,
                'target_type': NSGROUP}

    def get_ip_cidr_reference(self, ip_cidr_block, ip_protocol):
        target_type = IPV4ADDRESS if ip_protocol == IPV4 else IPV6ADDRESS
        return {'target_id': ip_cidr_block,
                'target_type': target_type}

    def get_firewall_rule_dict(self, display_name, source=None,
                               destination=None,
                               direction=IN_OUT, ip_protocol=IPV4_IPV6,
                               service=None, action=ALLOW, logged=False):
        return {'display_name': display_name,
                'sources': [source] if source else [],
                'destinations': [destination] if destination else [],
                'direction': direction,
                'ip_protocol': ip_protocol,
                'services': [service] if service else [],
                'action': action,
                'logged': logged}

    def add_rule_in_section(self, rule, section_id):
        resource = 'firewall/sections/%s/rules' % section_id
        params = '?operation=insert_bottom'
        return self.client.create(resource + params, rule)

    def add_rules_in_section(self, rules, section_id):
        resource = 'firewall/sections/%s/rules' % section_id
        params = '?action=create_multiple&operation=insert_bottom'
        return self.client.create(resource + params, {'rules': rules})

    def delete_rule(self, section_id, rule_id):
        resource = 'firewall/sections/%s/rules/%s' % (section_id, rule_id)
        return self.client.delete(resource)

    def get_section_rules(self, section_id):
        resource = 'firewall/sections/%s/rules' % section_id
        return self.client.get(resource)
