# Copyright (c) 2015 Mirantis 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
import six

from manila.common import constants
from manila import exception
from manila.i18n import _, _LI
from manila import utils

LOG = log.getLogger(__name__)


class ShareInstanceAccess(object):

    def __init__(self, db, driver):
        self.db = db
        self.driver = driver

    def update_access_rules(self, context, share_instance_id, add_rules=None,
                            delete_rules=None, share_server=None):
        """Update access rules in driver and database for given share instance.

        :param context: current context
        :param share_instance_id: Id of the share instance model
        :param add_rules: list with ShareAccessMapping models or None - rules
        which should be added
        :param delete_rules: list with ShareAccessMapping models, "all", None
        - rules which should be deleted. If "all" is provided - all rules will
        be deleted.
        :param share_server: Share server model or None
        """
        share_instance = self.db.share_instance_get(
            context, share_instance_id, with_share_data=True)
        share_id = share_instance["share_id"]

        @utils.synchronized(
            "update_access_rules_for_share_%s" % share_id, external=True)
        def _update_access_rules_locked(*args, **kwargs):
            return self._update_access_rules(*args, **kwargs)

        _update_access_rules_locked(
            context=context,
            share_instance_id=share_instance_id,
            add_rules=add_rules,
            delete_rules=delete_rules,
            share_server=share_server,
        )

    def _update_access_rules(self, context, share_instance_id, add_rules=None,
                             delete_rules=None, share_server=None):
        # Reget share instance
        share_instance = self.db.share_instance_get(
            context, share_instance_id, with_share_data=True)

        # NOTE (rraja): preserve error state to trigger maintenance mode
        if share_instance['access_rules_status'] != constants.STATUS_ERROR:
            self.db.share_instance_update_access_status(
                context,
                share_instance_id,
                constants.STATUS_UPDATING)

        add_rules = add_rules or []
        delete_rules = delete_rules or []
        remove_rules = None

        if six.text_type(delete_rules).lower() == "all":
            # NOTE(ganso): if we are deleting an instance or clearing all
            # the rules, we want to remove only the ones related
            # to this instance.
            delete_rules = self.db.share_access_get_all_for_instance(
                context, share_instance['id'])
            rules = []
        else:
            _rules = self.db.share_access_get_all_for_instance(
                context, share_instance['id'])
            rules = _rules
            if delete_rules:
                delete_ids = [rule['id'] for rule in delete_rules]
                rules = list(filter(lambda r: r['id'] not in delete_ids,
                                    rules))
                # NOTE(ganso): trigger maintenance mode
                if share_instance['access_rules_status'] == (
                        constants.STATUS_ERROR):
                    remove_rules = [
                        rule for rule in _rules
                        if rule["id"] in delete_ids]
                    delete_rules = []

        try:
            access_keys = None
            try:
                access_keys = self.driver.update_access(
                    context,
                    share_instance,
                    rules,
                    add_rules=add_rules,
                    delete_rules=delete_rules,
                    share_server=share_server
                )
            except NotImplementedError:
                # NOTE(u_glide): Fallback to legacy allow_access/deny_access
                # for drivers without update_access() method support

                self._update_access_fallback(add_rules, context, delete_rules,
                                             remove_rules, share_instance,
                                             share_server)

            if access_keys:
                self._validate_access_keys(rules, add_rules, delete_rules,
                                           access_keys)

                for access_id, access_key in access_keys.items():
                    self.db.share_access_update_access_key(
                        context, access_id, access_key)

        except Exception:
            self.db.share_instance_update_access_status(
                context,
                share_instance['id'],
                constants.STATUS_ERROR)
            raise

        # NOTE(ganso): remove rules after maintenance is complete
        if remove_rules:
            delete_rules = remove_rules

        self._remove_access_rules(context, delete_rules, share_instance['id'])

        share_instance = self.db.share_instance_get(context, share_instance_id,
                                                    with_share_data=True)

        if self._check_needs_refresh(context, rules, share_instance):
            self._update_access_rules(context, share_instance_id,
                                      share_server=share_server)
        else:
            self.db.share_instance_update_access_status(
                context,
                share_instance['id'],
                constants.STATUS_ACTIVE
            )

            LOG.info(_LI("Access rules were successfully applied for "
                         "share instance: %s"),
                     share_instance['id'])

    @staticmethod
    def _validate_access_keys(access_rules, add_rules, delete_rules,
                              access_keys):
        if not isinstance(access_keys, dict):
            msg = _("The access keys must be supplied as a dictionary that "
                    "maps rule IDs to access keys.")
            raise exception.Invalid(message=msg)

        actual_rule_ids = sorted(access_keys)
        expected_rule_ids = []
        if not (add_rules or delete_rules):
            expected_rule_ids = [rule['id'] for rule in access_rules]
        else:
            expected_rule_ids = [rule['id'] for rule in add_rules]
        if actual_rule_ids != sorted(expected_rule_ids):
            msg = (_("The rule IDs supplied: %(actual)s do not match the "
                     "rule IDs that are expected: %(expected)s.")
                   % {'actual': actual_rule_ids,
                      'expected': expected_rule_ids})
            raise exception.Invalid(message=msg)

        for access_key in access_keys.values():
            if not isinstance(access_key, six.string_types):
                msg = (_("Access key %s is not string type.") % access_key)
                raise exception.Invalid(message=msg)

    def _check_needs_refresh(self, context, rules, share_instance):
        rule_ids = set([rule['id'] for rule in rules])
        queried_rules = self.db.share_access_get_all_for_instance(
            context, share_instance['id'])
        queried_ids = set([rule['id'] for rule in queried_rules])

        access_rules_status = share_instance['access_rules_status']

        return (access_rules_status == constants.STATUS_UPDATING_MULTIPLE or
                rule_ids != queried_ids)

    def _update_access_fallback(self, add_rules, context, delete_rules,
                                remove_rules, share_instance, share_server):
        for rule in add_rules:
            LOG.info(
                _LI("Applying access rule '%(rule)s' for share "
                    "instance '%(instance)s'"),
                {'rule': rule['id'], 'instance': share_instance['id']}
            )

            self.driver.allow_access(
                context,
                share_instance,
                rule,
                share_server=share_server
            )

        # NOTE(ganso): Fallback mode temporary compatibility workaround
        if remove_rules:
            delete_rules = remove_rules
        for rule in delete_rules:
            LOG.info(
                _LI("Denying access rule '%(rule)s' from share "
                    "instance '%(instance)s'"),
                {'rule': rule['id'], 'instance': share_instance['id']}
            )

            self.driver.deny_access(
                context,
                share_instance,
                rule,
                share_server=share_server
            )

    def _remove_access_rules(self, context, access_rules, share_instance_id):
        if not access_rules:
            return

        for rule in access_rules:
            access_mapping = self.db.share_instance_access_get(
                context, rule['id'], share_instance_id)

            self.db.share_instance_access_delete(context, access_mapping['id'])
