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

from oslo_log import log as logging
from oslo_utils import timeutils
import six

from heat.common.i18n import _
from heat.common.i18n import _LE
from heat.common import param_utils
from heat.common import template_format
from heat.engine import constraints as constr
from heat.rpc import api as rpc_api

LOG = logging.getLogger(__name__)


def extract_args(params):
    """Extract arguments passed as parameters and return them as a dictionary.

    Extract any arguments passed as parameters through the API and return them
    as a dictionary. This allows us to filter the passed args and do type
    conversion where appropriate
    """
    kwargs = {}
    timeout_mins = params.get(rpc_api.PARAM_TIMEOUT)
    if timeout_mins not in ('0', 0, None):
        try:
            timeout = int(timeout_mins)
        except (ValueError, TypeError):
            LOG.exception(_LE('Timeout conversion failed'))
        else:
            if timeout > 0:
                kwargs[rpc_api.PARAM_TIMEOUT] = timeout
            else:
                raise ValueError(_('Invalid timeout value %s') % timeout)

    name = rpc_api.PARAM_DISABLE_ROLLBACK
    if name in params:
        disable_rollback = param_utils.extract_bool(name, params[name])
        kwargs[name] = disable_rollback

    name = rpc_api.PARAM_SHOW_DELETED
    if name in params:
        params[name] = param_utils.extract_bool(name, params[name])

    adopt_data = params.get(rpc_api.PARAM_ADOPT_STACK_DATA)
    if adopt_data:
        try:
            adopt_data = template_format.simple_parse(adopt_data)
        except ValueError as exc:
            raise ValueError(_('Invalid adopt data: %s') % exc)
        kwargs[rpc_api.PARAM_ADOPT_STACK_DATA] = adopt_data

    tags = params.get(rpc_api.PARAM_TAGS)
    if tags:
        if not isinstance(tags, list):
            raise ValueError(_('Invalid tags, not a list: %s') % tags)

        for tag in tags:
            if not isinstance(tag, six.string_types):
                raise ValueError(_('Invalid tag, "%s" is not a string') % tag)

            if len(tag) > 80:
                raise ValueError(_('Invalid tag, "%s" is longer than 80 '
                                   'characters') % tag)

            # Comma is not allowed as per the API WG tagging guidelines
            if ',' in tag:
                raise ValueError(_('Invalid tag, "%s" contains a comma') % tag)

        kwargs[rpc_api.PARAM_TAGS] = tags

    return kwargs


def _parse_object_status(status):
    """Parse input status into action and status if possible.

    This function parses a given string (or list of strings) and see if it
    contains the action part. The action part is exacted if found.

    :param status: A string or a list of strings where each string contains
                   a status to be checked.
    :returns: (actions, statuses) tuple, where actions is a set of actions
              extracted from the input status and statuses is a set of pure
              object status.
    """

    if not isinstance(status, list):
        status = [status]

    status_set = set()
    action_set = set()
    for val in status:
        # Note: cannot reference Stack.STATUSES due to circular reference issue
        for s in ('COMPLETE', 'FAILED', 'IN_PROGRESS'):
            index = val.rfind(s)
            if index != -1:
                status_set.add(val[index:])
                if index > 1:
                    action_set.add(val[:index - 1])
                break

    return action_set, status_set


def translate_filters(params):
    """Translate filter names to their corresponding DB field names.

    :param params: A dictionary containing keys from engine.api.STACK_KEYS
                    and other keys previously leaked to users.
    :returns: A dict containing only valid DB filed names.
    """
    key_map = {
        rpc_api.STACK_NAME: 'name',
        rpc_api.STACK_ACTION: 'action',
        rpc_api.STACK_STATUS: 'status',
        rpc_api.STACK_STATUS_DATA: 'status_reason',
        rpc_api.STACK_DISABLE_ROLLBACK: 'disable_rollback',
        rpc_api.STACK_TIMEOUT: 'timeout',
        rpc_api.STACK_OWNER: 'username',
        rpc_api.STACK_PARENT: 'owner_id',
        rpc_api.STACK_USER_PROJECT_ID: 'stack_user_project_id',
    }

    for key, field in key_map.items():
        value = params.pop(key, None)
        if not value:
            continue

        fld_value = params.get(field, None)
        if fld_value:
            if not isinstance(fld_value, list):
                fld_value = [fld_value]
            if not isinstance(value, list):
                value = [value]

            value.extend(fld_value)

        params[field] = value

    # Deal with status which might be of form <ACTION>_<STATUS>, e.g.
    # "CREATE_FAILED". Note this logic is still not ideal due to the fact
    # that action and status are stored separately.
    if 'status' in params:
        a_set, s_set = _parse_object_status(params['status'])
        statuses = sorted(s_set)
        params['status'] = statuses[0] if len(statuses) == 1 else statuses

        if a_set:
            a = params.get('action', [])
            action_set = set(a) if isinstance(a, list) else set([a])
            actions = sorted(action_set.union(a_set))

            params['action'] = actions[0] if len(actions) == 1 else actions

    return params


def format_stack_outputs(stack, outputs):
    """Return a representation of the given output template.

    Return a representation of the given output template for the given stack
    that matches the API output expectations.
    """
    def format_stack_output(k):
        output = {
            rpc_api.OUTPUT_DESCRIPTION: outputs[k].get('Description',
                                                       'No description given'),
            rpc_api.OUTPUT_KEY: k,
            rpc_api.OUTPUT_VALUE: stack.output(k)
        }
        if outputs[k].get('error_msg'):
            output.update({rpc_api.OUTPUT_ERROR: outputs[k].get('error_msg')})
        return output

    return [format_stack_output(key) for key in outputs]


def format_stack(stack, preview=False):
    """Return a representation of the given stack.

    Return a representation of the given stack that matches the API output
    expectations.
    """
    updated_time = stack.updated_time and stack.updated_time.isoformat()
    created_time = stack.created_time or timeutils.utcnow()
    info = {
        rpc_api.STACK_NAME: stack.name,
        rpc_api.STACK_ID: dict(stack.identifier()),
        rpc_api.STACK_CREATION_TIME: created_time.isoformat(),
        rpc_api.STACK_UPDATED_TIME: updated_time,
        rpc_api.STACK_NOTIFICATION_TOPICS: [],  # TODO(?) Not implemented yet
        rpc_api.STACK_PARAMETERS: stack.parameters.map(str),
        rpc_api.STACK_DESCRIPTION: stack.t[stack.t.DESCRIPTION],
        rpc_api.STACK_TMPL_DESCRIPTION: stack.t[stack.t.DESCRIPTION],
        rpc_api.STACK_CAPABILITIES: [],   # TODO(?) Not implemented yet
        rpc_api.STACK_DISABLE_ROLLBACK: stack.disable_rollback,
        rpc_api.STACK_TIMEOUT: stack.timeout_mins,
        rpc_api.STACK_OWNER: stack.username,
        rpc_api.STACK_PARENT: stack.owner_id,
        rpc_api.STACK_USER_PROJECT_ID: stack.stack_user_project_id,
        rpc_api.STACK_TAGS: stack.tags,
    }