# Copyright (c) 2016 EMC Corporation, 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 oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import importutils

storops = importutils.try_import('storops')
if storops:
    from storops import exception as storops_ex

from cinder import exception
from cinder.i18n import _, _LW, _LE
from cinder import utils as cinder_utils
from cinder.volume.drivers.emc.vnx import common
from cinder.volume.drivers.emc.vnx import const
from cinder.volume.drivers.emc.vnx import utils


LOG = logging.getLogger(__name__)


class Condition(object):
    """Defines some condition checker which are used in wait_until, .etc."""

    @staticmethod
    def is_lun_io_ready(lun):
        utils.update_res_without_poll(lun)
        if not lun.existed:
            return False
        lun_state = lun.state
        if lun_state == common.LUNState.INITIALIZING:
            return False
        elif lun_state in [common.LUNState.READY,
                           common.LUNState.FAULTED]:
            return lun.operation == 'None'
        else:
            # Quick exit wait_until when the lun is other state to avoid
            # long-time timeout.
            msg = (_('Volume %(name)s was created in VNX, '
                     'but in %(state)s state.')
                   % {'name': lun.name,
                      'state': lun_state})
            raise exception.VolumeBackendAPIException(data=msg)

    @staticmethod
    def is_object_existed(vnx_obj):
        utils.update_res_without_poll(vnx_obj)
        return vnx_obj.existed

    @staticmethod
    def is_lun_ops_ready(lun):
        utils.update_res_without_poll(lun)
        return 'None' == lun.operation

    @staticmethod
    def is_lun_expanded(lun, new_size):
        utils.update_res_without_poll(lun)
        return new_size == lun.total_capacity_gb

    @staticmethod
    def is_mirror_synced(mirror):
        utils.update_res_without_poll(mirror)
        return (
            mirror.secondary_image.state ==
            storops.VNXMirrorImageState.SYNCHRONIZED)


