from functools import wraps
import logging
import json
from datetime import timedelta

from demands import HTTPServiceError
from django.shortcuts import render, redirect, resolve_url
from django.utils.decorators import available_attrs
from django.utils.six.moves.urllib.parse import urlparse
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.sites.shortcuts import get_current_site
from django.contrib import messages
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse
from django.utils.http import is_safe_url
from django.utils.timezone import now
from django.http import HttpResponseRedirect, JsonResponse
from django.template.defaulttags import register
from django.template.response import TemplateResponse
from django.core.context_processors import csrf
from django.conf import settings
from django import forms
import dateutil.parser

from seed_services_client import (
    ControlInterfaceApiClient,
    HubApiClient,
    IdentityStoreApiClient,
    MessageSenderApiClient,
    SchedulerApiClient,
    StageBasedMessagingApiClient,
)
from seed_services_client.metrics import MetricsApiClient

from urlobject import URLObject

from .forms import (AuthenticationForm, IdentitySearchForm,
                    RegistrationFilterForm, SubscriptionFilterForm,
                    ChangeFilterForm, ReportGenerationForm,
                    AddSubscriptionForm, DeactivateSubscriptionForm,
                    ChangeSubscriptionForm, MsisdnReportGenerationForm,
                    UserDetailSearchForm)
from . import utils

logger = logging.getLogger(__name__)

@register.filter
def get_identity_addresses(identity):
    details = identity.get('details', {})
    default_addr_type = details.get('default_addr_type', None)
    addresses = details.get('addresses', {})
    if not default_addr_type:
        logger.warning('No default_addr_type specified for: %r' % (identity,))
        return {}
    return addresses.get(default_addr_type, {})

@register.filter
def get_item(dictionary, key):
    return dictionary.get(key)

@register.filter
def get_date(date_string):
    if date_string is not None:
        return dateutil.parser.parse(date_string)

@register.simple_tag
def replace_query_param(url, parameter, value):
    url = URLObject(url)
    return url.set_query_params([(parameter, value)])

ciApi = ControlInterfaceApiClient(
    api_url=settings.CONTROL_INTERFACE_SERVICE_URL,
    auth_token=settings.CONTROL_INTERFACE_SERVICE_TOKEN
)

def request_passes_test(test_func, login_url=None,
                        redirect_field_name=REDIRECT_FIELD_NAME):
    """
    Decorator for views that checks that the request passes the given test,
    redirecting to the log-in page if necessary. The test should be a callable
    that takes the requst object and returns True if the request passes.
    """

    def decorator(view_func):
        @wraps(view_func, assigned=available_attrs(view_func))
        def _wrapped_view(request, *args, **kwargs):
            if test_func(request):
                return view_func(request, *args, **kwargs)
            path = request.build_absolute_uri()
            resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
            # If the login url is the same scheme and net location then just
            # use the path as the "next" url.
            login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
            current_scheme, current_netloc = urlparse(path)[:2]
            if ((not login_scheme or login_scheme == current_scheme) and
                    (not login_netloc or login_netloc == current_netloc)):
                path = request.get_full_path()
            from django.contrib.auth.views import redirect_to_login
            return redirect_to_login(
                path, resolved_login_url, redirect_field_name)
        return _wrapped_view
    return decorator

def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME,
                   login_url=None):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = request_passes_test(
        lambda r: r.session.get('user_token'),
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

def has_permission(permissions, permission, object_id=None):
    ids = [p['object_id'] for p in permissions if p['type'] == permission]
    if object_id is None and len(ids) == 1:
        return True
    elif object_id is not None and object_id in ids:
        return True
    return False

def permission_required(function=None, permission=None, object_id=None,
                        redirect_field_name=REDIRECT_FIELD_NAME,
                        login_url=None):
    """
    Decorator for views that checks that the user is logged in, redirecting
    to the log-in page if necessary.
    """
    actual_decorator = request_passes_test(
        lambda r: has_permission(r.session.get('user_permissions'), permission, object_id),  # noqa
        login_url=login_url,
        redirect_field_name=redirect_field_name
    )
    if function:
        return actual_decorator(function)
    return actual_decorator

def tokens_required(service_list):
    """
    Ensure the user has the necessary tokens for the specified services
    """
    def decorator(func):
        @wraps(func)
        def inner(request, *args, **kwargs):
            for service in service_list:
                if service not in request.session["user_tokens"]:
                    return redirect('denied')
            return func(request, *args, **kwargs)
        return inner
    return decorator

def login(request, template_name='ci/login.html',
          redirect_field_name=REDIRECT_FIELD_NAME,
          authentication_form=AuthenticationForm):
    """
    Displays the login form and handles the login action.
    """
    redirect_to = request.POST.get(redirect_field_name,
                                   request.GET.get(redirect_field_name, ''))

    if request.method == "POST":
        form = authentication_form(request, data=request.POST)
        if form.is_valid():

            # Ensure the user-originating redirection url is safe.
            if not is_safe_url(url=redirect_to, host=request.get_host()):
                redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)

            # Okay, security check complete. Get the user object from auth api.
            user = form.get_user()
            request.session['user_token'] = user["token"]
            request.session['user_email'] = user["email"]
            request.session['user_permissions'] = user["permissions"]
            request.session['user_id'] = user["id"]
            request.session['user_list'] = user["user_list"]

            if not settings.HIDE_DASHBOARDS:
                # Set user dashboards because they are slow to change
                dashboards = ciApi.get_user_dashboards(user["id"])
                dashboard_list = list(dashboards['results'])
                if len(dashboard_list) > 0:
                    request.session['user_dashboards'] = \
                        dashboard_list[0]["dashboards"]
                    request.session['user_default_dashboard'] = \
                        dashboard_list[0]["default_dashboard"]["id"]
                else:
                    request.session['user_dashboards'] = []
                    request.session['user_default_dashboard'] = None

            # Get the user access tokens too and format for easy access
            tokens = ciApi.get_user_service_tokens(
                params={"user_id": user["id"]})
            token_list = list(tokens['results'])
            user_tokens = {}
            if len(token_list) > 0:
                for token in token_list:
                    user_tokens[token["service"]["name"]] = {
                        "token": token["token"],
                        "url": token["service"]["url"] + "/api/v1"
                    }
            request.session['user_tokens'] = user_tokens

            return HttpResponseRedirect(redirect_to)
    else:
        form = authentication_form(request)

    current_site = get_current_site(request)

    context = {
        'form': form,
        redirect_field_name: redirect_to,
        'site': current_site,
        'site_name': current_site.name,
    }

    return TemplateResponse(request, template_name, context)

