# 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.


from vmware_nsx.shell.admin.plugins.common import constants
from vmware_nsx.shell.admin.plugins.common import formatters
import vmware_nsx.shell.admin.plugins.common.utils as admin_utils
from vmware_nsx.shell.admin.plugins.nsxv.resources import utils
import vmware_nsx.shell.resources as shell

from neutron_lib.callbacks import registry
from neutron_lib import context as n_context
from oslo_config import cfg
from oslo_log import log as logging

from vmware_nsx.common import locking
from vmware_nsx.db import nsxv_db
from vmware_nsx.extensions import routersize
from vmware_nsx.plugins.nsx_v import availability_zones as nsx_az
from vmware_nsx.plugins.nsx_v import md_proxy
from vmware_nsx.plugins.nsx_v.vshield import edge_utils
from vmware_nsx.plugins.nsx_v.vshield import vcns_driver

LOG = logging.getLogger(__name__)


def delete_old_edge(context, old_edge_id):
    LOG.info("Deleting the old edge: %s", old_edge_id)

    # clean it up from the DB
    nsxv_db.clean_edge_router_binding(context.session, old_edge_id)
    nsxv_db.clean_edge_vnic_binding(context.session, old_edge_id)
    nsxv_db.cleanup_nsxv_edge_firewallrule_binding(context.session,
                                                   old_edge_id)

    with locking.LockManager.get_lock(old_edge_id):
        # Delete from NSXv backend
        # Note - If we will not delete the edge, but free it - it will be
        # immediately used as the new one, So it is better to delete it.
        try:
            nsxv = utils.get_nsxv_client()
            nsxv.delete_edge(old_edge_id)
        except Exception as e:
            LOG.warning("Failed to delete the old edge %(id)s: %(e)s",
                        {'id': old_edge_id, 'e': e})
            # Continue the process anyway
            # The edge may have been already deleted at the backend


def _get_router_az_from_plugin_router(router):
    # If the router edge was already deployed the availability_zones will
    # return the az
    az_name = router.get('availability_zones', [''])[0]
    if not az_name:
        # If it was not deployed - it may be in the creation hints
        az_name = router.get('availability_zones_hints', [''])[0]
    if not az_name:
        # If not - the default az was used.
        az_name = nsx_az.DEFAULT_NAME
    return az_name


def nsx_recreate_router_edge(old_edge_id):
    # init the plugin and edge manager
    cfg.CONF.set_override('core_plugin',
                          'vmware_nsx.shell.admin.plugins.nsxv.resources'
                          '.utils.NsxVPluginWrapper')
    with utils.NsxVPluginWrapper() as plugin:
        nsxv_manager = vcns_driver.VcnsDriver(
            edge_utils.NsxVCallbacks(plugin))
        edge_manager = edge_utils.EdgeManager(nsxv_manager, plugin)
        context = n_context.get_admin_context()

        # verify that this is a Router edge
        router_ids = edge_manager.get_routers_on_edge(context, old_edge_id)
        if not router_ids:
            LOG.error("Edge %(edge_id)s is not a router edge",
                     {'edge_id': old_edge_id})
            return

        # all the routers on the same edge have the same type, so it
        # is ok to check the type once
        example_router = plugin.get_router(context, router_ids[0])
        if example_router.get('distributed'):
            LOG.error("Recreating a distributed router edge is not "
                      "supported")
            return
        router_driver = plugin._router_managers.get_tenant_router_driver(
            context, example_router['router_type'])

        # load all the routers before deleting their binding
        routers = []
        for router_id in router_ids:
            routers.append(plugin.get_router(context, router_id))

        # delete the backend edge and all the relevant DB entries
        delete_old_edge(context, old_edge_id)

        # Go over all the relevant routers
        for router in routers:
            router_id = router['id']
            az_name = _get_router_az_from_plugin_router(router)
            # clean up other objects related to this router
            if plugin.metadata_proxy_handler:
                md_proxy_h = plugin.get_metadata_proxy_handler(az_name)
                md_proxy_h.cleanup_router_edge(context, router_id)

            # attach the router to a new edge
            appliance_size = router.get(routersize.ROUTER_SIZE)
            router_driver.attach_router(context, router_id,
                                        {'router': router},
                                        appliance_size=appliance_size)
            # find out who is the new edge to print it
            new_edge_id = router_driver._get_edge_id_or_raise(
                context, router_id)
            LOG.info("Router %(router)s was attached to edge %(edge)s",
                     {'router': router_id, 'edge': new_edge_id})


