from PyQt5 import QtCore, QtGui, QtWidgets, QtWebSockets, QtWebChannel, QtNetwork
import json
import auth_session
import logging
import requests
log = logging.getLogger("wizart_desktop")

class JsonPyQtEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, QtCore.QJsonValue):
            if obj.isNull():
                return None
            elif obj.isObject():
                return obj.toObject()
            elif obj.isArray():
                return obj.toArray()
            elif obj.isString():
                return obj.toString()
            elif obj.isBool():
                return obj.toBool()
            else:
                return obj.toInt()
        return json.JSONEncoder.default(self, obj)


class WebSocketTransport(QtWebChannel.QWebChannelAbstractTransport):
    def __init__(self, socket):
        super(WebSocketTransport, self).__init__(socket)
        self.socket = socket
        self.socket.textMessageReceived.connect(self.textMessageReceived)

    def sendMessage(self, message):
        json_raw = json.dumps(message, cls=JsonPyQtEncoder)
        self.socket.sendTextMessage(json_raw)

    def textMessageReceived(self, message_data):
        json_dict = json.loads(message_data)
        self.messageReceived.emit(json_dict, self)


class WebSocketClientWrapper(QtCore.QObject):
    client_connected = QtCore.pyqtSignal(QtWebChannel.QWebChannelAbstractTransport)

    def __init__(self, server, parent=None):
        super(WebSocketClientWrapper, self).__init__(parent)
        self.server = server
        self.server.newConnection.connect(self.handle_new_connection)

    def handle_new_connection(self):
        self.client_connected.emit(
            WebSocketTransport(self.server.nextPendingConnection())
        )


from command import ProdtrackCommand
from asset_browser_command import AssetBrowserCommand
from command import CommandExecutionStatus
from command import ExecuteCommandError


class ProdtrackDesktop(QtCore.QObject):
    def __init__(self, parent=None):
        super(ProdtrackDesktop, self).__init__(parent)
        self.task_watcher = None

    @QtCore.pyqtSlot(result=list)
    def get_commands(self):
        result = []
        for cmd_class in ProdtrackCommand.__subclasses__():
            result.append(cmd_class.command_type_as_dict())
        return result

    @QtCore.pyqtSlot(str, QtCore.QJsonValue, result=QtCore.QVariant)
    def execute_command(self, command_name, entity_ids_value):
        command_type = ProdtrackCommand.get_command_type_by_name(command_name)
        try:
            if not command_type:
                raise ExecuteCommandError(
                    "Not found command with name '%s'" % command_name
                )

            if not entity_ids_value.isArray():
                raise ExecuteCommandError(
                    "Incorrect type for %s entity_ids_value, expected list"
                    % entity_ids_value
                )
            entity_ids_value_array = entity_ids_value.toArray()
            entity_ids = []
            for i, val in enumerate(entity_ids_value_array):
                if not val.isDouble():
                    raise ExecuteCommandError(
                        "Incorrect value types for entity_ids[%d], expected int" % i
                    )
                else:
                    entity_ids.append(int(val.toDouble()))
            cmd = command_type(entity_ids)
            result = cmd.execute()
            if cmd.task_params:
                self.task_watcher.add_tasks(cmd.task_params)
                self.task_watcher.session = auth_session.get_fs_ctrl_session()
                self.task_watcher.start(10000)
                
            if isinstance(result, CommandExecutionStatus):
                return int(result)
        except ExecuteCommandError, e:
            log.error("Execute Command Error: %s" % e)
            return int(CommandExecutionStatus.Failed)


class AssetBrowserDesktop(QtCore.QObject):
    fs_ctrl_update = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super(AssetBrowserDesktop, self).__init__(parent)
        self.task_watcher = None

    def send_task_result(self, message):
        self.fs_ctrl_update.emit(message)

    @QtCore.pyqtSlot(result=list)
    def get_commands(self):
        result = []
        for cmd_class in AssetBrowserCommand.__subclasses__():
            result.append(cmd_class.command_type_as_dict())
        return result

    @QtCore.pyqtSlot(str, str, result=QtCore.QVariant)
    def execute_command(self, command_name, fs_data):
        command_type = AssetBrowserCommand.get_command_type_by_name(command_name)
        try:
            if not command_type:
                raise ExecuteCommandError(
                    "Not found command with name '%s'" % command_name
                )

            fs_data = json.loads(fs_data)
            cmd = command_type(**fs_data)

            result = cmd.execute()
            if cmd.task_params:
                self.task_watcher.add_tasks(cmd.task_params)
                self.task_watcher.session = auth_session.get_fs_ctrl_session()
                self.task_watcher.start(10000)

            if isinstance(result, CommandExecutionStatus):
                return int(result)

        except ExecuteCommandError, e:
            log.error("Execute Command Error: %s" % e)
            return int(CommandExecutionStatus.Failed)