def logout(request):
    try:
        del request.session['user_token']
        del request.session['user_email']
        del request.session['user_permissions']
        del request.session['user_id']
        del request.session['user_dashboards']
        del request.session['user_default_dashboard']
    except KeyError:
        pass
    return redirect('index')

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def index(request):
    if "user_default_dashboard" in request.session and \
            request.session["user_default_dashboard"] is not None and \
            not settings.HIDE_DASHBOARDS:
        return HttpResponseRedirect(reverse('dashboard', args=(
            request.session["user_default_dashboard"],)))
    else:
        return render(request, 'ci/index.html')

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def health_messages(request):
    if settings.HIDE_HEALTH:
        return redirect('denied')
    if request.is_ajax():
        METRIC_SENT_SUM = 'message.sent.sum'
        client = MetricsApiClient(
            settings.METRIC_API_URL,
            auth=(settings.METRIC_API_USER, settings.METRIC_API_PASSWORD))
        chart_type = request.GET.get('chart_type', None)
        today = now()
        if chart_type == 'estimated-vs-sent':
            get_days = today.weekday() + 1
            sent = client.get_metrics(
                m=METRIC_SENT_SUM, from_='-%sd' % get_days, interval='1d',
                nulls='zeroize')
            sent_data = utils.get_ranged_data_from_timeseries(
                sent, today, range_type='week')

            # The estimate data is stored as .last metrics with 0 - 6
            # representing the days of the week. The cron format specifies
            # 0 = Sunday whereas Python datetime.weekday() specifies
            # 0 = Monday.
            estimate_data = []
            for day in range(7):
                estimated = client.get_metrics(
                    m='subscriptions.send.estimate.%s.last' % day, from_='-7d',
                    interval='1d', nulls='zeroize')
                estimate_data.append(
                    utils.get_last_value_from_timeseries(estimated))
            return JsonResponse({
                'Estimated': estimate_data,
                'Sent': sent_data
            })

        elif chart_type == 'sent-today':
            get_hours = today.hour
            sent = client.get_metrics(
                m=METRIC_SENT_SUM, from_='-%sh' % get_hours, interval='1h',
                nulls='zeroize')
            sent_data = utils.get_ranged_data_from_timeseries(
                sent, today, range_type='day')
            return JsonResponse({
                'Today': sent_data
            })

        elif chart_type == 'sent-this-week':
            get_days = today.weekday() + 7  # Include last week in the set.
            sent = client.get_metrics(
                m=METRIC_SENT_SUM, from_='-%sd' % get_days, interval='1d',
                nulls='zeroize')
            this_week_data = utils.get_ranged_data_from_timeseries(
                sent, today, range_type='week')
            last_week_data = utils.get_ranged_data_from_timeseries(
                sent, today-timedelta(weeks=1), range_type='week')
            return JsonResponse({
                'Last week': last_week_data,
                'This week': this_week_data
            })

    return render(request, 'ci/health_messages.html')

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def health_subscriptions(request):
    if settings.HIDE_HEALTH:
        return redirect('denied')
    if request.is_ajax():
        METRIC_SUBSCRIPTIONS_SUM = 'subscriptions.created.sum'
        client = MetricsApiClient(
            settings.METRIC_API_URL,
            auth=(settings.METRIC_API_USER, settings.METRIC_API_PASSWORD))
        chart_type = request.GET.get('chart_type', None)
        today = now()
        if chart_type == 'subscriptions-today':
            get_hours = today.hour + 24  # Include yesterday in the set.
            subscriptions = client.get_metrics(
                m=METRIC_SUBSCRIPTIONS_SUM, from_='-%sh' % get_hours,
                interval='1h', nulls='zeroize')
            today_data = utils.get_ranged_data_from_timeseries(
                subscriptions, today, range_type='day')
            yesterday_data = utils.get_ranged_data_from_timeseries(
                subscriptions, today - timedelta(days=1), range_type='day')
            return JsonResponse({
                'Yesterday': yesterday_data,
                'Today': today_data
            })

        elif chart_type == 'subscriptions-this-week':
            get_days = today.weekday() + 7  # Include last week in the set.
            subscriptions = client.get_metrics(
                m=METRIC_SUBSCRIPTIONS_SUM, from_='-%sd' % get_days,
                interval='1d', nulls='zeroize')
            this_week_data = utils.get_ranged_data_from_timeseries(
                subscriptions, today, range_type='week')
            last_week_data = utils.get_ranged_data_from_timeseries(
                subscriptions, today-timedelta(weeks=1), range_type='week')
            return JsonResponse({
                'Last week': last_week_data,
                'This week': this_week_data
            })

    return render(request, 'ci/health_subscriptions.html')

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def health_registrations(request):
    if settings.HIDE_HEALTH:
        return redirect('denied')
    if request.is_ajax():
        METRIC_REGISTRATIONS_SUM = 'registrations.created.sum'
        client = MetricsApiClient(
            settings.METRIC_API_URL,
            auth=(settings.METRIC_API_USER, settings.METRIC_API_PASSWORD))
        chart_type = request.GET.get('chart_type', None)
        today = now()
        if chart_type == 'registrations-today':
            get_hours = today.hour + 24  # Include yesterday in the set.
            registrations = client.get_metrics(
                m=METRIC_REGISTRATIONS_SUM, from_='-%sh' % get_hours,
                interval='1h', nulls='zeroize')
            today_data = utils.get_ranged_data_from_timeseries(
                registrations, today, range_type='day')
            yesterday_data = utils.get_ranged_data_from_timeseries(
                registrations, today - timedelta(days=1), range_type='day')
            return JsonResponse({
                'Yesterday': yesterday_data,
                'Today': today_data
            })

        elif chart_type == 'registrations-this-week':
            get_days = today.weekday() + 7  # Include last week in the set.
            registrations = client.get_metrics(
                m=METRIC_REGISTRATIONS_SUM, from_='-%sd' % get_days,
                interval='1d', nulls='zeroize')
            this_week_data = utils.get_ranged_data_from_timeseries(
                registrations, today, range_type='week')
            last_week_data = utils.get_ranged_data_from_timeseries(
                registrations, today-timedelta(weeks=1), range_type='week')
            return JsonResponse({
                'Last week': last_week_data,
                'This week': this_week_data
            })

    return render(request, 'ci/health_registrations.html')

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def dashboard(request, dashboard_id):
    if settings.HIDE_DASHBOARDS:
        return redirect('denied')
    dashboard = ciApi.get_dashboard(int(dashboard_id))
    context = {"dashboard": dashboard}
    return render(request, 'ci/dashboard.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def dashboard_metric(request):
    if settings.HIDE_DASHBOARDS:
        return redirect('denied')
    client = MetricsApiClient(
        settings.METRIC_API_URL,
        auth=(settings.METRIC_API_USER, settings.METRIC_API_PASSWORD))
    response = {"objects": []}
    filters = {
        "m": [],
        "start": "",
        "interval": "",
        "nulls": ""
    }

    for k, v in request.GET.lists():
        filters[k] = v

    if filters.get('from') is not None:
        filters['from'] = filters['start']

    results = client.get_metrics(**filters)
    for metric in filters['m']:
        if metric in results:
            response["objects"].append({
                "key": metric, "values": results[metric]})
        else:
            response["objects"].append({
                "key": metric, "values": []})

    return JsonResponse(response)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def denied(request):
    return render(request, 'ci/denied.html')

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def not_found(request):
    return render(request, 'ci/not_found.html')

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_IDENTITY_SERVICE'])
def identities(request):
    context = {}
    idApi = IdentityStoreApiClient(
        api_url=request.session["user_tokens"]["SEED_IDENTITY_SERVICE"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_IDENTITY_SERVICE"]["token"]  # noqa
    )
    if 'address_value' in request.GET:
        form = IdentitySearchForm(request.GET)
        if form.is_valid():
            results = idApi.get_identity_by_address(
                address_type=form.cleaned_data['address_type'],
                address_value=form.cleaned_data['address_value'])['results']
        else:
            results = []
    else:
        form = IdentitySearchForm()
        results = idApi.get_identities()['results']

    identities = utils.get_page_of_iterator(
        results, settings.IDENTITY_LIST_PAGE_SIZE,
        request.GET.get('page')
    )

    context['identities'] = identities
    context['form'] = form
    return render(request, 'ci/identities.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_IDENTITY_SERVICE', 'HUB',
                  'SEED_STAGE_BASED_MESSAGING'])
def identity(request, identity):
    idApi = IdentityStoreApiClient(
        api_url=request.session["user_tokens"]["SEED_IDENTITY_SERVICE"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_IDENTITY_SERVICE"]["token"]  # noqa
    )
    hubApi = HubApiClient(
        api_url=request.session["user_tokens"]["HUB"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["HUB"]["token"]  # noqa
    )
    sbmApi = StageBasedMessagingApiClient(
        api_url=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["token"]  # noqa
    )
    msApi = MessageSenderApiClient(
        api_url=request.session["user_tokens"]["SEED_MESSAGE_SENDER"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_MESSAGE_SENDER"]["token"]  # noqa
    )

    messagesets_results = sbmApi.get_messagesets()
    messagesets = {}
    schedules = {}
    choices = []
    for messageset in messagesets_results["results"]:
        messagesets[messageset["id"]] = messageset["short_name"]
        schedules[messageset["id"]] = messageset["default_schedule"]
        choices.append((messageset["id"], messageset["short_name"]))

    results = idApi.get_identity(identity)
    sbm_filter = {
        "identity": identity
    }
    subscriptions = sbmApi.get_subscriptions(params=sbm_filter)

    if request.method == "POST":
        if 'add_subscription' in request.POST:
            form = AddSubscriptionForm(request.POST)
            language = results['details'].get(settings.LANGUAGE_FIELD)

            if language:

                if form.is_valid():
                    subscription = {
                        "active": True,
                        "identity": identity,
                        "completed": False,
                        "lang": language,
                        "messageset": form.cleaned_data['messageset'],
                        "next_sequence_number": 1,
                        "schedule":
                            schedules[form.cleaned_data['messageset']],
                        "process_status": 0,
                    }
                    sbmApi.create_subscription(subscription)

                    messages.add_message(
                        request,
                        messages.INFO,
                        'Successfully created a subscription.',
                        extra_tags='success'
                    )

                    ciApi.create_auditlog({
                        "identity_id": identity,
                        "action": "Create",
                        "action_by": request.session['user_id'],
                        "model": "subscription"
                    })
            else:
                messages.add_message(
                    request,
                    messages.ERROR,
                    'No language value in {} on the identity.'.format(
                        settings.LANGUAGE_FIELD),
                    extra_tags='danger'
                )

        elif 'deactivate_subscription' in request.POST:
            form = DeactivateSubscriptionForm(request.POST)

            if form.is_valid():
                data = {
                    "active": False
                }
                sbmApi.update_subscription(
                    form.cleaned_data['subscription_id'], data)

                messages.add_message(
                    request,
                    messages.INFO,
                    'Successfully deactivated the subscription.',
                    extra_tags='success'
                )

                ciApi.create_auditlog({
                    "identity_id": identity,
                    "subscription_id": form.cleaned_data['subscription_id'],
                    "action": "Update",
                    "action_by": request.session['user_id'],
                    "model": "subscription",
                    "detail": "Deactivated subscription"
                })

        elif 'optout_identity' in request.POST:
            try:
                details = results.get('details', {})
                addresses = details.get('addresses', {})

                for address_type, addresses in addresses.items():
                    for address, info in addresses.items():
                        idApi.create_optout({
                            "identity": identity,
                            "optout_type": "stop",
                            "address_type": address_type,
                            "address": address,
                            "request_source": "ci"})

                        info['optedout'] = True

                hubApi.create_optout_admin({
                    settings.IDENTITY_FIELD: identity
                })

                messages.add_message(
                    request,
                    messages.INFO,
                    'Successfully opted out.',
                    extra_tags='success'
                )

                ciApi.create_auditlog({
                    "identity_id": identity,
                    "action": "Update",
                    "action_by": request.session['user_id'],
                    "model": "identity",
                    "detail": "Optout identity"
                })
            except:
                messages.add_message(
                    request,
                    messages.ERROR,
                    'Optout failed.',
                    extra_tags='danger'
                )

    hub_filter = {
        settings.IDENTITY_FIELD: identity
    }
    registrations = hubApi.get_registrations(params=hub_filter)
    changes = hubApi.get_changes(params=hub_filter)
    if results is None:
        return redirect('not_found')

    outbound_message_params = {
        'to_identity': identity,
        'ordering': '-created_at',
    }
    outbound_messages = msApi.get_outbounds(params=outbound_message_params)
    outbound_page = request.GET.get('outbound_page')
    outbound_paginator = Paginator(
        list(outbound_messages['results']),
        settings.IDENTITY_MESSAGES_PAGE_SIZE)

    try:
        outbound_messages = outbound_paginator.page(outbound_page)
    except PageNotAnInteger:
        outbound_messages = outbound_paginator.page(1)
    except EmptyPage:
        outbound_messages = outbound_paginator.page(
            outbound_paginator.num_pages)

    inbound_message_params = {
        'from_identity': identity,
        'ordering': '-created_at',
    }
    inbound_messages = msApi.get_inbounds(inbound_message_params)
    inbound_page = request.GET.get('inbound_page')
    inbound_paginator = Paginator(
        list(inbound_messages['results']),
        settings.IDENTITY_MESSAGES_PAGE_SIZE)

    try:
        inbound_messages = inbound_paginator.page(inbound_page)
    except PageNotAnInteger:
        inbound_messages = inbound_paginator.page(1)
    except EmptyPage:
        inbound_messages = inbound_paginator.page(inbound_paginator.num_pages)

    deactivate_subscription_form = DeactivateSubscriptionForm()
    add_subscription_form = AddSubscriptionForm()
    add_subscription_form.fields['messageset'] = forms.ChoiceField(
        choices=choices)

    optout_visible = False
    details = results.get('details', {})
    addresses = details.get('addresses', {})
    msisdns = addresses.get('msisdn', {})
    optout_visible = any(
        (not d.get('optedout') for _, d in msisdns.items()))

    audit_logs = ciApi.get_auditlogs({"identity_id": identity})
    context = {
        "identity": results,
        "registrations": registrations,
        "changes": changes,
        "messagesets": messagesets,
        "subscriptions": subscriptions,
        "outbound_messages": outbound_messages,
        "add_subscription_form": add_subscription_form,
        "deactivate_subscription_form": deactivate_subscription_form,
        "inbound_messages": inbound_messages,
        "optout_visible": optout_visible,
        "audit_logs": audit_logs,
        "users": request.session['user_list']
    }

    context.update(csrf(request))
    return render(request, 'ci/identities_detail.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['HUB'])
def registrations(request):
    context = {}
    hubApi = HubApiClient(
        api_url=request.session["user_tokens"]["HUB"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["HUB"]["token"]  # noqa
    )
    if 'mother_id' in request.GET:
        form = RegistrationFilterForm(request.GET)
        if form.is_valid():
            reg_filter = {
                settings.STAGE_FIELD: form.cleaned_data['stage'],
                "validated": form.cleaned_data['validated'],
                settings.IDENTITY_FIELD:
                    form.cleaned_data['mother_id']
            }
            registrations = hubApi.get_registrations(
                params=reg_filter)['results']
        else:
            registrations = []
    else:
        form = RegistrationFilterForm()
        registrations = hubApi.get_registrations()['results']

    context['form'] = form
    context['registrations'] = utils.get_page_of_iterator(
        registrations,
        settings.REGISTRATION_LIST_PAGE_SIZE,
        request.GET.get('page')
    )

    return render(request, 'ci/registrations.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['HUB'])
def registration(request, registration):
    hubApi = HubApiClient(
        api_url=request.session["user_tokens"]["HUB"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["HUB"]["token"]  # noqa
    )
    if request.method == "POST":
        pass
    else:
        results = hubApi.get_registration(registration)
        if results is None:
            return redirect('not_found')
    context = {
        "registration": results
    }
    context.update(csrf(request))
    return render(request, 'ci/registrations_detail.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['HUB'])
def changes(request):
    hubApi = HubApiClient(
        api_url=request.session["user_tokens"]["HUB"]["url"],
        auth_token=request.session["user_tokens"]["HUB"]["token"]
    )
    if 'mother_id' in request.GET:
        form = ChangeFilterForm(request.GET)
        if form.is_valid():
            change_filter = {
                "action": form.cleaned_data['action'],
                "validated": form.cleaned_data['validated'],
                settings.IDENTITY_FIELD:
                    form.cleaned_data['mother_id']
            }
            changes = hubApi.get_changes(params=change_filter)['results']
        else:
            changes = []
    else:
        form = ChangeFilterForm()
        changes = hubApi.get_changes()['results']

    changes = utils.get_page_of_iterator(
        changes, settings.CHANGE_LIST_PAGE_SIZE, request.GET.get('page'))

    context = {
        "changes": changes,
        "form": form
    }
    return render(request, 'ci/changes.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['HUB'])
def change(request, change):
    hubApi = HubApiClient(
        api_url=request.session["user_tokens"]["HUB"]["url"],
        auth_token=request.session["user_tokens"]["HUB"]["token"]
    )
    if request.method == "POST":
        pass
    else:
        results = hubApi.get_change(change)
        if results is None:
            return redirect('not_found')
    context = {"change": results}
    context.update(csrf(request))
    return render(request, 'ci/changes_detail.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_STAGE_BASED_MESSAGING'])
def subscriptions(request):
    sbmApi = StageBasedMessagingApiClient(
        api_url=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["token"]  # noqa
    )

    messagesets_results = sbmApi.get_messagesets()
    messagesets = {}
    for messageset in messagesets_results["results"]:
        messagesets[messageset["id"]] = messageset["short_name"]

    if 'identity' in request.GET:
        form = SubscriptionFilterForm(request.GET)
        if form.is_valid():
            sbm_filter = {
                "identity": form.cleaned_data['identity'],
                "active": form.cleaned_data['active'],
                "completed": form.cleaned_data['completed']
            }
            subscriptions = sbmApi.get_subscriptions(
                params=sbm_filter)['results']
        else:
            subscriptions = []
    else:
        form = SubscriptionFilterForm()
        subscriptions = sbmApi.get_subscriptions()['results']

    subscriptions = utils.get_page_of_iterator(
        subscriptions, settings.SUBSCRIPTION_LIST_PAGE_SIZE,
        request.GET.get('page'))

    context = {
        "subscriptions": subscriptions,
        "messagesets": messagesets,
        "form": form
    }
    context.update(csrf(request))
    return render(request, 'ci/subscriptions.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_STAGE_BASED_MESSAGING'])
def subscription(request, subscription):
    sbmApi = StageBasedMessagingApiClient(
        api_url=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["token"]  # noqa
    )
    messagesets_results = sbmApi.get_messagesets()
    messagesets = {}
    for messageset in messagesets_results["results"]:
        messagesets[messageset["id"]] = messageset["short_name"]

    results = sbmApi.get_subscription(subscription)
    if results is None:
        return redirect('not_found')

    if request.method == "POST":
        try:
            form = ChangeSubscriptionForm(request.POST)
            if form.is_valid():

                lang = form.cleaned_data["language"]
                messageset = form.cleaned_data["messageset"]

                if (lang != results["lang"] or
                        messageset != results["messageset"]):
                    hubApi = HubApiClient(
                        request.session["user_tokens"]["HUB"]["token"],
                        api_url=request.session["user_tokens"]["HUB"]["url"])  # noqa

                    change = {
                        settings.IDENTITY_FIELD: results["identity"],
                        "subscription": subscription
                    }

                    if lang != results["lang"]:
                        change["language"] = lang
                    if messageset != results["messageset"]:
                        change["messageset"] = messagesets[messageset]

                    hubApi.create_change_admin(change)

                    messages.add_message(
                        request,
                        messages.INFO,
                        'Successfully added change.',
                        extra_tags='success'
                    )

                    if lang != results["lang"]:
                        ciApi.create_auditlog({
                            "identity_id": results["identity"],
                            "action": "Update",
                            "action_by": request.session['user_id'],
                            "model": "subscription",
                            "detail": "Updated language: {} to {}".format(
                                results["lang"], lang)
                        })
                    if messageset != results["messageset"]:
                        ciApi.create_auditlog({
                            "identity_id": results["identity"],
                            "action": "Update",
                            "action_by": request.session['user_id'],
                            "model": "subscription",
                            "detail": "Updated messageset: {} to {}".format(
                                messagesets[results["messageset"]],
                                messagesets[messageset])
                        })

        except:
            messages.add_message(
                request,
                messages.ERROR,
                'Change failed.',
                extra_tags='danger'
            )

    languages = sbmApi.get_messageset_languages()

    context = {
        "subscription": results,
        "messagesets": messagesets,
        "languages": json.dumps(languages)
    }
    context.update(csrf(request))
    return render(request, 'ci/subscriptions_detail.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def services(request):
    services = ciApi.get_services()
    context = {"services": services}
    return render(request, 'ci/services.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
def service(request, service):
    results = ciApi.get_service(service)
    service_status = ciApi.get_service_status(service)
    context = {
        "service": results,
        "service_status": service_status
    }
    return render(request, 'ci/services_detail.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_STAGE_BASED_MESSAGING'])
def subscription_failures(request):
    sbmApi = StageBasedMessagingApiClient(
        api_url=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["token"]  # noqa
    )
    if request.method == "POST":
        requeue = sbmApi.requeue_failed_tasks()
        if ('requeued_failed_tasks' in requeue and
                requeue['requeued_failed_tasks']):
            messages.add_message(
                request,
                messages.INFO,
                'Successfully re-queued all subscription tasks'
            )
        else:
            messages.add_message(
                request,
                messages.ERROR,
                'Could not re-queued all subscription tasks'
            )
    failures = sbmApi.get_failed_tasks()['results']
    failures = utils.get_page_of_iterator(
        failures, settings.FAILURE_LIST_PAGE_SIZE, request.GET.get('page'))
    context = {
        'failures': failures
    }
    context.update(csrf(request))
    return render(request, 'ci/failures_subscriptions.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_SCHEDULER'])
def schedule_failures(request):
    schdApi = SchedulerApiClient(
        request.session["user_tokens"]["SEED_SCHEDULER"]["token"],  # noqa
        api_url=request.session["user_tokens"]["SEED_SCHEDULER"]["url"]  # noqa
    )
    if request.method == "POST":
        requeue = schdApi.requeue_failed_tasks()
        if ('requeued_failed_tasks' in requeue and
                requeue['requeued_failed_tasks']):
            messages.add_message(
                request,
                messages.INFO,
                'Successfully re-queued all scheduler tasks'
            )
        else:
            messages.add_message(
                request,
                messages.ERROR,
                'Could not re-queued all scheduler tasks'
            )
    failures = schdApi.get_failed_tasks()['results']
    failures = utils.get_page_of_iterator(
        failures, settings.FAILURE_LIST_PAGE_SIZE, request.GET.get('page'))
    context = {
        'failures': failures,
    }
    context.update(csrf(request))
    return render(request, 'ci/failures_schedules.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_MESSAGE_SENDER'])
def outbound_failures(request):
    msApi = MessageSenderApiClient(
        request.session["user_tokens"]["SEED_MESSAGE_SENDER"]["token"],  # noqa
        api_url=request.session["user_tokens"]["SEED_MESSAGE_SENDER"]["url"]  # noqa
    )
    if request.method == "POST":
        requeue = msApi.requeue_failed_tasks()
        if ('requeued_failed_tasks' in requeue and
                requeue['requeued_failed_tasks']):
            messages.add_message(
                request,
                messages.INFO,
                'Successfully re-queued all outbound tasks'
            )
        else:
            messages.add_message(
                request,
                messages.ERROR,
                'Could not re-queued all outbound tasks'
            )
    failures = msApi.get_failed_tasks()['results']
    failures = utils.get_page_of_iterator(
        failures, settings.FAILURE_LIST_PAGE_SIZE, request.GET.get('page'))
    context = {
        'failures': failures
    }
    context.update(csrf(request))
    return render(request, 'ci/failures_outbounds.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['HUB'])
def report_generation(request):
    hubApi = HubApiClient(
        request.session["user_tokens"]["HUB"]["token"],
        api_url=request.session["user_tokens"]["HUB"]["url"])

    if request.method == "POST":
        report_type = request.POST['report_type']
        if report_type == 'registration':
            reg_form = ReportGenerationForm(
                request.POST, auto_id='registration_%s')
            posted_form = reg_form
            msisdn_form = MsisdnReportGenerationForm(auto_id='cohort_%s')
        elif report_type == 'cohort':
            msisdn_form = MsisdnReportGenerationForm(
                request.POST, request.FILES, auto_id='cohort_%s')
            posted_form = msisdn_form
            reg_form = ReportGenerationForm(auto_id='registration_%s')

        if posted_form.is_valid():
            # Remove fields that weren't supplied
            if posted_form.cleaned_data.get('start_date') is None:
                del posted_form.cleaned_data['start_date']
            if posted_form.cleaned_data.get('end_date') is None:
                del posted_form.cleaned_data['end_date']
            if posted_form.cleaned_data.get('email_to') == []:
                del posted_form.cleaned_data['email_to']
            if posted_form.cleaned_data.get('email_from') == "":
                del posted_form.cleaned_data['email_from']
            if posted_form.cleaned_data.get('email_subject') == "":
                del posted_form.cleaned_data['email_subject']

            try:
                results = hubApi.trigger_report_generation(
                        posted_form.cleaned_data)
            except HTTPServiceError as e:
                logger.error('Report generation failed: %s' % e.details)
                messages.add_message(
                    request,
                    messages.ERROR,
                    'Could not start report generation'
                )
            else:
                if 'report_generation_requested' in results:
                    messages.add_message(
                        request,
                        messages.INFO,
                        'Successfully started report generation'
                    )
                else:
                    messages.add_message(
                        request,
                        messages.ERROR,
                        'Could not start report generation'
                    )
    else:
        reg_form = ReportGenerationForm(auto_id='registration_%s')
        msisdn_form = MsisdnReportGenerationForm(auto_id='cohort_%s')
        report_type = ""

    report_tasks = hubApi.get_report_tasks()

    context = {
        "forms": {"registration_form": reg_form, "cohort_form": msisdn_form},
        "report_tasks": report_tasks,
        "report_type": report_type
    }
    context.update(csrf(request))
    return render(request, 'ci/reports.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_IDENTITY_SERVICE'])
def user_management(request):
    if not settings.SHOW_USER_DETAILS:
        return redirect('denied')

    hubApi = HubApiClient(
        api_url=request.session["user_tokens"]["HUB"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["HUB"]["token"]  # noqa
    )

    page = int(request.GET.get('page', 1))
    filters = {"page": page}
    form = UserDetailSearchForm(request.GET)
    if form.is_valid():
        for key, value in form.cleaned_data.items():
            if value:
                filters[key] = value

    results = hubApi.get_user_details(filters)

    states = [('*', 'All')]
    for state in hubApi.get_states()['results']:
        states.append((state['name'], state['name']))

    form.fields['state'] = forms.ChoiceField(choices=states)

    context = {}
    context['users'] = results['results']
    context['has_next'] = results['has_next']
    context['has_previous'] = results['has_previous']
    context['next_page_number'] = page + 1
    context['previous_page_number'] = page - 1
    context['form'] = form

    return render(request, 'ci/user_management.html', context)

@login_required(login_url='/login/')
@permission_required(permission='ci:view', login_url='/login/')
@tokens_required(['SEED_IDENTITY_SERVICE', 'HUB',
                  'SEED_STAGE_BASED_MESSAGING'])
def user_management_detail(request, identity):
    idApi = IdentityStoreApiClient(
        api_url=request.session["user_tokens"]["SEED_IDENTITY_SERVICE"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_IDENTITY_SERVICE"]["token"]  # noqa
    )
    sbmApi = StageBasedMessagingApiClient(
        api_url=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["SEED_STAGE_BASED_MESSAGING"]["token"]  # noqa
    )
    hubApi = HubApiClient(
        api_url=request.session["user_tokens"]["HUB"]["url"],  # noqa
        auth_token=request.session["user_tokens"]["HUB"]["token"]  # noqa
    )
    messagesets_results = sbmApi.get_messagesets()

    messagesets = {}
    linked_to = {}
    operator_id = {}

    for messageset in messagesets_results["results"]:
        messagesets[messageset["id"]] = messageset["short_name"]

    results = idApi.get_identity(identity)

    if results['details'].get('linked_to'):
        linked_to = idApi.get_identity(results['details']['linked_to'])

    operator_id = results['details'].get('operator', results.get('operator'))
    if operator_id:
        operator_id = idApi.get_identity(operator_id)

    hub_filter = {
        settings.IDENTITY_FIELD: identity
    }
    registrations = hubApi.get_registrations(params=hub_filter)

    sbm_filter = {
        "identity": identity
    }
    subscriptions = sbmApi.get_subscriptions(params=sbm_filter)

    context = {
        "identity": results,
        "registrations": registrations,
        "messagesets": messagesets,
        "subscriptions": subscriptions,
        "linked_to": linked_to,
        "operator": operator_id
    }

    context.update(csrf(request))
    return render(request, 'ci/user_management_detail.html', context)


from importlib import import_module
from pkgutil import iter_modules

def build(cli, path, package):
    """Build CLI dynamically based on the package structure.
    """
    for _, name, ispkg in iter_modules(path):
        module = import_module(f'.{name}', package)
        if ispkg:
            build(cli.group(name)(module.group),
                  module.__path__,
                  module.__package__)
        else:
            cli.command(name)(module.command)


__all__ = ['Fridge']
__version__ = '0.2'

import json
import errno

class Fridge(dict):
    """
    Fridge is a subclass of :class:`dict` and thus fully conforms to its interface.

    Fridge keeps an open file until it's closed, so you have to call :meth:`close`
    when you're done using it.

    Fridge implements :meth:`__enter__` and :meth:`__exit__` so you can use
    `with` statement.

    :param path: a path to a file that will be used to load and save the data
    :param file: a file object that will be used to load and save the data.
        This file object in not closed by fridge automatically.
    :param dump_args: dictionary of arguments that are passed to :func:`json.dump`.
    :param load_args: dictionary of arguments that are passed to :func:`json.load`.

    `path` and `file` arguments are mutually exclusive.
    """

    default_args = {}

    @classmethod
    def readonly(cls, *args, **kwargs):
        """
        Return an already closed read-only instance of Fridge.
        Arguments are the same as for the constructor.
        """
        fridge = cls(*args, **kwargs)
        fridge.close()
        return fridge

    @classmethod
    def _getdefault(cls):
        default = cls.default_args
        path = default.get('path')
        file = default.get('file')
        return path, file

    def __new__(cls, path=None, file=None, *args, **kwargs):
        if path is None and file is None:
            path, file = cls._getdefault()

        if path is None and file is None:
            raise ValueError('No path or file specified')
        elif path is not None and file is not None:
            raise ValueError('Only path or only file can be passed')

        fridge = super(Fridge, cls).__new__(cls)
        return fridge

    def __init__(self, path=None, file=None, dump_args=None, load_args=None):
        if path is None and file is None:
            path, file = self._getdefault()

        self.dump_args = dump_args or {}
        self.load_args = load_args or {}

        # so that __del__ doesn't try to close the file if its opening fails
        self.closed = True

        if file is not None:
            self.file = file
            self.close_file = False
        else:
            try:
                self.file = open(path, 'r+')
            except IOError as e:
                if e.errno == errno.ENOENT:
                    self.file=open(path, 'w+')
                else:
                    raise
            self.close_file = True

        #: True after :meth:`close` is called, False otherwise.
        self.closed = False

        self.load()

    def _check_open(self):
        if self.closed:
            raise ValueError('Operation on a closed fridge object')

    def load(self):
        """
        Force reloading the data from the file.
        All data in the in-memory dictionary is discarded.
        This method is called automatically by the constructor, normally you
        don't need to call it.
        """
        self._check_open()
        try:
            data = json.load(self.file, **self.load_args)
        except ValueError:
            data = {}
        if not isinstance(data, dict):
            raise ValueError('Root JSON type must be dictionary')
        self.clear()
        self.update(data)

    def save(self):
        """
        Force saving the dictionary to the file.
        All data in the file is discarded.
        This method is called automatically by :meth:`close`.
        """
        self._check_open()
        self.file.truncate(0)
        self.file.seek(0)
        json.dump(self, self.file, **self.dump_args)

    def close(self):
        """
        Close the fridge.
        Calls :meth:`save` and closes the underlying file object unless
        an already open file was passed to the constructor.
        This method has no effect if the object is already closed.

        After the fridge is closed :meth:`save` and :meth:`load` will raise an exception
        but you will still be able to use it as an ordinary dictionary.
        """
        if not self.closed:
            self.save()
            if self.close_file:
                self.file.close()
        self.closed = True

    def __enter__(self):
        return self

    def __exit__(self, *_):
        self.close()
        return False

    def __del__(self):
        self.close()


import json

from cryptojwt.utils import as_unicode
from cryptojwt.jws.utils import alg2keytype
from cryptojwt.jws.jws import JWS
from cryptojwt.jws.jws import factory

from fedoidcmsg import MetadataStatement
from fedoidcmsg.bundle import jwks_to_keyjar

from oidcmsg.oidc import JsonWebToken
from cryptojwt.jwt import JWT

def self_sign_jwks(keyjar, iss, kid='', lifetime=3600):
    """
    Create a signed JWT containing a JWKS. The JWT is signed by one of the
    keys in the JWKS.

    :param keyjar: A KeyJar instance with at least one private signing key
    :param iss: issuer of the JWT, should be the owner of the keys
    :param kid: A key ID if a special key should be used otherwise one
        is picked at random.
    :param lifetime: The lifetime of the signed JWT
    :return: A signed JWT
    """

    # _json = json.dumps(jwks)
    _jwt = JWT(keyjar, iss=iss, lifetime=lifetime)

    jwks = keyjar.export_jwks(issuer=iss)

    return _jwt.pack(payload={'jwks': jwks}, owner=iss, kid=kid)

def verify_self_signed_jwks(sjwt):
    """
    Verify the signature of a signed JWT containing a JWKS.
    The JWT is signed by one of the keys in the JWKS.
    In the JWT the JWKS is stored using this format ::

        'jwks': {
            'keys': [ ]
        }

    :param sjwt: Signed Jason Web Token
    :return: Dictionary containing 'jwks' (the JWKS) and 'iss' (the issuer of
        the JWT)
    """

    _jws = factory(sjwt)
    _json = _jws.jwt.part[1]
    _body = json.loads(as_unicode(_json))
    iss = _body['iss']
    _jwks = _body['jwks']

    _kj = jwks_to_keyjar(_jwks, iss)

    try:
        _kid = _jws.jwt.headers['kid']
    except KeyError:
        _keys = _kj.get_signing_key(owner=iss)
    else:
        _keys = _kj.get_signing_key(owner=iss, kid=_kid)

    _ver = _jws.verify_compact(sjwt, _keys)
    return {'jwks': _ver['jwks'], 'iss': iss}

def request_signed_by_signing_keys(keyjar, msreq, iss, lifetime, kid=''):
    """
    A metadata statement signing request with 'signing_keys' signed by one
    of the keys in 'signing_keys'.

    :param keyjar: A KeyJar instance with the private signing key
    :param msreq: Metadata statement signing request. A MetadataStatement
        instance.
    :param iss: Issuer of the signing request also the owner of the signing
        keys.
    :return: Signed JWT where the body is the metadata statement
    """

    try:
        jwks_to_keyjar(msreq['signing_keys'], iss)
    except KeyError:
        jwks = keyjar.export_jwks(issuer=iss)
        msreq['signing_keys'] = jwks

    _jwt = JWT(keyjar, iss=iss, lifetime=lifetime)

    return _jwt.pack(owner=iss, kid=kid, payload=msreq.to_dict())

def verify_request_signed_by_signing_keys(smsreq):
    """
    Verify that a JWT is signed with a key that is inside the JWT.

    :param smsreq: Signed Metadata Statement signing request
    :return: Dictionary containing 'ms' (the signed request) and 'iss' (the
        issuer of the JWT).
    """

    _jws = factory(smsreq)
    _json = _jws.jwt.part[1]
    _body = json.loads(as_unicode(_json))
    iss = _body['iss']
    _jwks = _body['signing_keys']

    _kj = jwks_to_keyjar(_jwks, iss)

    try:
        _kid = _jws.jwt.headers['kid']
    except KeyError:
        _keys = _kj.get_signing_key(owner=iss)
    else:
        _keys = _kj.get_signing_key(owner=iss, kid=_kid)

    _ver = _jws.verify_compact(smsreq, _keys)
    # remove the JWT specific claims
    for k in JsonWebToken.c_param.keys():
        try:
            del _ver[k]
        except KeyError:
            pass
    try:
        del _ver['kid']
    except KeyError:
        pass

    return {'ms': MetadataStatement(**_ver), 'iss': iss}

def get_signing_keys(claims, keyjar, httpcli):
    if 'signed_jwks_uri' not in claims:
        return None

    res = httpcli(method='GET', url=claims['signed_jwks_uri'])

    if res.status == 200:
        _jws = JWS()

def store_signed_jwks(keyjar, sign_keyjar, path, alg, iss=''):
    _jwks = keyjar.export_jwks()
    _jws = JWS(_jwks, alg=alg)
    _jwt = _jws.sign_compact(
        sign_keyjar.get_signing_key(owner=iss, key_type=alg2keytype(alg)))
    fp = open(path, 'w')
    fp.write(_jwt)
    fp.close()

def replace_jwks_key_bundle(keyjar, owner, new_kb):
    try:
        kbl = keyjar.issuer_keys[owner]
    except KeyError:
        pass
    else:
        res = [new_kb]
        for kb in kbl:
            if kb.imp_jwks is None:
                res.append(kb)
        keyjar.issuer_keys[owner] = res


"""Greencard implementation."""
from functools import wraps

TESTS = []
SINGLES = []

def card(func):
    """
    A decorator for providing a unittesting function/method with every card in
    a librarian card library database when it is called.
    """
    @wraps(func)
    def wrapped(*args, **kwargs):
        """Transparent wrapper."""
        return func(*args, **kwargs)
    TESTS.append(wrapped)
    return wrapped

def library(func):
    """
    A decorator for providing a unittest with a library and have it called only
    once.
    """
    @wraps(func)
    def wrapped(*args, **kwargs):
        """Transparent wrapper."""
        return func(*args, **kwargs)
    SINGLES.append(wrapped)
    return wrapped

def descovery(testdir):
    """Descover and load greencard tests."""
    from os.path import join, exists, isdir, splitext, basename, sep
    if not testdir or not exists(testdir) or not isdir(testdir):
        return None

    from os import walk
    import fnmatch
    import imp

    for root, _, filenames in walk(testdir):
        for filename in fnmatch.filter(filenames, '*.py'):
            path = join(root, filename)
            modulepath = splitext(root)[0].replace(sep, '.')
            imp.load_source(modulepath, path)

def execute_tests(library):
    """
    Runs the library through each single test and each card in the library
    through each card test then returns ``(cardcount, passes, failures)``.
    """
    passes = 0
    failures = 0
    cardcount = 0

    for test in SINGLES:
        failed = False
        try:
            test(library)
        except AssertionError:
            print("Library failed {0}".format(test.__name__))
            failed = True
        if failed:
            failures += 1
        else:
            passes += 1

    for card in library.retrieve_all():
        cardcount += 1
        failed = False
        for test in TESTS:
            try:
                test(card)
            except AssertionError:
                print("{0} failed {1}".format(card.__repr__(), test.__name__))
                failed = True
        if failed:
            failures += 1
        else:
            passes += 1
    return cardcount, passes, failures

RESULTS = """
Results:
{0} library tests
{1} card tests
{2} cards
{3} passed
{4} failed"""

def main(clargs=None):
    """Command line entry point."""
    from argparse import ArgumentParser
    from librarian.library import Library
    import sys

    parser = ArgumentParser(
        description="A test runner for each card in a librarian library.")
    parser.add_argument("library", help="Library database")
    parser.add_argument("-t", "--tests", default="test/",
                        help="Test directory")
    args = parser.parse_args(clargs)

    descovery(args.tests)

    library = Library(args.library)
    cardcount, passes, failures = execute_tests(library)
    print(RESULTS.format(len(SINGLES), len(TESTS), cardcount, passes,
                         failures))
    sys.exit(failures)


"""Scrabble related functions, score counters, etc."""

import os

def letter_score(letter):
    """Returns the Scrabble score of a letter.

    Args:
        letter: a single character string

    Raises:
        TypeError if a non-Scrabble character is supplied
    """

    score_map = {
        1: ["a", "e", "i", "o", "u", "l", "n", "r", "s", "t"],
        2: ["d", "g"],
        3: ["b", "c", "m", "p"],
        4: ["f", "h", "v", "w", "y"],
        5: ["k"],
        8: ["j", "x"],
        10: ["q", "z"],
    }

    for score, letters in score_map.items():
        if letter.lower() in letters:
            return score
    else:
        raise TypeError("Invalid letter: %s", letter)

def word_score(word, input_letters, questions=0):
    """Checks the Scrabble score of a single word.

    Args:
        word: a string to check the Scrabble score of
        input_letters: the letters in our rack
        questions: integer of the tiles already on the board to build on

    Returns:
        an integer Scrabble score amount for the word
    """

    score = 0
    bingo = 0
    filled_by_blanks = []
    rack = list(input_letters)  # make a copy to speed up find_anagrams()
    for letter in word:
        if letter in rack:
            bingo += 1
            score += letter_score(letter)
            rack.remove(letter)
        else:
            filled_by_blanks.append(letter_score(letter))

    # we can have both ?'s and _'s in the word. this will apply the ?s to the
    # highest scrabble score value letters and leave the blanks for low points.
    for blank_score in sorted(filled_by_blanks, reverse=True):
        if questions > 0:
            score += blank_score
            questions -= 1

    # 50 bonus points for using all the tiles in your rack
    if bingo > 6:
        score += 50

    return score

def blank_tiles(input_word):
    """Searches a string for blank tile characters ("?" and "_").

    Args:
        input_word: the user supplied string to search through

    Returns:
        a tuple of:
            input_word without blanks
            integer number of blanks (no points)
            integer number of questions (points)
    """

    blanks = 0
    questions = 0
    input_letters = []
    for letter in input_word:
        if letter == "_":
            blanks += 1
        elif letter == "?":
            questions += 1
        else:
            input_letters.append(letter)
    return input_letters, blanks, questions

def word_list(sowpods=False, start="", end=""):
    """Opens the word list file.

    Args:
        sowpods: a boolean to declare using the sowpods list or TWL (default)
        start: a string of starting characters to find anagrams based on
        end: a string of ending characters to find anagrams based on

    Yeilds:
        a word at a time out of 178691 words for TWL, 267751 for sowpods. Much
        less if either start or end are used (filtering is applied here)
    """

    location = os.path.join(
        os.path.dirname(os.path.realpath(__file__)),
        "wordlists",
    )

    if sowpods:
        filename = "sowpods.txt"
    else:
        filename = "twl.txt"

    filepath = os.path.join(location, filename)

    with open(filepath) as wordfile:
        for word in wordfile.readlines():
            word = word.strip()
            if start and end and word.startswith(start) and word.endswith(end):
                yield word
            elif start and word.startswith(start) and not end:
                yield word
            elif end and word.endswith(end) and not start:
                yield word
            elif not start and not end:
                yield word

def valid_scrabble_word(word):
    """Checks if the input word could be played with a full bag of tiles.

    Returns:
        True or false
    """

    letters_in_bag = {
        "a": 9,
        "b": 2,
        "c": 2,
        "d": 4,
        "e": 12,
        "f": 2,
        "g": 3,
        "h": 2,
        "i": 9,
        "j": 1,
        "k": 1,
        "l": 4,
        "m": 2,
        "n": 6,
        "o": 8,
        "p": 2,
        "q": 1,
        "r": 6,
        "s": 4,
        "t": 6,
        "u": 4,
        "v": 2,
        "w": 2,
        "x": 1,
        "y": 2,
        "z": 1,
        "_": 2,
    }

    for letter in word:
        if letter == "?":
            continue
        try:
            letters_in_bag[letter] -= 1
        except KeyError:
            return False
        if letters_in_bag[letter] < 0:
            letters_in_bag["_"] -= 1
            if letters_in_bag["_"] < 0:
                return False
    return True


#!/usr/bin/env python

import argparse
from stackexchange import *
from pyquery import PyQuery as pq
import sys

class NoResult(Exception):
    pass

class SOSearch(Site):
    def __init__(self, qs, tags=None, **kw):
        self.qs = qs
        self.tags = '' if tags == None else tags
        self.domain = 'api.stackoverflow.com'
        super(SOSearch, self).__init__(self.domain, **kw)
        self.qs = self.search(order=DESC, sort='votes', tagged=self.tags,
                    intitle=self.qs, filter='!-u2CTCMX')
        if self.qs.total == 0:
            raise NoResult

    def first_q(self):
        self._populate_one_question(self.qs[0])
        return self.qs[0]

    def has_code(self, answer):
        return '<code>' in answer.body

    def _find_best_answer(self, question):
        if hasattr(question, 'is_accepted_id'):
            for a in question.answers:
                if not self.has_code(a):
                    continue
                if a.id == question.is_accepted_id:
                    question.best_answer = a
        else:
            for a in question.answers:
                if self.has_code(a):
                    question.best_answer = question.answers[0]
                else:
                    continue

    def _order_answers(self, qs):
        ordered = sorted(qs.answers, key=lambda a: a.score, reverse=True)
        qs.answers = ordered

    def _populate_one_question(self, qs):
        qs.fetch()
        qs.answers = self.answers([a.id for a in qs.answers], order=DESC, body='true')
        for a in qs.answers:
            html = pq(a.body)
            el = html('code')
            a.code = el.text()
        self._order_answers(qs)
        self._find_best_answer(qs)

def main(args):
    """docstring for main"""
    try:
        args.query = ' '.join(args.query).replace('?', '')
        so = SOSearch(args.query, args.tags)
        result =  so.first_q().best_answer.code
        if result != None:
            print result
        else:
            print("Sorry I can't find your answer, try adding tags")
    except NoResult, e:
        print("Sorry I can't find your answer, try adding tags")

def cli_run():
    """docstring for argparse"""
    parser = argparse.ArgumentParser(description='Stupidly simple code answers from StackOverflow')
    parser.add_argument('query', help="What's the problem ?", type=str, nargs='+')
    parser.add_argument('-t','--tags', help='semicolon separated tags -> python;lambda')
    args = parser.parse_args()
    main(args)

if __name__ == '__main__':
    cli_run()


"""
JSON AMP dialect.

This is a *huge* hack.
"""
from json import dumps, loads
from twisted.internet import defer, protocol
from twisted.protocols import amp, basic
from txampext import exposed

def _default(obj):
    # TODO: Seriously? This? What the ugh.
    try:
        return list(obj)
    except TypeError:
        return obj.isoformat()

class JSONAMPDialectReceiver(basic.NetstringReceiver):
    """
    JSON AMP dialect speaking protocol.
    """
    def __init__(self, remote):
        self._remote = remote

    def stringReceived(self, string):
        """Handle a JSON AMP dialect request.

        First, the JSON is parsed. Then, all JSON dialect specific
        values in the request are turned into the correct objects.
        Then, finds the correct responder function, calls it, and
        serializes the result (or error).

        """
        request = loads(string)

        identifier = request.pop("_ask")
        commandName = request.pop("_command")
        command, responder = self._getCommandAndResponder(commandName)

        self._parseRequestValues(request, command)
        d = self._runResponder(responder, request, command, identifier)
        d.addCallback(self._writeResponse)

    def _getCommandAndResponder(self, commandName):
        """Gets the command class and matching responder function for the
        given command name.

        """
        # DISGUSTING IMPLEMENTATION DETAIL EXPLOITING HACK
        locator = self._remote.boxReceiver.locator
        responder = locator.locateResponder(commandName)
        responderFunction = responder.func_closure[1].cell_contents
        command = responder.func_closure[2].cell_contents
        return command, responderFunction

    def _parseRequestValues(self, request, command):
        """Parses all the values in the request that are in a form specific
        to the JSON AMP dialect.

        """
        for key, ampType in command.arguments:
            ampClass = ampType.__class__

            if ampClass is exposed.ExposedResponderLocator:
                request[key] = self._remote
                continue

            decoder = _decoders.get(ampClass)
            if decoder is not None:
                value = request.get(key)
                request[key] = decoder(value, self)

    def _runResponder(self, responder, request, command, identifier):
        """Run the responser function. If it succeeds, add the _answer key.
        If it fails with an error known to the command, serialize the
        error.

        """
        d = defer.maybeDeferred(responder, **request)

        def _addIdentifier(response):
            """Return the response with an ``_answer`` key.

            """
            response["_answer"] = identifier
            return response

        def _serializeFailure(failure):
            """
            If the failure is serializable by this AMP command, serialize it.
            """
            key = failure.trap(*command.allErrors)
            response = {
                "_error_code": command.allErrors[key],
                "_error_description": str(failure.value),
                "_error": identifier
            }
            return response

        d.addCallbacks(_addIdentifier, _serializeFailure)
        return d

    def _writeResponse(self, response):
        """
        Serializes the response to JSON, and writes it to the transport.
        """
        encoded = dumps(response, default=_default)
        self.transport.write(encoded)

    def connectionLost(self, reason):
        """
        Tells the box receiver to stop receiving boxes.
        """
        self._remote.boxReceiver.stopReceivingBoxes(reason)
        return basic.NetstringReceiver.connectionLost(self, reason)

_decoders = {}
def _addDecoder(ampClass):
    """
    Registers a method as a decoder for a given AMP argument class.
    """
    def decorator(method):
        _decoders[ampClass] = method
        return method
    return decorator

@_addDecoder(amp.String)
def _unicodeToString(value, _receiver):
    """
    Encodes the unicode string as UTF-8.
    """
    return value.encode("utf-8")

class JSONAMPDialectFactory(protocol.Factory):
    """
    A factory for JSON AMP dialects.
    """
    def __init__(self, factory):
        """
        Initializes a JSON AMP dialect factory.
        """
        self._factory = factory

    def buildProtocol(self, addr):
        """
        Builds a bridge and associates it with an AMP protocol instance.
        """
        proto = self._factory.buildProtocol(addr)
        return JSONAMPDialectReceiver(proto)


#!/usr/bin/env python3
import json
import os
from urllib.parse import quote_plus
from urllib.parse import unquote_plus

from cryptojwt.jwt import JWT
from cryptojwt.key_jar import build_keyjar
from cryptojwt.key_jar import init_key_jar
from cryptojwt.key_jar import KeyJar

from fedoidcmsg.file_system import FileSystem
from fedoidcmsg.signing_service import KJ_SPECS

class JWKSBundle(object):
    """
    A class to keep a number of signing keys from different issuers.
    Behaves as a dictionary with issuer IDs as keys and
    :py:class:`oidcmsg.key_jar.KeyJar` instances as values.
    """

    def __init__(self, iss, sign_keys=None):
        """

        :param iss: Issuer identifier, will be used as the value of 'iss'
            when a signed JWT containg the bundle is constructed.
        :param sign_keys: Keys that this entity can use to sign JWTs.
        :type sign_keys: py:class:`oidcmsg.key_jar.KeyJar` instance
        """
        self.iss = iss
        self.sign_keys = sign_keys
        self.bundle = {}  # In memory database

    def __setitem__(self, key, value):
        """
        Add a set of keys, as a KeyJar or a JWKS under an issuer ID

        :param key: issuer ID
        :type: String
        :param value: Cryptographic keys that should be connected to to an
         issuer ID.
        :type value: KeyJar or a JWKS (JSON document)
        """
        if not isinstance(value, KeyJar):
            kj = KeyJar()
            if isinstance(value, dict):
                kj.import_jwks(value, issuer=key)
            else:
                kj.import_jwks_as_json(value, issuer=key)
            value = kj
        else:
            _val = value.copy()
            _iss = list(_val.owners())
            if _iss == ['']:
                _val.issuer_keys[key] = _val.issuer_keys['']
                del _val.issuer_keys['']
            elif len(_iss) == 1:
                if _iss[0] != key:
                    _val.issuer_keys[key] = _val.issuer_keys[_iss[0]]
                    del _val.issuer_keys[_iss[0]]
            else:
                raise ValueError('KeyJar contains to many issuers')

            value = _val

        self.bundle[key] = value

    def __getitem__(self, item):
        """
        Returns a KeyJar instance representing the keys belonging to an
        issuer

        :param item: Issuer ID
        :return: A KeyJar instance
        """
        kj = self.bundle[item]
        if item not in list(kj.issuer_keys.keys()):
            kj.issuer_keys[item] = kj.issuer_keys['']
            del kj.issuer_keys['']

        return kj

    def __delitem__(self, key):
        """
        Remove the KeyJar that belong to a specific issuer

        :param key: Issuer ID
        """
        del self.bundle[key]

    def create_signed_bundle(self, sign_alg='RS256', iss_list=None):
        """
        Create a signed JWT containing a dictionary with Issuer IDs as keys
        and JWKSs as values. If iss_list is empty then all available issuers are
        included.

        :param sign_alg: Which algorithm to use when signing the JWT
        :param iss_list: A list of issuer IDs who's keys should be included in
            the signed bundle.
        :return: A signed JWT
        """
        data = self.dict(iss_list)
        _jwt = JWT(self.sign_keys, iss=self.iss, sign_alg=sign_alg)
        return _jwt.pack({'bundle':data})

    def loads(self, jstr):
        """
        Upload a bundle from an unsigned JSON document

        :param jstr: A bundle as a dictionary or a JSON document
        """
        if isinstance(jstr, dict):
            _info = jstr
        else:
            _info = json.loads(jstr)

        for iss, jwks in _info.items():
            kj = KeyJar()
            if isinstance(jwks, dict):
                kj.import_jwks(jwks, issuer=iss)
            else:
                kj.import_jwks_as_json(jwks, issuer=iss)
            self.bundle[iss] = kj
        return self

    def dumps(self, iss_list=None):
        """
        Dumps a bundle of keys into a string. If iss_list is empty then all
        available issuers are included

        :param iss_list: List of issuers who's keys should be dumped
        :return: A JSON document
        """
        return json.dumps(self.dict(iss_list))

    def __str__(self):
        return json.dumps(self.dict())

    def keys(self):
        """
        Return a list of all issuers kept in this bundle.

        :return: List of Issuer IDs
        """
        return self.bundle.keys()

    def items(self):
        return self.bundle.items()

    def dict(self, iss_list=None):
        """
        Return the bundle of keys as a dictionary with the issuer IDs as
        the keys and the key sets represented as JWKS instances.

        :param iss_list: List of Issuer IDs that should be part of the
         output
        :rtype: Dictionary
        """
        _int = {}
        for iss, kj in self.bundle.items():
            if iss_list is None or iss in iss_list:
                try:
                    _int[iss] = kj.export_jwks_as_json(issuer=iss)
                except KeyError:
                    _int[iss] = kj.export_jwks_as_json()
        return _int

    def upload_signed_bundle(self, sign_bundle, ver_keys):
        """
        Input is a signed JWT with a JSON document representing the key bundle
        as body. This method verifies the signature and the updates the instance
        bundle with whatever was in the received package. Note, that as with
        dictionary update if an Issuer ID already exists in the instance bundle
        that will be overwritten with the new information.

        :param sign_bundle: A signed JWT
        :param ver_keys: Keys that can be used to verify the JWT signature.
        """
        jwt = verify_signed_bundle(sign_bundle, ver_keys)
        self.loads(jwt['bundle'])

    def as_keyjar(self):
        """
        Convert a key bundle into a KeyJar instance.

        :return: An :py:class:`oidcmsg.key_jar.KeyJar` instance
        """
        kj = KeyJar()
        for iss, k in self.bundle.items():
            try:
                kj.issuer_keys[iss] = k.issuer_keys[iss]
            except KeyError:
                kj.issuer_keys[iss] = k.issuer_keys['']
        return kj

def verify_signed_bundle(signed_bundle, ver_keys):
    """
    Verify the signature of a signed JWT.

    :param signed_bundle: A signed JWT where the body is a JWKS bundle
    :param ver_keys: Keys that can be used to verify signatures of the
        signed_bundle.
    :type ver_keys: A :py:class:`oidcmsg.key_jar.KeyJar` instance
    :return: The bundle or None
    """
    _jwt = JWT(ver_keys)
    return _jwt.unpack(signed_bundle)

def get_bundle(iss, ver_keys, bundle_file):
    """
    Read a signed JWKS bundle from disc, verify the signature and
    instantiate a JWKSBundle instance with the information from the file.

    :param iss:
    :param ver_keys:
    :param bundle_file:
    :return:
    """
    fp = open(bundle_file, 'r')
    signed_bundle = fp.read()
    fp.close()
    return JWKSBundle(iss, None).upload_signed_bundle(signed_bundle, ver_keys)

def get_signing_keys(eid, keydef, key_file):
    """
    If the *key_file* file exists then read the keys from there, otherwise
    create the keys and store them a file with the name *key_file*.

    :param eid: The ID of the entity that the keys belongs to
    :param keydef: What keys to create
    :param key_file: A file name
    :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance
    """
    if os.path.isfile(key_file):
        kj = KeyJar()
        kj.import_jwks(json.loads(open(key_file, 'r').read()), eid)
    else:
        kj = build_keyjar(keydef)[1]
        # make it know under both names
        fp = open(key_file, 'w')
        fp.write(json.dumps(kj.export_jwks()))
        fp.close()
        kj.issuer_keys[eid] = kj.issuer_keys['']

    return kj

def jwks_to_keyjar(jwks, iss=''):
    """
    Convert a JWKS to a KeyJar instance.

    :param jwks: String representation of a JWKS
    :return: A :py:class:`oidcmsg.key_jar.KeyJar` instance
    """
    if not isinstance(jwks, dict):
        try:
            jwks = json.loads(jwks)
        except json.JSONDecodeError:
            raise ValueError('No proper JSON')

    kj = KeyJar()
    kj.import_jwks(jwks, issuer=iss)
    return kj

def k_to_j(keyjar, private=False):
    k = list(keyjar.owners())
    if len(k) == 1:
        return json.dumps(keyjar.export_jwks(issuer=k[0], private=private))
    elif len(k) == 2 and '' in k:
        k.remove('')
        return json.dumps(keyjar.export_jwks(issuer=k[0], private=private))
    else:
        raise ValueError('Too many issuers')

def keyjar_to_jwks(keyjar):
    """
    Convert a KeyJar instance to a JWKS (JSON document).

    :param keyjar: A :py:class:`oidcmsg.key_jar.KeyJar` instance
    """
    return k_to_j(keyjar)

def keyjar_to_jwks_private(keyjar):
    """
    Convert a KeyJar instance to a JWKS (JSON document).
    Including the private key.

    :param keyjar: A :py:class:`oidcmsg.key_jar.KeyJar` instance
    """
    return k_to_j(keyjar, private=True)

class FSJWKSBundle(JWKSBundle):
    """
    A JWKSBundle that keeps the key information in a
    :py:class:`fedoidc.file_system.FileSystem` instance.
    """
    def __init__(self, iss, sign_keys=None, fdir='./', key_conv=None):
        """

        :param iss: Issuer ID for this entity
        :param sign_keys: Signing Keys used by this entity to sign JWTs
        :param fdir: A directory where JWKS can be stored
        :param key_conv: Specification of directory key to file name conversion.
            A set of keys are represented in the local cache as a KeyJar
            instance and as a JWKS on disc.
        """
        JWKSBundle.__init__(self, iss, sign_keys=sign_keys)
        self.bundle = FileSystem(fdir, key_conv=key_conv,
                                 value_conv={'to': keyjar_to_jwks,
                                             'from': jwks_to_keyjar})

    def clear(self):
        self.bundle.clear()

def make_jwks_bundle(config, eid):
    _args = dict([(k,v) for k,v in config.items() if k in KJ_SPECS])
    _kj = init_key_jar(**_args)

    if 'dir' in config:
        jb = FSJWKSBundle(eid, _kj, config['dir'],
                          key_conv={'to': quote_plus, 'from': unquote_plus})
    else:
        jb = JWKSBundle(eid, _kj)
        if 'bundle' in config:
            jb.loads(open(config['bundle']).read())
        elif 'signed_bundle' in config:
            _kj = jwks_to_keyjar(open(config['verification_keys']).read())
            jb.upload_signed_bundle(open(config['signed_bundle']).read(),
                                    _kj)
    return jb


from os import stat
from sys import stdout, stderr
import envoy

def run(cmd, *args, **kwargs):
    if 'quiet' in kwargs:
        del kwargs['quiet']
    else:
        print cmd

    out = envoy.run(cmd, *args, **kwargs)
    if out.status_code > 0:
        stdout.write("error running command: %s\n" % cmd)
        stdout.write(out.std_out)
        stdout.write(out.std_err)
        exit(out.status_code)

    stdout.write(out.std_out) if out.std_out else stderr.write(out.std_err)
    return out

def make_shortcut(cmd):
    """return a function which runs the given cmd

    make_shortcut('ls') returns a function which executes
    envoy.run('ls ' + arguments)"""
    def _(cmd_arguments, *args, **kwargs):
        return run("%s %s" % (cmd, cmd_arguments), *args, **kwargs)
    return _

def newer(f1, f2):
    return stat(f1).st_mtime > stat(f2).st_mtime


"""
ternya.process
==============

This module define the process to deal with openstack notification.
"""
import logging

from ternya.annotation import (nova_customer_process, nova_customer_process_wildcard,
                               cinder_customer_process, cinder_customer_process_wildcard,
                               neutron_customer_process, neutron_customer_process_wildcard,
                               glance_customer_process, glance_customer_process_wildcard,
                               swift_customer_process, swift_customer_process_wildcard,
                               keystone_customer_process, keystone_customer_process_wildcard,
                               heat_customer_process, heat_customer_process_wildcard)
from ternya import Openstack

log = logging.getLogger(__name__)

class ProcessFactory:
    @staticmethod
    def process(openstack_component):
        return process_mapping[openstack_component]

def nova_process(body, message):
    """
    This function deal with the nova notification.

    First, find process from customer_process that not include wildcard.
    if not find from customer_process, then find process from customer_process_wildcard.
    if not find from customer_process_wildcard, then use ternya default process.
    :param body: dict of openstack notification.
    :param message: kombu Message class
    :return:
    """
    event_type = body['event_type']
    process = nova_customer_process.get(event_type)
    if process is not None:
        process(body, message)
    else:
        matched = False
        process_wildcard = None
        for pattern in nova_customer_process_wildcard.keys():
            if pattern.match(event_type):
                process_wildcard = nova_customer_process_wildcard.get(pattern)
                matched = True
                break
        if matched:
            process_wildcard(body, message)
        else:
            default_process(body, message)
    message.ack()

def cinder_process(body, message):
    """
    This function deal with the cinder notification.

    First, find process from customer_process that not include wildcard.
    if not find from customer_process, then find process from customer_process_wildcard.
    if not find from customer_process_wildcard, then use ternya default process.
    :param body: dict of openstack notification.
    :param message: kombu Message class
    :return:
    """
    event_type = body['event_type']
    process = cinder_customer_process.get(event_type)
    if process is not None:
        process(body, message)
    else:
        matched = False
        process_wildcard = None
        for pattern in cinder_customer_process_wildcard.keys():
            if pattern.match(event_type):
                process_wildcard = cinder_customer_process_wildcard.get(pattern)
                matched = True
                break
        if matched:
            process_wildcard(body, message)
        else:
            default_process(body, message)
    message.ack()

def neutron_process(body, message):
    """
    This function deal with the neutron notification.

    First, find process from customer_process that not include wildcard.
    if not find from customer_process, then find process from customer_process_wildcard.
    if not find from customer_process_wildcard, then use ternya default process.
    :param body: dict of openstack notification.
    :param message: kombu Message class
    :return:
    """
    event_type = body['event_type']
    process = neutron_customer_process.get(event_type)
    if process is not None:
        process(body, message)
    else:
        matched = False
        process_wildcard = None
        for pattern in neutron_customer_process_wildcard.keys():
            if pattern.match(event_type):
                process_wildcard = neutron_customer_process_wildcard.get(pattern)
                matched = True
                break
        if matched:
            process_wildcard(body, message)
        else:
            default_process(body, message)
    message.ack()

def glance_process(body, message):
    """
    This function deal with the glance notification.

    First, find process from customer_process that not include wildcard.
    if not find from customer_process, then find process from customer_process_wildcard.
    if not find from customer_process_wildcard, then use ternya default process.
    :param body: dict of openstack notification.
    :param message: kombu Message class
    :return:
    """
    event_type = body['event_type']
    process = glance_customer_process.get(event_type)
    if process is not None:
        process(body, message)
    else:
        matched = False
        process_wildcard = None
        for pattern in glance_customer_process_wildcard.keys():
            if pattern.match(event_type):
                process_wildcard = glance_customer_process_wildcard.get(pattern)
                matched = True
                break
        if matched:
            process_wildcard(body, message)
        else:
            default_process(body, message)
    message.ack()

def swift_process(body, message):
    """
    This function deal with the swift notification.

    First, find process from customer_process that not include wildcard.
    if not find from customer_process, then find process from customer_process_wildcard.
    if not find from customer_process_wildcard, then use ternya default process.
    :param body: dict of openstack notification.
    :param message: kombu Message class
    :return:
    """
    event_type = body['event_type']
    process = swift_customer_process.get(event_type)
    if process is not None:
        process(body, message)
    else:
        matched = False
        process_wildcard = None
        for pattern in swift_customer_process_wildcard.keys():
            if pattern.match(event_type):
                process_wildcard = swift_customer_process_wildcard.get(pattern)
                matched = True
                break
        if matched:
            process_wildcard(body, message)
        else:
            default_process(body, message)
    message.ack()

def keystone_process(body, message):
    """
    This function deal with the keystone notification.

    First, find process from customer_process that not include wildcard.
    if not find from customer_process, then find process from customer_process_wildcard.
    if not find from customer_process_wildcard, then use ternya default process.
    :param body: dict of openstack notification.
    :param message: kombu Message class
    :return:
    """
    event_type = body['event_type']
    process = keystone_customer_process.get(event_type)
    if process is not None:
        process(body, message)
    else:
        matched = False
        process_wildcard = None
        for pattern in keystone_customer_process_wildcard.keys():
            if pattern.match(event_type):
                process_wildcard = keystone_customer_process_wildcard.get(pattern)
                matched = True
                break
        if matched:
            process_wildcard(body, message)
        else:
            default_process(body, message)
    message.ack()

def heat_process(body, message):
    """
    This function deal with the heat notification.

    First, find process from customer_process that not include wildcard.
    if not find from customer_process, then find process from customer_process_wildcard.
    if not find from customer_process_wildcard, then use ternya default process.
    :param body: dict of openstack notification.
    :param message: kombu Message class
    :return:
    """
    event_type = body['event_type']
    process = heat_customer_process.get(event_type)
    if process is not None:
        process(body, message)
    else:
        matched = False
        process_wildcard = None
        for pattern in heat_customer_process_wildcard.keys():
            if pattern.match(event_type):
                process_wildcard = heat_customer_process_wildcard.get(pattern)
                matched = True
                break
        if matched:
            process_wildcard(body, message)
        else:
            default_process(body, message)
    message.ack()

def default_process(body, message):
    event_type = body['event_type']
    log.debug("event_type:" + event_type)
    log.debug(body)

process_mapping = {
    Openstack.Nova: nova_process,
    Openstack.Cinder: cinder_process,
    Openstack.Neutron: neutron_process,
    Openstack.Glance: glance_process,
    Openstack.Swift: swift_process,
    Openstack.Keystone: keystone_process,
    Openstack.Heat: heat_process,
}

# define default method to deal with event_type that appear a lot of times.
# customer can override it if need.
nova_customer_process['compute.metrics.update'] = default_process
nova_customer_process['compute.instance.exists'] = default_process


# coding: utf-8
from webob import Request, Response

class App(object):

    def __init__(self):
        self.afters = []
        self.befores = []
        self.views = []

    def __call__(self, environ, start_response):
        request = Request(environ)
        response = None

        # match route or 404
        view_fn = self.get_view_for_route(request)
        if view_fn is None:
            response = Response()
            response.status_int = 404
            return response(environ, start_response)

        # before middlewares
        for before in self.befores:
            response = before(request)
            if response is not None:
                return self.render_response(environ, start_response, response)

        # run view
        response = view_fn(request)
        if isinstance(response, dict):
            response = Response(json=response)

        # after middlewares
        for after in self.afters:
            a_response = after(request, response)
            if a_response is not None:
                return self.render_response(environ, start_response, response)

        # Support dict as view/middleware response
        return self.render_response(environ, start_response, response)

    def render_response(self, environ, start_response, response):
        if isinstance(response, dict):
            response = Response(json=response)
        return response(environ, start_response)

    def after(self, after_fn):
        self.afters.append(after_fn)
        def decorator(*args, **kwargs):
            return after_fn(*args, **kwargs)
        return decorator

    def before(self, before_fn):
        self.befores.append(before_fn)
        def decorator(*args, **kwargs):
            return before_fn(*args, **kwargs)
        return decorator

    def get_view_for_route(self, request):
        def f(view):
            methods = view.punch_methods
            return ((methods is None or request.method in methods)
                     and request.path == view.punch_path)
        views = list(filter(f, self.views))
        if not views:
            return
        return views[0]

    def serve(self, server=None):
        """Serve app using wsgiref or provided server.

        Args:
        - server (callable): An callable
        """
        if server is None:
            from wsgiref.simple_server import make_server
            server = lambda app: make_server('', 8000, app).serve_forever()
            print('Listening on 0.0.0.0:8000')
        try:
            server(self)
        finally:
            server.socket.close()

    def view(self, path='', methods=None, **kwargs):
        def decorator(view_fn):
            view_fn.punch_path = path
            view_fn.punch_methods = methods
            view_fn.punch_kwargs = kwargs
            self.views.append(view_fn)
            return view_fn
        return decorator


import sys

def pout(msg, log=None):
    """Print 'msg' to stdout, and option 'log' at info level."""
    _print(msg, sys.stdout, log_func=log.info if log else None)

def perr(msg, log=None):
    """Print 'msg' to stderr, and option 'log' at info level."""
    _print(msg, sys.stderr, log_func=log.error if log else None)

def _print(msg, file, log_func=None):
    print(msg, file=file)
    if log_func:
        log_func(msg)