# Copyright (C) 2020 NTT DATA
# 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 datetime import datetime
import functools
import hashlib
import inspect
import os
import re
import traceback

from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
from oslo_utils import excutils
from oslo_utils import timeutils
from toscaparser import tosca_template

from tacker.common import driver_manager
from tacker.common import exceptions
from tacker.common import log
from tacker.common import safe_utils
from tacker.common import utils
from tacker.conductor.conductorrpc import vnf_lcm_rpc
from tacker import manager
from tacker import objects
from tacker.objects import fields
from tacker.objects.fields import ErrorPoint as EP
from tacker.vnflcm import abstract_driver
from tacker.vnflcm import utils as vnflcm_utils

LOG = logging.getLogger(__name__)
CONF = cfg.CONF
DEFAULT_VNFLCM_MGMT_DRIVER = "vnflcm_noop"


@utils.expects_func_args('vnf_info', 'vnf_instance', 'scale_vnf_request')
def revert_to_error_scale(function):
    """Decorator to revert task_state to error  on failure."""

    @functools.wraps(function)
    def decorated_function(self, context, *args, **kwargs):
        try:
            return function(self, context, *args, **kwargs)
        except Exception as ex:
            with excutils.save_and_reraise_exception():
                wrapped_func = safe_utils.get_wrapped_function(function)
                keyed_args = inspect.getcallargs(wrapped_func, self, context,
                                                 *args, **kwargs)
                try:
                    vnf_info = keyed_args['vnf_info']
                    vnf_instance = keyed_args['vnf_instance']
                    scale_vnf_request = keyed_args['scale_vnf_request']
                    vim_info = vnflcm_utils._get_vim(context,
                            vnf_instance.vim_connection_info)
                    vim_connection_info = \
                        objects.VimConnectionInfo.obj_from_primitive(
                            vim_info, context)
                    if vnf_info.get('resource_changes'):
                        resource_changes = vnf_info.get('resource_changes')
                    else:
                        resource_changes = self._scale_resource_update(context,
                                            vnf_info,
                                            vnf_instance,
                                            scale_vnf_request,
                                            vim_connection_info,
                                            error=True)
                except Exception as e:
                    LOG.warning(traceback.format_exc())
                    LOG.warning("Failed to scale resource update "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})

                try:
                    self._vnfm_plugin._update_vnf_scaling_status_err(context,
                                                                     vnf_info)
                except Exception as e:
                    LOG.warning("Failed to revert scale info for event "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})
                try:
                    vnf_instance.task_state = None
                    self._vnf_instance_update(context, vnf_instance)
                except Exception as e:
                    LOG.warning("Failed to revert instantiation info for vnf "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})
                problem = objects.ProblemDetails(status=500,
                                                 detail=str(ex))

                try:
                    timestamp = datetime.utcnow()
                    vnf_lcm_op_occ = vnf_info['vnf_lcm_op_occ']
                    vnf_lcm_op_occ.operation_state = 'FAILED_TEMP'
                    vnf_lcm_op_occ.state_entered_time = timestamp
                    vnf_lcm_op_occ.resource_changes = resource_changes
                    vnf_lcm_op_occ.error = problem
                    vnf_lcm_op_occ.error_point = \
                        vnf_info['current_error_point']
                    vnf_lcm_op_occ.save()
                except Exception as e:
                    LOG.warning("Failed to update vnf_lcm_op_occ for vnf "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})

                try:
                    notification = vnf_info['notification']
                    notification['notificationStatus'] = 'RESULT'
                    notification['operationState'] = 'FAILED_TEMP'
                    notification['error'] = problem.to_dict()
                    resource_dict = resource_changes.to_dict()
                    if resource_dict.get('affected_vnfcs'):
                        notification['affectedVnfcs'] =\
                            jsonutils.dump_as_bytes(
                            resource_dict.get('affected_vnfcs'))
                    if resource_dict.get('affected_virtual_links'):
                        notification['affectedVirtualLinks'] =\
                            jsonutils.dump_as_bytes(
                            resource_dict.get('affected_virtual_links'))
                    if resource_dict.get('affected_virtual_storages'):
                        notification['affectedVirtualStorages'] =\
                            jsonutils.dump_as_bytes(
                            resource_dict.get('affected_virtual_storages'))
                    self.rpc_api.send_notification(context, notification)
                except Exception as e:
                    LOG.warning("Failed to revert scale info for vnf "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})

    return decorated_function


