import logging
import threading
import uuid

from messagebus import (
    ApplicationErrorMessage,
    DefaultToken,
    LowLevelMessageBus,
    Message,
)
from service_locator_api.shared.error import (
    KeepAliveError,
    LocateError,
)
from service_locator_api.shared.exception import (
    AddressChanged,
    RegistrationNotFound,
    ServiceNotFound,
)
from service_locator_api.shared.token import (
    TK_SERVICE_LOCATOR_ADDRESS_CHANGED,
    TK_SERVICE_LOCATOR_KEEP_ALIVE,
    TK_SERVICE_LOCATOR_LOOKUP_SERVICE,
    TK_SERVICE_LOCATOR_REGISTER_SERVICE,
)

logger = logging.getLogger(__name__)


class ServiceLocator(object):
    def __init__(
        self,
        message_bus: LowLevelMessageBus,
        service_address: str,
    ):
        self.message_bus = message_bus
        self.service_address = service_address

    def register(
        self,
        service_name: str,
        address: str,
        timeout: int = 2000,
    ) -> str:
        req_id = self._get_req_id()
        logger.debug(
            "operation: register, req_id: %s, service_name: %s, address: %s",
            req_id,
            service_name,
            address,
        )
        reply = self.message_bus.send_direct_message(
            address=self.service_address,
            message=Message(
                token=TK_SERVICE_LOCATOR_REGISTER_SERVICE,
                data=service_name.encode() + b"\0" + address.encode(),
                timeout=timeout,
                headers={
                    "req_id": req_id
                },
            )
        )

        if reply.token != DefaultToken.TK_SYS_ACK:
            raise self._build_internal_error(message=reply)

        return reply.data.decode()

    def lookup(
        self,
        service_name: str,
        timeout: int = 2000,
    ) -> str:
        req_id = self._get_req_id()
        logger.debug("operation: lookup, service_name: service_name, req_id: %s", service_name, req_id)

        threading.current_thread().req_id = req_id
        message = Message(
            token=TK_SERVICE_LOCATOR_LOOKUP_SERVICE,
            data=service_name.encode(),
            timeout=timeout,
            headers={
                "req_id": req_id
            },
        )

        reply = self.message_bus.send_direct_message(
            address=self.service_address,
            message=message
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return reply.data.decode()

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            error = ApplicationErrorMessage.build_from_message(message=reply)
            if error.error_code == LocateError.SERVICE_NOT_FOUND.code:
                raise ServiceNotFound()

        raise self._build_internal_error(message=reply)

    def keep_alive(
        self,
        reg_id: str,
        timeout: int = 2000,
    ) -> None:
        req_id = self._get_req_id()
        logger.debug("operation: keep_alive, reg_id: %s, req_id: %s", reg_id, req_id)

        reply = self.message_bus.send_direct_message(
            address=self.service_address,
            message=Message(
                token=TK_SERVICE_LOCATOR_KEEP_ALIVE,
                data=reg_id.encode(),
                timeout=timeout,
                headers={
                    "req_id": req_id
                },
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return

        if reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            error = ApplicationErrorMessage.build_from_message(message=reply)
            if error.error_code == KeepAliveError.SUBSCRIPTION_ID_NOT_FOUND.code:
                raise RegistrationNotFound()
        elif reply.token == TK_SERVICE_LOCATOR_ADDRESS_CHANGED:
            current_address = reply.data.decode()
            raise AddressChanged(current_address=current_address)

        raise self._build_internal_error(message=reply)

    @staticmethod
    def _build_internal_error(
        message,        # type: Message
    ):
        # type: (...) -> RuntimeError

        try:
            data_text = message.data.decode("utf-8")
        except UnicodeDecodeError:
            data_text = message.data.hex()
        return RuntimeError("Invalid response: {} - {}".format(message.token, data_text))

    @staticmethod
    def _get_req_id():
        current_thread = threading.current_thread()
        if hasattr(current_thread, "_processor_req_id"):
            return getattr(current_thread, "_processor_req_id")

        return str(uuid.uuid4())
