# Copyright 2014 VMware, Inc.
# 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.

from neutron_lib import constants
from neutron_lib import exceptions
from oslo_log import log
from oslo_serialization import jsonutils
from oslo_utils import excutils

from vmware_nsx._i18n import _LW
from vmware_nsx.common import utils
from vmware_nsx.nsxlib import mh as nsxlib

HTTP_GET = "GET"
HTTP_POST = "POST"
HTTP_DELETE = "DELETE"
HTTP_PUT = "PUT"

SECPROF_RESOURCE = "security-profile"

LOG = log.getLogger(__name__)


def mk_body(**kwargs):
    """Convenience function creates and dumps dictionary to string.

    :param kwargs: the key/value pirs to be dumped into a json string.
    :returns: a json string.
    """
    return jsonutils.dumps(kwargs, ensure_ascii=False)


def query_security_profiles(cluster, fields=None, filters=None):
    return nsxlib.get_all_query_pages(
        nsxlib._build_uri_path(SECPROF_RESOURCE,
                               fields=fields,
                               filters=filters),
        cluster)


def create_security_profile(cluster, tenant_id, neutron_id, security_profile):
    """Create a security profile on the NSX backend.

    :param cluster: a NSX cluster object reference
    :param tenant_id: identifier of the Neutron tenant
    :param neutron_id: neutron security group identifier
    :param security_profile: dictionary with data for
    configuring the NSX security profile.
    """
    path = "/ws.v1/security-profile"
    # Allow all dhcp responses and all ingress traffic
    hidden_rules = {'logical_port_egress_rules':
                    [{'ethertype': 'IPv4',
                      'protocol': constants.PROTO_NUM_UDP,
                      'port_range_min': constants.DHCP_RESPONSE_PORT,
                      'port_range_max': constants.DHCP_RESPONSE_PORT,
                      'ip_prefix': '0.0.0.0/0'}],
                    'logical_port_ingress_rules':
                    [{'ethertype': 'IPv4'},
                     {'ethertype': 'IPv6'}]}
    display_name = utils.check_and_truncate(security_profile.get('name'))
    # NOTE(salv-orlando): neutron-id tags are prepended with 'q' for
    # historical reasons
    body = mk_body(
        tags=utils.get_tags(os_tid=tenant_id, q_sec_group_id=neutron_id),
        display_name=display_name,
        logical_port_ingress_rules=(
            hidden_rules['logical_port_ingress_rules']),
        logical_port_egress_rules=hidden_rules['logical_port_egress_rules']
    )
    rsp = nsxlib.do_request(HTTP_POST, path, body, cluster=cluster)
    if security_profile.get('name') == 'default':
        # If security group is default allow ip traffic between
        # members of the same security profile is allowed and ingress traffic
        # from the switch
        rules = {'logical_port_egress_rules': [{'ethertype': 'IPv4',
                                                'profile_uuid': rsp['uuid']},
                                               {'ethertype': 'IPv6',
                                                'profile_uuid': rsp['uuid']}],
                 'logical_port_ingress_rules': [{'ethertype': 'IPv4'},
                                                {'ethertype': 'IPv6'}]}

        update_security_group_rules(cluster, rsp['uuid'], rules)
    LOG.debug("Created Security Profile: %s", rsp)
    return rsp


def update_security_group_rules(cluster, spid, rules):
    path = "/ws.v1/security-profile/%s" % spid

    # Allow all dhcp responses in
    rules['logical_port_egress_rules'].append(
        {'ethertype': 'IPv4', 'protocol': constants.PROTO_NUM_UDP,
         'port_range_min': constants.DHCP_RESPONSE_PORT,
         'port_range_max': constants.DHCP_RESPONSE_PORT,
         'ip_prefix': '0.0.0.0/0'})
    # If there are no ingress rules add bunk rule to drop all ingress traffic
    if not rules['logical_port_ingress_rules']:
        rules['logical_port_ingress_rules'].append(
            {'ethertype': 'IPv4', 'ip_prefix': '127.0.0.1/32'})
    try:
        body = mk_body(
            logical_port_ingress_rules=summarize_security_group_rules(rules[
                'logical_port_ingress_rules']),
            logical_port_egress_rules=summarize_security_group_rules(rules[
                'logical_port_egress_rules']))
        rsp = nsxlib.do_request(HTTP_PUT, path, body, cluster=cluster)
    except exceptions.NotFound as e:
        LOG.error(nsxlib.format_exception("Unknown", e, locals()))
        #FIXME(salvatore-orlando): This should not raise NeutronException
        raise exceptions.NeutronException()
    LOG.debug("Updated Security Profile: %s", rsp)
    return rsp


