
from __future__ import absolute_import

import contextlib
import functools
import uuid

from vdsm.api import vdsmapi

from yajsonrpc import stompclient
from yajsonrpc import stomp

import yajsonrpc


DEFAULT_PORT = 54321


def connect(
        host, port=DEFAULT_PORT, use_tls=True, timeout=60,
        gluster_enabled=False, incoming_heartbeat=stomp.DEFAULT_INCOMING,
        outgoing_heartbeat=stomp.DEFAULT_OUTGOING,
        nr_retries=stomp.NR_RETRIES):
    try:
        client = stompclient.SimpleClient(
            host, port, use_tls, incoming_heartbeat=incoming_heartbeat,
            outgoing_heartbeat=outgoing_heartbeat, nr_retries=nr_retries)

    except Exception as e:
        raise ConnectionError(host, port, use_tls, timeout, e)

    return _Client(client, timeout, gluster_enabled)


class Error(Exception):
    msg = ""

    def __str__(self):
        return self.msg.format(self=self)


class ConnectionError(Error):
    msg = ("Connection to {self.host}:{self.port} with use_tls={self.use_tls},"
           " timeout={self.timeout} failed: {self.reason}")

    def __init__(self, host, port, use_tls, timeout, reason):
        self.host = host
        self.port = port
        self.use_tls = use_tls
        self.timeout = timeout
        self.reason = reason


class MissingSchemaError(Error):
    msg = "Error parsing schema: {self.reason}"

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


class TimeoutError(Error):
    msg = ("Request {self.cmd} with args {self.params} timed out "
           "after {self.timeout} seconds")

    def __init__(self, cmd, params, timeout):
        self.cmd = cmd
        self.params = params
        self.timeout = timeout


class ClientError(Error):
    msg = ("Request {self.cmd} with args {self.params} failed: {self.reason}")

    def __init__(self, cmd, params, reason):
        self.cmd = cmd
        self.params = params
        self.reason = reason


class ServerError(Error):
    msg = ("Command {self.cmd} with args {self.params} failed:\n"
           "(code={self.code}, message={self.message})")

    def __init__(self, cmd, params, code, message):
        self.cmd = cmd
        self.params = params
        self.code = code
        self.message = message


class Namespace(object):
    def __init__(self, name, call):
        self._name = name
        self._call = call
        self.methods = []

    def __getattr__(self, method_name):
        return functools.partial(self._call, self._name, method_name)


class _Client(object):
    def __init__(self, client, default_timeout, gluster_enabled=False):
        self._client = client
        self._default_timeout = default_timeout
        self._flow_id = None
        self._init_schema(gluster_enabled)
        self._create_namespaces()

    def _init_schema(self, gluster_enabled):
        try:
            self._schema = vdsmapi.Schema.vdsm_api(
                strict_mode=False, with_gluster=gluster_enabled)
            self._event_schema = vdsmapi.Schema.vdsm_events(strict_mode=False)
        except vdsmapi.SchemaNotFound as e:
            raise MissingSchemaError(e)

    def _create_namespaces(self):
        for method in self._schema.get_methods:
            namespace, method = method.split('.', 1)
            if not hasattr(self, namespace):
                setattr(self, namespace, Namespace(namespace, self._call))
            getattr(self, namespace).methods.append(method)

    def _call(self, namespace, method_name, **kwargs):
        method = namespace + "." + method_name
        timeout = kwargs.pop("_timeout", self._default_timeout)

        req = yajsonrpc.JsonRpcRequest(
            method, kwargs, reqId=str(uuid.uuid4()))

        try:
            responses = self._client.call(
                req, timeout=timeout, flow_id=self._flow_id)
        except EnvironmentError as e:
            raise ClientError(method, kwargs, e)

        if not responses:
            raise TimeoutError(method, kwargs, timeout)


        resp = responses[0]
        if resp.error:
            raise ServerError(
                method, kwargs, resp.error.code, str(resp.error))

        return resp.result

    def close(self):
        self._client.close()

    @contextlib.contextmanager
    def flow(self, flow_id):
        try:
            self._flow_id = flow_id
            yield
        finally:
            self._flow_id = None

    def subscribe(self, queue_name, event_queue=None):
        return self._client.subscribe(queue_name, event_queue)

    def unsubscribe(self, sub_id):
        self._client.unsubscribe(sub_id)

    def notify(self, event_id, dest, params=None):
        self._client.notify(event_id, dest, self._event_schema, params)
