# -*- coding: utf-8 -*-
import asyncio
from collections import namedtuple
import copy
import logging

from aiohttp import web
from bthlabs_jsonrpc_core import register_method

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

CapabilitySpec = namedtuple('CapabilitySpec', [
    'handler', 'public', 'public_content_type',
])


class BaseServiceError(Exception):
    pass


class ServiceData:
    def __init__(self, data=None, exception=None):
        super(ServiceData, self).__init__()
        self.data = data
        self.exception = exception

    def raise_if_needed(self):
        if self.exception is not None:
            raise self.exception

    def to_json(self):
        result = {}

        if self.data is not None:
            result['data'] = self.data

        if self.exception is not None:
            result['error'] = self.exception.args[0]

        return result


class BaseService:
    IS_GLOBAL = False
    INSTANCE = None

    def __init__(self, app, instance, characteristics):
        super(BaseService, self).__init__()
        self.app = app
        self.instance = instance
        self.characteristics = characteristics

        self.capabilities = {}
        self.state = self.app['STATE'].get()['backend'].get(
            self.state_key(), None,
        )

    @staticmethod
    def service(klass, app, instance, characteristics):
        if klass.IS_GLOBAL:
            if klass.INSTANCE is None:
                klass.INSTANCE = klass(app, None, characteristics)

            return klass.INSTANCE
        else:
            return klass(app, instance, characteristics)

    def state_key(self):
        return '{kind}.{instance}'.format(
            kind=self.KIND, instance=self.instance,
        )

    async def current_data(self):
        return None

    async def start(self):
        raise NotImplementedError('TODO')

    async def stop(self):
        raise NotImplementedError('TODO')

    async def shutdown(self):
        await self.stop()

    async def notify(self):
        data = ServiceData()
        try:
            if asyncio.iscoroutinefunction(self.current_data):
                data = await self.current_data()
            else:
                data = self.current_data()
        except Exception as exc:
            data = ServiceData(exception=exc)

        LOGGER.debug('BaseService.notify(): {kind} {instance} {data}'.format(
            kind=self.KIND,
            instance=self.instance,
            data=str(data.to_json()),
        ))

        await self.app.notify_subscribers({
            'type': 'SERVICE_NOTIFICATION',
            'kind': self.KIND,
            'instance': self.instance if not self.IS_GLOBAL else None,
            'data': data.to_json(),
        })

    def set_state(self, state):
        self.state = state

        new_state = copy.deepcopy(self.app['STATE'].get())
        new_state['backend'][self.state_key()] = state

        self.app['STATE'].save(new_state)

    def register_capability(self,
                            name,
                            handler,
                            public=False,
                            public_content_type=None):
        self.capabilities[name] = CapabilitySpec(
            handler, public, public_content_type,
        )


def _get_service_key(request, kind, instance):
    klass = request.config_dict['SETTINGS'].SERVICES.get(kind, None)
    if not klass:
        raise BaseServiceError(f'Unknown service: {kind}')

    if klass.IS_GLOBAL:
        instance = None

    return f'{kind}.{instance}'


def _lookup_service(request, kind, instance):
    service_key = _get_service_key(request, kind, instance)
    return request.config_dict['SERVICES'].get(service_key, None)


@register_method('services.start')
async def start_service(request, kind, instance, characteristics):
    data = None

    klass = request.config_dict['SETTINGS'].SERVICES.get(kind, None)
    if not klass:
        raise BaseServiceError(f'Unknown service: {kind}')
    else:
        service = _lookup_service(request, kind, instance)

        if not service:
            service_key = _get_service_key(request, kind, instance)
            service = BaseService.service(
                klass, request.app, instance, characteristics,
            )

            await service.start()
            request.config_dict['SERVICES'][service_key] = service

        data = await service.current_data()
        data.raise_if_needed()

    return data.data


@register_method('services.stop')
async def stop_service(request, kind, instance):
    result = 'ok'

    service_key = _get_service_key(request, kind, instance)
    if service_key not in request.config_dict['SERVICES']:
        raise BaseServiceError(f'Unknown service: {instance} of {kind}')
    else:
        service = request.config_dict['SERVICES'].pop(service_key)
        await service.stop()

    return result


def get_service_capability(request, kind, instance, capability):
    result = None

    service = _lookup_service(request, kind, instance)
    if not service:
        raise BaseServiceError(f'Unknown service: {instance} of {kind}')
    else:
        capability_spec = service.capabilities.get(capability, None)
        if not capability_spec:
            raise BaseServiceError(f'Unknown capability: {kind}.{capability}')
        else:
            result = capability_spec

    return result


@register_method('services.use')
async def use_service(request, kind, instance, capability, params):
    capability_spec = get_service_capability(
        request, kind, instance, capability,
    )

    result = await capability_spec.handler(*params)
    return result


async def web_use_service(request):
    try:
        kind = request.query.get('kind', None)
        capability = request.query.get('capability', None)

        capability_spec = get_service_capability(
            request,
            kind,
            request.query.get('instance', None),
            capability,
        )

        if capability_spec.public is False:
            raise BaseServiceError(f'Unknown capability: {kind}.{capability}')
    except BaseServiceError as exception:
        LOGGER.error(
            'web_use_service(): Capability lookup error.', exc_info=exception,
        )

        response_body = 'Bad Request'
        is_debug_or_testing = any((
            request.config_dict['SETTINGS'].DEBUG,
            request.config_dict['SETTINGS'].TESTING,
        ))
        if is_debug_or_testing is True:
            response_body = exception.args[0]

        return web.Response(
            body=response_body,
            content_type='text/plain',
            charset='utf-8',
            status=400,
        )

    content_type = capability_spec.public_content_type

    params = request.query.getall('params', [])
    result = await capability_spec.handler(*params)
    if isinstance(result, str):
        result = result.encode('utf-8')

    return web.Response(
        body=result, content_type=content_type, charset='utf-8',
    )


def setup(app):
    app['SERVICES'] = {}

    app.add_routes([
        web.get('/services/use', web_use_service),
    ])

    async def on_cleanup(app):
        for service in app['SERVICES'].values():
            await service.shutdown()

    app.on_cleanup.append(on_cleanup)
