# -*- coding: utf-8 -*-
import gevent.monkey
gevent.monkey.patch_all()
import logging
from januscloud.common.utils import error_to_janus_msg, create_janus_msg, get_monotonic_time, random_uint64
from januscloud.common.error import JanusCloudError, JANUS_ERROR_SESSION_CONFLICT, \
    JANUS_ERROR_BAD_GATEWAY, JANUS_ERROR_GATEWAY_TIMEOUT, JANUS_ERROR_SERVICE_UNAVAILABLE
from januscloud.common.schema import Schema, Optional, DoNotCare, \
    Use, IntVal, Default, SchemaError, BoolVal, StrRe, ListVal, Or, STRING, \
    FloatVal, AutoDel
import time
import gevent
from gevent.event import Event
from januscloud.transport.ws import WSClient
from januscloud.proxy.core.backend_handle import BackendHandle

log = logging.getLogger(__name__)


BACKEND_SESSION_STATE_CREATING = 1
BACKEND_SESSION_STATE_ACTIVE = 2
BACKEND_SESSION_STATE_DESTROYED = 3


class BackendTransaction(object):
    def __init__(self, transaction_id, request_msg, url, ignore_ack=True):
        self.transaction_id = transaction_id
        self.request_msg = request_msg
        self._response_ready = Event()
        self._response = None
        self._ignore_ack = ignore_ack
        self._url = url

    def wait_response(self, timeout=None):
        ready = self._response_ready.wait(timeout=timeout)
        if not ready:
            raise JanusCloudError('Request {} Timeout for backend Janus server: {}'.format(self.request_msg, self._url),
                                  JANUS_ERROR_GATEWAY_TIMEOUT)
        return self._response

    @property
    def response(self):
        return self._response

    @response.setter
    def response(self, response):
        method = response.get('janus', None)
        if self._ignore_ack and method == 'ack':
            return   # not consider ack is response
        self._response = response
        self._response_ready.set()


