# Copyright 2017 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 sys

from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils

from neutron.extensions import securitygroup as ext_sg
from neutron_lib.callbacks import registry
from neutron_lib import context as n_context
from neutron_lib import exceptions

from vmware_nsx.api_replay import utils as replay_utils
from vmware_nsx.db import db
from vmware_nsx.extensions import projectpluginmap
from vmware_nsx.shell.admin.plugins.common import constants
from vmware_nsx.shell.admin.plugins.common import utils as admin_utils
from vmware_nsx.shell.admin.plugins.nsxv.resources import utils as v_utils
from vmware_nsx.shell.admin.plugins.nsxv3.resources import utils as v3_utils
from vmware_nsx.shell import resources as shell

LOG = logging.getLogger(__name__)
# list of supported objects to migrate in order of deletion (creation will be
# in the opposite order)
migrated_resources = ["floatingip", "router", "port", "subnet",
                      "network", "security_group"]
#TODO(asarfaty): add other resources of different service plugins like
#vpnaas, fwaas, lbaas, qos, subnetpool, etc


@admin_utils.output_header
def import_projects(resource, event, trigger, **kwargs):
    """Import existing openstack projects to the current plugin"""
    # TODO(asarfaty): get the projects list from keystone
    # get the plugin name from the user
    if not kwargs.get('property'):
        LOG.error("Need to specify plugin and project parameters")
        return
    else:
        properties = admin_utils.parse_multi_keyval_opt(kwargs['property'])
        plugin = properties.get('plugin')
        project = properties.get('project')
        if not plugin or not project:
            LOG.error("Need to specify plugin and project parameters")
            return
    if plugin not in projectpluginmap.VALID_TYPES:
        LOG.error("The supported plugins are %s", projectpluginmap.VALID_TYPES)
        return

    ctx = n_context.get_admin_context()
    curr_map = db.get_project_plugin_mapping(ctx.session, project)
    if not curr_map:
        db.add_project_plugin_mapping(ctx.session, project, plugin)
        LOG.info('Done.')
    else:
        if curr_map.plugin == plugin:
            LOG.info("%s is already the plugin of project %s", plugin, project)
        else:
            LOG.error("Project %s is mapped to plugin %s. Existing mapping "
                      "cannot be modified.", project, curr_map.plugin)


def get_resource_file_name(project_id, resource):
    return "%s_nsxv_%ss" % (project_id, resource)


def read_v_resources_to_files(context, project_id):
    """Read all relevant NSX-V resources from a specific project

    and write them into a json file
    """
    results = {}
    with v_utils.NsxVPluginWrapper() as plugin:
        filters = {'project_id': [project_id]}
        for resource in migrated_resources:
            filename = get_resource_file_name(project_id, resource)
            file = open(filename, 'w')
            get_objects = getattr(plugin, "get_%ss" % resource)
            objects = get_objects(context, filters=filters)

            # also add router gateway ports of the relevant routers
            # (don't have the project id)
            if resource == 'port':
                rtr_ids = [rtr['id'] for rtr in results['router']]
                gw_filters = {'device_owner': ['network:router_gateway'],
                              'device_id': rtr_ids}
                gw_ports = plugin.get_ports(context, filters=gw_filters,
                                            filter_project=False)
                # ignore metadata gw ports
                objects.extend([port for port in gw_ports
                                if not port['tenant_id']])

            file.write(jsonutils.dumps(objects, sort_keys=True, indent=4))
            file.close()
            results[resource] = objects

    return results


def read_v_resources_from_files(project_id):
    """Read all relevant NSX-V resources from a json file"""
    results = {}
    for resource in migrated_resources:
        filename = get_resource_file_name(project_id, resource)
        file = open(filename, 'r')
        results[resource] = jsonutils.loads(file.read())
        file.close()
    return results


def delete_router_routes_and_interfaces(context, plugin, router):
    if router.get('routes'):
        plugin.update_router(context, router['id'],
                             {'router': {'routes': []}})

    interfaces = plugin._get_router_interfaces(context, router['id'])
    for port in interfaces:
        plugin.remove_router_interface(context, router['id'],
                                       {'port_id': port['id']})


