# Copyright (c) 2016 Hitachi Data Systems, 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_config import cfg
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import units

from manila.common import constants
from manila import exception
from manila.i18n import _, _LE, _LI
from manila.share import driver
from manila.share.drivers.hitachi.hsp import rest

LOG = log.getLogger(__name__)

hitachi_hsp_opts = [
    cfg.StrOpt('hitachi_hsp_host',
               required=True,
               help="HSP management host for communication between Manila "
                    "controller and HSP."),
    cfg.StrOpt('hitachi_hsp_username',
               required=True,
               help="HSP username to perform tasks such as create filesystems"
                    " and shares."),
    cfg.StrOpt('hitachi_hsp_password',
               required=True,
               secret=True,
               help="HSP password for the username provided."),
]


class HitachiHSPDriver(driver.ShareDriver):
    """Manila HSP Driver implementation.

    1.0.0 - Initial Version.
    """

    def __init__(self, *args, **kwargs):
        super(self.__class__, self).__init__(
            [False], *args, config_opts=[hitachi_hsp_opts], **kwargs)

        self.private_storage = kwargs.get('private_storage')

        self.backend_name = self.configuration.safe_get('share_backend_name')
        self.hsp_host = self.configuration.safe_get('hitachi_hsp_host')

        self.hsp = rest.HSPRestBackend(
            self.hsp_host,
            self.configuration.safe_get('hitachi_hsp_username'),
            self.configuration.safe_get('hitachi_hsp_password')
        )

    def _update_share_stats(self, data=None):
        LOG.debug("Updating Backend Capability Information - Hitachi HSP.")

        reserved = self.configuration.safe_get('reserved_share_percentage')
        max_over_subscription_ratio = self.configuration.safe_get(
            'max_over_subscription_ratio')
        hsp_cluster = self.hsp.get_cluster()

        total_space = hsp_cluster['properties']['total-storage-capacity']
        free_space = hsp_cluster['properties']['total-storage-available']

        data = {
            'share_backend_name': self.backend_name,
            'vendor_name': 'Hitachi',
            'driver_version': '1.0.0',
            'storage_protocol': 'NFS',
            'pools': [{
                'reserved_percentage': reserved,
                'pool_name': 'HSP',
                'thin_provisioning': True,
                'total_capacity_gb': total_space / units.Gi,
                'free_capacity_gb': free_space / units.Gi,
                'max_over_subscription_ratio': max_over_subscription_ratio,
                'qos': False,
                'dedupe': False,
                'compression': False,
            }],
        }

        LOG.info(_LI("Hitachi HSP Capabilities: %(data)s."),
                 {'data': data})
        super(HitachiHSPDriver, self)._update_share_stats(data)

    def create_share(self, context, share, share_server=None):
        LOG.debug("Creating share in HSP: %(shr)s", {'shr': share['id']})

        if share['share_proto'].lower() != 'nfs':
            msg = _("Only NFS protocol is currently supported.")
            raise exception.InvalidShare(reason=msg)

        self.hsp.add_file_system(share['id'], share['size'] * units.Gi)
        filesystem_id = self.hsp.get_file_system(share['id'])['id']

        try:
            self.hsp.add_share(share['id'], filesystem_id)
        except exception.HSPBackendException:
            with excutils.save_and_reraise_exception():
                self.hsp.delete_file_system(filesystem_id)
                msg = _LE("Could not create share %s on HSP.")
                LOG.exception(msg, share['id'])

        uri = self.hsp_host + ':/' + share['id']

        LOG.debug("Share created successfully on path: %(uri)s.",
                  {'uri': uri})
        return [{
            "path": uri,
            "metadata": {},
            "is_admin_only": False,
        }]

    def delete_share(self, context, share, share_server=None):
        LOG.debug("Deleting share in HSP: %(shr)s.", {'shr': share['id']})

        filesystem_id = hsp_share_id = None

        try:
            filesystem_id = self.hsp.get_file_system(share['id'])['id']
            hsp_share_id = self.hsp.get_share(filesystem_id)['id']
        except exception.HSPItemNotFoundException:
            LOG.info(_LI("Share %(shr)s already removed from backend."),
                     {'shr': share['id']})

        if hsp_share_id:
            # Clean all rules from share before deleting it
            current_rules = self.hsp.get_access_rules(hsp_share_id)
            for rule in current_rules:
                try:
                    self.hsp.delete_access_rule(hsp_share_id,
                                                rule['name'])
                except exception.HSPBackendException as e:
                    if 'No matching access rule found.' in e.msg:
                        LOG.debug("Rule %(rule)s already deleted in "
                                  "backend.", {'rule': rule['name']})
                    else:
                        raise

            self.hsp.delete_share(hsp_share_id)

        if filesystem_id:
            self.hsp.delete_file_system(filesystem_id)

        LOG.debug("Export and share successfully deleted: %(shr)s.",
                  {'shr': share['id']})

    def update_access(self, context, share, access_rules, add_rules,
                      delete_rules, share_server=None):

        LOG.debug("Updating access rules for share: %(shr)s.",
                  {'shr': share['id']})

        try:
            filesystem_id = self.hsp.get_file_system(share['id'])['id']
            hsp_share_id = self.hsp.get_share(filesystem_id)['id']
        except exception.HSPItemNotFoundException:
            raise exception.ShareResourceNotFound(share_id=share['id'])

        if not (add_rules or delete_rules):
            # Recovery mode
            current_rules = self.hsp.get_access_rules(hsp_share_id)

            # Indexing the rules for faster searching
            hsp_rules_dict = {
                rule['host-specification']: rule['read-write']
                for rule in current_rules
            }

            manila_rules_dict = {}

            for rule in access_rules:
                if rule['access_type'].lower() != 'ip':
                    msg = _("Only IP access type currently supported.")
                    raise exception.InvalidShareAccess(reason=msg)

                access_to = rule['access_to']
                is_rw = rule['access_level'] == constants.ACCESS_LEVEL_RW
                manila_rules_dict[access_to] = is_rw

            # Remove the rules that exist on HSP but not on manila
            remove_rules = self._get_complement(hsp_rules_dict,
                                                manila_rules_dict)

            # Add the rules that exist on manila but not on HSP
            add_rules = self._get_complement(manila_rules_dict, hsp_rules_dict)

            for rule in remove_rules:
                rule_name = self._get_hsp_rule_name(hsp_share_id, rule[0])
                self.hsp.delete_access_rule(hsp_share_id, rule_name)

            for rule in add_rules:
                self.hsp.add_access_rule(hsp_share_id, rule[0], rule[1])
        else:
            for rule in delete_rules:
                if rule['access_type'].lower() != 'ip':
                    continue

                # get the real rule name in HSP
                rule_name = self._get_hsp_rule_name(hsp_share_id,
                                                    rule['access_to'])
                try:
                    self.hsp.delete_access_rule(hsp_share_id,
                                                rule_name)
                except exception.HSPBackendException as e:
                    if 'No matching access rule found.' in e.msg:
                        LOG.debug("Rule %(rule)s already deleted in "
                                  "backend.", {'rule': rule['access_to']})
                    else:
                        raise

            for rule in add_rules:
                if rule['access_type'].lower() != 'ip':
                    msg = _("Only IP access type currently supported.")
                    raise exception.InvalidShareAccess(reason=msg)

                try:
                    self.hsp.add_access_rule(
                        hsp_share_id, rule['access_to'],
                        (rule['access_level'] == constants.ACCESS_LEVEL_RW))
                except exception.HSPBackendException as e:
                    if 'Duplicate NFS access rule exists' in e.msg:
                        LOG.debug("Rule %(rule)s already exists in "
                                  "backend.", {'rule': rule['access_to']})
                    else:
                        raise

        LOG.debug("Successfully updated share %(shr)s rules.",
                  {'shr': share['id']})

    def _get_hsp_rule_name(self, share_id, host_to):
        rule_name = share_id + host_to
        all_rules = self.hsp.get_access_rules(share_id)
        for rule in all_rules:
            # check if this rule has other name in HSP
            if rule['host-specification'] == host_to:
                rule_name = rule['name']
                break

        return rule_name

    def _get_complement(self, rules_a, rules_b):
        """Returns the rules of list A that are not on list B"""
        complement = []
        for rule, is_rw in rules_a.items():
            if rule not in rules_b or rules_b[rule] != is_rw:
                complement.append((rule, is_rw))

        return complement

    def extend_share(self, share, new_size, share_server=None):
        LOG.debug("Extending share in HSP: %(shr_id)s.",
                  {'shr_id': share['id']})

        old_size = share['size']
        hsp_cluster = self.hsp.get_cluster()
        free_space = hsp_cluster['properties']['total-storage-available']
        free_space = free_space / units.Gi

        if (new_size - old_size) < free_space:
            filesystem_id = self.hsp.get_file_system(share['id'])['id']
            self.hsp.resize_file_system(filesystem_id, new_size * units.Gi)
        else:
            msg = (_("Share %s cannot be extended due to insufficient space.")
                   % share['id'])
            raise exception.HSPBackendException(msg=msg)

        LOG.info(_LI("Share %(shr_id)s successfully extended to "
                     "%(shr_size)sG."),
                 {'shr_id': share['id'],
                  'shr_size': new_size})

    def shrink_share(self, share, new_size, share_server=None):
        LOG.debug("Shrinking share in HSP: %(shr_id)s.",
                  {'shr_id': share['id']})

        file_system = self.hsp.get_file_system(share['id'])
        usage = file_system['properties']['used-capacity'] / units.Gi

        LOG.debug("Usage for share %(shr_id)s in HSP: %(usage)sG.",
                  {'shr_id': share['id'], 'usage': usage})

        if new_size > usage:
            self.hsp.resize_file_system(file_system['id'], new_size * units.Gi)
        else:
            raise exception.ShareShrinkingPossibleDataLoss(
                share_id=share['id'])

        LOG.info(_LI("Share %(shr_id)s successfully shrunk to "
                     "%(shr_size)sG."),
                 {'shr_id': share['id'],
                  'shr_size': new_size})

    def manage_existing(self, share, driver_options):
        LOG.debug("Managing share in HSP: %(shr_id)s.",
                  {'shr_id': share['id']})

        ip, share_name = share['export_locations'][0]['path'].split(':')

        try:
            hsp_share = self.hsp.get_share(name=share_name.strip('/'))
        except exception.HSPItemNotFoundException:
            msg = _("The share %s trying to be managed was not found on "
                    "backend.") % share['id']
            raise exception.ManageInvalidShare(reason=msg)

        self.hsp.rename_file_system(hsp_share['properties']['file-system-id'],
                                    share['id'])

        original_name = hsp_share['properties']['file-system-name']
        private_storage_content = {
            'old_name': original_name,
            'new_name': share['id'],
        }
        self.private_storage.update(share['id'], private_storage_content)

        LOG.debug("Filesystem %(original_name)s was renamed to %(name)s.",
                  {'original_name': original_name,
                   'name': share['id']})

        file_system = self.hsp.get_file_system(share['id'])

        LOG.info(_LI("Share %(shr_path)s was successfully managed with ID "
                     "%(shr_id)s."),
                 {'shr_path': share['export_locations'][0]['path'],
                  'shr_id': share['id']})

        export_locations = [{
            "path": share['export_locations'][0]['path'],
            "metadata": {},
            "is_admin_only": False,
        }]

        return {'size': file_system['properties']['quota'] / units.Gi,
                'export_locations': export_locations}

    def unmanage(self, share):
        original_name = self.private_storage.get(share['id'], 'old_name')

        LOG.debug("Filesystem %(name)s that was originally named "
                  "%(original_name)s will no longer be managed.",
                  {'original_name': original_name,
                   'name': share['id']})

        self.private_storage.delete(share['id'])

        LOG.info(_LI("The share with current path %(shr_path)s and ID "
                     "%(shr_id)s is no longer being managed."),
                 {'shr_path': share['export_locations'][0]['path'],
                  'shr_id': share['id']})

    def get_default_filter_function(self):
        return "share.size >= 128"