def nsx_recreate_router(router_id):
    # init the plugin and edge manager
    cfg.CONF.set_override('core_plugin',
                          'vmware_nsx.shell.admin.plugins.nsxv.resources'
                          '.utils.NsxVPluginWrapper')
    with utils.NsxVPluginWrapper() as plugin:
        context = n_context.get_admin_context()

        router = plugin.get_router(context, router_id)
        if router.get('distributed'):
            LOG.error("Recreating a distributed router is not supported")
            return
        router_driver = plugin._router_managers.get_tenant_router_driver(
            context, router['router_type'])

        # Check if it is already attached to an edge
        binding = nsxv_db.get_nsxv_router_binding(context.session,
                                                  router_id)
        if binding:
            old_edge_id = binding['edge_id']
            # detach the router from this edge
            LOG.info("Detaching the router from edge %s", old_edge_id)
            router_driver.detach_router(context, router_id,
                                        {'router': router})

        # attach the router to a new edge
        appliance_size = router.get(routersize.ROUTER_SIZE)
        router_driver.attach_router(context, router_id,
                                    {'router': router},
                                    appliance_size=appliance_size)
        # find out who is the new edge to print it
        new_edge_id = router_driver._get_edge_id_or_raise(
            context, router_id)
        LOG.info("Router %(router)s was attached to edge %(edge)s",
                 {'router': router_id, 'edge': new_edge_id})


@admin_utils.output_header
@admin_utils.unpack_payload
def nsx_recreate_router_or_edge(resource, event, trigger, **kwargs):
    """Recreate a router edge with all the data on a new NSXv edge"""
    if not kwargs.get('property'):
        LOG.error("Need to specify edge-id or router-id parameter")
        return

    # input validation
    properties = admin_utils.parse_multi_keyval_opt(kwargs['property'])
    old_edge_id = properties.get('edge-id')
    router_id = properties.get('router-id')
    if (not old_edge_id and not router_id) or (old_edge_id and router_id):
        LOG.error("Need to specify edge-id or router-id parameter")
        return

    if old_edge_id:
        LOG.info("ReCreating NSXv Router Edge: %s", old_edge_id)
        return nsx_recreate_router_edge(old_edge_id)
    LOG.info("ReCreating NSXv Router: %s", router_id)
    return nsx_recreate_router(router_id)


@admin_utils.output_header
@admin_utils.unpack_payload
def migrate_distributed_routers_dhcp(resource, event, trigger, **kwargs):
    context = n_context.get_admin_context()
    nsxv = utils.get_nsxv_client()
    with utils.NsxVPluginWrapper() as plugin:
        nsxv_manager = vcns_driver.VcnsDriver(
            edge_utils.NsxVCallbacks(plugin))
        edge_manager = edge_utils.EdgeManager(nsxv_manager, plugin)
        routers = plugin.get_routers(context)
        for router in routers:
            if router.get('distributed', False):
                binding = nsxv_db.get_nsxv_router_binding(context.session,
                                                          router['id'])
                if binding:
                    edge_id = binding['edge_id']
                    with locking.LockManager.get_lock(edge_id):
                        route_obj = nsxv.get_routes(edge_id)[1]
                        routes = route_obj.get('staticRoutes', {}
                                               ).get('staticRoutes', [])
                        new_routes = [route for route in routes if route.get(
                            'network') != '169.254.169.254/32']
                        route_obj['staticRoutes']['staticRoutes'] = new_routes

                        nsxv.update_routes(edge_id, route_obj)

                        _update_vdr_fw_config(nsxv, edge_id)
                        plr_id = edge_manager.get_plr_by_tlr_id(context,
                                                                router['id'])

                        if plr_id:
                            binding = nsxv_db.get_nsxv_router_binding(
                                context.session, plr_id)
                            if binding:
                                _update_vdr_fw_config(nsxv, binding['edge_id'])


