# Copyright 2018 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 exceptions as n_exc
from oslo_log import log as logging
from oslo_utils import excutils

from vmware_nsx._i18n import _
from vmware_nsx.services.lbaas import base_mgr
from vmware_nsx.services.lbaas import lb_const
from vmware_nsx.services.lbaas.nsx_p.implementation import lb_utils as p_utils
from vmware_nsx.services.lbaas.nsx_v3.implementation import lb_utils

LOG = logging.getLogger(__name__)


def _translate_member_state(state):
    return lb_const.ENABLED if state else lb_const.DISABLED


class EdgeMemberManagerFromDict(base_mgr.NsxpLoadbalancerBaseManager):
    def _get_info_from_fip(self, context, fip):
        filters = {'floating_ip_address': [fip]}
        floating_ips = self.core_plugin.get_floatingips(context,
                                                        filters=filters)
        if floating_ips:
            return floating_ips[0]['fixed_ip_address']
        else:
            msg = (_('Member IP %(fip)s is an external IP, and is expected to '
                     'be a floating IP') % {'fip': fip})
            raise n_exc.BadRequest(resource='lbaas-vip', msg=msg)

    def _validate_member_lb_connectivity(self, context, member, completor):
        lb = member['pool'].get('loadbalancer')

        if not lb:
            msg = (_('Member %s loadbalancer object is missing') %
                   member['id'])
            raise n_exc.BadRequest(resource='lbaas-vip', msg=msg)

        subnet_id = lb.get('vip_subnet_id')
        network = lb_utils.get_network_from_subnet(
            context, self.core_plugin, subnet_id)

        if network and not network.get('router:external'):
            return

        # If VIP is attached to an external network, loadbalancer_mgr might not
        # attach it to a router. If not, set the LB service connectivity path
        # to the member subnet's router.
        service_client = self.core_plugin.nsxpolicy.load_balancer.lb_service
        service = service_client.get(lb['id'])
        if not service.get('connectivity_path'):
            router_id = lb_utils.get_router_from_network(
                context, self.core_plugin, member['subnet_id'])
            # Validate that there is no other LB on this router
            # as NSX does not allow it
            if self.core_plugin.service_router_has_loadbalancers(router_id):
                completor(success=False)
                msg = (_('Cannot attach a loadbalancer %(lb_id)s on router '
                         '%(router)s, as it already has a loadbalancer') %
                       {'lb_id': lb['id'], 'router': router_id})
                raise n_exc.BadRequest(resource='lbaas-router', msg=msg)

            if not self.core_plugin.service_router_has_services(context,
                                                                router_id):
                self.core_plugin.create_service_router(context, router_id)

            connectivity_path = self.core_plugin.nsxpolicy.tier1.get_path(
                router_id)
            tags = p_utils.get_tags(self.core_plugin,
                                    router_id,
                                    lb_const.LR_ROUTER_TYPE,
                                    lb.get('tenant_id'), context.project_name)
            try:
                service_client.update(lb['id'],
                                      tags=tags,
                                      connectivity_path=connectivity_path)
                p_utils.update_router_lb_vip_advertisement(
                    context, self.core_plugin, router_id)
            except Exception as e:
                with excutils.save_and_reraise_exception():
                    completor(success=False)
                    LOG.error('Failed to set connectivity for loadbalancer '
                              '%(lb)s on subnet %(sub)s with error %(err)s',
                              {'lb': lb['id'],
                               'sub': member['subnet_id'],
                               'err': e})

    def create(self, context, member, completor):
        pool_client = self.core_plugin.nsxpolicy.load_balancer.lb_pool
        self._validate_member_lb_connectivity(context, member, completor)
        network = lb_utils.get_network_from_subnet(
            context, self.core_plugin, member['subnet_id'])
        if network and network.get('router:external'):
            fixed_ip = self._get_info_from_fip(context, member['address'])
        else:
            fixed_ip = member['address']
        pool_id = member['pool']['id']
        try:
            pool_client.create_pool_member_and_add_to_pool(
                pool_id, fixed_ip,
                port=member['protocol_port'],
                display_name=member['name'][:218] + '_' + member['id'],
                weight=member['weight'],
                backup_member=member.get('backup', False),
                admin_state=_translate_member_state(member['admin_state_up']))
        except Exception as e:
            with excutils.save_and_reraise_exception():
                completor(success=False)
                LOG.error('Failed to create member %(member)s on pool %(pool)s'
                          ': %(err)s',
                          {'member': member['id'],
                           'pool': pool_id, 'err': e})
        completor(success=True)

    def update(self, context, old_member, new_member, completor):
        network = lb_utils.get_network_from_subnet(
            context, self.core_plugin, new_member['subnet_id'])
        if network and network.get('router:external'):
            fixed_ip = self._get_info_from_fip(context, new_member['address'])
        else:
            fixed_ip = new_member['address']
        pool_id = old_member['pool']['id']
        pool_client = self.core_plugin.nsxpolicy.load_balancer.lb_pool
        try:
            pool_client.update_pool_member(
                pool_id, fixed_ip, port=new_member['protocol_port'],
                display_name=new_member['name'][:219] + '_' + new_member['id'],
                weight=new_member['weight'],
                backup_member=new_member.get('backup', False),
                admin_state=_translate_member_state(
                    new_member['admin_state_up']))

        except Exception as e:
            with excutils.save_and_reraise_exception():
                completor(success=False)
                LOG.error('Failed to update member %(member)s on pool %(pool)s'
                          ': %(err)s',
                          {'member': new_member['id'],
                           'pool': pool_id, 'err': e})
        completor(success=True)

    def delete(self, context, member, completor):
        network = lb_utils.get_network_from_subnet(
            context, self.core_plugin, member['subnet_id'])
        if network and network.get('router:external'):
            fixed_ip = self._get_info_from_fip(context, member['address'])
        else:
            fixed_ip = member['address']
        pool_id = member['pool']['id']
        pool_client = self.core_plugin.nsxpolicy.load_balancer.lb_pool
        try:
            pool_client.remove_pool_member(
                pool_id, fixed_ip, port=member['protocol_port'])
        except Exception as e:
            with excutils.save_and_reraise_exception():
                completor(success=False)
                LOG.error('Failed to create member %(member)s on pool %(pool)s'
                          ': %(err)s',
                          {'member': member['id'],
                           'pool': pool_id, 'err': e})
        completor(success=True)

    def delete_cascade(self, context, member, completor):
        # No action should be taken on members delete cascade
        pass
