# Copyright 2016 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.
import logging

from neutron_lib.api import attributes as lib_attrs
from oslo_config import cfg
from oslo_utils import uuidutils
import webob.exc

logging.basicConfig(level=logging.INFO)
LOG = logging.getLogger(__name__)


def _fixup_res_dict(context, attr_name, res_dict, check_allow_post=True):
    # This method is a replacement of _fixup_res_dict which is used in
    # neutron_lib.plugin.utils. All this mock does is insert a uuid
    # for the id field if one is not found ONLY if running in api_replay_mode.
    if cfg.CONF.api_replay_mode and 'id' not in res_dict:
        res_dict['id'] = uuidutils.generate_uuid()
    attr_info = lib_attrs.RESOURCES[attr_name]
    attr_ops = lib_attrs.AttributeInfo(attr_info)
    try:
        attr_ops.populate_project_id(context, res_dict, True)
        lib_attrs.populate_project_info(attr_info)
        attr_ops.verify_attributes(res_dict)
    except webob.exc.HTTPBadRequest as e:
        # convert webob exception into ValueError as these functions are
        # for internal use. webob exception doesn't make sense.
        raise ValueError(e.detail)

    attr_ops.fill_post_defaults(res_dict, check_allow_post=check_allow_post)
    attr_ops.convert_values(res_dict)
    return res_dict


