# vim: set fileencoding=utf-8 :

from __future__ import absolute_import, print_function, unicode_literals

import functools
import json
import operator
import os
import pprint
import re
import sys
from itertools import chain

import flask.config
import klein
import logbook
import six
import treq
import werkzeug.exceptions
from twisted.internet import defer

import stethoscope.api.devices
import stethoscope.api.utils
import stethoscope.auth
import stethoscope.csrf
import stethoscope.plugins.utils
import stethoscope.utils
import stethoscope.validation


logger = logbook.Logger(__name__)


def log_response(name, extension_name, response, debug=False):
  msg = "retrieved {:d} {:s}(s) from '{:s}'".format(len(response), name, extension_name)
  if debug:
    msg += ":\n{:s}".format(pprint.pformat(response))
  logger.debug(msg)
  return response


def log_error(name, extension_name, response):
  logger.error("error retrieving {:s}(s) from '{:s}':\n{!s}", name, extension_name, response)
  return response


def log_access(record_type, userinfo, target, response, context=None):
  logger.notice("'{!s}' accessed {!s} records{!s} for '{!s}'", userinfo['sub'], record_type,
      " ({!s})".format(context) if context is not None else "", target)
  return response


def sort_events(events):
  return sorted(events, key=operator.itemgetter('timestamp'), reverse=True)


def merge_events(events):
  return sort_events(chain.from_iterable(_events for (status, _events) in events if status))


def serialized_endpoint(*_callbacks):
  """Decorator which wraps an endpoint by applying callbacks to the results then serializing to JSON.

  First, the decorated function is called and must return a `defer.Deferred`. The callbacks supplied
  to the decorator are then applied followed by any callbacks supplied as keyword arguments to the
  decorated function.  The result is then serialized and returned in the response (with
  ``Content-Type`` set to ``application/json``).
  """
  def decorator(func):
    @six.wraps(func)
    def wrapped(request, *args, **kwargs):
      callbacks = kwargs.pop('callbacks', [])
      # logger.debug("in wrapped:\nargs: {!r}\nkwargs: {!r}", args, kwargs)

      deferred_list = func(*args, **kwargs)

      for callback in list(_callbacks) + callbacks:
        deferred_list.addCallback(callback)

      deferred_list.addCallback(json.dumps, default=stethoscope.utils.json_serialize_datetime)
      request.setHeader('Content-Type', 'application/json')
      return deferred_list
    return wrapped
  return decorator


def get_events_by_email(email, extensions):
  deferreds = []
  for ext in extensions:
    deferred = ext.obj.get_events_by_email(email)
    deferred.addCallback(functools.partial(log_response, 'event', ext.name))
    deferreds.append(deferred)

  return defer.DeferredList(deferreds, consumeErrors=True)


@serialized_endpoint(merge_events)
def merged_events(*args, **kwargs):
  """Endpoint returning (as JSON) all events after merging."""
  return get_events_by_email(*args, **kwargs)


def get_accounts_by_email(email, extensions):
  deferreds = []
  for ext in extensions:
    deferred = ext.obj.get_account_by_email(email)
    deferred.addCallback(functools.partial(log_response, 'account', ext.name))
    deferreds.append(deferred)

  return defer.DeferredList(deferreds, consumeErrors=True)


def merge_accounts(accts):
  return list(acct for (status, acct) in accts if status)


@serialized_endpoint(merge_accounts)
def merged_accounts(*args, **kwargs):
  """Endpoint returning (as JSON) all accounts after merging."""
  return get_accounts_by_email(*args, **kwargs)


def sort_notifications(notifications):
  # TODO: replace with generic version
  def get_timestamp(notification):
    return notification['_source']['event_timestamp']
  return sorted(notifications, key=get_timestamp, reverse=True)


def get_notifications_by_email(email, extensions):
  deferreds = []
  for ext in extensions:
    deferred = ext.obj.get_notifications_by_email(email)
    deferred.addCallback(functools.partial(log_response, 'notifications', ext.name))
    deferreds.append(deferred)

  return defer.DeferredList(deferreds, consumeErrors=True)


