from __future__ import annotations

import logging
from pathlib import Path
from queue import Queue, Empty
from typing import Sequence, Optional

import wx
import wx.lib.newevent

from clarabot_nano.common.config.base import AbstractCoreZmqClientMixin
from clarabot_nano.common.core.message import (
    MSG_TYPE_SHUTDOWN,
    MSG_TYPE_UPDATED_CONFIG,
    MSG_TYPE_CONTROL_REQ_STATS,
    MSG_TYPE_CONTROL_INDEXER_WORK,
    MSG_TYPE_CONTROL_SERVICE_ERROR,
)
from clarabot_nano.common.log import LoggingBootstrap
from clarabot_nano.common.spawn.constants import ShortNames
from clarabot_nano.common.zmq.base import AbstractZmqSocketThread
from clarabot_nano.common.zmq.constants import REQUEST_MESSAGE, NOTIFY_ID, REPLY_MESSAGE
from clarabot_nano.common.zmq.dealer_client import ZmqDealerClientThreadMixin
from clarabot_nano.common.zmq.handler import ZmqMessageHandler
from clarabot_nano.common.zmq.inproc_client import InprocClient
from clarabot_nano.control.utility import Signal, EventSignal
from clarabot_nano.utility.serialize import msgpack_loads, uint2bytes

logger = logging.getLogger(__name__)

CoreMessageEvent, EVT_CORE_MESSAGE = wx.lib.newevent.NewEvent()


class _CoreZmqClient(AbstractCoreZmqClientMixin, ZmqDealerClientThreadMixin, AbstractZmqSocketThread):
    """
    GUI-event integrated core zmq client.
    """

    def __init__(self, core_port, core_auth_data, inproc_bind_name, core_buffer: WxCoreBuffer):
        AbstractCoreZmqClientMixin.__init__(self)
        ZmqDealerClientThreadMixin.__init__(self, core_port, core_auth_data)
        AbstractZmqSocketThread.__init__(self, inproc_bind_name)
        self._core_buffer = core_buffer

    def create_inproc_client(self):
        return InprocClient(self._inproc_queue, self.inproc_name)

    def _thread_socket_send_to_logic(self, data, inproc_socket, poll):
        self._core_buffer.queue_message(data)


class WxCoreBuffer(object):
    """
    Queue messages from the core-thread and notify the GUI thread when it is set-up.
    """

    def __init__(self):
        self.queue = Queue()
        self._wx_handler = None
        self.core, self.initial_config = _CoreZmqClient.create(self._core_exit_callback, self.__class__.__name__, self)
        LoggingBootstrap.setup_logging(self.initial_config["local"]["log_config"], ShortNames.CONTROL, with_pid=True)

    def close(self):
        self._wx_handler = None
        self.core.stop()

    def _core_exit_callback(self, *args):
        self.queue_message([MSG_TYPE_SHUTDOWN])

    def queue_message(self, message: Sequence[bytes]):
        self.queue.put_nowait(message)
        if self._wx_handler:
            wx.PostEvent(self._wx_handler, CoreMessageEvent())

    def set_handler(self, handler):
        self._wx_handler = handler
        wx.PostEvent(self._wx_handler, CoreMessageEvent())


