# Copyright 2015 Red Hat 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 sys
import time

from heatclient._i18n import _
from heatclient.common import utils
import heatclient.exc as exc
from heatclient.v1 import events as events_mod


def get_hook_events(hc, stack_id, event_args, nested_depth=0,
                    hook_type='pre-create'):
    if hook_type == 'pre-create':
        stack_action_reason = 'Stack CREATE started'
        hook_event_reason = 'CREATE paused until Hook pre-create is cleared'
        hook_clear_event_reason = 'Hook pre-create is cleared'
    elif hook_type == 'pre-update':
        stack_action_reason = 'Stack UPDATE started'
        hook_event_reason = 'UPDATE paused until Hook pre-update is cleared'
        hook_clear_event_reason = 'Hook pre-update is cleared'
    elif hook_type == 'pre-delete':
        stack_action_reason = 'Stack DELETE started'
        hook_event_reason = 'DELETE paused until Hook pre-delete is cleared'
        hook_clear_event_reason = 'Hook pre-delete is cleared'
    else:
        raise exc.CommandError(_('Unexpected hook type %s') % hook_type)

    events = get_events(hc, stack_id=stack_id, event_args=event_args,
                        nested_depth=nested_depth)

    # Get the most recent event associated with this action, which gives us the
    # event when we moved into IN_PROGRESS for the hooks we're interested in.
    stack_name = stack_id.split("/")[0]
    action_start_event = [e for e in enumerate(events)
                          if e[1].resource_status_reason == stack_action_reason
                          and e[1].stack_name == stack_name][-1]
    # Slice the events with the index from the enumerate
    action_start_index = action_start_event[0]
    events = events[action_start_index:]

    # Get hook events still pending by some list filtering/comparison
    # We build a map hook events per-resource, and remove any event
    # for which there is a corresponding hook-clear event.
    resource_event_map = {}
    for e in events:
        stack_resource = (e.stack_name, e.resource_name)
        if e.resource_status_reason == hook_event_reason:
            resource_event_map[(e.stack_name, e.resource_name)] = e
        elif e.resource_status_reason == hook_clear_event_reason:
            if resource_event_map.get(stack_resource):
                del resource_event_map[(e.stack_name, e.resource_name)]
    return list(resource_event_map.values())


def get_events(hc, stack_id, event_args, nested_depth=0,
               marker=None, limit=None):
    event_args = dict(event_args)
    if marker:
        event_args['marker'] = marker
    if limit:
        event_args['limit'] = limit
    if not nested_depth:
        # simple call with no nested_depth
        return _get_stack_events(hc, stack_id, event_args)

    # assume an API which supports nested_depth
    event_args['nested_depth'] = nested_depth
    events = _get_stack_events(hc, stack_id, event_args)

    if not events:
        return events

    first_links = getattr(events[0], 'links', [])
    root_stack_links = [link for link in first_links
                        if link.get('rel') == 'root_stack']
    if root_stack_links:
        # response has a root_stack link, indicating this is an API which
        # supports nested_depth
        return events

    # API doesn't support nested_depth, do client-side paging and recursive
    # event fetch
    marker = event_args.pop('marker', None)
    limit = event_args.pop('limit', None)
    event_args.pop('nested_depth', None)
    events = _get_stack_events(hc, stack_id, event_args)
    events.extend(_get_nested_events(hc, nested_depth,
                                     stack_id, event_args))
    # Because there have been multiple stacks events mangled into
    # one list, we need to sort before passing to print_list
    # Note we can't use the prettytable sortby_index here, because
    # the "start" option doesn't allow post-sort slicing, which
    # will be needed to make "--marker" work for nested_depth lists
    events.sort(key=lambda x: x.event_time)

    # Slice the list if marker is specified
    if marker:
        try:
            marker_index = [e.id for e in events].index(marker)
            events = events[marker_index:]
        except ValueError:
            pass

    # Slice the list if limit is specified
    if limit:
        limit_index = min(int(limit), len(events))
        events = events[:limit_index]
    return events


def _get_nested_ids(hc, stack_id):
    nested_ids = []
    try:
        resources = hc.resources.list(stack_id=stack_id)
    except exc.HTTPNotFound:
        raise exc.CommandError(_('Stack not found: %s') % stack_id)
    for r in resources:
        nested_id = utils.resource_nested_identifier(r)
        if nested_id:
            nested_ids.append(nested_id)
    return nested_ids