def merge_notifications(notifications):
  return sort_notifications(chain.from_iterable(notifs for (status, notifs) in notifications if
    status))


@serialized_endpoint(merge_notifications)
def merged_notifications(*args, **kwargs):
  """Endpoint returning (as JSON) all notifications after merging."""
  return get_notifications_by_email(*args, **kwargs)


def get_devices_by_email(email, extensions, debug=False):
  """Returns all devices matching given email."""
  deferreds = []
  for ext in extensions:
    if hasattr(ext.obj, 'get_devices_by_email'):
      deferred = ext.obj.get_devices_by_email(email)
      deferred.addErrback(stethoscope.api.utils.check_user_not_found)
      deferred.addCallback(functools.partial(log_response, 'device',
        ext.name + " ({!s})".format(email), debug=debug))
      deferreds.append(deferred)

  deferred_list = defer.DeferredList(deferreds, consumeErrors=True)
  deferred_list.addCallback(functools.partial(stethoscope.api.utils.filter_keyed_by_status,
    ["{!s}({!s})".format(ext.name, email) for ext in extensions],
    context=sys._getframe().f_code.co_name))
  deferred_list.addCallback(lambda d: chain.from_iterable(d.values()))
  deferred_list.addCallback(list)
  return deferred_list


def get_devices_by_macaddr(macaddr, extensions, debug=False):
  """Returns all devices matching given MAC address."""
  deferreds = []
  for ext in extensions:
    if hasattr(ext.obj, 'get_devices_by_macaddr'):
      deferred = ext.obj.get_devices_by_macaddr(macaddr)
      deferred.addErrback(stethoscope.api.utils.check_device_not_found)
      deferred.addCallback(functools.partial(log_response, 'device',
        ext.name + " ({!s})".format(macaddr), debug=debug))
      deferreds.append(deferred)

  deferred_list = defer.DeferredList(deferreds, consumeErrors=True)
  deferred_list.addCallback(functools.partial(stethoscope.api.utils.filter_keyed_by_status,
    ["{!s}({!s})".format(ext.name, macaddr) for ext in extensions],
    context=sys._getframe().f_code.co_name))
  deferred_list.addCallback(lambda d: chain.from_iterable(d.values()))
  deferred_list.addCallback(list)
  return deferred_list


def get_devices_by_serial(serial, extensions, debug=False):
  """Returns all devices matching given serial number."""
  deferreds = []
  for ext in extensions:
    if hasattr(ext.obj, 'get_devices_by_serial'):
      deferred = ext.obj.get_devices_by_serial(serial)
      deferred.addErrback(stethoscope.api.utils.check_device_not_found)
      deferred.addCallback(functools.partial(log_response, 'device',
        ext.name + " ({!s})".format(serial), debug=debug))
      deferreds.append(deferred)

  deferred_list = defer.DeferredList(deferreds, consumeErrors=True)
  deferred_list.addCallback(functools.partial(stethoscope.api.utils.filter_keyed_by_status,
    ["{!s}({!s})".format(ext.name, serial) for ext in extensions],
    context=sys._getframe().f_code.co_name))
  deferred_list.addCallback(lambda d: chain.from_iterable(d.values()))
  deferred_list.addCallback(list)
  return deferred_list


def _get_devices_by_serials(serials, extensions, debug=False):
  deferred_list = defer.DeferredList([get_devices_by_serial(serial, extensions, debug=debug) for
    serial in serials], consumeErrors=True)
  deferred_list.addCallback(functools.partial(stethoscope.api.utils.filter_keyed_by_status,
    serials, context=sys._getframe().f_code.co_name))
  deferred_list.addCallback(lambda d: chain.from_iterable(d.values()))
  deferred_list.addCallback(list)
  return deferred_list


def _get_devices_by_macaddrs(macaddrs, extensions, debug=False):
  deferred_list = defer.DeferredList([get_devices_by_macaddr(macaddr, extensions, debug=debug) for
    macaddr in macaddrs], consumeErrors=True)
  deferred_list.addCallback(functools.partial(stethoscope.api.utils.filter_keyed_by_status,
    macaddrs, context=sys._getframe().f_code.co_name))
  deferred_list.addCallback(lambda d: chain.from_iterable(d.values()))
  deferred_list.addCallback(list)
  return deferred_list


