# Copyright (c) 2016 EMC Corporation.
# 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
from oslo_utils import excutils
from oslo_utils import importutils

storops = importutils.try_import('storops')
if storops:
    # pylint: disable=import-error
    from storops import exception as storops_ex
    from storops.unity import enums

from manila.common import constants as const
from manila import exception
from manila.i18n import _
from manila.share.drivers.dell_emc.common.enas import utils as enas_utils
from manila.share.drivers.dell_emc.plugins.unity import utils

LOG = log.getLogger(__name__)


class UnityClient(object):
    def __init__(self, host, username, password):
        if storops is None:
            LOG.error('StorOps is required to run EMC Unity driver.')
        self.system = storops.UnitySystem(host, username, password)

    def create_cifs_share(self, resource, share_name):
        """Create CIFS share from the resource.

        :param resource: either UnityFilesystem or UnitySnap object
        :param share_name: CIFS share name
        :return: UnityCifsShare object
        """
        try:
            return resource.create_cifs_share(share_name)
        except storops_ex.UnitySmbShareNameExistedError:
            return self.get_share(share_name, 'CIFS')

    def create_nfs_share(self, resource, share_name):
        """Create NFS share from the resource.

        :param resource: either UnityFilesystem or UnitySnap object
        :param share_name: NFS share name
        :return: UnityNfsShare object
        """
        try:
            return resource.create_nfs_share(share_name)
        except storops_ex.UnityNfsShareNameExistedError:
            return self.get_share(share_name, 'NFS')

    def create_nfs_filesystem_and_share(self, pool, nas_server,
                                        share_name, size_gb):
        """Create filesystem and share from pool/NAS server.

        :param pool: pool for file system creation
        :param nas_server: nas server for file system creation
        :param share_name: file system and share name
        :param size_gb: file system size
        """
        size = utils.gib_to_byte(size_gb)
        pool.create_nfs_share(
            nas_server, share_name, size, user_cap=True)

    def get_share(self, name, share_proto):
        # Validate the share protocol
        proto = share_proto.upper()

        if proto == 'CIFS':
            return self.system.get_cifs_share(name=name)
        elif proto == 'NFS':
            return self.system.get_nfs_share(name=name)
        else:
            raise exception.BadConfigurationException(
                reason=_('Invalid NAS protocol supplied: %s.') % share_proto)

    @staticmethod
    def delete_share(share):
        share.delete()

    def create_filesystem(self, pool, nas_server, share_name, size_gb, proto):
        try:
            size = utils.gib_to_byte(size_gb)
            return pool.create_filesystem(nas_server,
                                          share_name,
                                          size,
                                          proto=proto,
                                          user_cap=True)
        except storops_ex.UnityFileSystemNameAlreadyExisted:
            LOG.debug('Filesystem %s already exists, '
                      'ignoring filesystem creation.', share_name)
            return self.system.get_filesystem(name=share_name)

    @staticmethod
    def delete_filesystem(filesystem):
        try:
            filesystem.delete()
        except storops_ex.UnityResourceNotFoundError:
            LOG.info('Filesystem %s is already removed.', filesystem.name)

    def create_nas_server(self, name, sp, pool, tenant=None):
        try:
            return self.system.create_nas_server(name, sp, pool,
                                                 tenant=tenant)
        except storops_ex.UnityNasServerNameUsedError:
            LOG.info('Share server %s already exists, ignoring share '
                     'server creation.', name)
            return self.get_nas_server(name)

    def get_nas_server(self, name):
        try:
            return self.system.get_nas_server(name=name)
        except storops_ex.UnityResourceNotFoundError:
            LOG.info('NAS server %s not found.', name)
            raise

    def delete_nas_server(self, name, username=None, password=None):
        tenant = None
        try:
            nas_server = self.get_nas_server(name=name)
            tenant = nas_server.tenant
            nas_server.delete(username=username, password=password)
        except storops_ex.UnityResourceNotFoundError:
            LOG.info('NAS server %s not found.', name)

        if tenant is not None:
            self._delete_tenant(tenant)

    @staticmethod
    def _delete_tenant(tenant):
        if tenant.nas_servers:
            LOG.debug('There are NAS servers belonging to the tenant %s. '
                      'Do not delete it.',
                      tenant.get_id())
            return
        try:
            tenant.delete(delete_hosts=True)
        except storops_ex.UnityException as ex:
            LOG.warning('Delete tenant %(tenant)s failed with error: '
                        '%(ex)s. Leave the tenant on the system.',
                        {'tenant': tenant.get_id(),
                         'ex': ex})

    @staticmethod
    def create_dns_server(nas_server, domain, dns_ip):
        try:
            nas_server.create_dns_server(domain, dns_ip)
        except storops_ex.UnityOneDnsPerNasServerError:
            LOG.info('DNS server %s already exists, '
                     'ignoring DNS server creation.', domain)

    @staticmethod
    def create_interface(nas_server, ip_addr, netmask, gateway, port_id,
                         vlan_id=None, prefix_length=None):
        try:
            nas_server.create_file_interface(port_id,
                                             ip_addr,
                                             netmask=netmask,
                                             v6_prefix_length=prefix_length,
                                             gateway=gateway,
                                             vlan_id=vlan_id)
        except storops_ex.UnityIpAddressUsedError:
            raise exception.IPAddressInUse(ip=ip_addr)

    @staticmethod
    def enable_cifs_service(nas_server, domain, username, password):
        try:
            nas_server.enable_cifs_service(
                nas_server.file_interface,
                domain=domain,
                domain_username=username,
                domain_password=password)
        except storops_ex.UnitySmbNameInUseError:
            LOG.info('CIFS service on NAS server %s is '
                     'already enabled.', nas_server.name)

    @staticmethod
    def enable_nfs_service(nas_server):
        try:
            nas_server.enable_nfs_service()
        except storops_ex.UnityNfsAlreadyEnabledError:
            LOG.info('NFS service on NAS server %s is '
                     'already enabled.', nas_server.name)

    @staticmethod
    def create_snapshot(filesystem, name):
        access_type = enums.FilesystemSnapAccessTypeEnum.CHECKPOINT
        try:
            return filesystem.create_snap(name, fs_access_type=access_type)
        except storops_ex.UnitySnapNameInUseError:
            LOG.info('Snapshot %(snap)s on Filesystem %(fs)s already '
                     'exists.', {'snap': name, 'fs': filesystem.name})

    def create_snap_of_snap(self, src_snap, dst_snap_name):
        if isinstance(src_snap, str):
            snap = self.get_snapshot(name=src_snap)
        else:
            snap = src_snap

        try:
            return snap.create_snap(dst_snap_name)
        except storops_ex.UnitySnapNameInUseError:
            return self.get_snapshot(dst_snap_name)

    def get_snapshot(self, name):
        return self.system.get_snap(name=name)

    @staticmethod
    def delete_snapshot(snap):
        try:
            snap.delete()
        except storops_ex.UnityResourceNotFoundError:
            LOG.info('Snapshot %s is already removed.', snap.name)

    def get_pool(self, name=None):
        return self.system.get_pool(name=name)

    def get_storage_processor(self, sp_id=None):
        sp = self.system.get_sp(sp_id)
        if sp_id is None:
            # `sp` is a list of SPA and SPB.
            return [s for s in sp if s is not None and s.existed]
        else:
            return sp if sp.existed else None

    def cifs_clear_access(self, share_name, white_list=None):
        share = self.system.get_cifs_share(name=share_name)
        share.clear_access(white_list)

    def nfs_clear_access(self, share_name, white_list=None):
        share = self.system.get_nfs_share(name=share_name)
        share.clear_access(white_list, force_create_host=True)

    def cifs_allow_access(self, share_name, user_name, access_level):
        share = self.system.get_cifs_share(name=share_name)

        if access_level == const.ACCESS_LEVEL_RW:
            cifs_access = enums.ACEAccessLevelEnum.WRITE
        else:
            cifs_access = enums.ACEAccessLevelEnum.READ

        share.add_ace(user=user_name, access_level=cifs_access)

    def nfs_allow_access(self, share_name, host_ip, access_level):
        share = self.system.get_nfs_share(name=share_name)
        host_ip = enas_utils.convert_ipv6_format_if_needed(host_ip)
        if access_level == const.ACCESS_LEVEL_RW:
            share.allow_read_write_access(host_ip, force_create_host=True)
            share.allow_root_access(host_ip, force_create_host=True)
        else:
            share.allow_read_only_access(host_ip, force_create_host=True)

    def cifs_deny_access(self, share_name, user_name):
        share = self.system.get_cifs_share(name=share_name)

        try:
            share.delete_ace(user=user_name)
        except storops_ex.UnityAclUserNotFoundError:
            LOG.debug('ACL User "%(user)s" does not exist.',
                      {'user': user_name})

    def nfs_deny_access(self, share_name, host_ip):
        share = self.system.get_nfs_share(name=share_name)

        try:
            share.delete_access(host_ip)
        except storops_ex.UnityHostNotFoundException:
            LOG.info('%(host)s access to %(share)s is already removed.',
                     {'host': host_ip, 'share': share_name})

    def get_file_ports(self):
        ports = self.system.get_file_port()
        link_up_ports = []
        for port in ports:
            if port.is_link_up and self._is_external_port(port.id):
                link_up_ports.append(port)

        return link_up_ports

    def extend_filesystem(self, fs, new_size_gb):
        size = utils.gib_to_byte(new_size_gb)
        try:
            fs.extend(size, user_cap=True)
        except storops_ex.UnityNothingToModifyError:
            LOG.debug('The size of the file system %(id)s is %(size)s '
                      'bytes.', {'id': fs.get_id(), 'size': size})
        return size

    def shrink_filesystem(self, share_id, fs, new_size_gb):
        size = utils.gib_to_byte(new_size_gb)
        try:
            fs.shrink(size, user_cap=True)
        except storops_ex.UnityNothingToModifyError:
            LOG.debug('The size of the file system %(id)s is %(size)s '
                      'bytes.', {'id': fs.get_id(), 'size': size})
        except storops_ex.UnityShareShrinkSizeTooSmallError:
            LOG.error('The used size of the file system %(id)s is '
                      'bigger than input shrink size,'
                      'it may cause date loss.', {'id': fs.get_id()})
            raise exception.ShareShrinkingPossibleDataLoss(share_id=share_id)
        return size

    @staticmethod
    def _is_external_port(port_id):
        return 'eth' in port_id or '_la' in port_id

    def get_tenant(self, name, vlan_id):
        if not vlan_id:
            # Do not create vlan for flat network
            return None

        tenant = None
        try:
            tenant_name = "vlan_%(vlan_id)s_%(name)s" % {'vlan_id': vlan_id,
                                                         'name': name}
            tenant = self.system.create_tenant(tenant_name, vlans=[vlan_id])
        except (storops_ex.UnityVLANUsedByOtherTenantError,
                storops_ex.UnityTenantNameInUseError,
                storops_ex.UnityVLANAlreadyHasInterfaceError):
            with excutils.save_and_reraise_exception() as exc:
                tenant = self.system.get_tenant_use_vlan(vlan_id)
                if tenant is not None:
                    LOG.debug("The VLAN %s is already added into a tenant. "
                              "Use the existing VLAN tenant.", vlan_id)
                    exc.reraise = False
        except storops_ex.SystemAPINotSupported:
            LOG.info("This system doesn't support tenant.")

        return tenant

    def restore_snapshot(self, snap_name):
        snap = self.get_snapshot(snap_name)
        return snap.restore(delete_backup=True)
