__version__ = '0.0.3'

import elbus
import sys
import msgpack
import logging
import time
import signal
import uuid

ACTION_CREATED = 0b0000_0000
ACTION_ACCEPTED = 0b0000_0001
ACTION_PENDING = 0b0000_0010
ACTION_RUNNING = 0b0000_1000
ACTION_COMPLETED = 0b0000_1111
ACTION_FAILED = 0b1000_0000
ACTION_CANCELED = 0b1000_0001
ACTION_TERMINATED = 0b1000_0010

ITEM_STATUS_ERROR = -1

ERR_CODE_NOT_FOUND = -32001
ERR_CODE_ACCESS_DENIED = -32002
ERR_CODE_SYSTEM_ERROR = -32003
ERR_CODE_OTHER = -32004
ERR_CODE_NOT_READY = -32005
ERR_CODE_UNSUPPORTED = -32006
ERR_CODE_CORE_ERROR = -32007
ERR_CODE_TIMEOUT = -32008
ERR_CODE_INVALID_DATA = -32009
ERR_CODE_FUNC_FAILED = -32010
ERR_CODE_ABORTED = -32011
ERR_CODE_ALREADY_EXISTS = -32012
ERR_CODE_BUSY = -32013
ERR_CODE_METHOD_NOT_IMPLEMENTED = -32014
ERR_CODE_TOKEN_RESTRICTED = -32015
ERR_CODE_IO = -32016
ERR_CODE_REGISTRY = -32017
ERR_CODE_EVAHI_AUTH_REQUIRED = -32018

ERR_CODE_PARSE = -32700
ERR_CODE_INVALID_REQUEST = -32600
ERR_CODE_METHOD_NOT_FOUND = -32601
ERR_CODE_INVALID_PARAMS = -32602
ERR_CODE_INTERNAL_RPC = -32603

ERR_CODE_ELBUS_CLIENT_NOT_REGISTERED = -32113
ERR_CODE_ELBUS_DATA = -32114
ERR_CODE_ELBUS_IO = -32115
ERR_CODE_ELBUS_OTHER = -32116
ERR_CODE_ELBUS_NOT_SUPPORTED = -32117
ERR_CODE_ELBUS_BUSY = -32118
ERR_CODE_ELBUS_NOT_DELIVERED = -32119
ERR_CODE_ELBUS_TIMEOUT = -32120


def _action_event_payload(u, status, out=None, err=None):
    payload = {
        'uuid': u.bytes,
        'status': status,
    }
    if out is not None:
        payload['out'] = out
    if err is not None:
        payload['err'] = err
    return msgpack.dumps(payload)


class OID:

    def __init__(self, s):
        self.kind, self.full_id = s.split(':', maxsplit=1)
        self.oid = s

    def __str__(self):
        return self.oid

    def to_path(self):
        return f'{self.kind}/{self.full_id}'


class ServiceInfo:

    def __init__(self, author='', description='', version=''):
        self.author = author
        self.description = description
        self.version = version
        self.methods = {}

    def add_method(self, method, description='', required=[], optional=[]):
        params = {}
        for p in required:
            params[p] = {'required': True}
        for p in optional:
            params[p] = {'required': False}
        m = {'description': description, 'params': params}
        self.methods[method] = m

    def serialize(self):
        d = {
            'author': self.author,
            'description': self.description,
            'version': self.version
        }
        if self.methods:
            d['methods'] = self.methods
        return d


class Action:

    def __init__(self, event):
        a = msgpack.loads(event.get_payload(), raw=False)
        self.uuid = uuid.UUID(bytes=a['uuid'])
        self.i = OID(a['i'])
        self.timeout = a['timeout']
        self.priority = a['priority']
        self.params = a['params']


class Controller:

    def __init__(self, bus):
        self.bus = bus

    def event_pending(self, action):
        self.bus.send(
            f'ACT/{action.i.to_path()}',
            elbus.client.Frame(payload=_action_event_payload(
                action.uuid, ACTION_PENDING),
                               tp=elbus.client.OP_PUBLISH,
                               qos=0))

    def event_running(self, action):
        self.bus.send(
            f'ACT/{action.i.to_path()}',
            elbus.client.Frame(payload=_action_event_payload(
                action.uuid, ACTION_RUNNING),
                               tp=elbus.client.OP_PUBLISH,
                               qos=0))

    def event_completed(self, action, out=None):
        self.bus.send(
            f'ACT/{action.i.to_path()}',
            elbus.client.Frame(payload=_action_event_payload(action.uuid,
                                                             ACTION_COMPLETED,
                                                             out=out),
                               tp=elbus.client.OP_PUBLISH,
                               qos=0))

    def event_failed(self, action, out=None, err=None):
        self.bus.send(
            f'ACT/{action.i.to_path()}',
            elbus.client.Frame(payload=_action_event_payload(action.uuid,
                                                             ACTION_FAILED,
                                                             out=out,
                                                             err=err),
                               tp=elbus.client.OP_PUBLISH,
                               qos=0))

    def event_canceled(self, action):
        self.bus.send(
            f'ACT/{action.i.to_path()}',
            elbus.client.Frame(payload=_action_event_payload(
                action.uuid, ACTION_CANCELED),
                               tp=elbus.client.OP_PUBLISH,
                               qos=0))

    def event_terminated(self, action):
        self.bus.send(
            f'ACT/{action.i.to_path()}',
            elbus.client.Frame(payload=_action_event_payload(
                action.uuid, ACTION_TERMINATED),
                               tp=elbus.client.OP_PUBLISH,
                               qos=0))