def delete_v_resources(context, objects):
    """Delete a list of objects from the V plugin"""
    with v_utils.NsxVPluginWrapper() as plugin:
        LOG.info(">>>>Deleting all NSX-V objects of the project.")
        for resource in migrated_resources:
            get_object = getattr(plugin, "get_%s" % resource)
            del_object = getattr(plugin, "delete_%s" % resource)
            for obj in objects[resource]:
                # verify that this object still exists
                try:
                    get_object(context, obj['id'])
                except exceptions.NotFound:
                    # prevent logger from logging this exception
                    sys.exc_clear()
                    continue

                try:
                    # handle special cases before delete
                    if resource == 'router':
                        delete_router_routes_and_interfaces(
                            context, plugin, obj)
                    elif resource == 'port':
                        if obj['device_owner'] == 'network:dhcp':
                            continue
                    # delete the objects from the NSX-V plugin
                    del_object(context, obj['id'])
                    LOG.info(">>Deleted %(resource)s %(name)s",
                             {'resource': resource,
                              'name': obj.get('name') or obj['id']})
                except Exception as e:
                    LOG.warning(">>Failed to delete %(resource)s %(name)s: "
                                "%(e)s",
                                {'resource': resource,
                                 'name': obj.get('name') or obj['id'], 'e': e})
    LOG.info(">>>>Done deleting all NSX-V objects.")


def get_router_by_id(objects, router_id):
    for rtr in objects.get('router', []):
        if rtr['id'] == router_id:
            return rtr


