# Copyright 2015 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 copy

from oslo_log import helpers as log_helpers
from oslo_log import log as logging
from oslo_utils import excutils

from neutron_lib import exceptions as n_exc

from vmware_nsx._i18n import _
from vmware_nsx.common import exceptions as nsxv_exc
from vmware_nsx.common import locking
from vmware_nsx.db import nsxv_db
from vmware_nsx.plugins.nsx_v.vshield.common import exceptions as vcns_exc
from vmware_nsx.services.lbaas import base_mgr
from vmware_nsx.services.lbaas import lb_common
from vmware_nsx.services.lbaas import lb_const
from vmware_nsx.services.lbaas.nsx_v import lbaas_common as lb_v_common

LOG = logging.getLogger(__name__)


def listener_to_edge_app_profile(listener, edge_cert_id):
    edge_app_profile = {
        'insertXForwardedFor': False,
        'name': listener['id'],
        'serverSslEnabled': False,
        'sslPassthrough': False,
        'template': lb_const.PROTOCOL_MAP[listener['protocol']],
    }

    if (listener['protocol'] == lb_const.LB_PROTOCOL_HTTPS or
        listener['protocol'] == lb_const.LB_PROTOCOL_TERMINATED_HTTPS):
        if edge_cert_id:
            edge_app_profile['clientSsl'] = {
                'caCertificate': [],
                'clientAuth': 'ignore',
                'crlCertificate': [],
                'serviceCertificate': [edge_cert_id]}
        else:
            edge_app_profile['sslPassthrough'] = True

    if (listener.get('default_pool') and
        listener['default_pool'].get('session_persistence')):
        pool_sess_persist = listener['default_pool']['session_persistence']
        sess_persist_type = pool_sess_persist['type']
        persistence = {
            'method':
                lb_const.SESSION_PERSISTENCE_METHOD_MAP.get(
                    sess_persist_type)}

        if (sess_persist_type in
                lb_const.SESSION_PERSISTENCE_COOKIE_MAP):
            cookie_name = pool_sess_persist.get('cookie_name', None)
            if cookie_name is None:
                cookie_name = lb_const.SESSION_PERSISTENCE_DEFAULT_COOKIE_NAME
            persistence.update({
                'cookieName': cookie_name,
                'cookieMode': lb_const.SESSION_PERSISTENCE_COOKIE_MAP[
                    sess_persist_type]})

        edge_app_profile['persistence'] = persistence

    return edge_app_profile


def listener_to_edge_vse(context, listener, vip_address, default_pool,
                         app_profile_id):
    if listener['connection_limit']:
        connection_limit = max(0, listener['connection_limit'])
    else:
        connection_limit = 0

    vse = {
        'name': 'vip_' + listener['id'],
        'description': listener['description'],
        'ipAddress': vip_address,
        'protocol': lb_const.PROTOCOL_MAP[listener['protocol']],
        'port': listener['protocol_port'],
        'connectionLimit': connection_limit,
        'defaultPoolId': default_pool,
        'accelerationEnabled': (
            listener['protocol'] == lb_const.LB_PROTOCOL_TCP),
        'applicationProfileId': app_profile_id,
        'enabled': listener['admin_state_up']}

    # Add the L7 policies
    if listener['l7_policies']:
        app_rule_ids = []
        for pol in listener['l7_policies']:
            pol_id = pol.get('id', pol.get('l7policy_id'))
            if pol_id:
                binding = nsxv_db.get_nsxv_lbaas_l7policy_binding(
                    context.session, pol_id)
                if binding:
                    app_rule_ids.append(binding['edge_app_rule_id'])
            vse['applicationRuleId'] = app_rule_ids

    return vse


def update_app_profile(vcns, context, listener, edge_id, edge_cert_id=None):
    lb_id = listener['loadbalancer_id']
    listener_binding = nsxv_db.get_nsxv_lbaas_listener_binding(
        context.session, lb_id, listener['id'])
    if not listener_binding:
        return
    app_profile_id = listener_binding['app_profile_id']
    app_profile = listener_to_edge_app_profile(listener, edge_cert_id)
    with locking.LockManager.get_lock(edge_id):
        vcns.update_app_profile(
            edge_id, app_profile_id, app_profile)
    return app_profile_id