class PrepareObjectForMigration(object):
    """Helper class to modify source objects before creating them in dest"""
    # Remove some fields before creating the new object.
    # Some fields are not supported for a new object, and some are not
    # supported by the destination plugin
    basic_ignore_fields = ['updated_at',
                           'created_at',
                           'tags',
                           'revision',
                           'revision_number']

    drop_sg_rule_fields = basic_ignore_fields
    drop_sg_fields = basic_ignore_fields + ['policy']
    drop_router_fields = basic_ignore_fields + [
        'status',
        'routes',
        'ha',
        'external_gateway_info',
        'router_type',
        'availability_zones',
        'distributed',
        'flavor_id']
    drop_subnetpool_fields = basic_ignore_fields + [
        'id',
        'ip_version']

    drop_subnet_fields = basic_ignore_fields + [
        'advanced_service_providers',
        'id',
        'service_types']

    drop_port_fields = basic_ignore_fields + [
        'status',
        'binding:vif_details',
        'binding:vif_type',
        'binding:host_id',
        'vnic_index',
        'dns_assignment']

    drop_network_fields = basic_ignore_fields + [
        'status',
        'subnets',
        'availability_zones',
        'ipv4_address_scope',
        'ipv6_address_scope',
        'mtu']

    drop_fip_fields = basic_ignore_fields + [
        'status', 'router_id', 'id', 'revision']

    drop_qos_rule_fields = ['revision', 'type', 'qos_policy_id', 'id']
    drop_qos_policy_fields = ['revision']

    drop_fwaas_rule_fields = ['firewall_policy_id']
    drop_fwaas_policy_fields = []
    drop_fwaas_group_fields = ['status']

    def drop_fields(self, item, drop_fields):
        body = {}
        for k, v in item.items():
            if k in drop_fields:
                continue
            body[k] = v
        return body

    def fix_description(self, body):
        # neutron doesn't like description being None even though its
        # what it returns to us.
        if 'description' in body and body['description'] is None:
            body['description'] = ''

    # direct_call arg means that the object is prepared for calling the plugin
    # create method directly
    def prepare_security_group_rule(self, sg_rule, direct_call=False):
        self.fix_description(sg_rule)
        return self.drop_fields(sg_rule, self.drop_sg_rule_fields)

    def prepare_security_group(self, sg, direct_call=False):
        self.fix_description(sg)
        return self.drop_fields(sg, self.drop_sg_fields)

    def prepare_router(self, rtr, dest_azs=None, direct_call=False):
        self.fix_description(rtr)
        body = self.drop_fields(rtr, self.drop_router_fields)
        if dest_azs:
            if body.get('availability_zone_hints'):
                az = body['availability_zone_hints'][0]
                if az not in dest_azs:
                    if az != 'default':
                        LOG.warning("Ignoring AZ %s in router %s as it is not "
                                    "defined in destination", az, rtr['id'])
                    body['availability_zone_hints'] = []
        elif direct_call:
            body['availability_zone_hints'] = []
        return body

    def prepare_subnetpool(self, pool, direct_call=False):
        self.fix_description(pool)
        return self.drop_fields(pool, self.drop_subnetpool_fields)

    def prepare_network(self, net, dest_default_public_net=True,
                        remove_qos=False, dest_azs=None, direct_call=False):
        self.fix_description(net)
        body = self.drop_fields(net, self.drop_network_fields)

        if remove_qos:
            body = self.drop_fields(body, ['qos_policy_id'])

        # neutron doesn't like some fields being None even though its
        # what it returns to us.
        for field in ['provider:physical_network',
                      'provider:segmentation_id']:
            if field in body and body[field] is None:
                del body[field]

        # vxlan network with segmentation id should be translated to a regular
        # network in nsx-v3/P.
        if (body.get('provider:network_type') == 'vxlan' and
            body.get('provider:segmentation_id') is not None):
            del body['provider:network_type']
            del body['provider:segmentation_id']

        # flat network should be translated to a regular network in nsx-v3/P.
        if (body.get('provider:network_type') == 'flat'):
            del body['provider:network_type']
            if 'provider:physical_network' in body:
                del body['provider:physical_network']

        # external networks needs some special care
        if body.get('router:external'):
            fields_reset = False
            for field in ['provider:network_type', 'provider:segmentation_id',
                          'provider:physical_network']:
                if field in body:
                    if body[field] is not None:
                        fields_reset = True
                    del body[field]
            if fields_reset:
                LOG.warning("Ignoring provider network fields while migrating "
                            "external network %s", body['id'])
            if body.get('is_default') and dest_default_public_net:
                body['is_default'] = False
                LOG.warning("Public network %s was set to non default network",
                            body['id'])
        if dest_azs:
            if body.get('availability_zone_hints'):
                az = body['availability_zone_hints'][0]
                if az not in dest_azs:
                    if az != 'default':
                        LOG.warning("Ignoring AZ %s in net %s as it is not "
                                    "defined in destination", az, body['id'])
                    body['availability_zone_hints'] = []
        elif direct_call:
            body['availability_zone_hints'] = []
        return body

    def prepare_subnet(self, subnet, direct_call=False):
        self.fix_description(subnet)
        body = self.drop_fields(subnet, self.drop_subnet_fields)

        # Drops v6 fields on subnets that are v4 as server doesn't allow them.
        v6_fields_to_remove = ['ipv6_address_mode', 'ipv6_ra_mode']
        if body['ip_version'] == 4:
            for field in v6_fields_to_remove:
                if field in body:
                    body.pop(field)
        return body

    def prepare_port(self, port, remove_qos=False, direct_call=False):
        self.fix_description(port)
        body = self.drop_fields(port, self.drop_port_fields)
        if remove_qos:
            body = self.drop_fields(body, ['qos_policy_id'])

        if 'allowed_address_pairs' in body:
            if not body['allowed_address_pairs']:
                # remove allowed_address_pairs if empty:
                del body['allowed_address_pairs']
            else:
                # remove unsupported allowed_address_pairs
                for pair in body['allowed_address_pairs']:
                    ip = pair.get('ip_address')
                    if len(ip.split('/')) > 1:
                        LOG.warning("ignoring allowed_address_pair %s for "
                                    "port %s as cidr is not supported",
                                    pair, port['id'])
                        body['allowed_address_pairs'].remove(pair)

        # remove port security if mac learning is enabled
        if (body.get('mac_learning_enabled') and
            body.get('port_security_enabled')):
            LOG.warning("Disabling port security of port %s: The plugin "
                        "doesn't support mac learning with port security",
                        body['id'])
            body['port_security_enabled'] = False
            body['security_groups'] = []

        if direct_call:
            if 'device_id' not in body:
                body['device_id'] = ""
            if 'device_owner' not in body:
                body['device_owner'] = ""

        return body

    def prepare_floatingip(self, fip, direct_call=False):
        self.fix_description(fip)
        return self.drop_fields(fip, self.drop_fip_fields)

    def prepare_qos_rule(self, rule, direct_call=False):
        self.fix_description(rule)
        return self.drop_fields(rule, self.drop_qos_rule_fields)

    def prepare_qos_policy(self, policy, direct_call=False):
        self.fix_description(policy)
        return self.drop_fields(policy, self.drop_qos_policy_fields)

    def prepare_fwaas_rule(self, rule):
        self.fix_description(rule)
        return self.drop_fields(rule, self.drop_fwaas_rule_fields)

    def prepare_fwaas_policy(self, policy):
        self.fix_description(policy)
        return self.drop_fields(policy, self.drop_fwaas_policy_fields)

    def prepare_fwaas_group(self, group):
        self.fix_description(group)
        return self.drop_fields(group, self.drop_fwaas_group_fields)