def update_security_profile(cluster, spid, name):
    return nsxlib.do_request(
        HTTP_PUT,
        nsxlib._build_uri_path(SECPROF_RESOURCE, resource_id=spid),
        jsonutils.dumps({"display_name": utils.check_and_truncate(name)}),
        cluster=cluster)


def delete_security_profile(cluster, spid):
    path = "/ws.v1/security-profile/%s" % spid

    try:
        nsxlib.do_request(HTTP_DELETE, path, cluster=cluster)
    except exceptions.NotFound:
        with excutils.save_and_reraise_exception():
            # This is not necessarily an error condition
            LOG.warning(_LW("Unable to find security profile %s on NSX "
                            "backend"), spid)


def summarize_security_group_rules(logical_port_rules):
    """
    Summarizes security group rules and remove duplicates. Given a set of
    arbitrary security group rules, determining the optimum (minimum) rule set
    is a complex (NP-hard) problem. This method does not attempt to obtain the
    optimum rules. Instead, it summarizes a set of common rule patterns.
    """

    # Remove port_range_min & port_range_max if it covers the entire port
    # range. Also, remove quad-zero default IPv4 and default IPv6 routes
    for rule in logical_port_rules:
        if ('port_range_min' in rule and 'port_range_max' in rule and
                rule['port_range_min'] <= 1 and
                rule['port_range_max'] == 65535):
            del rule['port_range_min']
            del rule['port_range_max']

        if ('ip_prefix' in rule and
                rule['ip_prefix'] in ['0.0.0.0/0', '::/0']):
            del rule['ip_prefix']

    # Remove duplicate rules. Loop through each rule rule_i and exclude a
    # rule if it is part of another rule.
    logical_port_rules_summarized = []
    for i in range(len(logical_port_rules)):
        for j in range(len(logical_port_rules)):
            if i != j:
                if is_sg_rules_identical(logical_port_rules[i],
                                         logical_port_rules[j]):
                    pass
                elif is_sg_rule_subset(logical_port_rules[i],
                                       logical_port_rules[j]):
                    break
        else:
            logical_port_rules_summarized.append(logical_port_rules[i])

    return logical_port_rules_summarized


def is_sg_rules_identical(sgr1, sgr2):
    """
    determines if security group rule sgr1 and sgr2 are identical
    """
    return (sgr1['ethertype'] == sgr2['ethertype'] and
            sgr1.get('protocol') == sgr2.get('protocol') and
            sgr1.get('port_range_min') == sgr2.get('port_range_min') and
            sgr1.get('port_range_max') == sgr2.get('port_range_max') and
            sgr1.get('ip_prefix') == sgr2.get('ip_prefix') and
            sgr1.get('profile_uuid') == sgr2.get('profile_uuid'))


def is_sg_rule_subset(sgr1, sgr2):
    """
    determine if security group rule sgr1 is a strict subset of sgr2
    """
    all_protocols = set(range(256))
    sgr1_protocols = {sgr1['protocol']} if 'protocol' in sgr1 else \
        all_protocols
    sgr2_protocols = {sgr2['protocol']} if 'protocol' in sgr2 else \
        all_protocols

    return (sgr1['ethertype'] == sgr2['ethertype'] and
            sgr1_protocols.issubset(sgr2_protocols) and
            sgr1.get('port_range_min', 0) >= sgr2.get('port_range_min', 0) and
            sgr1.get('port_range_max', 65535) <= sgr2.get('port_range_max',
                                                          65535) and
            (sgr2.get('ip_prefix') is None or
             sgr1.get('ip_prefix') == sgr2.get('prefix')) and
            (sgr2.get('profile_uuid') is None or
             sgr1.get('profile_uuid') == sgr2.get('profile_uuid')))