class EvaLogHandler(logging.Handler):

    def __init__(self, bus):
        self.bus = bus
        super().__init__()

    def emit(self, record):
        msg = self.format(record)
        if record.levelno == 1:
            topic = 'LOG/IN/trace'
        elif record.levelno <= 10:
            topic = 'LOG/IN/debug'
        elif record.levelno <= 20:
            topic = 'LOG/IN/info'
        elif record.levelno <= 30:
            topic = 'LOG/IN/warn'
        else:
            topic = 'LOG/IN/error'
        self.bus.send(
            topic,
            elbus.client.Frame(msg.encode(), tp=elbus.client.OP_PUBLISH, qos=0))


class Service:

    def __init__(self):
        self.active = True
        self.initial = msgpack.loads(sys.stdin.buffer.read(), raw=False)
        self.name = self.initial['name']
        self.system_name = self.initial['system_name']
        self.sleep_step = 0.1
        self.timeout = self.initial['timeout']
        self.log_format = ('mod:%(module)s fn:%(funcName)s '
                           'th:%(threadName)s :: %(message)s')
        self.log_formatter = logging.Formatter(self.log_format)
        self.on_rpc_call = elbus.rpc.on_call_default

    def get_config(self):
        return self.initial.get('config', {})

    def init_bus(self):
        bus_config = self.initial['bus']
        if bus_config['type'] != 'elbus':
            raise ValueError(f'bus {bus_config["type"]} is not supported')
        self.bus = elbus.client.Client(bus_config['path'], self.name)
        self.bus.buf_size = bus_config['buf_size']
        self.bus.timeout = bus_config['timeout']
        self.bus.ping_interval = bus_config['ping_interval']
        self.bus.connect()

    def init_rpc(self, svc_info):
        self._svc_info = msgpack.dumps(svc_info.serialize())
        self.rpc = elbus.rpc.Rpc(self.bus)
        self.rpc.on_call = self._handle_rpc_call

    def _handle_rpc_call(self, event):
        method = event.method
        if method == b'test':
            return
        elif method == b'info':
            return self._svc_info
        elif method == b'stop':
            self.active = False
            return
        else:
            return self.on_rpc_call(event)

    def init_logs(self):
        level = self.initial['core']['log_level']
        logging.basicConfig(level=level)
        logger = logging.getLogger()
        while logger.hasHandlers():
            logger.removeHandler(logger.handlers[0])
        self.log_handler = EvaLogHandler(self.bus)
        self.log_handler.setLevel(level=level)
        self.log_handler.setFormatter(self.log_formatter)
        logger.addHandler(self.log_handler)

        def log_trace(msg, *args, **kwargs):
            logger.log(1, msg, *args, **kwargs)

        logger.trace = log_trace
        return logger

    def block(self):
        sleep_step = self.sleep_step
        while self.active and self.bus.is_connected():
            time.sleep(sleep_step)

    def mark_ready(self):
        self._mark('ready')

    def mark_terminating(self):
        self.active = False
        self._mark('terminating')

    def _mark(self, status):
        self.bus.send(
            'SVC/ST',
            elbus.client.Frame(msgpack.dumps({'status': status}),
                               tp=elbus.client.OP_PUBLISH,
                               qos=0))

    def register_signals(self):
        signal.signal(signal.SIGINT, self._term_handler)
        signal.signal(signal.SIGTERM, self._term_handler)

    def _term_handler(self, sig, frame):
        self.active = False


def no_rpc_method():
    raise elbus.rpc.RpcException('no such method',
                                 elbus.rpc.RPC_ERROR_CODE_METHOD_NOT_FOUND)


def log_traceback():
    import traceback
    print(traceback.format_exc(), flush=True, file=sys.stderr)
