# Copyright (c) 2013 Mirantis Inc.
#
# 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 datetime

import eventlet
from oslo.config import cfg
import six
from stevedore import enabled

from climate.db import api as db_api
from climate.db import exceptions as db_ex
from climate import exceptions as common_ex
from climate import manager
from climate.manager import exceptions
from climate.notification import api as notification_api
from climate.openstack.common.gettextutils import _
from climate.openstack.common import log as logging
from climate.utils import service as service_utils
from climate.utils import trusts

manager_opts = [
    cfg.ListOpt('plugins',
                default=['dummy.vm.plugin'],
                help='All plugins to use (one for every resource type to '
                     'support.)'),
    cfg.IntOpt('notify_hours_before_lease_end',
               default=48,
               help='Number of hours prior to lease end in which a '
                    'notification of lease close to expire will be sent. If '
                    'this is set to 0, then this notification will '
                    'not be sent.')
]

CONF = cfg.CONF
CONF.register_opts(manager_opts, 'manager')
LOG = logging.getLogger(__name__)

LEASE_DATE_FORMAT = "%Y-%m-%d %H:%M"


class ManagerService(service_utils.RPCServer):
    """Service class for the climate-manager service.

    Responsible for working with Climate DB, scheduling logic, running events,
    working with plugins, etc.
    """

    def __init__(self):
        target = manager.get_target()
        super(ManagerService, self).__init__(target)
        self.plugins = self._get_plugins()
        self.resource_actions = self._setup_actions()

    def start(self):
        super(ManagerService, self).start()
        self.tg.add_timer(10, self._event)

    def _get_plugins(self):
        """Return dict of resource-plugin class pairs."""
        config_plugins = CONF.manager.plugins
        plugins = {}

        extension_manager = enabled.EnabledExtensionManager(
            check_func=lambda ext: ext.name in config_plugins,
            namespace='climate.resource.plugins',
            invoke_on_load=False
        )

        for ext in extension_manager.extensions:
            try:
                plugin_obj = ext.plugin()
            except Exception as e:
                LOG.warning("Could not load {0} plugin "
                            "for resource type {1} '{2}'".format(
                                ext.name, ext.plugin.resource_type, e))
            else:
                if plugin_obj.resource_type in plugins:
                    msg = ("You have provided several plugins for "
                           "one resource type in configuration file. "
                           "Please set one plugin per resource type.")
                    raise exceptions.PluginConfigurationError(error=msg)

                plugins[plugin_obj.resource_type] = plugin_obj
        return plugins

    def _setup_actions(self):
        """Setup actions for each resource type supported.

        BasePlugin interface provides only on_start and on_end behaviour now.
        If there are some configs needed by plugin, they should be returned
        from get_plugin_opts method. These flags are registered in
        [resource_type] group of configuration file.
        """
        actions = {}

        for resource_type, plugin in six.iteritems(self.plugins):
            plugin = self.plugins[resource_type]
            CONF.register_opts(plugin.get_plugin_opts(), group=resource_type)

            actions[resource_type] = {}
            actions[resource_type]['on_start'] = plugin.on_start
            actions[resource_type]['on_end'] = plugin.on_end
            plugin.setup(None)
        return actions

    @service_utils.with_empty_context
    def _event(self):
        """Tries to commit event.

        If there is an event in Climate DB to be done, do it and change its
        status to 'DONE'.
        """
        LOG.debug('Trying to get event from DB.')
        event = db_api.event_get_first_sorted_by_filters(
            sort_key='time',
            sort_dir='asc',
            filters={'status': 'UNDONE'}
        )

        if not event:
            return

        if event['time'] < datetime.datetime.utcnow():
            db_api.event_update(event['id'], {'status': 'IN_PROGRESS'})
            event_type = event['event_type']
            event_fn = getattr(self, event_type, None)
            if event_fn is None:
                raise exceptions.EventError(error='Event type %s is not '
                                                  'supported' % event_type)
            try:
                eventlet.spawn_n(service_utils.with_empty_context(event_fn),
                                 event['lease_id'], event['id'])
                lease = db_api.lease_get(event['lease_id'])
                with trusts.create_ctx_from_trust(lease['trust_id']) as ctx:
                    self._send_notification(lease,
                                            ctx,
                                            events=['event.%s' % event_type])
            except Exception:
                db_api.event_update(event['id'], {'status': 'ERROR'})
                LOG.exception(_('Error occurred while event handling.'))

    def _date_from_string(self, date_string, date_format=LEASE_DATE_FORMAT):
        try:
            date = datetime.datetime.strptime(date_string, date_format)
        except ValueError:
            raise exceptions.InvalidDate(date=date_string,
                                         date_format=date_format)

        return date

    def get_lease(self, lease_id):
        return db_api.lease_get(lease_id)

    def list_leases(self, project_id=None):
        return db_api.lease_list(project_id)

    def create_lease(self, lease_values):
        """Create a lease with reservations.

        Return either the model of created lease or None if any error.
        """
        try:
            trust_id = lease_values.pop('trust_id')
        except KeyError:
            raise exceptions.MissingTrustId()

        # Remove and keep reservation values
        reservations = lease_values.pop("reservations", [])

        # Create the lease without the reservations
        start_date = lease_values['start_date']
        end_date = lease_values['end_date']

        now = datetime.datetime.utcnow()
        now = datetime.datetime(now.year,
                                now.month,
                                now.day,
                                now.hour,
                                now.minute)
        if start_date == 'now':
            start_date = now
        else:
            start_date = self._date_from_string(start_date)
        end_date = self._date_from_string(end_date)

        if start_date < now:
            raise common_ex.NotAuthorized(
                'Start date must later than current date')

        with trusts.create_ctx_from_trust(trust_id) as ctx:
            lease_values['user_id'] = ctx.user_id
            lease_values['project_id'] = ctx.project_id
            lease_values['start_date'] = start_date
            lease_values['end_date'] = end_date

            if not lease_values.get('events'):
                lease_values['events'] = []

            lease_values['events'].append({'event_type': 'start_lease',
                                           'time': start_date,
                                           'status': 'UNDONE'})
            lease_values['events'].append({'event_type': 'end_lease',
                                           'time': end_date,
                                           'status': 'UNDONE'})

            before_end_date = lease_values.get('before_end_notification', None)
            if before_end_date:
                # incoming param. Validation check
                try:
                    before_end_date = self._date_from_string(
                        before_end_date)
                    self._check_date_within_lease_limits(before_end_date,
                                                         lease_values)
                except common_ex.ClimateException as e:
                    LOG.error("Invalid before_end_date param. %s" % e.message)
                    raise e
            elif CONF.manager.notify_hours_before_lease_end > 0:
                delta = datetime.timedelta(
                    hours=CONF.manager.notify_hours_before_lease_end)