def apply_device_transforms(devices, transforms):
  for transform in transforms:
    devices = transform.obj.transform(devices)
  return devices


@defer.inlineCallbacks
def get_devices_by_stages(email, pre_extensions, extensions, transforms, debug=False):
  """Return all devices found via two-stage lookup process.

  First, lookup the user in `pre_extensions` by email address. Retrieve all relevant devices and
  extract serials numbers. Merge the results of looking up the serial numbers in compatible
  extensions (from `extensions`) with those from looking up the user's devices in `extensions` by
  email address.
  """
  email_devices_deferred = get_devices_by_email(email, extensions, debug=debug)
  pre_devices = yield get_devices_by_email(email, pre_extensions, debug=debug)
  pre_devices = apply_device_transforms(pre_devices, transforms)

  logger.debug("[{!s}] retrieved {:d} pre-devices", email, len(pre_devices))

  serials = set()
  macaddrs = set()
  for device in pre_devices:
    identifiers = device.get('identifiers', {})
    if 'serial' in identifiers:
      serials.add(identifiers['serial'])
    if 'mac_addresses' in identifiers:
      for macaddr in identifiers['mac_addresses']:
        macaddrs.add(stethoscope.validation.canonicalize_macaddr(macaddr))

  logger.debug("[{!s}] {:d} serials: {!s}", email, len(serials), serials)
  logger.debug("[{!s}] {:d} macaddrs: {!s}", email, len(macaddrs), macaddrs)

  serial_devices_deferred = _get_devices_by_serials(serials, extensions, debug=debug)
  macaddr_devices_deferred = _get_devices_by_macaddrs(macaddrs, extensions, debug=debug)

  deferred_list = defer.DeferredList([defer.succeed(pre_devices), email_devices_deferred,
    serial_devices_deferred, macaddr_devices_deferred], consumeErrors=True)
  deferred_list.addCallback(stethoscope.api.utils.filter_by_status,
      context=sys._getframe().f_code.co_name)
  deferred_list.addCallback(chain.from_iterable)
  deferred_list.addCallback(list)

  devices = yield deferred_list
  logger.info("[{!s}] retrieved {:d} devices", email, len(devices))

  defer.returnValue(devices)


def _add_route(ext, app, auth, name, argname, **kwargs):
  """Add a GET route to Klein app `app` which calls `ext.obj.get_{name}`.

  The route takes the form `/{name}/{ext.name}/<string:email>`, e.g.,
  `/devices/google/<string:email>`.
  """
  method_name = 'get_' + name + "_by_" + argname
  if not hasattr(ext.obj, method_name):
    return None

  callbacks = kwargs.pop('callbacks', [])
  log_hooks = kwargs.pop('log_hooks', [])

  @auth.match_required
  @getattr(stethoscope.validation, 'check_valid_' + argname)
  def _get(request, arg, **kwargs):
    """Return a `Deferred` which calls an extension'a `get_{name}` method with the given `email`
    and gives the result as a JSON resource."""
    userinfo = kwargs.pop('userinfo')
    if len(kwargs) > 0:
      raise werkzeug.exceptions.BadRequest("unexpected parameters: {:s}",
          stethoscope.utils.html_escape(str(kwargs)))

    deferred = getattr(ext.obj, method_name)(arg)
    deferred.addCallback(functools.partial(log_response, name, ext.name))
    # deferred.addCallback(functools.partial(log_access, name, userinfo, arg, context=ext.name))
    for hook in log_hooks:
      deferred.addCallback(functools.partial(hook.obj.log, name, userinfo, arg, context=ext.name))
    deferred.addErrback(functools.partial(log_error, name, ext.name))

    for callback in callbacks:
      deferred.addCallback(callback)

    deferred.addCallback(json.dumps, default=stethoscope.utils.json_serialize_datetime)
    request.setHeader('Content-Type', 'application/json')
    return deferred

  # note: setting the endpoint manually is necessary for Klein to direct flows properly
  kwargs['endpoint'] = '-'.join([name, ext.name, argname])
  kwargs.setdefault('methods', ['GET'])

  url = '/' + '/'.join([name, ext.name, argname, '<string:{:s}>'.format(argname)])

  logger.debug("registering extension:\n  extension: {!r}\n  object: {!r}\n  function: {!r}\n"
               "  url: {!r}\n  kwargs: {!r}".format(ext, ext.obj, _get, url, kwargs))
  app.route(url, **kwargs)(_get)


