# Copyright 2016 IBM Corp.
#
# 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 copy
import hashlib
import os
import retrying
import six
import tempfile
import types

from nova_powervm import conf as cfg
from nova_powervm.conf import powervm
from nova_powervm.virt.powervm.i18n import _
from nova_powervm.virt.powervm.i18n import _LI
from nova_powervm.virt.powervm.i18n import _LW
from nova_powervm.virt.powervm.nvram import api

from oslo_concurrency import lockutils
from oslo_log import log as logging
from swiftclient import exceptions as swft_exc
from swiftclient import service as swft_srv

LOG = logging.getLogger(__name__)
CONF = cfg.CONF


class SwiftNvramStore(api.NvramStore):

    def __init__(self):
        super(SwiftNvramStore, self).__init__()
        self.container = CONF.powervm.swift_container
        # Build the swift service options
        self.options = self._init_swift_options()
        self.swift_service = swft_srv.SwiftService(options=self.options)
        self._container_found = False

    @staticmethod
    def _init_swift_options():
        """Initialize all the options needed to communicate with Swift."""

        for opt in powervm.swift_opts:
            if opt.required and getattr(CONF.powervm, opt.name) is None:
                raise api.NVRAMConfigOptionNotSet(option=opt.name)

        options = {
            'auth_version': CONF.powervm.swift_auth_version,
            'os_username': CONF.powervm.swift_username,
            'os_user_domain_name': CONF.powervm.swift_user_domain_name,
            'os_password': CONF.powervm.swift_password,
            'os_project_name': CONF.powervm.swift_project_name,
            'os_project_domain_name': CONF.powervm.swift_project_domain_name,
            'os_auth_url': CONF.powervm.swift_auth_url,
            'os_cacert': CONF.powervm.swift_cacert,
        }

        return options

    def _run_operation(self, f, *args, **kwargs):
        """Convenience method to call the Swift client service."""

        # Get the function to call
        func = getattr(self.swift_service, f)
        try:
            result = func(*args, **kwargs)
            # For generators we have to copy the results because the
            # service is going out of scope.
            if isinstance(result, types.GeneratorType):
                results = []
                LOG.debug('SwiftOperation results:')
                for r in result:
                    results.append(copy.deepcopy(r))
                    LOG.debug(str(r))
                result = results
            else:
                LOG.debug('SwiftOperation result: %s' % str(result))
            return result
        except swft_srv.SwiftError as e:
            LOG.exception(e)
            raise

    @classmethod
    def _get_name_from_listing(cls, results):
        names = []
        for result in results:
            if result['success']:
                for obj in result['listing']:
                    names.append(obj['name'])
        return names

    def _get_container_names(self):
        results = self._run_operation('list', options={'long': True})
        return self._get_name_from_listing(results)

    def _get_object_names(self, container, prefix=None):
        # If this is the first pass, the container may not exist yet.  Check
        # to make sure it does, otherwise the list of the object names will
        # fail.
        if not self._container_found:
            container_names = self._get_container_names()
            self._container_found = (container in container_names)

            # If the container was still not found, then just return an empty
            # list.  There are no objects.
            if not self._container_found:
                return []

        results = self._run_operation(
            'list', options={'long': True, 'prefix': prefix},
            container=container)
        return self._get_name_from_listing(results)

    def _store(self, inst_key, inst_name, data, exists=None):
        """Store the NVRAM into the storage service.

        :param instance: instance object
        :param data: the NVRAM data base64 encoded string
        :param exists: (Optional, Default: None) If specified, tells the upload
                       whether or not the object exists.  Should be a boolean
                       or None.  If left as None, the method will look up
                       whether or not it exists.
        """
        source = six.StringIO(data)

        # If the object doesn't exist, we tell it to 'leave_segments'.  This
        # prevents a lookup and saves the logs from an ERROR in the swift
        # client (that really isn't an error...sigh).  It should be empty
        # if not the first upload (which defaults to leave_segments=False)
        # so that it overrides the existing element on a subsequent upload.
        if exists is None:
            exists = self._exists(inst_key)
        options = dict(leave_segments=True) if not exists else None
        obj = swft_srv.SwiftUploadObject(source, object_name=inst_key)

        # The swift client already has a retry opertaion. The retry method
        # takes a 'reset' function as a parameter. This parameter is 'None'
        # for all operations except upload. For upload, it's set to a default
        # method that throws a ClientException if the object to upload doesn't
        # implement tell/see/reset. If the authentication error occurs during
        # upload, this ClientException is raised with no retry. For any other
        # operation, swift client will retry and succeed.
        @retrying.retry(retry_on_result=lambda result: not result,
                        wait_fixed=250, stop_max_attempt_number=2)
        def _run_upload_operation():
            try:
                return self._run_operation('upload', self.container,
                                           [obj], options=options)
            except swft_exc.ClientException:
                # Upload operation failed due to expired Keystone token.
                # Retry SwiftClient operation to allow regeneration of token.
                return None

        try:
            results = _run_upload_operation()
        except retrying.RetryError as re:
            # The upload failed.
            reason = (_('Unable to store NVRAM after %d attempts') %
                      re.last_attempt.attempt_number)
            raise api.NVRAMUploadException(instance=inst_name, reason=reason)

        for result in results:
            if not result['success']:
                # The upload failed.
                raise api.NVRAMUploadException(instance=inst_name,
                                               reason=result)

    @lockutils.synchronized('nvram')
    def store(self, instance, data, force=True):
        """Store the NVRAM into the storage service.

        :param instance: instance object
        :param data: the NVRAM data base64 encoded string
        :param force: boolean whether an update should always be saved,
                      otherwise, check to see if it's changed.
        """
        exists = self._exists(instance.uuid)
        if not force and exists:
            # See if the entry exists and has not changed.
            results = self._run_operation('stat', options={'long': True},
                                          container=self.container,
                                          objects=[instance.uuid])
            result = results[0]
            if result['success']:
                existing_hash = result['headers']['etag']
                if six.PY3:
                    data = data.encode('ascii')
                md5 = hashlib.md5(data).hexdigest()
                if existing_hash == md5:
                    LOG.info(_LI('NVRAM has not changed for instance: %s'),
                             instance.name, instance=instance)
                    return

        self._store(instance.uuid, instance.name, data, exists=exists)
        LOG.debug('NVRAM updated for instance: %s' % instance.name)

    def store_slot_map(self, inst_key, data):
        """Store the Slot Map to Swift.

        :param inst_key: The instance key to use for the storage operation.
        :param data: The data of the object to store.  This should be a string.
        """
        self._store(inst_key, inst_key, data)

    def fetch_slot_map(self, inst_key):
        """Fetch the Slot Map object.

        :param inst_key: The instance key to use for the storage operation.
        :returns: The slot map (as a string)
        """
        return self._fetch(inst_key)[0]

    def fetch(self, instance):
        """Fetch the NVRAM from the storage service.

        :param instance: instance object
        :returns: the NVRAM data base64 encoded string
        """
        data, result = self._fetch(instance.uuid)
        if not data:
            raise api.NVRAMDownloadException(instance=instance.name,
                                             reason=result)
        return data

    def _exists(self, object_key):
        # Search by prefix, but since this is just a prefix, we need to loop
        # and do a check to make sure it fully looks.
        obj_names = self._get_object_names(self.container, prefix=object_key)
        for obj in obj_names:
            if object_key == obj:
                return True
        return False

    def _fetch(self, object_key):
        # Check if the object exists.  If not, return a result accordingly.
        if not self._exists(object_key):
            return None, _('Object does not exist in Swift.')

        try:
            # Create a temp file for download into
            with tempfile.NamedTemporaryFile(delete=False) as f:
                options = {
                    'out_file': f.name
                }
            # The file is now created and closed for the swift client to use.
            results = self._run_operation(
                'download', container=self.container, objects=[object_key],
                options=options)
            for result in results:
                if result['success']:
                    with open(f.name, 'r') as f:
                        return f.read(), result
                else:
                    return None, result
        finally:
            try:
                os.remove(f.name)
            except Exception:
                LOG.warning(_LW('Could not remove temporary file: %s'), f.name)

    def delete_slot_map(self, inst_key):
        """Delete the Slot Map from Swift.

        :param inst_key: The instance key to use for the storage operation.
        """
        for result in self._run_operation(
            'delete', container=self.container, objects=[inst_key]):

            LOG.debug('Delete slot map result: %s' % str(result))
            if not result['success']:
                raise api.NVRAMDeleteException(reason=result,
                                               instance=inst_key)

    def delete(self, instance):
        """Delete the NVRAM into the storage service.

        :param instance: instance object
        """
        for result in self._run_operation(
            'delete', container=self.container, objects=[instance.uuid]):

            LOG.debug('Delete result: %s' % str(result), instance=instance)
            if not result['success']:
                raise api.NVRAMDeleteException(instance=instance.name,
                                               reason=result)