def _get_nested_events(hc, nested_depth, stack_id, event_args):
    # FIXME(shardy): this is very inefficient, we should add nested_depth to
    # the event_list API in a future heat version, but this will be required
    # until kilo heat is EOL.
    nested_ids = _get_nested_ids(hc, stack_id)
    nested_events = []
    for n_id in nested_ids:
        stack_events = _get_stack_events(hc, n_id, event_args)
        if stack_events:
            nested_events.extend(stack_events)
        if nested_depth > 1:
            next_depth = nested_depth - 1
            nested_events.extend(_get_nested_events(
                hc, next_depth, n_id, event_args))
    return nested_events


def _get_stack_name_from_links(event):
    links = {link.get('rel'): link.get('href')
             for link in getattr(event, 'links', [])}
    href = links.get('stack')
    if not href:
        return
    return href.split('/stacks/', 1)[-1].split('/')[0]


def _get_stack_events(hc, stack_id, event_args):
    event_args['stack_id'] = stack_id
    try:
        events = hc.events.list(**event_args)
    except exc.HTTPNotFound as ex:
        # it could be the stack or resource that is not found
        # just use the message that the server sent us.
        raise exc.CommandError(str(ex))
    else:
        stack_name = stack_id.split("/")[0]
        # Show which stack the event comes from (for nested events)
        for e in events:
            e.stack_name = _get_stack_name_from_links(e) or stack_name
        return events


def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None,
                    out=None, nested_depth=0):
    """Continuously poll events and logs for performed action on stack."""

    if action:
        stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action)
        stop_check = lambda a: a in stop_status  # noqa: E731
    else:
        stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED')  # noqa E731

    no_event_polls = 0
    msg_template = _("\n Stack %(name)s %(status)s \n")
    if not out:
        out = sys.stdout
    event_log_context = utils.EventLogContext()

    def is_stack_event(event):
        if getattr(event, 'resource_name', '') != stack_name:
            return False

        phys_id = getattr(event, 'physical_resource_id', '')
        links = {link.get('rel'): link.get('href')
                 for link in getattr(event, 'links', [])}
        stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1]
        return stack_id == phys_id

    while True:
        events = get_events(hc, stack_id=stack_name, nested_depth=nested_depth,
                            event_args={'sort_dir': 'asc',
                                        'marker': marker})

        if len(events) == 0:
            no_event_polls += 1
        else:
            no_event_polls = 0
            # set marker to last event that was received.
            marker = getattr(events[-1], 'id', None)
            events_log = utils.event_log_formatter(events, event_log_context)
            out.write(events_log)
            out.write('\n')

            for event in events:
                # check if stack event was also received
                if is_stack_event(event):
                    stack_status = getattr(event, 'resource_status', '')
                    msg = msg_template % dict(
                        name=stack_name, status=stack_status)
                    if stop_check(stack_status):
                        return stack_status, msg

        if no_event_polls >= 2:
            # after 2 polls with no events, fall back to a stack get
            stack = hc.stacks.get(stack_name, resolve_outputs=False)
            stack_status = stack.stack_status
            msg = msg_template % dict(
                name=stack_name, status=stack_status)
            if stop_check(stack_status):
                return stack_status, msg
            # go back to event polling again
            no_event_polls = 0

        time.sleep(poll_period)


def wait_for_events(ws, stack_name, out=None):
    """Receive events over the passed websocket and wait for final status."""
    msg_template = _("\n Stack %(name)s %(status)s \n")
    if not out:
        out = sys.stdout
    event_log_context = utils.EventLogContext()
    while True:
        data = ws.recv()['body']
        event = events_mod.Event(None, data['payload'], True)
        # Keep compatibility with the HTTP API
        event.event_time = data['timestamp']
        event.resource_status = '%s_%s' % (event.resource_action,
                                           event.resource_status)
        events_log = utils.event_log_formatter([event], event_log_context)
        out.write(events_log)
        out.write('\n')
        if data['payload']['resource_name'] == stack_name:
            stack_status = data['payload']['resource_status']
            if stack_status in ('COMPLETE', 'FAILED'):
                msg = msg_template % dict(
                    name=stack_name, status=event.resource_status)
                return '%s_%s' % (event.resource_action, stack_status), msg