def register_merged_device_endpoints(app, config, auth, device_plugins, apply_practices,
    transforms, log_hooks=[]):
  """Registers endpoints which provide merged devices without the ownership attribution stage."""

  def setup_endpoint_kwargs(endpoint_type, args, kwargs):
    userinfo = kwargs.pop('userinfo')

    kwargs['callbacks'] = [transform.obj.transform for transform in transforms] + [
      functools.partial(log_response, 'device', endpoint_type),
      functools.partial(log_access, 'device', userinfo, *args),
    ] + [functools.partial(hook.obj.log, 'device', userinfo, *args) for hook in log_hooks]

    kwargs.setdefault('debug', config.get('DEBUG', False))
    return kwargs

  @serialized_endpoint(apply_practices, stethoscope.api.devices.merge_devices)
  def merged_devices_by_email(*args, **kwargs):
    """Endpoint returning (as JSON) all devices for given email after merging."""
    return get_devices_by_email(*args, **kwargs)

  @auth.match_required
  @stethoscope.validation.check_valid_email
  def __get_devices_by_email(request, email, **_kwargs):
    # actual function is required so that app.route can get a '__name__' attribute
    _kwargs = setup_endpoint_kwargs('email', (email,), _kwargs)
    return merged_devices_by_email(request, email, device_plugins, **_kwargs)
  app.route('/devices/email/<string:email>', endpoint='devices-email',
      methods=['GET'])(__get_devices_by_email)

  @serialized_endpoint(apply_practices, stethoscope.api.devices.merge_devices)
  def merged_devices_by_serial(*args, **kwargs):
    """Endpoint returning (as JSON) all devices for given serial after merging."""
    return get_devices_by_serial(*args, **kwargs)

  @auth.token_required
  @stethoscope.validation.check_valid_serial
  def __get_devices_by_serial(request, serial, **_kwargs):
    # actual function is required so that app.route can get a '__name__' attribute
    _kwargs = setup_endpoint_kwargs('serial', (serial,), _kwargs)
    return merged_devices_by_serial(request, serial, device_plugins, **_kwargs)
  app.route('/devices/serial/<string:serial>', endpoint='devices-serial',
      methods=['GET'])(__get_devices_by_serial)

  @serialized_endpoint(apply_practices, stethoscope.api.devices.merge_devices)
  def merged_devices_by_macaddr(*args, **kwargs):
    """Endpoint returning (as JSON) all devices for given macaddr after merging."""
    return get_devices_by_macaddr(*args, **kwargs)

  @auth.token_required
  @stethoscope.validation.check_valid_macaddr
  def __get_devices_by_macaddr(request, macaddr, **_kwargs):
    # actual function is required so that app.route can get a '__name__' attribute
    _kwargs = setup_endpoint_kwargs('macaddr', (macaddr,), _kwargs)
    return merged_devices_by_macaddr(request, macaddr, device_plugins, **_kwargs)
  app.route('/devices/macaddr/<string:macaddr>', endpoint='devices-macaddr',
      methods=['GET'])(__get_devices_by_macaddr)