class BackendSession(object):
    """ This backend session represents a session of the backend Janus server """

    def __init__(self, url, auto_destroy=False, api_secret=''):
        self.url = url
        self._ws_client = None
        self._transactions = {}
        self.session_id = 0
        self.state = BACKEND_SESSION_STATE_CREATING
        self._handles = {}
        self._auto_destroy = int(auto_destroy)
        self._auto_destroy_greenlet = None
        self._keepalive_interval = 10
        self._keepalive_greenlet = None
        self._api_secret = api_secret
        _sessions[url] = self

    def init(self):
        try:
            self._ws_client = WSClient(self.url, self._recv_msg_cbk, self._close_cbk, protocols=['janus-protocol'])
            session_timeout = self._get_session_timeout()
            if session_timeout:
                self._keepalive_interval = int(session_timeout / 3)
            self.session_id = self._create_janus_session()
            self.state = BACKEND_SESSION_STATE_ACTIVE
            self._keepalive_greenlet = gevent.spawn(self._keepalive_routine)
        except Exception:
            if self._ws_client:
                self._ws_client.close()
                self._ws_client = None
            self.session_id = 0
            self._keepalive_greenlet = None
            self.state = BACKEND_SESSION_STATE_CREATING
            raise

    def attach_handle(self, plugin_package_name, opaque_id=None, handle_listener=None):
        """

        :param plugin_pacakge_name:  str plugin package name
        :param opaque_id:   str opaque id
        :param handle_listener: handle related  callback listener which cannot block
        :return: BackendHandle object
        """
        if self.state == BACKEND_SESSION_STATE_DESTROYED:
            raise JanusCloudError('Session has destroy for Janus server: {}'.format(self.url),
                                  JANUS_ERROR_SERVICE_UNAVAILABLE)

        attach_request_msg = create_janus_msg('attach', plugin=plugin_package_name)
        if opaque_id:
            attach_request_msg['opaque_id'] = opaque_id

        response = self.send_request(attach_request_msg)  # would block for IO
        if response['janus'] == 'success':
             handle_id = response['data']['id']
        elif response['janus'] == 'error':
            raise JanusCloudError(
                'attach error for Janus server {} with reason {}'.format(self.url, response['error']['reason']),
                response['error']['code'])
        else:
            raise JanusCloudError(
                'attach error for Janus server: {} with invalid response {}'.format(self.url, response),
                JANUS_ERROR_BAD_GATEWAY)

        # check again when wake up from block IO
        if self.state == BACKEND_SESSION_STATE_DESTROYED:
            raise JanusCloudError('Session has destroy for Janus server: {}'.format(self.url),
                                  JANUS_ERROR_SERVICE_UNAVAILABLE)

        handle = BackendHandle(handle_id, plugin_package_name, self,
                               opaque_id=opaque_id, handle_listener=handle_listener)
        self._handles[handle_id] = handle
        if self._auto_destroy_greenlet:
            gevent.kill(self._auto_destroy_greenlet)
            self._auto_destroy_greenlet = None
        return handle

    def get_handle(self, handle_id, default=None):
        return self._handles.get(handle_id, default)

    def on_handle_detached(self, handle_id):
        self._handles.pop(handle_id, None)

    def send_request(self, msg, ignore_ack=True, timeout=30):

        if self.state == BACKEND_SESSION_STATE_DESTROYED:
            raise JanusCloudError('Session has destroy for Janus server: {}'.format(self.url),
                                  JANUS_ERROR_SERVICE_UNAVAILABLE)
        transaction_id = self._genrate_new_tid()

        send_msg = dict.copy(msg)
        send_msg['session_id'] = self.session_id
        send_msg['transaction'] = transaction_id
        if self._api_secret:
            send_msg['apisecret'] = self._api_secret
        transaction = BackendTransaction(transaction_id, send_msg, url=self.url, ignore_ack=ignore_ack)
        try:
            self._transactions[transaction_id] = transaction
            log.debug('Send Request {} to Janus server: {}'.format(send_msg, self.url))
            self._ws_client.send_message(send_msg)
            response = transaction.wait_response(timeout=timeout)
            log.debug('Receive Response {} from Janus server: {}'.format(response, self.url))
            return response
        finally:
            self._transactions.pop(transaction_id, None)

    def destroy(self):
        if self.state == BACKEND_SESSION_STATE_DESTROYED:
            return
        self.state = BACKEND_SESSION_STATE_DESTROYED
        if _sessions.get(self.url) == self:
            _sessions.pop(self.url)

        if self._auto_destroy_greenlet:
            gevent.kill(self._auto_destroy_greenlet)
            self._auto_destroy_greenlet = None

        for handle in self._handles.values():
            handle.on_close()
        self._handles.clear()

        if self._ws_client:
            try:
                self._ws_client.close()
            except Exception:
                pass
            self._ws_client = None

    def _close_cbk(self):
        if self.state == BACKEND_SESSION_STATE_DESTROYED:
            return
        log.info('Backend session {} is closed by under network'.format(self.session_id))
        self._ws_client = None
        self.destroy()


    def _auto_destroy_routine(self):
        log.info('Backend session {} is auto destroyed'.format(self.session_id))
        self._auto_destroy_greenlet = None
        self.destroy()

    def _recv_msg_cbk(self, msg):
        try:
            if 'transaction' in msg:
                transaction = self._transactions.get(msg['transaction'], None)
                if transaction:
                    transaction.response = msg
            elif msg['janus'] == 'timeout':
                log.debug('Receive session timeout from Janus server: {}'.format(self.url))
                self.destroy()
            elif msg['janus'] == 'detached':
                log.debug('Receive async event {} from Janus server: {}'.format(msg, self.url))
                handle = self._handles.pop(msg['sender'], None)
                if handle:
                    handle.on_close()
            elif 'sender' in msg:
                log.debug('Receive async event {} from Janus server: {}'.format(msg, self.url))
                handle = self._handles.get(msg['sender'], None)
                if handle:
                    handle.on_async_event(msg)
            else:
                log.warn('Receive a invalid message {} on session {} for server {}'.format(msg, self.session_id, self.url))
        except Exception:
            log.exception('Received a malformat msg {}'.format(msg))

    def _genrate_new_tid(self):
        tid = str(random_uint64())
        while tid in self._transactions:
            tid = str(random_uint64())
        return tid

    def _get_session_timeout(self):
        response = self.send_request(create_janus_msg('info'))
        if response['janus'] == 'server_info':
            return response.get('session-timeout', 30)
        elif response['janus'] == 'error':
            raise JanusCloudError(
                'Create session error for Janus server {} with reason {}'.format(self.url, response['error']['reason']),
                response['error']['code'])
        else:
            raise JanusCloudError(
                'Create session error for Janus server: {} with invalid response {}'.format(self.url, response),
                JANUS_ERROR_BAD_GATEWAY)

    def _create_janus_session(self):
        response = self.send_request(create_janus_msg('create'))
        if response['janus'] == 'success':
            return response['data']['id']
        elif response['janus'] == 'error':
            raise JanusCloudError(
                'Create session error for Janus server {} with reason {}'.format(self.url, response['error']['reason']),
                response['error']['code'])
        else:
            raise JanusCloudError(
                'Create session error for Janus server: {} with invalid response {}'.format(self.url, response),
                JANUS_ERROR_BAD_GATEWAY)

    def _keepalive_routine(self):
        gevent.sleep(self._keepalive_interval)
        keepalive_msg = create_janus_msg('keepalive')
        while self.state == BACKEND_SESSION_STATE_ACTIVE:
            try:
                # if there is no handle existed and auto destroy is enabled, just schedule the destroy route
                if not self._handles:
                    if self._auto_destroy and self._auto_destroy_greenlet is None:
                        self._auto_destroy_greenlet = gevent.spawn_later(self._auto_destroy, self._auto_destroy_routine)

                self.send_request(keepalive_msg, ignore_ack=False)

            except Exception as e:
                log.exception('Keepalive failed for backend session {}'.format(self.url))
                self.destroy()
            else:
                gevent.sleep(self._keepalive_interval)


_sessions = {}

_api_secret = ''


def get_backend_session(server_url, auto_destroy=False):
    session = _sessions.get(server_url)
    if session is None:
        # create new session
        session = \
            BackendSession(server_url, auto_destroy=auto_destroy, api_secret=_api_secret)
        try:
            session.init()
        except Exception as e:
            session.destroy()
            raise JanusCloudError('Failed to create backend session for Janus server: {} for reason:{}'
                                  .format(server_url, str(e)), JANUS_ERROR_BAD_GATEWAY)

    # wait for session init complete
    while session.state != BACKEND_SESSION_STATE_ACTIVE:
        if session.state == BACKEND_SESSION_STATE_CREATING:
            gevent.sleep(0.01)
        elif session.state == BACKEND_SESSION_STATE_DESTROYED:
            raise JanusCloudError('Failed to create backend session for Janus server: {}'.format(server_url),
                                    JANUS_ERROR_BAD_GATEWAY)
    return session


def set_api_secret(api_secret):
    global _api_secret
    _api_secret = api_secret

if __name__ == '__main__':
    from januscloud.common.logger import test_config
    test_config(debug=True)
    session = get_backend_session('ws://127.0.0.1:8188', auto_destroy=5)
    print('create session successful')
    handle = session.attach_handle('janus.plugin.echotest1')
    gevent.sleep(5)
    handle.detach()
    gevent.sleep(5)
    print('destroy session')
    session.destroy()
    gevent.sleep(20)