# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4


from contextlib import contextmanager
import inspect
import sys
import json
import datetime

import tornado.ioloop
import tornado.web
import tornado.gen
import tornado.concurrent
from threading import Thread
from functools import wraps

import logging
logger = logging.getLogger(__name__)


def json_response(data=None, status=200, text='ok'):
    if not data:
        data = {}
    return {'status': {'code': status, 'text': text}, 'data': data}

def _to_json(python_object):
    if isinstance(python_object, datetime.datetime):
        if python_object.year < 1900:
            tt = python_object.timetuple()
            return '%d-%02d-%02dT%02d:%02d%02dZ' % tuple(list(tt)[:6])
        return python_object.strftime('%Y-%m-%dT%H:%M:%SZ')
    raise TypeError('%s %s is not JSON serializable' % (repr(python_object), type(python_object)))

def json_dumps(obj):
    indent = 2
    return json.dumps(obj, indent=indent, default=_to_json, ensure_ascii=False).encode()

def run_async(func):
  @wraps(func)
  def async_func(*args, **kwargs):
    func_hl = Thread(target = func, args = args, kwargs = kwargs)
    func_hl.start()
    return func_hl

  return async_func

def trim(docstring):
    if not docstring:
        return ''
    # Convert tabs to spaces (following the normal Python rules)
    # and split into a list of lines:
    lines = docstring.expandtabs().splitlines()
    # Determine minimum indentation (first line doesn't count):
    indent = sys.maxsize
    for line in lines[1:]:
        stripped = line.lstrip()
        if stripped:
            indent = min(indent, len(line) - len(stripped))
    # Remove indentation (first line is special):
    trimmed = [lines[0].strip()]
    if indent < sys.maxsize:
        for line in lines[1:]:
            trimmed.append(line[indent:].rstrip())
    # Strip off trailing and leading blank lines:
    while trimmed and not trimmed[-1]:
        trimmed.pop()
    while trimmed and not trimmed[0]:
        trimmed.pop(0)
    # Return a single string:
    return '\n'.join(trimmed)

@contextmanager
def defaultcontext():
    yield

@run_async
def api_task(context, request, callback):
    if context == None:
        context = defaultcontext
    action = request.arguments.get('action', [None])[0].decode('utf-8')
    data = request.arguments.get('data', [b'{}'])[0]
    data = json.loads(data.decode('utf-8')) if data else {}
    if not action:
        methods = list(actions.keys())
        api = []
        for f in sorted(methods):
            api.append({'name': f,
                        'doc': actions.doc(f).replace('\n', '<br>\n')})
        response = json_response(api)
    else:
        logger.debug('API %s %s', action, data)
        f = actions.get(action)
        if f:
            with context():
                try:
                    response = f(data)
                except:
                    logger.debug('FAILED %s %s', action, data, exc_info=True)
                    response = json_response(status=500, text='%s failed' % action)
        else:
            response = json_response(status=400, text='Unknown action %s' % action)
    callback(response)

class ApiHandler(tornado.web.RequestHandler):
    def initialize(self, context=None):
        self._context = context

    def get(self):
        self.write('use POST')

    @tornado.web.asynchronous
    @tornado.gen.coroutine
    def post(self):
        if 'origin' in self.request.headers and self.request.host not in self.request.headers['origin']:
            logger.debug('reject cross site attempt to access api %s', self.request)
            self.set_status(403)
            self.write('')
            return

        response = yield tornado.gen.Task(api_task, self._context, self.request)
        if not 'status' in response:
            response = json_response(response)
        response = json_dumps(response)
        self.set_header('Content-Type', 'application/json')
        self.write(response)

class ApiActions(dict):
    properties = {}
    versions = {}
    def __init__(self):

        def api(data):
            '''
                returns list of all known api actions
                takes {
                    docs: bool
                }
                if docs is true, action properties contain docstrings
                returns {
                    actions: {
                        'api': {
                            cache: true,
                            doc: 'recursion'
                        },
                        'hello': {
                            cache: true,
                            ..
                        }
                        ...
                    }
                }
            '''
            data = data or {}
            docs = data.get('docs', False)
            code = data.get('code', False)
            _actions = list(self.keys())
            _actions.sort()
            actions = {}
            for a in _actions:
                actions[a] = self.properties[a]
                if docs:
                    actions[a]['doc'] = self.doc(a)
                if code:
                    actions[a]['code'] = self.code(a)
            return {'actions': actions}
        self.register(api)

    def doc(self, name):
        f = self[name]
        return trim(f.__doc__)

    def code(self, name, version=None):
        f = self[name]
        if name != 'api' and hasattr(f, 'func_closure') and f.__closure__:
            fc = [c for c in f.__closure__ if hasattr(c.cell_contents, '__call__')]
            f = fc[len(fc)-1].cell_contents 
        info = f.__code__.co_filename
        info = '%s:%s' % (info, f.__code__.co_firstlineno)
        return info, trim(inspect.getsource(f))

    def register(self, method, action=None, cache=True, version=None):
        if not action:
            action = method.__name__
        if version:
            if not version in self.versions:
                self.versions[version] = {}
            self.versions[version][action] = method
        else:
            self[action] = method
        self.properties[action] = {'cache': cache}

    def unregister(self, action):
        if action in self:
            del self[action]

actions = ApiActions()