def register_device_api_endpoints(app, config, auth, log_hooks=[]):
  practices = stethoscope.plugins.utils.instantiate_practices(config,
      namespace='stethoscope.plugins.practices.devices')

  # for use by endpoints to apply each practice to each device
  def apply_practices(devices):
    for device in devices:
      practices.map_method('inject_status', device)
    return devices

  # transforms to apply to device lists
  transforms = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.transform.devices')

  # initial-stage plugins provide ownership attribution
  predevice_plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.sources.predevices')

  if config.get('ENABLE_PREDEVICE_ENDPOINTS', config['DEBUG']) and \
      len(predevice_plugins.names()) > 0:
    # individual endpoints for each plugin for device lookup by mac, email, serial
    predevice_plugins.map(_add_route, app, auth, 'devices', 'email', log_hooks=log_hooks)
    predevice_plugins.map(_add_route, app, auth, 'devices', 'macaddr', log_hooks=log_hooks)
    predevice_plugins.map(_add_route, app, auth, 'devices', 'serial', log_hooks=log_hooks)

  # instantiate the second-stage plugins which provide detailed device data
  device_plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.sources.devices')

  if config.get('ENABLE_DEVICE_ENDPOINTS', config['DEBUG']) and len(device_plugins.names()) > 0:
    # individual endpoints for each plugin for device lookup by mac, email, serial
    device_plugins.map(_add_route, app, auth, 'devices', 'email', log_hooks=log_hooks)
    device_plugins.map(_add_route, app, auth, 'devices', 'macaddr', log_hooks=log_hooks)
    device_plugins.map(_add_route, app, auth, 'devices', 'serial', log_hooks=log_hooks)

    # 'merged' endpoints which merge device data across all second-stage device plugins
    # (without the initial ownership-attribution stage) for lookup by mac, email, serial
    register_merged_device_endpoints(app, config, auth, device_plugins, apply_practices,
        transforms=transforms, log_hooks=log_hooks)

  # primary device api endpoint ('merged' or 'staged') which merges device data across all
  # device plugins (both initial and second-stage)
  @serialized_endpoint(apply_practices, stethoscope.api.devices.merge_devices)
  def merged_devices_by_stages(*args, **kwargs):
    """Endpoint returning (as JSON) all devices for given email (including second-stage lookups)."""
    return get_devices_by_stages(*args, **kwargs)

  @auth.match_required
  @stethoscope.validation.check_valid_email
  def __get_devices_by_stages(request, email, **_kwargs):
    userinfo = _kwargs.pop('userinfo')

    # required so that app.route can get a '__name__' attribute from decorated function
    _kwargs['callbacks'] = [transform.obj.transform for transform in transforms] + [
      functools.partial(log_response, 'device', 'staged'),
      functools.partial(log_access, 'device', userinfo, email, context='merged'),
    ] + [functools.partial(hook.obj.log, 'device', userinfo, email, context='merged')
        for hook in log_hooks]
    logger.debug("callbacks:\n{!s}", pprint.pformat(_kwargs['callbacks']))
    _kwargs.setdefault('debug', config.get('DEBUG', False))
    return merged_devices_by_stages(request, email, predevice_plugins, device_plugins, transforms,
        **_kwargs)
  app.route('/devices/staged/<string:email>', endpoint='devices-staged',
      methods=['GET'])(__get_devices_by_stages)
  app.route('/devices/merged/<string:email>', endpoint='devices-merged',
      methods=['GET'])(__get_devices_by_stages)


def register_event_api_endpoints(app, config, auth, log_hooks=[]):
  event_plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.sources.events')

  if config.get('ENABLE_EVENT_ENDPOINTS', config['DEBUG']) and len(event_plugins.names()) > 0:
    event_plugins.map(_add_route, app, auth, 'events', 'email', callbacks=[sort_events],
        log_hooks=log_hooks)

  # gather hooks to transform events (e.g., by adding geolocation data)
  hooks = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.transform.events')

  @auth.match_required
  @stethoscope.validation.check_valid_email
  def _merged_events(request, email, **_kwargs):
    userinfo = _kwargs.pop('userinfo')

    # required so that app.route can get a '__name__' attribute from decorated function
    _kwargs['callbacks'] = [hook.obj.transform for hook in hooks] + [
      functools.partial(log_response, 'event', 'merged'),
      functools.partial(log_access, 'event', userinfo, email, context='merged'),
    ] + [functools.partial(hook.obj.log, 'event', userinfo, email, context='merged')
        for hook in log_hooks]
    return merged_events(request, email, event_plugins, **_kwargs)
  app.route('/events/merged/<string:email>', endpoint='events-merged',
      methods=['GET'])(_merged_events)