@utils.expects_func_args('vnf_instance')
def revert_to_error_task_state(function):
    """Decorator to revert task_state to error  on failure."""

    @functools.wraps(function)
    def decorated_function(self, context, *args, **kwargs):
        try:
            return function(self, context, *args, **kwargs)
        except Exception:
            with excutils.save_and_reraise_exception():
                wrapped_func = safe_utils.get_wrapped_function(function)
                keyed_args = inspect.getcallargs(wrapped_func, self, context,
                                                 *args, **kwargs)
                vnf_instance = keyed_args['vnf_instance']
                previous_task_state = vnf_instance.task_state
                try:
                    self._vnf_instance_update(context, vnf_instance,
                        task_state=fields.VnfInstanceTaskState.ERROR)
                    LOG.info("Successfully reverted task state from "
                             "%(state)s to %(error)s on failure for vnf "
                             "instance %(id)s.",
                             {"state": previous_task_state,
                              "id": vnf_instance.id,
                              "error": fields.VnfInstanceTaskState.ERROR})
                except Exception as e:
                    LOG.warning("Failed to revert task state for vnf "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})

    return decorated_function


@utils.expects_func_args('vnf_info', 'vnf_instance', 'operation_params')
def revert_to_error_rollback(function):
    """Decorator to revert task_state to error  on failure."""

    @functools.wraps(function)
    def decorated_function(self, context, *args, **kwargs):
        try:
            return function(self, context, *args, **kwargs)
        except Exception as ex:
            with excutils.save_and_reraise_exception():
                wrapped_func = safe_utils.get_wrapped_function(function)
                keyed_args = inspect.getcallargs(wrapped_func, self, context,
                                                 *args, **kwargs)
                resource_changes = None
                try:
                    vnf_info = keyed_args['vnf_info']
                    vnf_instance = keyed_args['vnf_instance']
                    operation_params = keyed_args['operation_params']
                    vim_info = vnflcm_utils._get_vim(context,
                            vnf_instance.vim_connection_info)
                    vim_connection_info =\
                        objects.VimConnectionInfo.obj_from_primitive(
                            vim_info, context)
                    vnf_lcm_op_occs = vnf_info['vnf_lcm_op_occ']
                    if vnf_info.get('resource_changes'):
                        resource_changes = vnf_info.get('resource_changes')
                    else:
                        if vnf_lcm_op_occs.operation == 'SCALE':
                            scale_vnf_request =\
                                objects.ScaleVnfRequest.obj_from_primitive(
                                    operation_params, context=context)
                            scale_vnf_request_copy = \
                                copy.deepcopy(scale_vnf_request)
                            scale_vnf_request_copy.type = 'SCALE_IN'
                            resource_changes = self._scale_resource_update(
                                context,
                                vnf_info,
                                vnf_instance,
                                scale_vnf_request_copy,
                                vim_connection_info,
                                error=True)
                        else:
                            resource_changes = self._term_resource_update(
                                context,
                                vnf_info,
                                vnf_instance)
                except Exception as e:
                    LOG.warning(traceback.format_exc())
                    LOG.warning("Failed to scale resource update "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})

                try:
                    self._update_vnf_rollback_status_err(context, vnf_info)
                except Exception as e:
                    LOG.warning("Failed to revert scale info for event "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})
                try:
                    self._vnf_instance_update(context, vnf_instance)
                except Exception as e:
                    LOG.warning("Failed to revert instantiation info for vnf "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})
                problem = objects.ProblemDetails(status=500,
                                                 detail=str(ex))

                try:
                    timestamp = datetime.utcnow()
                    vnf_lcm_op_occ = vnf_info['vnf_lcm_op_occ']
                    vnf_lcm_op_occ.operation_state = 'FAILED_TEMP'
                    vnf_lcm_op_occ.state_entered_time = timestamp
                    if resource_changes:
                        vnf_lcm_op_occ.resource_changes = resource_changes
                    vnf_lcm_op_occ.error = problem
                    vnf_lcm_op_occ.save()
                except Exception as e:
                    LOG.warning("Failed to update vnf_lcm_op_occ for vnf "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})

                try:
                    notification = vnf_info['notification']
                    notification['notificationStatus'] = 'RESULT'
                    notification['operationState'] = 'FAILED_TEMP'
                    notification['error'] = problem.to_dict()
                    if resource_changes:
                        resource_dict = resource_changes.to_dict()
                        if resource_dict.get('affected_vnfcs'):
                            notification['affectedVnfcs'] = \
                                jsonutils.dump_as_bytes(
                                    resource_dict.get('affected_vnfcs'))
                        if resource_dict.get('affected_virtual_links'):
                            notification['affectedVirtualLinks'] = \
                                jsonutils.dump_as_bytes(
                                    resource_dict.get(
                                        'affected_virtual_links'))
                        if resource_dict.get('affected_virtual_storages'):
                            notification['affectedVirtualStorages'] = \
                                jsonutils.dump_as_bytes(
                                    resource_dict.get(
                                        'affected_virtual_storages'))
                    self.rpc_api.send_notification(context, notification)
                except Exception as e:
                    LOG.warning("Failed to revert scale info for vnf "
                                "instance %(id)s. Error: %(error)s",
                                {"id": vnf_instance.id, "error": e})
    return decorated_function