class Client(object):
    def __init__(self, ip, username, password, scope,
                 naviseccli, sec_file):
        self.naviseccli = naviseccli
        self.vnx = storops.VNXSystem(ip=ip,
                                     username=username,
                                     password=password,
                                     scope=scope,
                                     naviseccli=naviseccli,
                                     sec_file=sec_file)

    def create_lun(self, pool, name, size, provision,
                   tier, cg_id=None, ignore_thresholds=False):
        pool = self.vnx.get_pool(name=pool)
        try:
            lun = pool.create_lun(lun_name=name,
                                  size_gb=size,
                                  provision=provision,
                                  tier=tier,
                                  ignore_thresholds=ignore_thresholds)
        except storops_ex.VNXLunNameInUseError:
            lun = self.vnx.get_lun(name=name)

        utils.wait_until(condition=Condition.is_lun_io_ready, lun=lun)
        if cg_id:
            cg = self.vnx.get_cg(name=cg_id)
            cg.add_member(lun)
        return lun

    def get_lun(self, name=None, lun_id=None):
        return self.vnx.get_lun(name=name, lun_id=lun_id)

    def get_lun_id(self, volume):
        """Retrieves the LUN ID of volume."""
        if volume.provider_location:
            return int(utils.extract_provider_location(
                volume.provider_location, 'id'))
        else:
            # In some cases, cinder will not update volume info in DB with
            # provider_location returned by us. We need to retrieve the id
            # from array. For example, cinder backup-create doesn't use the
            # provider_location returned from create_cloned_volume.
            lun = self.get_lun(name=volume.name)
            return lun.lun_id

    def delete_lun(self, name, force=False):
        """Deletes a LUN or mount point."""
        lun = self.get_lun(name=name)
        smp_attached_snap = (lun.attached_snapshot if lun.is_snap_mount_point
                             else None)

        try:
            # Do not delete the snapshots of the lun.
            lun.delete(force_detach=True, detach_from_sg=force)
            if smp_attached_snap:
                smp_attached_snap.delete()
        except storops_ex.VNXLunNotFoundError as ex:
            LOG.warning(_LW("LUN %(name)s is already deleted. "
                            "Message: %(msg)s"),
                        {'name': name, 'msg': ex.message})
            pass  # Ignore the failure that due to retry.

    @cinder_utils.retry(const.VNXLunPreparingError, retries=1,
                        backoff_rate=1)
    def expand_lun(self, name, new_size, poll=True):

        lun = self.get_lun(name=name)

        try:
            lun.poll = poll
            lun.expand(new_size, ignore_thresholds=True)
        except storops_ex.VNXLunExpandSizeError as ex:
            LOG.warning(_LW("LUN %(name)s is already expanded. "
                            "Message: %(msg)s."),
                        {'name': name, 'msg': ex.message})

        except storops_ex.VNXLunPreparingError as ex:
            # The error means the operation cannot be performed because the LUN
            # is 'Preparing'. Wait for a while so that the LUN may get out of
            # the transitioning state.
            with excutils.save_and_reraise_exception():
                LOG.warning(_LW("LUN %(name)s is not ready for extension: "
                                "%(msg)s"),
                            {'name': name, 'msg': ex.message})

                utils.wait_until(Condition.is_lun_ops_ready, lun=lun)

        utils.wait_until(Condition.is_lun_expanded, lun=lun, new_size=new_size)

    def modify_lun(self):
        pass

    @cinder_utils.retry(exceptions=const.VNXTargetNotReadyError,
                        interval=15,
                        retries=5, backoff_rate=1)
    def migrate_lun(self, src_id, dst_id,
                    rate=const.MIGRATION_RATE_HIGH):
        src = self.vnx.get_lun(lun_id=src_id)
        src.migrate(dst_id, rate)

    def session_finished(self, src_lun):
        session = self.vnx.get_migration_session(src_lun)
        if not session.existed:
            return True
        elif session.current_state in ('FAULTED', 'STOPPED'):
            LOG.warning(_LW('Session is %s, need to handled then.'),
                        session.current_state)
            return True
        else:
            return False

    def verify_migration(self, src_id, dst_id, dst_wwn):
        """Verify whether migration session finished successfully.

        :param src_id:  source LUN id
        :param dst_id:  destination LUN id
        :param dst_wwn: destination LUN WWN
        :returns Boolean: True or False
        """
        src_lun = self.vnx.get_lun(lun_id=src_id)

        utils.wait_until(condition=self.session_finished,
                         interval=common.INTERVAL_30_SEC,
                         src_lun=src_lun)
        new_lun = self.vnx.get_lun(lun_id=dst_id)
        new_wwn = new_lun.wwn
        if not new_wwn or new_wwn != dst_wwn:
            return True
        else:
            return False

    def cleanup_migration(self, src_id, dst_id):
        """Invoke when migration meets error.

        :param src_id:  source LUN id
        :param dst_id:  destination LUN id
        """
        # if migration session is still there
        # we need to cancel the session
        session = self.vnx.get_migration_session(src_id)
        src_lun = self.vnx.get_lun(lun_id=src_id)
        if session.existed:
            LOG.warning(_LW('Cancelling migration session: '
                            '%(src_id)s -> %(dst_id)s.'),
                        {'src_id': src_id,
                         'dst_id': dst_id})
            src_lun.cancel_migrate()

    def create_snapshot(self, lun_id, snap_name):
        """Creates a snapshot."""

        lun = self.get_lun(lun_id=lun_id)
        try:
            lun.create_snap(snap_name, allow_rw=True, auto_delete=False)
        except storops_ex.VNXSnapNameInUseError as ex:
            LOG.warning(_LW('Snapshot %(name)s already exists. '
                            'Message: %(msg)s'),
                        {'name': snap_name, 'msg': ex.message})

    def delete_snapshot(self, snapshot_name):
        """Deletes a snapshot."""

        snap = self.vnx.get_snap(name=snapshot_name)
        try:
            snap.delete()
        except storops_ex.VNXSnapNotExistsError as ex:
            LOG.warning(_LW("Snapshot %(name)s may be deleted already. "
                            "Message: %(msg)s"),
                        {'name': snapshot_name, 'msg': ex.message})
        except storops_ex.VNXDeleteAttachedSnapError as ex:
            with excutils.save_and_reraise_exception():
                LOG.warning(_LW("Failed to delete snapshot %(name)s "
                                "which is in use. Message: %(msg)s"),
                            {'name': snapshot_name, 'msg': ex.message})

    def copy_snapshot(self, snap_name, new_snap_name):
        snap = self.vnx.get_snap(name=snap_name)
        snap.copy(new_name=new_snap_name)

    def create_mount_point(self, lun_name, smp_name):
        lun = self.vnx.get_lun(name=lun_name)
        try:
            return lun.create_mount_point(name=smp_name)
        except storops_ex.VNXLunNameInUseError as ex:
            LOG.warning(_LW('Mount point %(name)s already exists. '
                            'Message: %(msg)s'),
                        {'name': smp_name, 'msg': ex.message})
            # Ignore the failure that due to retry.
            return self.vnx.get_lun(name=smp_name)

    def attach_snapshot(self, smp_name, snap_name):
        lun = self.vnx.get_lun(name=smp_name)
        try:
            lun.attach_snap(snap=snap_name)
        except storops_ex.VNXSnapAlreadyMountedError as ex:
            LOG.warning(_LW("Snapshot %(snap_name)s is attached to "
                            "snapshot mount point %(smp_name)s already. "
                            "Message: %(msg)s"),
                        {'snap_name': snap_name,
                         'smp_name': smp_name,
                         'msg': ex.message})

    def detach_snapshot(self, smp_name):
        lun = self.vnx.get_lun(name=smp_name)
        try:
            lun.detach_snap()
        except storops_ex.VNXSnapNotAttachedError as ex:
            LOG.warning(_LW("Snapshot mount point %(smp_name)s is not "
                            "currently attached. Message: %(msg)s"),
                        {'smp_name': smp_name, 'msg': ex.message})

    def modify_snapshot(self, snap_name, allow_rw=None, auto_delete=None):
        snap = self.vnx.get_snap(name=snap_name)
        snap.modify(allow_rw=allow_rw, auto_delete=auto_delete)

    def create_consistency_group(self, cg_name, lun_id_list=None):
        try:
            cg = self.vnx.create_cg(name=cg_name, members=lun_id_list)
        except storops_ex.VNXConsistencyGroupNameInUseError:
            cg = self.vnx.get_cg(name=cg_name)
        # Wait until cg is found on VNX, or deletion will fail afterwards
        utils.wait_until(Condition.is_object_existed, vnx_obj=cg)
        return cg

    def delete_consistency_group(self, cg_name):
        cg = self.vnx.get_cg(cg_name)
        try:
            cg.delete()
        except storops_ex.VNXConsistencyGroupNotFoundError:
            pass

    def create_cg_snapshot(self, cg_snap_name, cg_name):
        cg = self.vnx.get_cg(cg_name)
        try:
            snap = cg.create_snap(cg_snap_name, allow_rw=True)
        except storops_ex.VNXSnapNameInUseError:
            snap = self.vnx.get_snap(cg_snap_name)
        utils.wait_until(Condition.is_object_existed,
                         vnx_obj=snap)
        return snap

    def delete_cg_snapshot(self, cg_snap_name):
        self.delete_snapshot(cg_snap_name)

    def get_serial(self):
        return self.vnx.serial

    def get_pools(self):
        return self.vnx.get_pool()

    def get_pool(self, name):
        return self.vnx.get_pool(name=name)

    def get_iscsi_targets(self, sp=None, port_id=None, vport_id=None):
        return self.vnx.get_iscsi_port(sp=sp, port_id=port_id,
                                       vport_id=vport_id,
                                       has_ip=True)

    def get_fc_targets(self, sp=None, port_id=None):
        return self.vnx.get_fc_port(sp=sp, port_id=port_id)

    def get_enablers(self):
        return self.vnx.get_ndu()

    def is_fast_enabled(self):
        return self.vnx.is_auto_tiering_enabled()

    def is_compression_enabled(self):
        return self.vnx.is_compression_enabled()

    def is_dedup_enabled(self):
        return self.vnx.is_dedup_enabled()

    def is_fast_cache_enabled(self):
        return self.vnx.is_fast_cache_enabled()

    def is_thin_enabled(self):
        return self.vnx.is_thin_enabled()

    def is_snap_enabled(self):
        return self.vnx.is_snap_enabled()

    def is_mirror_view_enabled(self):
        return self.vnx.is_mirror_view_sync_enabled()

    def get_pool_feature(self):
        return self.vnx.get_pool_feature()

    def lun_has_snapshot(self, lun):
        """Checks lun has snapshot.

        :param lun: instance of VNXLun
        """
        snaps = lun.get_snap()
        return len(snaps) != 0

    def enable_compression(self, lun):
        """Enables compression on lun.

        :param lun: instance of VNXLun
        """
        try:
            lun.enable_compression(ignore_thresholds=True)
        except storops_ex.VNXCompressionAlreadyEnabledError:
            LOG.warning(_LW("Compression has already been enabled on %s."),
                        lun.name)

    def get_vnx_enabler_status(self):
        return common.VNXEnablerStatus(
            dedup=self.is_dedup_enabled(),
            compression=self.is_compression_enabled(),
            thin=self.is_thin_enabled(),
            fast=self.is_fast_enabled(),
            snap=self.is_snap_enabled())

    def create_storage_group(self, name):
        try:
            return self.vnx.create_sg(name)
        except storops_ex.VNXStorageGroupNameInUseError as ex:
            # Ignore the failure due to retry
            LOG.warning(_LW('Storage group %(name)s already exists. '
                            'Message: %(msg)s'),
                        {'name': name, 'msg': ex.message})
            return self.vnx.get_sg(name=name)

    def get_storage_group(self, name):
        return self.vnx.get_sg(name)

    def register_initiator(self, storage_group, host, initiator_port_map):
        """Registers the initiators of `host` to the `storage_group`.

        :param storage_group: the storage group object.
        :param host: the ip and name information of the initiator.
        :param initiator_port_map: the dict specifying which initiators are
                                   bound to which ports.
        """
        for (initiator_id, ports_to_bind) in initiator_port_map.items():
            for port in ports_to_bind:
                try:
                    storage_group.connect_hba(port, initiator_id, host.name,
                                              host_ip=host.ip)
                except storops_ex.VNXStorageGroupError as ex:
                    LOG.warning(_LW('Failed to set path to port %(port)s for '
                                    'initiator %(hba_id)s. Message: %(msg)s'),
                                {'port': port, 'hba_id': initiator_id,
                                 'msg': ex.message})

        if initiator_port_map:
            utils.update_res_with_poll(storage_group)

    def ping_node(self, port, ip_address):
        iscsi_port = self.get_iscsi_targets(sp=port.sp,
                                            port_id=port.port_id,
                                            vport_id=port.vport_id)
        try:
            iscsi_port.ping_node(ip_address, count=1)
            return True
        except storops_ex.VNXPingNodeError:
            return False

    def add_lun_to_sg(self, storage_group, lun, max_retries):
        """Adds the `lun` to `storage_group`."""
        try:
            return storage_group.attach_alu(lun, max_retries)
        except storops_ex.VNXAluAlreadyAttachedError as ex:
            # Ignore the failure due to retry.
            return storage_group.get_hlu(lun)
        except storops_ex.VNXNoHluAvailableError as ex:
            with excutils.save_and_reraise_exception():
                # Reach the max times of retry, fail the attach action.
                LOG.error(_LE('Failed to add %(lun)s into %(sg)s after '
                              '%(tried)s tries. Reach the max retry times. '
                              'Message: %(msg)s'),
                          {'lun': lun.lun_id, 'sg': storage_group.name,
                           'tried': max_retries, 'msg': ex.message})

    def get_wwn_of_online_fc_ports(self, ports):
        """Returns wwns of online fc ports.

        wwn of a certain port will not be included in the return list when it
        is not present or down.
        """
        wwns = set()
        ports_with_all_info = self.vnx.get_fc_port()
        for po in ports:
            online_list = list(
                filter(lambda p: (p == po and p.link_status == 'Up'
                                  and p.port_status == 'Online'),
                       ports_with_all_info))

            wwns.update([p.wwn for p in online_list])
        return list(wwns)

    def sg_has_lun_attached(self, sg):
        return bool(sg.get_alu_hlu_map())

    def deregister_initiators(self, initiators):
        if not isinstance(initiators, list):
            initiators = [initiators]
        for initiator_uid in initiators:
            self.vnx.remove_hba(initiator_uid)

    def update_consistencygroup(self, cg, lun_ids_to_add, lun_ids_to_remove):
        lun_ids_in_cg = (set([l.lun_id for l in cg.lun_list]) if cg.lun_list
                         else set())

        # lun_ids_to_add and lun_ids_to_remove never overlap.
        lun_ids_updated = ((lun_ids_in_cg | set(lun_ids_to_add)) -
                           set(lun_ids_to_remove))

        if lun_ids_updated:
            cg.replace_member(*[self.get_lun(lun_id=lun_id)
                                for lun_id in lun_ids_updated])
        else:
            # Need to remove all LUNs from cg. However, replace_member cannot
            # handle empty list. So use delete_member.
            cg.delete_member(*[self.get_lun(lun_id=lun_id)
                               for lun_id in lun_ids_in_cg])

    def get_cg(self, name):
        return self.vnx.get_cg(name=name)

    def get_available_ip(self):
        return self.vnx.alive_sp_ip

    def get_mirror(self, mirror_name):
        return self.vnx.get_mirror_view(mirror_name)

    def create_mirror(self, mirror_name, primary_lun_id):
        src_lun = self.vnx.get_lun(lun_id=primary_lun_id)
        try:
            mv = self.vnx.create_mirror_view(mirror_name, src_lun)
        except storops_ex.VNXMirrorNameInUseError:
            mv = self.vnx.get_mirror_view(mirror_name)
        return mv

    def delete_mirror(self, mirror_name):
        mv = self.vnx.get_mirror_view(mirror_name)
        try:
            mv.delete()
        except storops_ex.VNXMirrorNotFoundError:
            pass

    def add_image(self, mirror_name, sp_ip, secondary_lun_id):
        mv = self.vnx.get_mirror_view(mirror_name)
        mv.add_image(sp_ip, secondary_lun_id)
        # Secondary image info usually did not appear, so
        # here add a poll to update.
        utils.update_res_with_poll(mv)
        utils.wait_until(Condition.is_mirror_synced, mirror=mv)

    def remove_image(self, mirror_name):
        mv = self.vnx.get_mirror_view(mirror_name)
        mv.remove_image()

    def fracture_image(self, mirror_name):
        mv = self.vnx.get_mirror_view(mirror_name)
        mv.fracture_image()

    def sync_image(self, mirror_name):
        mv = self.vnx.get_mirror_view(mirror_name)
        mv.sync_image()
        utils.wait_until(Condition.is_mirror_synced, mirror=mv)

    def promote_image(self, mirror_name):
        mv = self.vnx.get_mirror_view(mirror_name)
        mv.promote_image()

    def get_pool_name(self, lun_name):
        lun = self.get_lun(name=lun_name)
        utils.update_res_without_poll(lun)
        return lun.pool_name