class EdgeListenerManagerFromDict(base_mgr.EdgeLoadbalancerBaseManager):
    @log_helpers.log_method_call
    def __init__(self, vcns_driver):
        super(EdgeListenerManagerFromDict, self).__init__(vcns_driver)

    def _upload_certificate(self, context, edge_id, cert_id, certificate):
        cert_binding = nsxv_db.get_nsxv_lbaas_certificate_binding(
            context.session, cert_id, edge_id)
        if cert_binding:
            return cert_binding['edge_cert_id']

        request = {
            'pemEncoding': certificate.get('certificate'),
            'privateKey': certificate.get('private_key')}
        passphrase = certificate.get('passphrase')
        if passphrase:
            request['passphrase'] = passphrase
        cert_obj = self.vcns.upload_edge_certificate(edge_id, request)[1]
        cert_list = cert_obj.get('certificates', {})
        if cert_list:
            if len(cert_list) > 1:
                LOG.warning(
                    'Certificate object contains multiple certificates. '
                    'Using first signed certificate of the bundle')
            edge_cert_id = None
            for cert in cert_list:
                if cert['certificateType'] in ['certificate_signed',
                                               'certificate_self_signed']:
                    edge_cert_id = cert['objectId']
                    break
            if not edge_cert_id:
                error = _("No signed certificate found in certificate bundle")
                raise nsxv_exc.NsxPluginException(err_msg=error)
        else:
            error = _("Failed to upload a certificate to edge %s") % edge_id
            raise nsxv_exc.NsxPluginException(err_msg=error)
        nsxv_db.add_nsxv_lbaas_certificate_binding(
            context.session, cert_id, edge_id, edge_cert_id)
        return edge_cert_id

    def create(self, context, listener, completor, certificate=None):
        default_pool = None

        lb_id = listener['loadbalancer_id']
        lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding(
            context.session, lb_id)
        edge_id = lb_binding['edge_id']

        # Validate the listener protocol
        if (listener.get('protocol') and
            listener['protocol'] not in lb_const.PROTOCOL_MAP):
            completor(success=False)
            msg = (_("Listener protocol %s is not supported") %
                   listener['protocol'])
            raise n_exc.BadRequest(resource='edge-lbaas', msg=msg)

        if listener.get('default_pool') and listener['default_pool'].get('id'):
            pool_binding = nsxv_db.get_nsxv_lbaas_pool_binding(
                context.session, lb_id, listener['default_pool']['id'])
            if pool_binding:
                default_pool = pool_binding['edge_pool_id']

        edge_cert_id = None
        if certificate:
            try:
                edge_cert_id = self._upload_certificate(
                    context, edge_id, certificate['ref'],
                    certificate)
            except Exception:
                with excutils.save_and_reraise_exception():
                    completor(success=False)

        app_profile = listener_to_edge_app_profile(listener, edge_cert_id)
        app_profile_id = None

        try:
            with locking.LockManager.get_lock(edge_id):
                h = (self.vcns.create_app_profile(edge_id, app_profile))[0]
                app_profile_id = lb_v_common.extract_resource_id(h['location'])
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                completor(success=False)
                LOG.error('Failed to create app profile on edge: %s',
                          lb_binding['edge_id'])

        vse = listener_to_edge_vse(context, listener,
                                   lb_binding['vip_address'],
                                   default_pool,
                                   app_profile_id)

        try:
            with locking.LockManager.get_lock(edge_id):
                h = self.vcns.create_vip(edge_id, vse)[0]
                edge_vse_id = lb_v_common.extract_resource_id(h['location'])

            nsxv_db.add_nsxv_lbaas_listener_binding(context.session,
                                                    lb_id,
                                                    listener['id'],
                                                    app_profile_id,
                                                    edge_vse_id)
            completor(success=True)

        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                completor(success=False)
                LOG.error('Failed to create vip on Edge: %s', edge_id)
                self.vcns.delete_app_profile(edge_id, app_profile_id)

    def update(self, context, old_listener, new_listener, completor,
               certificate=None):
        default_pool = None
        if (new_listener.get('default_pool') and
            new_listener['default_pool'].get('id')):
            pool_binding = nsxv_db.get_nsxv_lbaas_pool_binding(
                context.session, new_listener['loadbalancer_id'],
                new_listener['default_pool']['id'])
            if pool_binding:
                default_pool = pool_binding['edge_pool_id']
            else:
                LOG.error("Couldn't find pool binding for pool %s",
                          new_listener['default_pool']['id'])

        lb_id = new_listener['loadbalancer_id']
        listener_binding = nsxv_db.get_nsxv_lbaas_listener_binding(
            context.session, lb_id, new_listener['id'])
        lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding(
            context.session, lb_id)
        edge_id = lb_binding['edge_id']

        edge_cert_id = None
        if certificate:
            if (lb_common.get_listener_cert_ref(old_listener) !=
                lb_common.get_listener_cert_ref(new_listener)):
                try:
                    edge_cert_id = self._upload_certificate(
                        context, edge_id,
                        certificate['ref'],
                        certificate)
                except Exception:
                    with excutils.save_and_reraise_exception():
                        completor(success=False)
            else:
                cert_binding = nsxv_db.get_nsxv_lbaas_certificate_binding(
                    context.session, certificate['ref'],
                    edge_id)
                edge_cert_id = cert_binding['edge_cert_id']

        try:
            app_profile_id = update_app_profile(
                self.vcns, context, new_listener,
                edge_id, edge_cert_id=edge_cert_id)
            vse = listener_to_edge_vse(context, new_listener,
                                       lb_binding['vip_address'],
                                       default_pool,
                                       app_profile_id)

            with locking.LockManager.get_lock(edge_id):
                self.vcns.update_vip(edge_id, listener_binding['vse_id'], vse)

            completor(success=True)
        except vcns_exc.VcnsApiException:
            with excutils.save_and_reraise_exception():
                completor(success=False)
                LOG.error('Failed to update app profile on edge: %s',
                          edge_id)

    def delete(self, context, listener, completor):
        lb_id = listener['loadbalancer_id']
        listener_binding = nsxv_db.get_nsxv_lbaas_listener_binding(
            context.session, lb_id, listener['id'])
        lb_binding = nsxv_db.get_nsxv_lbaas_loadbalancer_binding(
            context.session, lb_id)

        if lb_binding and listener_binding:
            edge_id = lb_binding['edge_id']
            edge_vse_id = listener_binding['vse_id']
            app_profile_id = listener_binding['app_profile_id']

            try:
                with locking.LockManager.get_lock(edge_id):
                    self.vcns.delete_vip(edge_id, edge_vse_id)

            except (vcns_exc.ResourceNotFound, vcns_exc.RequestBad):
                LOG.error('vip not found on edge: %s', edge_id)
            except vcns_exc.VcnsApiException:
                LOG.error('Failed to delete vip on edge: %s', edge_id)

            try:
                with locking.LockManager.get_lock(edge_id):
                    self.vcns.delete_app_profile(edge_id, app_profile_id)
            except (vcns_exc.ResourceNotFound, vcns_exc.RequestBad):
                LOG.error('app profile not found on edge: %s', edge_id)
            except vcns_exc.VcnsApiException:
                LOG.error('Failed to delete app profile on Edge: %s', edge_id)

            nsxv_db.del_nsxv_lbaas_listener_binding(context.session, lb_id,
                                                    listener['id'])

        completor(success=True)

    def delete_cascade(self, context, listener, completor):
        self.delete(context, listener, completor)


