# Copyright 2014 A10 Networks
#
#    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 abc
from neutron.plugins.common import constants
from oslo_log import log as logging
import six

from neutron_lbaas.db.loadbalancer import models
from neutron_lbaas.services.loadbalancer import constants as lb_const
from neutron_lbaas.services.loadbalancer import data_models


LOG = logging.getLogger(__name__)


@six.add_metaclass(abc.ABCMeta)
class BaseManagerMixin(object):

    def __init__(self, driver):
        self.driver = driver

    @abc.abstractproperty
    def db_delete_method(self):
        pass

    @abc.abstractmethod
    def create(self, context, obj):
        pass

    @abc.abstractmethod
    def update(self, context, obj_old, obj):
        pass

    @abc.abstractmethod
    def delete(self, context, obj):
        pass

    def successful_completion(self, context, obj, delete=False,
                              lb_create=False):
        """
        Sets the provisioning_status of the load balancer and obj to
        ACTIVE.  Should be called last in the implementor's BaseManagerMixin
        methods for successful runs.

        :param context: neutron context
        :param obj: instance of a
                    neutron_lbaas.services.loadbalancer.data_model
        :param delete: set True if being called from a delete method.  Will
                       most likely result in the obj being deleted from the db.
        :param lb_create: set True if this is being called after a successful
                          load balancer create.
        """
        LOG.debug("Starting successful_completion method after a successful "
                  "driver action.")
        obj_sa_cls = data_models.DATA_MODEL_TO_SA_MODEL_MAP[obj.__class__]
        if delete:
            # Check if driver is responsible for vip allocation.  If the driver
            # is responsible, then it is also responsible for cleaning it up.
            # At this point, the VIP should already be cleaned up, so we are
            # just doing neutron lbaas db cleanup.
            if (obj == obj.root_loadbalancer and
                    self.driver.load_balancer.allocates_vip):
                # NOTE(blogan): this is quite dumb to do but it is necessary
                # so that a false negative pep8 error does not get thrown. An
                # "unexpected-keyword-argument" pep8 error occurs bc
                # self.db_delete_method is a @property method that returns a
                # method.
                kwargs = {'delete_vip_port': False}
                self.db_delete_method(context, obj.id, **kwargs)
            else:
                self.db_delete_method(context, obj.id)
        if obj == obj.root_loadbalancer and delete:
            # Load balancer was deleted and no longer exists
            return
        lb_op_status = None
        lb_p_status = constants.ACTIVE
        if obj == obj.root_loadbalancer:
            # only set the status to online if this an operation on the
            # load balancer
            lb_op_status = lb_const.ONLINE

        # Update the load balancer's vip address and vip port id if the driver
        # was responsible for allocating the vip.
        if (self.driver.load_balancer.allocates_vip and lb_create and
                isinstance(obj, data_models.LoadBalancer)):
            self.driver.plugin.db.update_loadbalancer(
                context, obj.id, {'vip_address': obj.vip_address,
                                  'vip_port_id': obj.vip_port_id})
        if delete:
            # We cannot update the status of obj if it was deleted but if the
            # obj is not a load balancer, the root load balancer should be
            # updated
            if not isinstance(obj, data_models.LoadBalancer):
                self.driver.plugin.db.update_status(
                    context, models.LoadBalancer, obj.root_loadbalancer.id,
                    provisioning_status=lb_p_status,
                    operating_status=lb_op_status)
            return
        obj_op_status = lb_const.ONLINE
        if isinstance(obj, data_models.HealthMonitor):
            # Health Monitor does not have an operating status
            obj_op_status = None
        LOG.debug("Updating object of type {0} with id of {1} to "
                  "provisioning_status = {2}, operating_status = {3}".format(
                      obj.__class__, obj.id, constants.ACTIVE, obj_op_status))
        self.driver.plugin.db.update_status(
            context, obj_sa_cls, obj.id,
            provisioning_status=constants.ACTIVE,
            operating_status=obj_op_status)
        if not isinstance(obj, data_models.LoadBalancer):
            # Only update the status of the root_loadbalancer if the previous
            # update was not the root load balancer so we are not updating
            # it twice.
            self.driver.plugin.db.update_status(
                context, models.LoadBalancer, obj.root_loadbalancer.id,
                provisioning_status=lb_p_status,
                operating_status=lb_op_status)

    def failed_completion(self, context, obj):
        """
        Sets the provisioning status of the obj to ERROR.  If obj is a
        loadbalancer it will be set to ERROR, otherwise set to ACTIVE. Should
        be called whenever something goes wrong (raised exception) in an
        implementor's BaseManagerMixin methods.

        :param context: neutron context
        :param obj: instance of a
                    neutron_lbaas.services.loadbalancer.data_model
        """
        LOG.debug("Starting failed_completion method after a failed driver "
                  "action.")
        if isinstance(obj, data_models.LoadBalancer):
            LOG.debug("Updating load balancer {0} to provisioning_status = "
                      "{1}, operating_status = {2}.".format(
                          obj.root_loadbalancer.id, constants.ERROR,
                          lb_const.OFFLINE))
            self.driver.plugin.db.update_status(
                context, models.LoadBalancer, obj.root_loadbalancer.id,
                provisioning_status=constants.ERROR,
                operating_status=lb_const.OFFLINE)
            return
        obj_sa_cls = data_models.DATA_MODEL_TO_SA_MODEL_MAP[obj.__class__]
        LOG.debug("Updating object of type {0} with id of {1} to "
                  "provisioning_status = {2}, operating_status = {3}".format(
                      obj.__class__, obj.id, constants.ERROR,
                      lb_const.OFFLINE))
        self.driver.plugin.db.update_status(
            context, obj_sa_cls, obj.id,
            provisioning_status=constants.ERROR,
            operating_status=lb_const.OFFLINE)
        LOG.debug("Updating load balancer {0} to "
                  "provisioning_status = {1}".format(obj.root_loadbalancer.id,
                                                     constants.ACTIVE))
        self.driver.plugin.db.update_status(
            context, models.LoadBalancer, obj.root_loadbalancer.id,
            provisioning_status=constants.ACTIVE)

    def update_vip(self, context, loadbalancer_id, vip_address,
                   vip_port_id=None):
        lb_update = {'vip_address': vip_address}
        if vip_port_id:
            lb_update['vip_port_id'] = vip_port_id
        self.driver.plugin.db.update_loadbalancer(context, loadbalancer_id,
                                                  lb_update)


@six.add_metaclass(abc.ABCMeta)
class BaseRefreshMixin(object):

    @abc.abstractmethod
    def refresh(self, context, obj):
        pass


@six.add_metaclass(abc.ABCMeta)
class BaseStatsMixin(object):

    @abc.abstractmethod
    def stats(self, context, obj):
        pass