def create_t_resources(context, objects, ext_net):
    """Create a list of objects in the T plugin"""
    LOG.info(">>>>Creating all the objects of the project in NSX-T.")
    prepare = replay_utils.PrepareObjectForMigration()
    with v3_utils.NsxV3PluginWrapper() as plugin:
        # create the resource in the order opposite to the deletion
        # (but start with routers)
        ordered_resources = migrated_resources[::-1]
        ordered_resources.remove('router')
        ordered_resources = ['router'] + ordered_resources
        dhcp_subnets = []
        for resource in ordered_resources:
            total_num = len(objects[resource])
            LOG.info(">>>Creating %s %s%s.", total_num,
                     resource, 's' if total_num > 1 else '')
            get_object = getattr(plugin, "get_%s" % resource)
            create_object = getattr(plugin, "create_%s" % resource)
            # go over the objects of this resource
            for count, obj in enumerate(objects[resource], 1):
                # check if this object already exists
                try:
                    get_object(context, obj['id'])
                except exceptions.NotFound:
                    # prevent logger from logging this exception
                    sys.exc_clear()
                else:
                    # already exists (this will happen if we rerun from files,
                    # or if the deletion failed)
                    LOG.info(">>Skipping %(resource)s %(name)s %(count)s/"
                             "%(total)s as it was already created.",
                             {'resource': resource,
                              'name': obj.get('name') or obj['id'],
                              'count': count,
                              'total': total_num})
                    continue

                # fix object before creation using the api replay code
                orig_id = obj['id']
                prepare_object = getattr(prepare, "prepare_%s" % resource)
                # TODO(asarfaty): Add availability zones support too
                obj_data = prepare_object(obj, direct_call=True)
                enable_dhcp = False
                # special cases for different objects before create:
                if resource == 'subnet':
                    if obj_data['enable_dhcp']:
                        enable_dhcp = True
                        # disable dhcp for now, to avoid ip collisions
                        obj_data['enable_dhcp'] = False
                elif resource == 'security_group':
                    # security group rules should be added separately
                    sg_rules = obj_data.pop('security_group_rules')
                elif resource == 'floatingip':
                    # Create the floating IP on the T external network
                    obj_data['floating_network_id'] = ext_net
                    del obj_data['floating_ip_address']
                elif resource == 'port':
                    # remove the old subnet id field from ports fixed_ips dict
                    # since the subnet ids are changed
                    for fixed_ips in obj_data['fixed_ips']:
                        del fixed_ips['subnet_id']

                    if obj_data['device_owner'] == 'network:dhcp':
                        continue
                    if obj_data['device_owner'] == 'network:floatingip':
                        continue
                    if obj_data['device_owner'] == 'network:router_gateway':
                        # add a gateway on the new ext network for this router
                        router_id = obj_data['device_id']
                        # keep the original enable-snat value
                        router_data = get_router_by_id(objects, router_id)
                        enable_snat = router_data['external_gateway_info'].get(
                                'enable_snat', True)
                        rtr_body = {
                            "external_gateway_info":
                                {"network_id": ext_net,
                                 "enable_snat": enable_snat}}
                        try:
                            plugin.update_router(
                                context, router_id, {'router': rtr_body})
                            LOG.info(">>Uplinked router %(rtr)s to new "
                                     "external network %(net)s",
                                     {'rtr': router_id,
                                      'net': ext_net})

                        except Exception as e:
                            LOG.error(">>Failed to add router %(rtr)s "
                                      "gateway: %(e)s",
                                      {'rtr': router_id, 'e': e})
                            continue
                    if obj_data['device_owner'] == 'network:router_interface':
                        try:
                            # uplink router_interface ports by creating the
                            # port, and attaching it to the router
                            router_id = obj_data['device_id']
                            obj_data['device_owner'] = ""
                            obj_data['device_id'] = ""
                            created_port = plugin.create_port(
                                context,
                                {'port': obj_data})
                            LOG.info(">>Created interface port %(port)s, ip "
                                     "%(ip)s, mac %(mac)s)",
                                     {'port': created_port['id'],
                                      'ip': created_port['fixed_ips'][0][
                                            'ip_address'],
                                      'mac': created_port['mac_address']})
                            plugin.add_router_interface(
                                context,
                                router_id,
                                {'port_id': created_port['id']})
                            LOG.info(">>Uplinked router %(rtr)s to network "
                                     "%(net)s",
                                     {'rtr': router_id,
                                      'net': obj_data['network_id']})
                        except Exception as e:
                            LOG.error(">>Failed to add router %(rtr)s "
                                      "interface port: %(e)s",
                                      {'rtr': router_id, 'e': e})
                        continue

                # create the object on the NSX-T plugin
                try:
                    created_obj = create_object(context, {resource: obj_data})
                    LOG.info(">>Created %(resource)s %(name)s %(count)s/"
                             "%(total)s",
                             {'resource': resource, 'count': count,
                              'name': obj_data.get('name') or orig_id,
                              'total': total_num})
                except Exception as e:
                    # TODO(asarfaty): subnets ids are changed, so recreating a
                    # subnet will fail on overlapping ips.
                    LOG.error(">>Failed to create %(resource)s %(name)s: "
                              "%(e)s",
                              {'resource': resource, 'e': e,
                               'name': obj_data.get('name') or orig_id})
                    continue

                # special cases for different objects after create:
                if resource == 'security_group':
                    sg_id = obj_data.get('name') or obj_data['id']
                    for rule in sg_rules:
                        rule_data = prepare.prepare_security_group_rule(rule)
                        try:
                            plugin.create_security_group_rule(
                                context, {'security_group_rule': rule_data})
                        except ext_sg.SecurityGroupRuleExists:
                            # default rules were already created.
                            # prevent logger from logging this exception
                            sys.exc_clear()
                        except Exception as e:
                            LOG.error(
                                ">>Failed to create security group %(name)s "
                                "rules: %(e)s",
                                {'name': sg_id, 'e': e})
                elif resource == 'subnet':
                    if enable_dhcp:
                        dhcp_subnets.append(created_obj['id'])

        # Enable dhcp on all the relevant subnets (after creating all ports,
        # to maintain original IPs):
        if dhcp_subnets:
            for subnet_id in dhcp_subnets:
                try:
                    plugin.update_subnet(
                        context, subnet_id,
                        {'subnet': {'enable_dhcp': True}})

                except Exception as e:
                    LOG.error("Failed to enable DHCP on subnet %(subnet)s:"
                              " %(e)s",
                              {'subnet': subnet_id, 'e': e})

        # Add static routes (after all router interfaces and gateways are set)
        for obj_data in objects['router']:
            if 'routes' in obj_data:
                try:
                    plugin.update_router(
                        context, obj_data['id'],
                        {'router': {'routes': obj_data['routes']}})
                except Exception as e:
                    LOG.error("Failed to add routes to router %(rtr)s: "
                              "%(e)s",
                              {'rtr': obj_data['id'], 'e': e})

    LOG.info(">>>Done Creating all objects in NSX-T.")