class TaskWatcher(QtCore.QObject):
    """
    TaskWatcher was created to get information about the execution of commands
    on the FS and transfer this information to the logger
    """

    task_finish = QtCore.pyqtSignal(str)

    PREFIX = '{name}: {args}'
    SUCCESS = 'FS Task ID {id} {prefix} completed successfully.'
    FAILURE = 'FS Task ID {id} {prefix} failed.'
    ERROR = 'FS Task ID {id} {prefix} execution error. Code {status_code}: {reason}.'
    ATTEMPT = 'FS Task ID {id} {prefix} in progress. Task status {status}. Attempts left: {attempts_left}.'
    ATTEMPT_ENDED = 'FS Task ID {id} {prefix} in progress. Task status {status}.The attempts ended. Tracking stopped.'
    URL = 'http://fs_ctrl.burut.net/api/task_info/{}'

    def __init__(self, parent=None):
        super(TaskWatcher, self).__init__(parent)
        self.tasks = {}
        self.__timer = QtCore.QTimer(self)
        self.__timer.timeout.connect(self.check_tasks)
        self.session = None

    def start(self, interval):
        if self.tasks and not self.__timer.isActive():
            self.__timer.start(interval)

    def stop(self):
        self.__timer.stop()

    def add_task(self, identifier, prefix):
        if not identifier in self.tasks:
            self.tasks[identifier] = {'prefix': prefix, 'attempts_left': 10}

    def add_tasks(self, tasks):
        if tasks:
            for identifier, param in tasks.iteritems():
                prefix = self.PREFIX.format(**param)
                self.add_task(identifier, prefix)

    def remove_task(self, identifier):
        if identifier in self.tasks:
            del self.tasks[identifier]

    def push_msg_in_log(self, msg, is_error=False):
        log_mode = log.error if is_error else log.info
        for line in msg.split('\n'):
            if line:
                log_mode(line)

    def check_tasks(self):
        if not self.tasks:
            self.stop()
            return
        for identifier in self.tasks.keys():
            self.tasks[identifier]['attempts_left'] -= 1
            attempts = self.tasks[identifier]['attempts_left']
            url = self.URL.format(identifier)
            response = self.session.get(url)
            if response.status_code == requests.codes.ok:
                data = response.json()
                if data['status'] == 'SUCCESS':
                    result = json.loads(data['result'])
                    self.push_msg_in_log(result['log'])
                    message = self.SUCCESS.format(id=identifier, **self.tasks[identifier]) 
                    log.info(message)
                    self.task_finish.emit(message)
                    self.remove_task(identifier)
                elif data['status'] == 'FAILURE':
                    traceback = json.loads(data['traceback'])
                    self.push_msg_in_log(traceback, is_error=True)
                    message = self.FAILURE.format(id=identifier, **self.tasks[identifier])
                    log.error(message)
                    self.task_finish.emit(message)
                    self.remove_task(identifier)
                else:
                    msg = self.ATTEMPT if attempts else self.ATTEMPT_ENDED
                    log.info(msg.format(id=identifier, status=data['status'], **self.tasks[identifier]))
                    if not attempts:
                        self.remove_task(identifier)
            elif response.status_code == requests.codes.not_found:
                msg = self.ATTEMPT if attempts else self.ATTEMPT_ENDED
                status = '{code}: {reason}'.format(code=response.status_code, reason=response.reason)
                log.info(msg.format(id=identifier, status=status, **self.tasks[identifier]))
                if not attempts:
                    self.remove_task(identifier)
            else:
                status = '{code}: {reason}'.format(code=response.status_code, reason=response.reason)
                message = self.ERROR.format(id=identifier, status=status, **self.tasks[identifier])
                log.error(message)
                self.task_finish.emit(message)
                self.remove_task(identifier)


class CommandServer(QtCore.QObject):
    def __init__(self, parent=None):
        super(CommandServer, self).__init__(parent)
        self.port = 9000
        self.server = QtWebSockets.QWebSocketServer(
            "Wizart Desktop Command Server", QtWebSockets.QWebSocketServer.NonSecureMode
        )
        if not self.server.listen(QtNetwork.QHostAddress.LocalHost, self.port):
            log.warning("Failed to open websocket on port:%d" % self.port)
        else:
            log.info("Started Websocket Command Server on port:%d" % self.port)
        self.client_wrapper = WebSocketClientWrapper(self.server)
        self.channel = QtWebChannel.QWebChannel()
        self.client_wrapper.client_connected.connect(self.channel.connectTo)
        self.backend = ProdtrackDesktop(self)
        self.backend.task_watcher = TaskWatcher(self)
        self.ab_backend = AssetBrowserDesktop(self)
        self.ab_backend.task_watcher = TaskWatcher(self)
        self.ab_backend.task_watcher.task_finish.connect(self.ab_backend.send_task_result)
        self.channel.registerObject("ProdtrackDesktop", self.backend)
        self.channel.registerObject("AssetBrowserDesktop", self.ab_backend)

