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

"""
Stack endpoint for Heat v1 ReST API.
"""

from oslo_log import log as logging
import six
from six.moves.urllib import parse
from webob import exc

from heat.api.openstack.v1 import util
from heat.api.openstack.v1.views import stacks_view
from heat.common import environment_format
from heat.common.i18n import _
from heat.common.i18n import _LW
from heat.common import identifier
from heat.common import param_utils
from heat.common import serializers
from heat.common import template_format
from heat.common import urlfetch
from heat.common import wsgi
from heat.rpc import api as rpc_api
from heat.rpc import client as rpc_client

LOG = logging.getLogger(__name__)


class InstantiationData(object):
    """
    The data accompanying a PUT or POST request to create or update a stack.
    """

    PARAMS = (
        PARAM_STACK_NAME,
        PARAM_TEMPLATE,
        PARAM_TEMPLATE_URL,
        PARAM_USER_PARAMS,
        PARAM_ENVIRONMENT,
        PARAM_FILES,
    ) = (
        'stack_name',
        'template',
        'template_url',
        'parameters',
        'environment',
        'files',
    )

    def __init__(self, data, patch=False):
        """
        Initialise from the request object.
        If called from the PATCH api, insert a flag for the engine code
        to distinguish.
        """
        self.data = data
        if patch:
            self.data[rpc_api.PARAM_EXISTING] = True

    @staticmethod
    def format_parse(data, data_type):
        """
        Parse the supplied data as JSON or YAML, raising the appropriate
        exception if it is in the wrong format.
        """

        try:
            if data_type == 'Environment':
                return environment_format.parse(data)
            else:
                return template_format.parse(data)
        except ValueError as parse_ex:
            mdict = {'type': data_type, 'error': six.text_type(parse_ex)}
            msg = _("%(type)s not in valid format: %(error)s") % mdict
            raise exc.HTTPBadRequest(msg)

    def stack_name(self):
        """
        Return the stack name.
        """
        if self.PARAM_STACK_NAME not in self.data:
            raise exc.HTTPBadRequest(_("No stack name specified"))
        return self.data[self.PARAM_STACK_NAME]

    def template(self):
        """
        Get template file contents, either inline, from stack adopt data or
        from a URL, in JSON or YAML format.
        """
        if rpc_api.PARAM_ADOPT_STACK_DATA in self.data:
            adopt_data = self.data[rpc_api.PARAM_ADOPT_STACK_DATA]
            try:
                adopt_data = template_format.simple_parse(adopt_data)
                return adopt_data['template']
            except (ValueError, KeyError) as ex:
                err_reason = _('Invalid adopt data: %s') % ex
                raise exc.HTTPBadRequest(err_reason)
        elif self.PARAM_TEMPLATE in self.data:
            template_data = self.data[self.PARAM_TEMPLATE]
            if isinstance(template_data, dict):
                return template_data
        elif self.PARAM_TEMPLATE_URL in self.data:
            url = self.data[self.PARAM_TEMPLATE_URL]
            LOG.debug('TemplateUrl %s' % url)
            try:
                template_data = urlfetch.get(url)
            except IOError as ex:
                err_reason = _('Could not retrieve template: %s') % ex
                raise exc.HTTPBadRequest(err_reason)
        else:
            raise exc.HTTPBadRequest(_("No template specified"))

        return self.format_parse(template_data, 'Template')

    def environment(self):
        """
        Get the user-supplied environment for the stack in YAML format.
        If the user supplied Parameters then merge these into the
        environment global options.
        """
        env = {}
        if self.PARAM_ENVIRONMENT in self.data:
            env_data = self.data[self.PARAM_ENVIRONMENT]
            if isinstance(env_data, dict):
                env = env_data
            else:
                env = self.format_parse(env_data,
                                        'Environment')

        environment_format.default_for_missing(env)
        parameters = self.data.get(self.PARAM_USER_PARAMS, {})
        env[self.PARAM_USER_PARAMS].update(parameters)
        return env

    def files(self):
        return self.data.get(self.PARAM_FILES, {})

    def args(self):
        """
        Get any additional arguments supplied by the user.
        """
        params = self.data.items()
        return dict((k, v) for k, v in params if k not in self.PARAMS)


