# Copyright 2013 Hewlett-Packard Development Company, L.P.
# 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 timeutils

from trove.backup.state import BackupState
from trove.common import cfg
from trove.common.i18n import _
from trove.common.strategies.storage import get_storage_strategy
from trove.conductor import api as conductor_api
from trove.guestagent.dbaas import get_filesystem_volume_stats
from trove.guestagent.strategies.backup.base import BackupError
from trove.guestagent.strategies.backup.base import UnknownBackupType
from trove.guestagent.strategies.backup import get_backup_strategy
from trove.guestagent.strategies.restore import get_restore_strategy

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

CONFIG_MANAGER = CONF.get('mysql'
                          if not CONF.datastore_manager
                          else CONF.datastore_manager)

STRATEGY = CONFIG_MANAGER.backup_strategy
BACKUP_NAMESPACE = CONFIG_MANAGER.backup_namespace
RESTORE_NAMESPACE = CONFIG_MANAGER.restore_namespace
RUNNER = get_backup_strategy(STRATEGY, BACKUP_NAMESPACE)
EXTRA_OPTS = CONF.backup_runner_options.get(STRATEGY, '')

# Try to get the incremental strategy or return the default 'backup_strategy'
INCREMENTAL = CONFIG_MANAGER.backup_incremental_strategy.get(
    STRATEGY, STRATEGY)

INCREMENTAL_RUNNER = get_backup_strategy(INCREMENTAL, BACKUP_NAMESPACE)


class BackupAgent(object):
    def _get_restore_runner(self, backup_type):
        """Returns the RestoreRunner associated with this backup type."""
        try:
            runner = get_restore_strategy(backup_type, RESTORE_NAMESPACE)
        except ImportError:
            raise UnknownBackupType(_("Unknown Backup type: %(type)s in "
                                      "namespace %(ns)s")
                                    % {"type": backup_type,
                                       "ns": RESTORE_NAMESPACE})
        return runner

    def stream_backup_to_storage(self, context, backup_info, runner, storage,
                                 parent_metadata={}, extra_opts=EXTRA_OPTS):
        backup_id = backup_info['id']
        conductor = conductor_api.API(context)

        # Store the size of the filesystem before the backup.
        mount_point = CONFIG_MANAGER.mount_point
        stats = get_filesystem_volume_stats(mount_point)
        backup_state = {
            'backup_id': backup_id,
            'size': stats.get('used', 0.0),
            'state': BackupState.BUILDING,
        }
        conductor.update_backup(CONF.guest_id,
                                sent=timeutils.utcnow_ts(microsecond=True),
                                **backup_state)
        LOG.debug("Updated state for %s to %s.", backup_id, backup_state)

        try:
            with runner(filename=backup_id, extra_opts=extra_opts,
                        **parent_metadata) as bkup:
                LOG.debug("Starting backup %s.", backup_id)
                meta = {}
                meta['datastore'] = backup_info['datastore']
                meta['datastore_version'] = backup_info['datastore_version']
                success, note, checksum, location = storage.save(
                    bkup.manifest,
                    bkup,
                    metadata=meta)

                backup_state.update({
                    'checksum': checksum,
                    'location': location,
                    'note': note,
                    'success': success,
                    'backup_type': bkup.backup_type,
                })

                LOG.debug("Backup %(backup_id)s completed status: "
                          "%(success)s.", backup_state)
                LOG.debug("Backup %(backup_id)s file swift checksum: "
                          "%(checksum)s.", backup_state)
                LOG.debug("Backup %(backup_id)s location: "
                          "%(location)s.", backup_state)

                if not success:
                    raise BackupError(note)

                backup_state.update({'state': BackupState.COMPLETED})

                return meta

        except Exception:
            LOG.exception(
                "Error saving backup: %(backup_id)s.", backup_state)
            backup_state.update({'state': BackupState.FAILED})
            raise
        finally:
            LOG.info("Completed backup %(backup_id)s.", backup_state)
            conductor.update_backup(CONF.guest_id,
                                    sent=timeutils.utcnow_ts(
                                        microsecond=True),
                                    **backup_state)
            LOG.info("Updated state for %s to %s.", backup_id, backup_state)

    def execute_backup(self, context, backup_info,
                       runner=RUNNER, extra_opts=EXTRA_OPTS,
                       incremental_runner=INCREMENTAL_RUNNER):

        LOG.info("Running backup %(id)s.", backup_info)
        storage = get_storage_strategy(
            CONF.storage_strategy,
            CONF.storage_namespace)(context)

        # Check if this is an incremental backup and grab the parent metadata
        parent_metadata = {}
        if backup_info.get('parent'):
            runner = incremental_runner
            LOG.debug("Using incremental backup runner: %s.", runner.__name__)
            parent = backup_info['parent']
            parent_metadata = storage.load_metadata(parent['location'],
                                                    parent['checksum'])
            # The parent could be another incremental backup so we need to
            # reset the location and checksum to *this* parents info
            parent_metadata.update({
                'parent_location': parent['location'],
                'parent_checksum': parent['checksum']
            })

        self.stream_backup_to_storage(context, backup_info, runner, storage,
                                      parent_metadata, extra_opts)

    def execute_restore(self, context, backup_info, restore_location):
        try:
            restore_runner = self._get_restore_runner(backup_info['type'])
            storage = get_storage_strategy(
                CONF.storage_strategy,
                CONF.storage_namespace)(context)

            runner = restore_runner(storage, location=backup_info['location'],
                                    checksum=backup_info['checksum'],
                                    restore_location=restore_location)
            backup_info['restore_location'] = restore_location

            LOG.info("Restoring instance from backup %(id)s to "
                     "%(restore_location)s", backup_info)

            content_size = runner.restore()
            LOG.info("Restore from backup %(id)s completed successfully "
                     "to %(restore_location)s", backup_info)
            LOG.debug("Restore size: %s", content_size)
        except Exception:
            LOG.exception("Error restoring backup %(id)s", backup_info)
            raise
        else:
            LOG.debug("Restored backup %(id)s", backup_info)