@admin_utils.output_header
@admin_utils.unpack_payload
def update_edge_firewalls(resource, event, trigger, **kwargs):
    context = n_context.get_admin_context()
    updated_routers = []
    with utils.NsxVPluginWrapper() as plugin:
        shared_dr = plugin._router_managers.get_tenant_router_driver(
            context, 'shared')
        routers = plugin.get_routers(context)
        for router in routers:
            if router['id'] in updated_routers:
                continue
            if router.get('distributed', False):
                # Distributes firewall - Update plr and tlr
                router_db = plugin._get_router(context, router['id'])
                plugin._update_subnets_and_dnat_firewall(context, router_db)
                plr_id = plugin.edge_manager.get_plr_by_tlr_id(
                    context, router['id'])
                if plr_id:
                    plugin._update_subnets_and_dnat_firewall(
                        context, router, router_id=plr_id)
                updated_routers.append(router['id'])
            elif router.get('router_type') == 'shared':
                # Shared router
                router_ids = shared_dr.edge_manager.get_routers_on_same_edge(
                    context, router['id'])
                shared_dr._update_subnets_and_dnat_firewall_on_routers(
                    context, router['id'], router_ids)
                updated_routers.extend(router_ids)
            else:
                # Exclusive router
                router_db = plugin._get_router(context, router['id'])
                plugin._update_subnets_and_dnat_firewall(context, router_db)
                updated_routers.append(router['id'])

    LOG.info("Updated edge firewall rules for routers: %s", updated_routers)


def _update_vdr_fw_config(nsxv, edge_id):
    fw_config = nsxv.get_firewall(edge_id)[1]

    md_rule_names = [rule['name'] for rule in md_proxy.get_router_fw_rules()]

    fw_rules = fw_config.get('firewallRules', {}).get('firewallRules', [])
    if fw_rules:
        fw_rules = [rule for rule in fw_rules
                    if rule['name'] not in md_rule_names]

        fw_config['firewallRules']['firewallRules'] = fw_rules
        nsxv.update_firewall(edge_id, fw_config)


def is_router_conflicting_on_edge(context, driver, router_id):
    edge_id = edge_utils.get_router_edge_id(context, router_id)
    if not edge_id:
        return False
    (available_routers,
     conflict_routers) = driver._get_available_and_conflicting_ids(
        context, router_id)
    for conf_router in conflict_routers:
        conf_edge_id = edge_utils.get_router_edge_id(context, conf_router)
        if conf_edge_id == edge_id:
            LOG.info("Router %(rtr)s on edge %(edge)s is conflicting with "
                     "another router and will be moved",
                     {'rtr': router_id, 'edge': edge_id})
            return True
    return False


@admin_utils.output_header
@admin_utils.unpack_payload
def redistribute_routers(resource, event, trigger, **kwargs):
    """If any of the shared routers are on a conflicting edge move them"""
    context = n_context.get_admin_context()
    with utils.NsxVPluginWrapper() as plugin:
        router_driver = plugin._router_managers.get_tenant_router_driver(
            context, 'shared')
        routers = plugin.get_routers(context)
        for router in routers:
            if (not router.get('distributed', False) and
                router.get('router_type') == 'shared' and
                is_router_conflicting_on_edge(
                    context, router_driver, router['id'])):
                router_driver.detach_router(context, router['id'], router)
                router_driver.attach_router(context, router['id'], router)


@admin_utils.output_header
@admin_utils.unpack_payload
def list_orphaned_vnics(resource, event, trigger, **kwargs):
    """List router orphaned router vnics where the port was deleted"""
    orphaned_vnics = get_orphaned_vnics()
    if not orphaned_vnics:
        LOG.info("No orphaned router vnics found")
        return
    headers = ['edge_id', 'vnic_index', 'tunnel_index', 'network_id']
    LOG.info(formatters.output_formatter(constants.ORPHANED_VNICS,
                                         orphaned_vnics, headers))