def config_opts():
    return [('tacker', VnfLcmDriver.OPTS)]


class VnfLcmDriver(abstract_driver.VnfInstanceAbstractDriver):
    OPTS = [
        cfg.ListOpt(
            'vnflcm_infra_driver', default=['openstack', 'kubernetes'],
            help=_('Hosting vnf drivers tacker plugin will use')
        ),
        cfg.ListOpt(
            'vnflcm_mgmt_driver', default=[DEFAULT_VNFLCM_MGMT_DRIVER],
            help=_('MGMT driver to communicate with '
                   'Hosting VNF/logical service '
                   'instance tacker plugin will use')
        )
    ]
    cfg.CONF.register_opts(OPTS, 'tacker')

    def __init__(self):
        super(VnfLcmDriver, self).__init__()
        self.rpc_api = vnf_lcm_rpc.VNFLcmRPCAPI()
        self._vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM']
        self._vnf_manager = driver_manager.DriverManager(
            'tacker.tacker.vnfm.drivers',
            cfg.CONF.tacker.vnflcm_infra_driver)
        self._mgmt_manager = driver_manager.DriverManager(
            'tacker.tacker.mgmt.drivers', cfg.CONF.tacker.vnflcm_mgmt_driver)
        self._mgmt_driver_hash = self._init_mgmt_driver_hash()

    def _init_mgmt_driver_hash(self):

        driver_hash = {}
        for mgmt_driver in cfg.CONF.tacker.vnflcm_mgmt_driver:
            path = inspect.getfile(self._mgmt_manager[mgmt_driver].__class__)
            driver_hash[mgmt_driver] = self._get_file_hash(path)
        return driver_hash

    def _vnf_instance_update(self, context, vnf_instance, **kwargs):
        """Update vnf instance in the database using kwargs as value."""

        for k, v in kwargs.items():
            setattr(vnf_instance, k, v)
        vnf_instance.save()

    def _instantiate_vnf(self, context, vnf_instance, vnf_dict,
            vim_connection_info, instantiate_vnf_req):
        vnfd_dict = vnflcm_utils._get_vnfd_dict(context, vnf_instance.vnfd_id,
                instantiate_vnf_req.flavour_id)
        vnf_package_path = vnflcm_utils._get_vnf_package_path(
            context, vnf_instance.vnfd_id)

        param_for_subs_map = vnflcm_utils._get_param_data(vnfd_dict,
                instantiate_vnf_req)

        package_uuid = vnflcm_utils._get_vnf_package_id(context,
                vnf_instance.vnfd_id)
        vnf_software_images = vnflcm_utils._create_grant_request(vnfd_dict,
                package_uuid)
        vnf_resources = self._vnf_manager.invoke(
            vim_connection_info.vim_type, 'pre_instantiation_vnf',
            context=context, vnf_instance=vnf_instance,
            vim_connection_info=vim_connection_info,
            vnf_software_images=vnf_software_images,
            instantiate_vnf_req=instantiate_vnf_req,
            vnf_package_path=vnf_package_path)

        # save the vnf resources in the db
        for _, resources in vnf_resources.items():
            for vnf_resource in resources:
                vnf_resource.create()

        vnfd_dict_to_create_final_dict = copy.deepcopy(vnfd_dict)
        final_vnf_dict = vnflcm_utils._make_final_vnf_dict(
            vnfd_dict_to_create_final_dict, vnf_instance.id,
            vnf_instance.vnf_instance_name, param_for_subs_map, vnf_dict)

        final_vnf_dict['before_error_point'] = \
            vnf_dict['before_error_point']

        try:
            instance_id = self._vnf_manager.invoke(
                vim_connection_info.vim_type, 'instantiate_vnf',
                context=context, plugin=self._vnfm_plugin,
                vnf_instance=vnf_instance,
                vnfd_dict=final_vnf_dict, grant_response=vnf_resources,
                vim_connection_info=vim_connection_info,
                vnf_package_path=vnf_package_path,
                instantiate_vnf_req=instantiate_vnf_req)
        except Exception as exp:
            with excutils.save_and_reraise_exception():
                exp.reraise = False
                LOG.error("Unable to instantiate vnf instance "
                    "%(id)s due to error : %(error)s",
                    {"id": vnf_instance.id, "error":
                    encodeutils.exception_to_unicode(exp)})
                raise exceptions.VnfInstantiationFailed(
                    id=vnf_instance.id,
                    error=encodeutils.exception_to_unicode(exp))

        if vnf_instance.instantiated_vnf_info and\
                vnf_instance.instantiated_vnf_info.instance_id != instance_id: