# 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 copy

import eventlet
import heatclient.client as hclient
import heatclient.exc as heat_exc
from oslo_config import cfg
from oslo_log import log as logging
import six

from murano.common import auth_utils
from murano.common.i18n import _LW
from murano.dsl import dsl
from murano.dsl import helpers
from murano.dsl import session_local_storage

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

HEAT_TEMPLATE_VERSION = '2013-05-23'


class HeatStackError(Exception):
    pass


@dsl.name('io.murano.system.HeatStack')
class HeatStack(object):
    def __init__(self, this, name, description=None):
        self._name = name
        self._template = None
        self._parameters = {}
        self._files = {}
        self._hot_environment = ''
        self._applied = True
        self._description = description
        self._last_stack_timestamps = (None, None)
        self._tags = ''
        self._owner = this.find_owner('io.murano.Environment')

    @staticmethod
    def _create_client(session, region_name):
        parameters = auth_utils.get_session_client_parameters(
            service_type='orchestration', region=region_name,
            conf=CONF.heat, session=session)
        return hclient.Client('1', **parameters)

    @property
    def _client(self):
        region = None if self._owner is None else self._owner['region']
        return self._get_client(region)

    @staticmethod
    @session_local_storage.execution_session_memoize
    def _get_client(region_name):
        session = auth_utils.get_client_session(conf=CONF.heat)
        return HeatStack._create_client(session, region_name)

    def _get_token_client(self):
        ks_session = auth_utils.get_token_client_session(conf=CONF.heat)
        region = None if self._owner is None else self._owner['region']
        return self._create_client(ks_session, region)

    def current(self):
        if self._template is not None:
            return self._template
        try:
            stack_info = self._client.stacks.get(stack_id=self._name)
            template = self._client.stacks.template(
                stack_id='{0}/{1}'.format(
                    stack_info.stack_name,
                    stack_info.id))
            self._template = template
            self._parameters.update(
                HeatStack._remove_system_params(stack_info.parameters))
            self._applied = True
            return self._template.copy()
        except heat_exc.HTTPNotFound:
            self._applied = True
            self._template = {}
            self._parameters.clear()
            return {}

    def parameters(self):
        self.current()
        return self._parameters.copy()

    def reload(self):
        self._template = None
        self._parameters.clear()
        return self.current()

    def set_template(self, template):
        self._template = template
        self._parameters.clear()
        self._applied = False

    def set_parameters(self, parameters):
        self._parameters = parameters
        self._applied = False

    def set_files(self, files):
        self._files = files
        self._applied = False

    def set_hot_environment(self, hot_environment):
        self._hot_environment = hot_environment
        self._applied = False

    def update_template(self, template):
        template_version = template.get('heat_template_version',
                                        HEAT_TEMPLATE_VERSION)
        if template_version != HEAT_TEMPLATE_VERSION:
            err_msg = ("Currently only heat_template_version %s "
                       "is supported." % HEAT_TEMPLATE_VERSION)
            raise HeatStackError(err_msg)
        current = self.current()
        self._template = helpers.merge_dicts(self._template, template)
        self._applied = self._template == current and self._applied

    @staticmethod
    def _remove_system_params(parameters):
        return dict((k, v) for k, v in six.iteritems(parameters) if
                    not k.startswith('OS::'))

    def _get_status(self):
        status = [None]

        def status_func(state_value):
            status[0] = state_value
            return True

        self._wait_state(status_func)
        return status[0]

    def _wait_state(self, status_func, wait_progress=False):
        tries = 4
        delay = 1

        while tries > 0:
            while True:
                try:
                    stack_info = self._client.stacks.get(
                        stack_id=self._name)
                    status = stack_info.stack_status
                    tries = 4
                    delay = 1
                except heat_exc.HTTPNotFound:
                    stack_info = None
                    status = 'NOT_FOUND'
                except Exception:
                    tries -= 1
                    delay *= 2
                    if not tries:
                        raise
                    eventlet.sleep(delay)
                    break

                if 'IN_PROGRESS' in status:
                    eventlet.sleep(2)
                    continue

                last_stack_timestamps = self._last_stack_timestamps
                self._last_stack_timestamps = (None, None) if not stack_info \
                    else(stack_info.creation_time, stack_info.updated_time)

                if (wait_progress and last_stack_timestamps ==
                        self._last_stack_timestamps and
                        last_stack_timestamps != (None, None)):
                    eventlet.sleep(2)
                    continue

                if not status_func(status):
                    reason = ': {0}'.format(
                        stack_info.stack_status_reason) if stack_info else ''
                    raise EnvironmentError(
                        "Unexpected stack state {0}{1}".format(status, reason))

                try:
                    return dict([(t['output_key'], t['output_value'])
                                 for t in stack_info.outputs])
                except Exception:
                    return {}
        return {}

    def output(self):
        return self._wait_state(lambda status: True)

    def push(self):
        if self._applied or self._template is None:
            return

        self._tags = ','.join(CONF.heat.stack_tags)
        if 'heat_template_version' not in self._template:
            self._template['heat_template_version'] = HEAT_TEMPLATE_VERSION

        if 'description' not in self._template and self._description:
            self._template['description'] = self._description

        template = copy.deepcopy(self._template)
        LOG.debug('Pushing: {template}'.format(template=template))

        while True:
            try:
                current_status = self._get_status()
                resources = template.get('Resources') or template.get(
                    'resources')
                if current_status == 'NOT_FOUND':
                    if resources is not None:
                        token_client = self._get_token_client()
                        token_client.stacks.create(
                            stack_name=self._name,
                            parameters=self._parameters,
                            template=template,
                            files=self._files,
                            environment=self._hot_environment,
                            disable_rollback=True,
                            tags=self._tags)

                        self._wait_state(
                            lambda status: status == 'CREATE_COMPLETE')
                else:
                    if resources is not None:
                        self._client.stacks.update(
                            stack_id=self._name,
                            parameters=self._parameters,
                            files=self._files,
                            environment=self._hot_environment,
                            template=template,
                            disable_rollback=True,
                            tags=self._tags)
                        self._wait_state(
                            lambda status: status == 'UPDATE_COMPLETE', True)
                    else:
                        self.delete()
            except heat_exc.HTTPConflict as e:
                LOG.warning(_LW('Conflicting operation: {msg}').format(msg=e))
                eventlet.sleep(3)
            else:
                break

        self._applied = self._template == template

    def delete(self):
        while True:
            try:
                if not self.current():
                    return
                self._wait_state(lambda s: True)
                self._client.stacks.delete(stack_id=self._name)
                self._wait_state(
                    lambda status: status in ('DELETE_COMPLETE', 'NOT_FOUND'),
                    wait_progress=True)
            except heat_exc.NotFound:
                LOG.warning(_LW('Stack {stack_name} already deleted?')
                            .format(stack_name=self._name))
                break
            except heat_exc.HTTPConflict as e:
                LOG.warning(_LW('Conflicting operation: {msg}').format(msg=e))
                eventlet.sleep(3)
            else:
                break

        self._template = {}
        self._applied = True