def register_userinfo_api_endpoints(app, config, auth, log_hooks=[]):
  userinfo_plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.sources.userinfo')

  if config['DEBUG']:
    userinfo_plugins.map(_add_route, app, auth, 'userinfo', 'email', log_hooks=log_hooks)


def register_account_api_endpoints(app, config, auth, log_hooks=[]):
  account_plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.sources.accounts')

  if config.get('ENABLE_ACCOUNT_ENDPOINTS', config['DEBUG']) and len(account_plugins.names()) > 0:
    account_plugins.map(_add_route, app, auth, 'account', 'email', log_hooks=log_hooks)

  @auth.match_required
  @stethoscope.validation.check_valid_email
  def _merged_accounts(request, email, **_kwargs):
    userinfo = _kwargs.pop('userinfo')

    # required so that app.route can get a '__name__' attribute from decorated function
    _kwargs['callbacks'] = [
      functools.partial(log_response, 'account', 'merged'),
      functools.partial(log_access, 'account', userinfo, email, context='merged'),
    ] + [functools.partial(hook.obj.log, 'account', userinfo, email, context='merged')
        for hook in log_hooks]
    return merged_accounts(request, email, account_plugins, **_kwargs)
  app.route('/accounts/merged/<string:email>', endpoint='accounts-merged',
      methods=['GET'])(_merged_accounts)


def register_notification_api_endpoints(app, config, auth, log_hooks=[]):
  notification_plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.sources.notifications')

  if config.get('ENABLE_NOTIFICATION_ENDPOINTS', config['DEBUG']) \
      and len(notification_plugins.names()) > 0:
    notification_plugins.map(_add_route, app, auth, 'notifications', 'email', log_hooks=log_hooks)

  @auth.match_required
  @stethoscope.validation.check_valid_email
  def _merged_notifications(request, email, **_kwargs):
    userinfo = _kwargs.pop('userinfo')

    # required so that app.route can get a '__name__' attribute from decorated function
    _kwargs['callbacks'] = [
      functools.partial(log_response, 'notification', 'merged'),
      functools.partial(log_access, 'notification', userinfo, email, context='merged'),
    ] + [functools.partial(hook.obj.log, 'notification', userinfo, email, context='merged')
        for hook in log_hooks]
    return merged_notifications(request, email, notification_plugins, **_kwargs)
  app.route('/notifications/merged/<string:email>', endpoint='notifications-merged',
      methods=['GET'])(_merged_notifications)


def log_post_response(name, extension_name, result, debug=False):
  msg = "posted '{:s}' to '{:s}'".format(name, extension_name)
  if debug:
    msg += ":\n{:s}".format(pprint.pformat(result))
  logger.debug(msg)
  return result


def log_post_error(name, extension_name, result):
  logger.error("error posting '{:s}' to '{:s}':\n{!s}", name, extension_name, result)
  return result


def _add_post_route(ext, app, config, auth, csrf, name, **kwargs):
  method_name = 'post_' + name
  if not hasattr(ext.obj, method_name):
    return None

  callbacks = kwargs.pop('callbacks', [])
  # log_hooks = kwargs.pop('log_hooks', [])

  @auth.token_required  # TODO: consider auth
  @csrf.csrf_protect
  def _post(request, **kwargs):
    """Return a `Deferred` which calls an extension'a `post_{name}` method with the POST data
    and gives the result as a JSON resource."""
    userinfo = kwargs.pop('userinfo')
    if len(kwargs) > 0:
      raise werkzeug.exceptions.BadRequest("unexpected parameters: {:s}",
          stethoscope.utils.html_escape(str(kwargs)))

    content = json.loads(request.content.getvalue().decode('utf-8'))
    deferred = getattr(ext.obj, method_name)(userinfo['sub'], content)
    deferred.addCallback(functools.partial(log_post_response, name, ext.name))
    deferred.addErrback(functools.partial(log_post_error, name, ext.name))

    for callback in callbacks:
      deferred.addCallback(callback)

    deferred.addCallback(json.dumps, default=stethoscope.utils.json_serialize_datetime)
    request.setHeader('Content-Type', 'application/json')
    return deferred

  # note: setting the endpoint manually is necessary for Klein to direct flows properly
  kwargs['endpoint'] = '-'.join([name, ext.name])
  kwargs.setdefault('methods', ['POST'])

  url = '/' + '/'.join([name, ext.name])

  logger.debug("registering extension:\n  extension: {!r}\n  object: {!r}\n  function: {!r}\n"
               "  url: {!r}\n  kwargs: {!r}".format(ext, ext.obj, _post, url, kwargs))
  app.route(url, **kwargs)(_post)