def get_orphaned_vnics():
    orphaned_vnics = []
    context = n_context.get_admin_context()
    vnic_binds = nsxv_db.get_edge_vnic_bindings_with_networks(
        context.session)
    with utils.NsxVPluginWrapper() as plugin:
        for vnic_bind in vnic_binds:
            edge_id = vnic_bind['edge_id']
            # check if this is a router edge by the router bindings table
            router_bindings = nsxv_db.get_nsxv_router_bindings_by_edge(
                context.session, edge_id)
            if not router_bindings:
                # Only log it. this is a different type of orphaned
                LOG.warning("Router bindings for vnic %s not found", vnic_bind)
                continue

            router_ids = [b['router_id'] for b in router_bindings]
            routers = plugin.get_routers(context,
                                         filters={'id': router_ids})
            if routers:
                interface_found = False
                # check if any of those routers is attached to this network
                for router in routers:
                    if plugin._get_router_interface_ports_by_network(
                        context, router['id'], vnic_bind['network_id']):
                        interface_found = True
                        break
                if not interface_found:
                    # for later deleting the interface we need to know if this
                    # is a distributed router.
                    # All the routers on the same edge are of the same type,
                    # so we can check the first one.
                    vnic_bind['distributed'] = routers[0].get('distributed')
                    orphaned_vnics.append(vnic_bind)

    return orphaned_vnics


@admin_utils.output_header
@admin_utils.unpack_payload
def clean_orphaned_vnics(resource, event, trigger, **kwargs):
    """List router orphaned router vnics where the port was deleted"""
    orphaned_vnics = get_orphaned_vnics()
    if not orphaned_vnics:
        LOG.info("No orphaned router vnics found")
        return
    headers = ['edge_id', 'vnic_index', 'tunnel_index', 'network_id']
    LOG.info(formatters.output_formatter(constants.ORPHANED_VNICS,
                                         orphaned_vnics, headers))
    user_confirm = admin_utils.query_yes_no("Do you want to delete "
                                            "orphaned vnics",
                                            default="no")
    if not user_confirm:
        LOG.info("NSXv vnics deletion aborted by user")
        return

    context = n_context.get_admin_context()
    with utils.NsxVPluginWrapper() as plugin:
        nsxv_manager = vcns_driver.VcnsDriver(
            edge_utils.NsxVCallbacks(plugin))
        for vnic in orphaned_vnics:
            if not vnic['distributed']:
                try:
                    nsxv_manager.vcns.delete_interface(
                        vnic['edge_id'], vnic['vnic_index'])
                except Exception as e:
                    LOG.error("Failed to delete vnic from NSX: %s", e)
                nsxv_db.free_edge_vnic_by_network(
                    context.session, vnic['edge_id'], vnic['network_id'])
            else:
                try:
                    nsxv_manager.vcns.delete_vdr_internal_interface(
                        vnic['edge_id'], vnic['vnic_index'])
                except Exception as e:
                    LOG.error("Failed to delete vnic from NSX: %s", e)
                nsxv_db.delete_edge_vnic_binding_by_network(
                    context.session, vnic['edge_id'], vnic['network_id'])


registry.subscribe(nsx_recreate_router_or_edge,
                   constants.ROUTERS,
                   shell.Operations.NSX_RECREATE.value)

registry.subscribe(migrate_distributed_routers_dhcp,
                   constants.ROUTERS,
                   shell.Operations.MIGRATE_VDR_DHCP.value)

registry.subscribe(redistribute_routers,
                   constants.ROUTERS,
                   shell.Operations.NSX_REDISTRIBUTE.value)

registry.subscribe(update_edge_firewalls,
                   constants.ROUTERS,
                   shell.Operations.NSX_UPDATE_FW.value)

registry.subscribe(list_orphaned_vnics,
                   constants.ORPHANED_VNICS,
                   shell.Operations.NSX_LIST.value)

registry.subscribe(clean_orphaned_vnics,
                   constants.ORPHANED_VNICS,
                   shell.Operations.NSX_CLEAN.value)
