# -*- coding: utf-8 -*-
import logging
from types import MethodType

from aiohttp import web

LOGGER = logging.getLogger('homehub.lib.rpc')


class BaseJSONRPCError(Exception):
    def __init__(self, data=None):
        self.data = data

    def to_json(self):
        result = {
            'code': self.ERROR_CODE,
            'message': self.ERROR_MESSAGE,
        }

        if self.data:
            result['data'] = self.data

        return result


class JSONRPCParseError(BaseJSONRPCError):
    ERROR_CODE = -32700
    ERROR_MESSAGE = 'Parse error'


class JSONRPCInvalidRequestError(BaseJSONRPCError):
    ERROR_CODE = -32600
    ERROR_MESSAGE = 'Invalid Request'


class JSONRPCMethodNotFoundError(BaseJSONRPCError):
    ERROR_CODE = -32601
    ERROR_MESSAGE = 'Method not found'


class JSONRPCInvalidParamsError(BaseJSONRPCError):
    ERROR_CODE = -32602
    ERROR_MESSAGE = 'Invalid params'


class JSONRPCInternalErrorError(BaseJSONRPCError):
    ERROR_CODE = -32603
    ERROR_MESSAGE = 'Internal error'


async def post_rpc(request):
    responses = []
    results = []
    calls = []
    try:
        try:
            data = await request.json()
        except Exception as exc:
            LOGGER.error('Error deserializing RPC call!', exc_info=exc)
            raise JSONRPCParseError()

        if isinstance(data, list):
            calls = data
        else:
            calls.append(data)

        if len(calls) == 0:
            raise JSONRPCInvalidRequestError()

        for call in calls:
            call_method = None
            method_handler = None
            result = None

            try:
                try:
                    assert isinstance(call, dict), JSONRPCInvalidRequestError
                    assert call.get('jsonrpc') == '2.0', JSONRPCInvalidRequestError

                    call_method = call.get('method', None)
                    assert call_method is not None, JSONRPCInvalidRequestError

                    method_handler = request.config_dict['RPC_METHOD_REGISTRY'].get(call_method, None)
                    assert method_handler is not None, JSONRPCMethodNotFoundError
                except AssertionError as exc:
                    klass = exc.args[0]
                    raise klass()

                args = []
                kwargs = {}

                call_params = call.get('params', None)
                if call_params is not None:
                    if isinstance(call_params, list):
                        args = call_params
                    elif isinstance(call_params, dict):
                        kwargs = call_params
                    else:
                        raise JSONRPCInvalidParamsError()

                args.insert(0, request)

                try:
                    result = await method_handler(*args, **kwargs)
                except TypeError as exc:
                    LOGGER.error(
                        f'Error handling RPC method: {call_method}!',
                        exc_info=exc,
                    )
                    raise JSONRPCInvalidParamsError(str(exc))
            except Exception as exc:
                if isinstance(exc, BaseJSONRPCError):
                    result = exc
                else:
                    LOGGER.error(
                        f'Error handling RPC method: {call_method}!',
                        exc_info=exc,
                    )
                    result = JSONRPCInternalErrorError(str(exc))
            finally:
                results.append((call, result))
    except Exception as exc:
        if isinstance(exc, BaseJSONRPCError):
            results = [(None, exc)]
        else:
            raise

    for result in results:
        call, call_result = result

        response = {
            'jsonrpc': '2.0',
            'id': None,
        }

        if isinstance(call, dict) and call.get('id', None) is not None:
            response['id'] = call['id']

        if isinstance(call_result, BaseJSONRPCError):
            response['error'] = call_result.to_json()
        else:
            response.update({
                'method': call['method'],
                'result': call_result,
            })

        if 'error' in response or response['id'] is not None:
            responses.append(response)

    if len(responses) == 1:
        return web.json_response(responses[0])

    return web.json_response(responses)


async def list_methods(request):
    return sorted(request.config_dict['RPC_METHOD_REGISTRY'].keys())


def add_rpc_methods(app, methods):
    for name, method in methods:
        app['RPC_METHOD_REGISTRY'][name] = method


def setup(app):
    app['RPC_METHOD_REGISTRY'] = {}
    app.add_rpc_methods = MethodType(add_rpc_methods, app)

    app.add_routes([
        web.post('/backend/rpc', post_rpc),
    ])

    app.add_rpc_methods([
        ('system.list_methods', list_methods),
    ])
