# Copyright (c) 2020 Dell Inc. or its subsidiaries.
# 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 datetime
import platform
import time
import traceback
import types

from oslo_log import log as logging
import tabulate

from cinder.objects import volume
from cinder import version
from cinder.volume.drivers.dell_emc.powermax import utils
LOG = logging.getLogger(__name__)
CLEANUP_LIST = ['masking_view', 'child_storage_group', 'parent_storage_group',
                'initiator_group', 'port_group', 'storage_group']


def debug_required(func):
    """Only execute the function if debug is enabled."""

    def func_wrapper(*args, **kwargs):
        try:
            if args[0].is_debug:
                return func(*args, **kwargs)
            else:
                pass
        except Exception:
            LOG.warning("Volume metadata logging failure. "
                        "Exception is %s.", traceback.format_exc())

    return func_wrapper


class PowerMaxVolumeMetadata(object):
    """Gathers PowerMax/VMAX specific volume information.

    Also gathers Unisphere, Microcode OS/distribution and python versions.
    """

    def __init__(self, rest, version, is_debug):
        self.version_dict = {}
        self.rest = rest
        self.utils = utils.PowerMaxUtils()
        self.volume_trace_list = []
        self.is_debug = is_debug
        self.powermax_driver_version = version

    def _update_platform(self):
        """Update the platform."""
        try:
            self.version_dict['openstack_platform'] = platform.platform()
        except Exception as ex:
            LOG.warning("Unable to determine the platform. "
                        "Exception is %s.", ex)

    def _get_python_version(self):
        """Get the python version."""
        try:
            self.version_dict['python_version'] = platform.python_version()
        except Exception as ex:
            LOG.warning("Unable to determine the python version. "
                        "Exception is %s.", ex)

    def _update_version_from_version_string(self):
        """Update the version from the version string."""
        try:
            self.version_dict['openstack_version'] = (
                version.version_info.version_string())
        except Exception as ex:
            LOG.warning("Unable to determine the OS version. "
                        "Exception is %s.", ex)

    def _update_release_from_release_string(self):
        """Update the release from the release string."""
        try:
            self.version_dict['openstack_release'] = (
                version.version_info.release_string())
        except Exception as ex:
            LOG.warning("Unable to get release info. "
                        "Exception is %s.", ex)

    @staticmethod
    def _get_version_info_version():
        """Gets the version.

        :returns: string -- version
        """
        return version.version_info.version

    def _update_info_from_version_info(self):
        """Update class variables from version info."""
        try:
            ver = self._get_version_info_version()
            if ver:
                self.version_dict['openstack_version'] = ver
        except Exception as ex:
            LOG.warning("Unable to get version info. "
                        "Exception is %s.", ex)

    def _update_openstack_info(self):
        """Update openstack info."""
        self._update_version_from_version_string()
        self._update_release_from_release_string()
        self._update_platform()
        self._get_python_version()
        # Some distributions override with more meaningful information
        self._update_info_from_version_info()

    def _update_array_info(self, serial_number):
        """Update PowerMax/VMAX info.

        :param serial_number: the serial number of the array
        """
        u4p_version_dict = (
            self.rest.get_unisphere_version())
        self.version_dict['unisphere_for_powermax_version'] = (
            u4p_version_dict['version'])
        self.version_dict['serial_number'] = serial_number
        array_info_dict = self.rest.get_array_detail(serial_number)
        self.version_dict['storage_firmware_version'] = (
            array_info_dict.get(
                'ucode', array_info_dict.get('microcode')))
        self.version_dict['storage_model'] = array_info_dict['model']
        self.version_dict['powermax_cinder_driver_version'] = (
            self.powermax_driver_version)

    @debug_required
    def gather_version_info(self, serial_number):
        """Gather info on the array

        :param serial_number: the serial number of the array
        :returns: version_dict
        """
        try:
            self._update_openstack_info()
            self._update_array_info(serial_number)
            self.print_pretty_table(self.version_dict)
        except Exception as ex:
            LOG.warning("Unable to gather version info. "
                        "Exception is %s.", ex)
        return self.version_dict

    @debug_required
    def gather_volume_info(
            self, volume_id, successful_operation, append, **kwargs):
        """Gather volume information.

        :param volume_id: the unique volume id key
        :param successful_operation: the operation e.g "create"
        :param append: append flag
        :param kwargs: variable length argument list
        :returns: datadict
        """
        volume_trace_dict = {}
        volume_key_value = {}
        datadict = {}
        try:
            volume_trace_dict = self._fill_volume_trace_dict(
                volume_id, successful_operation, append, **kwargs)
            volume_trace_dict['volume_updated_time'] = (
                datetime.datetime.fromtimestamp(
                    int(time.time())).strftime('%Y-%m-%d %H:%M:%S'))
            volume_key_value[volume_id] = volume_trace_dict
            if not self.volume_trace_list:
                self.volume_trace_list.append(volume_key_value.copy())
            else:
                self._consolidate_volume_trace_list(
                    volume_id, volume_trace_dict, volume_key_value)
            for datadict in list(self.volume_trace_list):
                if volume_id in datadict:
                    if not append:
                        self.volume_trace_list.remove(datadict)
                    return datadict
        except Exception as ex:
            LOG.warning("Exception in gather volume metadata. "
                        "Exception is %s.", ex)
        return datadict

    def _fill_volume_trace_dict(
            self, volume_id, successful_operation, append, **kwargs):
        """Populates a dictionary with key value pairs

        :param volume_id: the unique volume id key
        :param successful_operation: the operation e.g "create"
        :param append: append flag
        :param kwargs: variable length argument list
        :returns: my_volume_trace_dict
        """
        param_dict = locals()
        my_volume_trace_dict = {}
        for k, v in param_dict.items():
            if self._param_condition(k, v):
                my_volume_trace_dict[k] = v
            if k == 'kwargs':
                for k2, v2 in v.items():
                    if self._param_condition(k2, v2):
                        my_volume_trace_dict[k2] = v2
                    elif k2 == 'mv_list' and v2:
                        for i, item in enumerate(v2, 1):
                            my_volume_trace_dict["masking_view_%d" % i] = item
                    elif k2 == 'sg_list' and v2:
                        for i, item in enumerate(v2, 1):
                            my_volume_trace_dict["storage_group_%d" % i] = item
        return my_volume_trace_dict

    def _param_condition(self, key, value):
        """Determines condition for inclusion.

        :param key: the key
        :param value: the value

        :returns: True or False
        """
        exclude_list = ('self', 'append', 'mv_list', 'sg_list')
        return (value is not None and key not in exclude_list and
                not isinstance(value, (dict,
                                       types.FunctionType,
                                       type)))

    @debug_required
    def print_pretty_table(self, datadict):
        """Prints the data in the dict.

        :param datadict: the data dictionary
        """
        rows = []
        for k, v in datadict.items():
            if v is not None:
                rows.append([k, v])
        t = tabulate.tabulate(rows, headers=['Key', 'Value'], tablefmt='psql')

        LOG.debug('\n%s\n', t)

    def _consolidate_volume_trace_list(
            self, volume_id, volume_trace_dict, volume_key_value):
        """Consolidate data into self.volume_trace_list

        :param volume_id: the unique volume identifier
        :param volume_trace_dict: the existing dict
        :param volume_key_value: the volume id key and dict value
        """
        is_merged = False
        for datadict in list(self.volume_trace_list):
            if volume_id in datadict:
                for key, dict_value in datadict.items():
                    merged_dict = (
                        self.utils.merge_dicts(
                            volume_trace_dict, dict_value))
                    self.volume_trace_list.remove(datadict)
                    volume_key_value[volume_id] = merged_dict
                    self.volume_trace_list.append(volume_key_value.copy())
                    is_merged = True
        if not is_merged:
            self.volume_trace_list.append(volume_key_value.copy())

    @debug_required
    def update_volume_info_metadata(self, datadict, version_dict):
        """Get update volume metadata with volume info

        :param datadict: volume info key value pairs
        :param version_dict: version dictionary
        :returns: volume_metadata
        """
        return self.utils.merge_dicts(
            version_dict, *datadict.values())

    @debug_required
    def capture_attach_info(
            self, volume, extra_specs, masking_view_dict, host,
            is_multipath, is_multiattach):
        """Captures attach info in volume metadata

        :param volume: the volume object
        :param extra_specs: extra specifications
        :param masking_view_dict: masking view dict
        :param host: host
        :param is_multipath: is mulitipath flag
        :param is_multiattach: is multi attach
        """
        mv_list, sg_list = [], []
        child_storage_group, parent_storage_group = None, None
        initiator_group, port_group = None, None
        child_storage_group_tag_list = None

        if is_multiattach:
            successful_operation = 'multi_attach'
            mv_list = masking_view_dict['mv_list']
            sg_list = masking_view_dict['sg_list']
        else:
            successful_operation = 'attach'
            child_storage_group = masking_view_dict[utils.SG_NAME]
            child_storage_group_tag_list = (
                masking_view_dict.get(utils.TAG_LIST, None))
            parent_storage_group = masking_view_dict[utils.PARENT_SG_NAME]
            initiator_group = masking_view_dict[utils.IG_NAME]
            port_group = masking_view_dict[utils.PORTGROUPNAME]

        sl, wl = self.utils.get_service_level_workload(extra_specs)
        datadict = self.gather_volume_info(
            volume.id, successful_operation, False,
            serial_number=extra_specs[utils.ARRAY],
            service_level=sl, workload=wl, srp=extra_specs[utils.SRP],
            masking_view=masking_view_dict[utils.MV_NAME],
            child_storage_group=child_storage_group,
            parent_storage_group=parent_storage_group,
            initiator_group=initiator_group,
            port_group=port_group,
            host=host, used_host_name=masking_view_dict[utils.USED_HOST_NAME],
            is_multipath=is_multipath,
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name,
            mv_list=mv_list, sg_list=sg_list,
            child_storage_group_tag_list=child_storage_group_tag_list,
            array_tag_list=masking_view_dict.get('array_tag_list', None))

        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)

        self.print_pretty_table(volume_metadata)

    @debug_required
    def capture_detach_info(
            self, volume, extra_specs, device_id, mv_list, sg_list):
        """Captures detach info in volume metadata

        :param volume: the volume object
        :param extra_specs: extra specifications
        :param device_id: masking view dict
        :param mv_list: masking view list
        :param sg_list: storage group list
        """
        default_sg = self.utils.derive_default_sg_from_extra_specs(extra_specs)
        sl, wl = self.utils.get_service_level_workload(extra_specs)
        datadict = self.gather_volume_info(
            volume.id, 'detach', False, device_id=device_id,
            serial_number=extra_specs[utils.ARRAY],
            service_level=sl, workload=wl,
            srp=extra_specs[utils.SRP], default_sg_name=default_sg,
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name,
            mv_list=mv_list, sg_list=sg_list
        )
        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)
        self.print_pretty_table(volume_metadata)

    @debug_required
    def capture_extend_info(
            self, volume, new_size, device_id, extra_specs, array):
        """Capture extend info in volume metadata

        :param volume: the volume object
        :param new_size: new size
        :param device_id: device id
        :param extra_specs: extra specifications
        :param array: array serial number
        """
        default_sg = self.utils.derive_default_sg_from_extra_specs(extra_specs)
        sl, wl = self.utils.get_service_level_workload(extra_specs)
        datadict = self.gather_volume_info(
            volume.id, 'extend', False, volume_size=new_size,
            device_id=device_id,
            default_sg_name=default_sg, serial_number=array,
            service_level=sl, workload=wl,
            srp=extra_specs[utils.SRP],
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name,
            is_compression_disabled=self.utils.is_compression_disabled(
                extra_specs))
        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)
        self.print_pretty_table(volume_metadata)

    @debug_required
    def capture_snapshot_info(
            self, source, extra_specs, successful_operation,
            snapshot_metadata):
        """Captures snapshot info in volume metadata

        :param source: the source volume object
        :param extra_specs: extra specifications
        :param successful_operation: snapshot operation
        :param snapshot_metadata: snapshot metadata
        """
        last_ss_name, snapshot_label, source_device_id = None, None, None
        source_device_label, snap_ids, is_snap_id = None, None, None
        if isinstance(source, volume.Volume):
            if 'create' or 'manage' in successful_operation:
                snapshot_count = str(len(source.snapshots))
                if snapshot_metadata:
                    last_ss_name = snapshot_metadata.get('snap_display_name')
                    snapshot_label = snapshot_metadata.get('SnapshotLabel')
                    source_device_id = snapshot_metadata.get('SourceDeviceID')
                    source_device_label = snapshot_metadata.get(
                        'SourceDeviceLabel')
                    snap_ids = snapshot_metadata.get('SnapIdList')
                    is_snap_id = snapshot_metadata.get('is_snap_id')
            else:
                snapshot_count = str(len(source.snapshots) - 1)
            default_sg = (
                self.utils.derive_default_sg_from_extra_specs(extra_specs))
            sl, wl = self.utils.get_service_level_workload(extra_specs)
            datadict = self.gather_volume_info(
                source.id, successful_operation, False,
                volume_size=source.size,
                default_sg_name=default_sg,
                serial_number=extra_specs[utils.ARRAY],
                service_level=sl, workload=wl,
                srp=extra_specs[utils.SRP],
                identifier_name=(
                    self.utils.get_volume_element_name(source.id)),
                openstack_name=source.display_name,
                snapshot_count=snapshot_count,
                last_ss_name=last_ss_name,
                snapshot_label=snapshot_label,
                is_snap_id=is_snap_id,
                snap_ids_or_gens=snap_ids,
                source_device_id=source_device_id,
                source_device_label=source_device_label)
            volume_metadata = self.update_volume_info_metadata(
                datadict, self.version_dict)
            self.print_pretty_table(volume_metadata)

    @debug_required
    def capture_modify_group(
            self, group_name, group_id, add_vols, remove_volumes, array):
        """Captures group info after a modify operation

        :param group_name: group name
        :param group_id: group id
        :param add_vols: add volume list
        :param remove_volumes: remove volume list
        :param array: array serial number
        """
        if not self.version_dict:
            self.version_dict = self.gather_version_info(array)
        for add_vol in add_vols:
            datadict = self.gather_volume_info(
                add_vol.id, 'addToGroup', True,
                group_name=group_name, group_id=group_id)
            add_volume_metadata = self.update_volume_info_metadata(
                datadict, self.version_dict)
            self.print_pretty_table(add_volume_metadata)

        for remove_volume in remove_volumes:
            datadict = self.gather_volume_info(
                remove_volume.id, 'removeFromGroup', True,
                group_name='Removed from %s' % group_name,
                group_id='Removed from %s' % group_id)
            remove_volume_metadata = self.update_volume_info_metadata(
                datadict, self.version_dict)
            self.print_pretty_table(remove_volume_metadata)

    @debug_required
    def capture_create_volume(
            self, device_id, volume, group_name, group_id, extra_specs,
            rep_info_dict, successful_operation, source_snapshot_id=None,
            source_device_id=None, temporary_snapvx=None,
            array_tag_list=None):
        """Captures create volume info in volume metadata

        :param device_id: device id
        :param volume: volume object
        :param group_name: group name
        :param group_id: group id
        :param extra_specs: additional info
        :param rep_info_dict: information gathered from replication
        :param successful_operation: the type of create operation
        :param source_snapshot_id: the source snapshot id
        :param temporary_snapvx: temporary snapVX
        :param array_tag_list: array tag list

        :returns: volume_metadata dict
        """
        rdf_group_no, target_name, remote_array, target_device_id = (
            None, None, None, None)
        rep_mode, replication_status, rdf_group_label, use_bias = (
            None, None, None, None)
        target_array_model, backend_id = None, None
        if rep_info_dict:
            rdf_group_no = rep_info_dict.get('rdf_group_no')
            target_name = rep_info_dict.get('target_name')
            remote_array = rep_info_dict.get('remote_array')
            target_device_id = rep_info_dict.get('target_device_id')
            rep_mode = rep_info_dict.get('rep_mode')
            replication_status = rep_info_dict.get('replication_status')
            rdf_group_label = rep_info_dict.get('rdf_group_label')
            backend_id = rep_info_dict.get('backend_id')

            if utils.METROBIAS in extra_specs:
                use_bias = extra_specs[utils.METROBIAS]
            target_array_model = rep_info_dict.get('target_array_model')

        default_sg = self.utils.derive_default_sg_from_extra_specs(
            extra_specs, rep_mode)
        sl, wl = self.utils.get_service_level_workload(extra_specs)
        datadict = self.gather_volume_info(
            volume.id, successful_operation, True, volume_size=volume.size,
            device_id=device_id,
            default_sg_name=default_sg,
            serial_number=extra_specs[utils.ARRAY],
            service_level=sl, workload=wl,
            srp=extra_specs[utils.SRP],
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name,
            source_volid=volume.source_volid,
            group_name=group_name, group_id=group_id,
            rdf_group_no=rdf_group_no, backend_id=backend_id,
            target_name=target_name, remote_array=remote_array,
            target_device_id=target_device_id,
            source_snapshot_id=source_snapshot_id,
            rep_mode=rep_mode, replication_status=replication_status,
            rdf_group_label=rdf_group_label, use_bias=use_bias,
            is_compression_disabled=self.utils.is_compression_disabled(
                extra_specs),
            source_device_id=source_device_id,
            temporary_snapvx=temporary_snapvx,
            target_array_model=target_array_model,
            array_tag_list=array_tag_list)
        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)
        self.print_pretty_table(volume_metadata)

    @debug_required
    def gather_replication_info(
            self, volume_id, successful_operation, append, **kwargs):
        """Gathers replication information

        :param volume_id: volume id
        :param successful_operation: the successful operation type
        :param append: boolean
        :param **kwargs: variable length of arguments
        :returns: rep_dict
        """
        return self._fill_volume_trace_dict(
            volume_id, successful_operation, append, **kwargs)

    @debug_required
    def capture_failover_volume(
            self, volume, target_device, remote_array, rdf_group, device_id,
            array, extra_specs, failover, vol_grp_name,
            replication_status, rep_mode):
        """Captures failover info in volume metadata

        :param volume: volume object
        :param target_device: the device to failover to
        :param remote_array: the array to failover to
        :param rdf_group: the rdf group
        :param device_id: the device to failover from
        :param array: the array to failover from
        :param extra_specs: additional info
        :param failover: failover flag
        :param vol_grp_name: async group name
        :param replication_status: volume replication status
        :param rep_mode: replication mode
        """
        operation = "Failover" if failover else "Failback"
        sl, wl = self.utils.get_service_level_workload(extra_specs)
        datadict = self.gather_volume_info(
            volume.id, operation, True, volume_size=volume.size,
            device_id=target_device,
            serial_number=remote_array,
            service_level=sl, workload=wl,
            srp=extra_specs[utils.SRP],
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name,
            source_volid=volume.source_volid,
            rdf_group_no=rdf_group, remote_array=array,
            target_device_id=device_id, vol_grp_name=vol_grp_name,
            replication_status=replication_status, rep_mode=rep_mode
        )

        self.version_dict = (
            self.gather_version_info(remote_array))
        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)
        self.print_pretty_table(volume_metadata)

    @debug_required
    def capture_manage_existing(
            self, volume, rep_info_dict, device_id, extra_specs):
        """Captures manage existing info in volume metadata

        :param volume: volume object
        :param rep_info_dict: information gathered from replication
        :param device_id: the PowerMax/VMAX device id
        :param extra_specs: the extra specs
        """
        successful_operation = "manage_existing_volume"
        rdf_group_no, target_name, remote_array, target_device_id = (
            None, None, None, None)
        rep_mode, replication_status, rdf_group_label, backend_id = (
            None, None, None, None)
        if rep_info_dict:
            rdf_group_no = rep_info_dict.get('rdf_group_no')
            target_name = rep_info_dict.get('target_name')
            remote_array = rep_info_dict.get('remote_array')
            target_device_id = rep_info_dict.get('target_device_id')
            rep_mode = rep_info_dict.get('rep_mode')
            replication_status = rep_info_dict.get('replication_status')
            rdf_group_label = rep_info_dict.get('rdf_group_label')
            backend_id = rep_info_dict.get('backend_id')

        default_sg = self.utils.derive_default_sg_from_extra_specs(
            extra_specs, rep_mode)
        sl, wl = self.utils.get_service_level_workload(extra_specs)
        datadict = self.gather_volume_info(
            volume.id, successful_operation, True, volume_size=volume.size,
            device_id=device_id,
            default_sg_name=default_sg,
            serial_number=extra_specs[utils.ARRAY],
            service_level=sl, workload=wl,
            srp=extra_specs[utils.SRP],
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name,
            source_volid=volume.source_volid,
            rdf_group_no=rdf_group_no, backend_id=backend_id,
            target_name=target_name, remote_array=remote_array,
            target_device_id=target_device_id,
            rep_mode=rep_mode, replication_status=replication_status,
            rdf_group_label=rdf_group_label
        )
        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)
        self.print_pretty_table(volume_metadata)

    @debug_required
    def capture_retype_info(
            self, volume, device_id, array, srp, target_slo,
            target_workload, target_sg_name, is_rep_enabled, rep_mode,
            is_compression_disabled, target_backend_id):
        """Captures manage existing info in volume metadata

        :param volume_id: volume identifier
        :param volume_size: volume size
        :param device_id: the PowerMax/VMAX device id
        :param array: the PowerMax/VMAX serialnumber
        :param srp: PowerMax/VMAX SRP
        :param target_slo: volume name
        :param target_workload: the PowerMax/VMAX device id
        :param is_rep_enabled: replication enabled flag
        :param rep_mode: replication mode
        :param is_compression_disabled: compression disabled flag
        :param target_backend_id: target replication backend id
        """
        successful_operation = "retype"
        if not target_slo:
            target_slo, target_workload = 'None', 'None'
        datadict = self.gather_volume_info(
            volume.id, successful_operation, False, volume_size=volume.size,
            device_id=device_id,
            target_sg_name=target_sg_name,
            serial_number=array,
            target_service_level=target_slo,
            target_workload=target_workload,
            srp=srp,
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name,
            is_rep_enabled=('yes' if is_rep_enabled else 'no'),
            backend_id=target_backend_id, rep_mode=rep_mode,
            is_compression_disabled=(
                True if is_compression_disabled else False))
        if not is_rep_enabled:
            delete_list = ['rdf_group_no', 'rep_mode', 'target_array_model',
                           'service_level', 'remote_array', 'target_device_id',
                           'replication_status', 'rdf_group_label',
                           'backend_id']
            self.utils.delete_values_from_dict(datadict, delete_list)
            update_list = [('default_sg_name', 'source_sg_name'),
                           ('service_level', 'source_service_level')]
            self.utils.update_values_in_dict(datadict, update_list)

        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)
        self.print_pretty_table(volume_metadata)

    @debug_required
    def capture_delete_info(self, volume):
        """Captures delete info in volume metadata

        :param volume: the volume object
        """
        datadict = self.gather_volume_info(
            volume.id, 'delete', False,
            identifier_name=self.utils.get_volume_element_name(volume.id),
            openstack_name=volume.display_name)
        volume_metadata = self.update_volume_info_metadata(
            datadict, self.version_dict)
        self.print_pretty_table(volume_metadata)