def stats_getter(context, core_plugin, ignore_list=None):
    """Update Octavia statistics for each listener (virtual server)"""
    stat_list = []
    vcns = core_plugin.nsx_v.vcns
    # go over all LB edges
    bindings = nsxv_db.get_nsxv_lbaas_loadbalancer_bindings(context.session)
    for binding in bindings:
        lb_id = binding['loadbalancer_id']
        if ignore_list and lb_id in ignore_list:
            continue
        edge_id = binding['edge_id']

        try:
            lb_stats = vcns.get_loadbalancer_statistics(edge_id)

            virtual_servers_stats = lb_stats[1].get('virtualServer', [])
            for vs_stats in virtual_servers_stats:
                # Get the stats of the virtual server
                stats = copy.copy(lb_const.LB_EMPTY_STATS)
                stats['bytes_in'] += vs_stats.get('bytesIn', 0)
                stats['bytes_out'] += vs_stats.get('bytesOut', 0)
                stats['active_connections'] += vs_stats.get('curSessions', 0)
                stats['total_connections'] += vs_stats.get('totalSessions', 0)
                stats['request_errors'] = 0  # currently unsupported

                # Find the listener Id
                vs_id = vs_stats.get('virtualServerId')
                list_bind = nsxv_db.get_nsxv_lbaas_listener_binding_by_vse(
                    context.session, lb_id, vs_id)
                if not list_bind:
                    continue
                stats['id'] = list_bind['listener_id']

                stat_list.append(stats)

        except vcns_exc.VcnsApiException as e:
            LOG.warning('Failed to read load balancer statistics for %s: %s',
                        edge_id, e)

    return stat_list