def register_feedback_api_endpoints(app, config, auth, csrf, log_hooks=[]):
  feedback_plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.feedback')

  if config.get('ENABLE_FEEDBACK_ENDPOINTS', config['DEBUG']) \
      and len(feedback_plugins.names()) > 0:
    feedback_plugins.map(_add_post_route, app, config, auth, csrf, 'feedback')


def get_config():
  config = flask.config.Config(os.environ.get('STETHOSCOPE_API_INSTANCE_PATH', './instance/'))

  # load default config from package
  config.from_object('stethoscope.api.defaults')

  # load config specified via env var
  # should be absolute path to a config file
  config.from_envvar('STETHOSCOPE_API_CONFIG', silent=True)

  # load instance config (config.py from instance/)
  config.from_pyfile('config.py')

  return config


def register_error_handlers(app, config, auth):
  plugins = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.logging.failure')
  logger.debug("loaded failure logging plugins: {!r}", [hook.name for hook in plugins])

  @app.handle_errors(Exception)
  def check_authorization(request, failure):
    logger.error("Exception: {!s}\n{!s}", failure.value, failure.getTraceback())

    # log to, e.g., a metrics backend like atlas
    for plugin in plugins:
      plugin.obj.log_failure(request, failure)

    # does this lead to infinite recursion?
    # will raise AuthError if token is invalid
    auth.check_token(request)
    return failure


@defer.inlineCallbacks
def check_upstream_response(response, request):
  content = yield treq.content(response)
  if response.code != 200:
    request.setResponseCode(502)
    defer.returnValue("Upstream response code: {:d}\n{!s}".format(response.code, content))
  else:
    defer.returnValue(content)


def register_endpoints(app, config, auth, csrf):
  @app.route('/healthcheck')
  def healthcheck(request):
    upstream = 'http://{!s}/healthcheck'.format(os.getenv("STETHOSCOPE_LOGIN_HOST",
      "127.0.0.1:5002"))
    deferred = treq.get(upstream, timeout=0.1, headers={'Host': request.getHost().host})
    deferred.addCallback(check_upstream_response, request)
    return deferred

  # gather hooks to external loggers
  log_hooks = stethoscope.plugins.utils.instantiate_plugins(config,
      namespace='stethoscope.plugins.logging.request')
  logger.debug("loaded request logging hooks: {!r}", [hook.name for hook in log_hooks])

  with app.subroute('/api/v1'):
    register_device_api_endpoints(app, config, auth, log_hooks=log_hooks)
    register_event_api_endpoints(app, config, auth, log_hooks=log_hooks)
    register_account_api_endpoints(app, config, auth, log_hooks=log_hooks)
    register_notification_api_endpoints(app, config, auth, log_hooks=log_hooks)
    register_feedback_api_endpoints(app, config, auth, csrf, log_hooks=log_hooks)
    # temporarily disabled: userinfo is not in use
    # register_userinfo_api_endpoints(app, config, auth, log_hooks=log_hooks)


def create_app():
  config = get_config()

  config['LOGBOOK'].push_application()

  app = klein.Klein()
  auth = stethoscope.auth.KleinAuthProvider(config)
  csrf = stethoscope.csrf.CSRFProtection(config)

  register_error_handlers(app, config, auth)
  register_endpoints(app, config, auth, csrf)

  logger.info("Shields up, weapons online.")

  return app
