import logging
import time

from .api import ClassApiMixin, PodApiMixin, ReservationApiMixin, SystemApiMixin, UserApiMixin, VmApiMixin
from .auth import get_system_config
from .connection import Connection, SSLConnection
from .errors import error_decode, ResponseFormatError
from .serializer import serialize, deserialize

log = logging.getLogger(__name__)


class Client(ClassApiMixin, PodApiMixin, ReservationApiMixin, SystemApiMixin, UserApiMixin, VmApiMixin):
    def __init__(self, system='default', config=None, config_path=None):
        """
        A client for communicating with a NETLAB+ server.

        :param str system: The name to identify a NETLAB+ system. Defaults to **'default'**.
        :param dict config: Configuration options. See below for all possible options for the :attr:`config`.
        :param str config_path: File path location of a JSON config file to be used instead of the default location.

        :returns: A client instance with which you can use to interact with a remote NETLAB+ system.
        """
        self.id = 0
        self.system = system
        self.config = get_system_config(system, config, config_path)

        if self.config.ssl:
            connection_class = SSLConnection
        else:
            connection_class = Connection

        self.connection = connection_class(self.config.user, self.config.token, self.config.host, self.config.port,
                                           socket_timeout=self.config.timeout)
        self.connection.connect()

    def __repr__(self):
        return "NetlabClient<system={system},user={user},host={host}>".format(
            system=self.system, **self.config._asdict()
        )

    def call(self, method, task_wait=True, task_poll=3, **kwargs):
        """
        Calls an API method.

        .. warning::
            This is a low-level API method and should not be used by most users.

        **Required:**

        :param str method: the method name

        **Optional:**

        :param bool task_wait: wait on task True/False
        :param int task_poll: number of seconds between task polling (default 3)

        **Keyword Arguments:**

        All other named arguments are passed as parameters to the
        API backend and are specific to the method type.  See the API
        documentation for method parameters.

        **Tasks:**

        Methods ending in ".task" are long running tasks.  Tasks may
        be invoked syncronously or asyncronously by setting task_wait.

        * If *task_wait == True*, call() will continuously poll the task,
          wait for completion or error, and return task result on success.
          This is the default if task_wait is not specified.
        * If *task_wait == False*, call() will return the task_id of the
          created task but will not wait.  This may be helpful if you
          want to do other work while waiting, or run tasks in parallel.
          Call task_check() to get the current task state, task progress,
          task result, or task error.
          Call task_wait() at a later time to wait on a task.

        :returns: A value or an object (check the API documentation).

        :raises NetlabError: if the NETLAB+ server returns an error response.
        """
        if method.endswith('.task'):
            if type(task_wait) is not bool:
                raise ResponseFormatError('argument "task_wait" must be bool')
            if type(task_poll) is not int or task_poll < 0:
                raise ResponseFormatError('task_poll: positive int required')
        else:
            task_wait = False

        if self.id < 2 ^ 62:
            self.id += 1
        else:
            self.id = 1
        rpc = {
            'id': self.id,
            'jsonrpc': '2.0',
            'method': method,
            'params': kwargs
        }
        request = serialize(rpc)
        log.debug("send %s", request)
        self.connection.send(request)

        raw_response = self.connection.read_response()
        log.debug("recv %s", raw_response)
        response = deserialize(raw_response)

        if type(response) is not dict:
            raise ResponseFormatError('Received invalid response, not dict')
        if response['id'] != self.id:
            raise ResponseFormatError('Reply id did not match request')
        if 'result' in response:
            if task_wait:
                task_id = response['result']
                result = self.task_wait(task_id, task_poll)
                return result
            else:
                return response['result']
        if 'error' in response:
            raise error_decode(response['error'])
        raise ResponseFormatError('Response did not contain result or error')

    def task_wait(self, task_id, task_poll=3):
        if task_poll <= 0:
            raise ResponseFormatError('task_poll must be int > 0')
        while True:
            status = self.task_check(task_id)
            if status['is_complete']:
                if status['error'] is not None:
                    raise error_decode(status['error'])
                return status['result']
            time.sleep(task_poll)

    def task_check(self, task_id):
        return self.call('task.check', task_id=task_id)