@admin_utils.output_header
def migrate_v_project_to_t(resource, event, trigger, **kwargs):
    """Migrate 1 project from v to t with all its resources"""

    # filter out the plugins INFO logging
    # TODO(asarfaty): Consider this for all admin utils
    LOG.logger.setLevel(logging.INFO)
    logging.getLogger(None).logger.setLevel(logging.WARN)

    # get the configuration: tenant + public network + from file flag
    usage = ("Usage: nsxadmin -r projects -o %s --property project-id=<> "
             "--property external-net=<NSX-T external network to be used> "
             "<--property from-file=True>" %
             shell.Operations.NSX_MIGRATE_V_V3.value)
    if not kwargs.get('property'):
        LOG.error("Missing parameters: %s", usage)
        return
    properties = admin_utils.parse_multi_keyval_opt(kwargs['property'])
    project = properties.get('project-id')
    ext_net_id = properties.get('external-net')
    from_file = properties.get('from-file', 'false').lower() == "true"
    # TODO(asarfaty): get files path
    if not project:
        LOG.error("Missing project-id parameter: %s", usage)
        return
    if not ext_net_id:
        LOG.error("Missing external-net parameter: %s", usage)
        return

    # check if files exist in the current directory
    try:
        filename = get_resource_file_name(project, 'network')
        file = open(filename, 'r')
        if file.read():
            if not from_file:
                from_file = admin_utils.query_yes_no(
                    "Use existing resources files for this project?",
                    default="yes")
        file.close()
    except Exception:
        sys.exc_clear()
        if from_file:
            LOG.error("Cannot run from file: files not found")
            return

    # validate tenant id and public network
    ctx = n_context.get_admin_context()
    mapping = db.get_project_plugin_mapping(ctx.session, project)
    current_plugin = mapping.plugin
    if not mapping:
        LOG.error("Project %s is unknown", project)
        return
    if not from_file and current_plugin != projectpluginmap.NsxPlugins.NSX_V:
        LOG.error("Project %s belongs to plugin %s.", project, mapping.plugin)
        return

    with v3_utils.NsxV3PluginWrapper() as plugin:
        try:
            plugin.get_network(ctx, ext_net_id)
        except exceptions.NetworkNotFound:
            LOG.error("Network %s was not found", ext_net_id)
            return
        if not plugin._network_is_external(ctx, ext_net_id):
            LOG.error("Network %s is not external", ext_net_id)
            return

    if from_file:
        # read resources from files
        objects = read_v_resources_from_files(project)
    else:
        # read all V resources and dump to a file
        objects = read_v_resources_to_files(ctx, project)

    # delete all the V resources (reading it from the files)
    if current_plugin == projectpluginmap.NsxPlugins.NSX_V:
        delete_v_resources(ctx, objects)

    # change the mapping of this tenant to T
    db.update_project_plugin_mapping(ctx.session, project,
                                     projectpluginmap.NsxPlugins.NSX_T)

    # use api replay flag to allow keeping the IDs
    cfg.CONF.set_override('api_replay_mode', True)

    # add resources 1 by one after adapting them to T (api-replay code)
    create_t_resources(ctx, objects, ext_net_id)

    # reset api replay flag to allow keeping the IDs
    cfg.CONF.set_override('api_replay_mode', False)


registry.subscribe(import_projects,
                   constants.PROJECTS,
                   shell.Operations.IMPORT.value)

registry.subscribe(migrate_v_project_to_t,
                   constants.PROJECTS,
                   shell.Operations.NSX_MIGRATE_V_V3.value)