class StackController(object):
    """
    WSGI controller for stacks resource in Heat v1 API
    Implements the API actions
    """
    # Define request scope (must match what is in policy.json)
    REQUEST_SCOPE = 'stacks'

    def __init__(self, options):
        self.options = options
        self.rpc_client = rpc_client.EngineClient()

    def default(self, req, **args):
        raise exc.HTTPNotFound()

    def _extract_bool_param(self, name, value):
        try:
            return param_utils.extract_bool(name, value)
        except ValueError as e:
            raise exc.HTTPBadRequest(six.text_type(e))

    def _extract_int_param(self, name, value,
                           allow_zero=True, allow_negative=False):
        try:
            return param_utils.extract_int(name, value,
                                           allow_zero, allow_negative)
        except ValueError as e:
            raise exc.HTTPBadRequest(six.text_type(e))

    def _extract_tags_param(self, tags):
        try:
            return param_utils.extract_tags(tags)
        except ValueError as e:
            raise exc.HTTPBadRequest(six.text_type(e))

    def _index(self, req, tenant_safe=True):
        filter_whitelist = {
            'id': 'mixed',
            'status': 'mixed',
            'name': 'mixed',
            'action': 'mixed',
            'tenant': 'mixed',
            'username': 'mixed',
            'owner_id': 'mixed',
        }
        whitelist = {
            'limit': 'single',
            'marker': 'single',
            'sort_dir': 'single',
            'sort_keys': 'multi',
            'show_deleted': 'single',
            'show_nested': 'single',
            'show_hidden': 'single',
            'tags': 'single',
            'tags_any': 'single',
            'not_tags': 'single',
            'not_tags_any': 'single',
        }
        params = util.get_allowed_params(req.params, whitelist)
        filter_params = util.get_allowed_params(req.params, filter_whitelist)

        show_deleted = False
        p_name = rpc_api.PARAM_SHOW_DELETED
        if p_name in params:
            params[p_name] = self._extract_bool_param(p_name, params[p_name])
            show_deleted = params[p_name]

        show_nested = False
        p_name = rpc_api.PARAM_SHOW_NESTED
        if p_name in params:
            params[p_name] = self._extract_bool_param(p_name, params[p_name])
            show_nested = params[p_name]

        key = rpc_api.PARAM_LIMIT
        if key in params:
            params[key] = self._extract_int_param(key, params[key])

        show_hidden = False
        p_name = rpc_api.PARAM_SHOW_HIDDEN
        if p_name in params:
            params[p_name] = self._extract_bool_param(p_name, params[p_name])
            show_hidden = params[p_name]

        tags = None
        if rpc_api.PARAM_TAGS in params:
            params[rpc_api.PARAM_TAGS] = self._extract_tags_param(
                params[rpc_api.PARAM_TAGS])
            tags = params[rpc_api.PARAM_TAGS]

        tags_any = None
        if rpc_api.PARAM_TAGS_ANY in params:
            params[rpc_api.PARAM_TAGS_ANY] = self._extract_tags_param(
                params[rpc_api.PARAM_TAGS_ANY])
            tags_any = params[rpc_api.PARAM_TAGS_ANY]

        not_tags = None
        if rpc_api.PARAM_NOT_TAGS in params:
            params[rpc_api.PARAM_NOT_TAGS] = self._extract_tags_param(
                params[rpc_api.PARAM_NOT_TAGS])
            not_tags = params[rpc_api.PARAM_NOT_TAGS]

        not_tags_any = None
        if rpc_api.PARAM_NOT_TAGS_ANY in params:
            params[rpc_api.PARAM_NOT_TAGS_ANY] = self._extract_tags_param(
                params[rpc_api.PARAM_NOT_TAGS_ANY])
            not_tags_any = params[rpc_api.PARAM_NOT_TAGS_ANY]

        # get the with_count value, if invalid, raise ValueError
        with_count = False
        if req.params.get('with_count'):
            with_count = self._extract_bool_param(
                'with_count',
                req.params.get('with_count'))

        if not filter_params:
            filter_params = None

        stacks = self.rpc_client.list_stacks(req.context,
                                             filters=filter_params,
                                             tenant_safe=tenant_safe,
                                             **params)

        count = None
        if with_count:
            try:
                # Check if engine has been updated to a version with
                # support to count_stacks before trying to use it.
                count = self.rpc_client.count_stacks(req.context,
                                                     filters=filter_params,
                                                     tenant_safe=tenant_safe,
                                                     show_deleted=show_deleted,
                                                     show_nested=show_nested,
                                                     show_hidden=show_hidden,
                                                     tags=tags,
                                                     tags_any=tags_any,
                                                     not_tags=not_tags,
                                                     not_tags_any=not_tags_any)
            except AttributeError as ex:
                LOG.warn(_LW("Old Engine Version: %s") % ex)

        return stacks_view.collection(req, stacks=stacks, count=count,
                                      tenant_safe=tenant_safe)

    @util.policy_enforce
    def global_index(self, req):
        return self._index(req, tenant_safe=False)

    @util.policy_enforce
    def index(self, req):
        """
        Lists summary information for all stacks
        """
        global_tenant = False
        name = rpc_api.PARAM_GLOBAL_TENANT
        if name in req.params:
            global_tenant = self._extract_bool_param(
                name,
                req.params.get(name))

        if global_tenant:
            return self.global_index(req, req.context.tenant_id)

        return self._index(req)

    @util.policy_enforce
    def detail(self, req):
        """
        Lists detailed information for all stacks
        """
        stacks = self.rpc_client.list_stacks(req.context)

        return {'stacks': [stacks_view.format_stack(req, s) for s in stacks]}

    @util.policy_enforce
    def preview(self, req, body):
        """
        Preview the outcome of a template and its params
        """

        data = InstantiationData(body)