class WxCoreInterface(wx.EvtHandler):
    """
    This is the window to the Nano instance's state.
    """

    _ID_WRAP = 2 ** 31 - 1

    def __init__(self, core_buffer: WxCoreBuffer):
        wx.EvtHandler.__init__(self)
        self._core_buffer = core_buffer
        self.__config = self._core_buffer.initial_config
        # create signals
        self.config_changed = Signal()
        self.gateway_command_result = Signal()
        self.request_stat_signal = Signal()
        self.indexer_work_signal = Signal()
        self.service_error_signal = Signal()
        self.core_died = EventSignal()
        self.core_reply_signal = Signal()
        # error reporting
        self._errors = 0
        # event map
        self.Bind(EVT_CORE_MESSAGE, self._on_core_message)
        # request tracking
        self._gateway_request_id = None
        self._gateway_command_timer = wx.Timer(self)
        self.Bind(wx.EVT_TIMER, self._on_gateway_command_timeout, self._gateway_command_timer)
        self._request_id = 1
        # Signal handling is not updated, the end of the core-thread will notify the application to terminate.
        self._core_client = self._core_buffer.core.create_inproc_client()
        self._handler = ZmqMessageHandler(
            {
                MSG_TYPE_SHUTDOWN: self._handle_core_shutdown,
                MSG_TYPE_UPDATED_CONFIG: self._handle_core_updated_config,
                MSG_TYPE_CONTROL_REQ_STATS: self._handle_request_stats,
                MSG_TYPE_CONTROL_INDEXER_WORK: self._handle_indexer_work,
                MSG_TYPE_CONTROL_SERVICE_ERROR: self._handle_service_error,
                REPLY_MESSAGE: self._handle_core_reply,
            }
        )
        # initialise the core-buffer and process buffered/waiting messages automatically later
        self._core_buffer.set_handler(self)

    def gateway_request(self, multipart_message: Sequence[bytes], timeout: int = 25000) -> Optional[bytes]:
        if self._gateway_request_id:
            return  # TODO: error?
        self._gateway_request_id = self.core_request(multipart_message, track_reply=True)
        self._gateway_command_timer.StartOnce(timeout)
        return self._gateway_request_id

    def core_request(self, multipart_message: Sequence[bytes], track_reply=False) -> Optional[bytes]:
        if not track_reply:
            request = [REQUEST_MESSAGE, NOTIFY_ID, *multipart_message]
            self._core_client.send_multipart(request)
        else:
            request = [REQUEST_MESSAGE, r_id := uint2bytes(self._request_id), *multipart_message]
            if self._request_id >= self._ID_WRAP:
                self._request_id = 1
            else:
                self._request_id += 1
            self._core_client.send_multipart(request)
            return r_id

    def request_shutdown(self, explicit_user_request=False):
        self.core_request([MSG_TYPE_SHUTDOWN, b"explicit-user-request" if explicit_user_request else b""])

    def get_config(self):
        """
        The config shall be acquired only by this method. The returned object is not going to change.
        """
        return self.__config

    def get_errors(self) -> int:
        return self._errors

    def clear_errors(self):
        self._errors = 0
        self.service_error_signal.emit(self._errors, None)

    def get_data_dir(self) -> Path:
        return Path(self.__config["local"]["data_dir"])

    def get_init_setup_path(self) -> Path:
        return self.get_data_dir() / ".init_config"

    def get_logs_dir(self) -> Path:
        return self.get_data_dir() / "logs"

    def get_access_log_path(self) -> Path:
        return self.get_logs_dir() / "access.log"

    def get_service_log_path(self) -> Path:
        return self.get_logs_dir() / "process.log"

    def close(self):
        self._core_client.close()

    def _handle_core_shutdown(self, message):
        self.core_died.set()

    def _on_gateway_command_timeout(self, *args):
        self.gateway_command_result.emit(False, None)

    def _handle_core_updated_config(self, message):
        try:
            config = msgpack_loads(message[1])
        except Exception as e:
            print("_EE0", e)  # TODO
            raise
        prev_config = self.__config
        self.__config = config
        self.config_changed.emit(config)
        if prev_config["state"].get("gateway_command_result_number") != config["state"].get(
            "gateway_command_result_number"
        ):
            self._gateway_command_timer.Stop()
            self._gateway_request_id = None
            self.gateway_command_result.emit(True, config["state"].get("gateway_command_result"))

    def _handle_request_stats(self, message):
        try:
            stat = msgpack_loads(message[1])
        except Exception as e:
            print("_EE0", e)  # TODO
            raise
        self.request_stat_signal.emit(stat)

    def _handle_indexer_work(self, message):
        try:
            work = msgpack_loads(message[1])
        except Exception as e:
            print("_EE0", e)  # TODO
            raise
        self.indexer_work_signal.emit(work)

    def _handle_service_error(self, message):
        try:
            error = msgpack_loads(message[1])
        except Exception as e:
            print("_EE0", e)  # TODO
            raise
        self._errors += 1
        self.service_error_signal.emit(self._errors, error["msg"])

    def _handle_core_reply(self, multipart_message: Sequence[bytes]):
        if len(multipart_message) > 2:
            request_id = multipart_message[1]
            try:
                reply = msgpack_loads(multipart_message[2])
            except Exception:
                reply = None
            if request_id == self._gateway_request_id:
                if not isinstance(reply, dict) or not reply.get("ok"):
                    self._gateway_command_timer.Stop()
                    self._gateway_request_id = None
                    self.gateway_command_result.emit(True, "gateway request failed at core")
            else:
                self.core_reply_signal.emit(request_id, reply)

    def _on_core_message(self, event):
        while True:
            try:
                message = self._core_buffer.queue.get_nowait()
            except Empty:
                break
            try:
                self._handler.handle(message)
            except Exception:
                logger.exception("Error while handling message from core")
                raise
