# Copyright 2013, Nachi Ueno, NTT MCL, 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 copy

import netaddr
from neutron_lib.api.definitions import l3 as l3_apidef
from neutron_lib.callbacks import events
from neutron_lib.callbacks import registry
from neutron_lib.callbacks import resources
from neutron_lib.exceptions import extraroute as xroute_exc
from neutron_lib.utils import helpers
from oslo_config import cfg
from oslo_log import log as logging

from neutron._i18n import _
from neutron.common import utils
from neutron.conf.db import extraroute_db
from neutron.db import _resource_extend as resource_extend
from neutron.db import l3_db
from neutron.objects import router as l3_obj


LOG = logging.getLogger(__name__)

extraroute_db.register_db_extraroute_opts()


@resource_extend.has_resource_extenders
class ExtraRoute_dbonly_mixin(l3_db.L3_NAT_dbonly_mixin):
    """Mixin class to support extra route configuration on router."""

    @staticmethod
    @resource_extend.extends([l3_apidef.ROUTERS])
    def _extend_router_dict_extraroute(router_res, router_db):
        router_res['routes'] = (ExtraRoute_dbonly_mixin.
                                _make_extra_route_list(
                                    router_db['route_list']
                                ))

    def update_router(self, context, id, router):
        r = router['router']
        if 'routes' in r:
            with context.session.begin(subtransactions=True):
                # check if route exists and have permission to access
                router_db = self._get_router(context, id)
                old_router = self._make_router_dict(router_db)
                routes_added, routes_removed = self._update_extra_routes(
                    context, router_db, r['routes'])
                router_data = copy.deepcopy(r)
                router_data['routes_added'] = routes_added
                router_data['routes_removed'] = routes_removed
                registry.publish(resources.ROUTER, events.PRECOMMIT_UPDATE,
                                 self, payload=events.DBEventPayload(
                                     context, request_body=router_data,
                                     states=(old_router,), resource_id=id,
                                     desired_state=router_db))
            # NOTE(yamamoto): expire to ensure the following update_router
            # see the effects of the above _update_extra_routes.
            context.session.expire(router_db, attribute_names=['route_list'])
        return super(ExtraRoute_dbonly_mixin, self).update_router(
            context, id, router)

    def _validate_routes_nexthop(self, cidrs, ips, routes, nexthop):
        # Note(nati): Nexthop should be connected,
        # so we need to check
        # nexthop belongs to one of cidrs of the router ports
        if not netaddr.all_matching_cidrs(nexthop, cidrs):
            raise xroute_exc.InvalidRoutes(
                routes=routes,
                reason=_('the nexthop is not connected with router'))
        # Note(nati) nexthop should not be same as fixed_ips
        if nexthop in ips:
            raise xroute_exc.InvalidRoutes(
                routes=routes,
                reason=_('the nexthop is used by router'))

    def _validate_routes(self, context,
                         router_id, routes):
        if len(routes) > cfg.CONF.max_routes:
            raise xroute_exc.RoutesExhausted(
                router_id=router_id,
                quota=cfg.CONF.max_routes)

        context = context.elevated()
        filters = {'device_id': [router_id]}
        ports = self._core_plugin.get_ports(context, filters)
        cidrs = []
        ips = []
        for port in ports:
            for ip in port['fixed_ips']:
                cidrs.append(self._core_plugin.get_subnet(
                    context, ip['subnet_id'])['cidr'])
                ips.append(ip['ip_address'])
        for route in routes:
            self._validate_routes_nexthop(
                cidrs, ips, routes, route['nexthop'])

    def _update_extra_routes(self, context, router, routes):
        self._validate_routes(context, router['id'], routes)
        old_routes = self._get_extra_routes_by_router_id(context, router['id'])
        added, removed = helpers.diff_list_of_dict(old_routes, routes)
        LOG.debug('Added routes are %s', added)
        for route in added:
            l3_obj.RouterRoute(
                context,
                router_id=router['id'],
                destination=utils.AuthenticIPNetwork(route['destination']),
                nexthop=netaddr.IPAddress(route['nexthop'])).create()

        LOG.debug('Removed routes are %s', removed)
        for route in removed:
            l3_obj.RouterRoute.get_object(
                context,
                router_id=router['id'],
                destination=route['destination'],
                nexthop=route['nexthop']).delete()
        return added, removed

    @staticmethod
    def _make_extra_route_list(extra_routes):
        # NOTE(yamamoto): the extra_routes argument is either object or db row
        return [{'destination': str(route['destination']),
                 'nexthop': str(route['nexthop'])}
                for route in extra_routes]

    def _get_extra_routes_by_router_id(self, context, id):
        router_objs = l3_obj.RouterRoute.get_objects(context, router_id=id)
        return self._make_extra_route_list(router_objs)

    def _confirm_router_interface_not_in_use(self, context, router_id,
                                             subnet_id):
        super(ExtraRoute_dbonly_mixin,
            self)._confirm_router_interface_not_in_use(
            context, router_id, subnet_id)
        subnet = self._core_plugin.get_subnet(context, subnet_id)
        subnet_cidr = netaddr.IPNetwork(subnet['cidr'])
        extra_routes = self._get_extra_routes_by_router_id(context, router_id)
        for route in extra_routes:
            if netaddr.all_matching_cidrs(route['nexthop'], [subnet_cidr]):
                raise xroute_exc.RouterInterfaceInUseByRoute(
                    router_id=router_id, subnet_id=subnet_id)


class ExtraRoute_db_mixin(ExtraRoute_dbonly_mixin, l3_db.L3_NAT_db_mixin):
    """Mixin class to support extra route configuration on router with rpc."""
    pass
