import json
from typing import (
    List,
    Optional,
    Union,
)
from xml.etree import ElementTree as eTree

from boss_api.shared import token
from boss_api.shared.exception import (
    AccessDenied,
    AuthenticationFailed,
    BossException,
)
from boss_api.shared.response import UserDto
from messagebus import (
    ApplicationErrorMessage,
    BadRequestMessage,
    DataType,
    DefaultToken,
    Message,
    MessageBus,
)


class Boss(object):

    def __init__(
        self,           # type: Boss
        message_bus,    # type: MessageBus
    ):
        # type: (...) -> None

        self.message_bus = message_bus

    def authenticate(
        self,               # type: Boss
        user_id,            # type: int
        passwords,          # type: Union[str, List[str]]
        min_level=None,     # type: Optional[int]
    ):
        # type: (...) -> UserDto

        if isinstance(passwords, list):
            passwords = "|".join(passwords)

        request = "{}\0{}".format(user_id or "", passwords or "").encode("utf-8")

        reply = self.message_bus.send_message(
            component="UserControl",
            message=Message(
                token=token.TK_USER_CONTROL_AUTHENTICATE,
                data_type=DataType.param,
                data=request,
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK and reply.data is not None:
            response = reply.data.decode("utf-8")
            fields = response.split("\0")

            user = UserDto(
                user_id=user_id,
                level=fields[0],
                user_name=fields[1],
                long_name=fields[2],
                status=fields[3],
                admission_date=fields[4],
                termination_date=fields[5],
            )
            if min_level is not None and user.level < min_level:
                raise AccessDenied()

            return user

        if reply.token == DefaultToken.TK_SYS_NAK:
            raise AuthenticationFailed()

        raise self._build_exception_from_reply(reply=reply)

    def authenticate_nfc(
        self,               # type: Boss
        nfc_uuid,           # type: str
        min_level=None,     # type: Optional[int]
    ):
        # type: (...) -> UserDto

        request = str(nfc_uuid).encode("utf-8")

        reply = self.message_bus.send_message(
            component="UserControl",
            message=Message(
                token=token.TK_USER_CONTROL_AUTHENTICATE_NFC,
                data_type=DataType.param,
                data=request,
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK and reply.data is not None:
            response = reply.data.decode("utf-8")
            fields = response.split("\0")

            user = UserDto(
                user_id=fields[0],
                level=fields[1],
                user_name=fields[2],
                long_name=fields[3],
                status=fields[4],
                admission_date=fields[5],
                termination_date=fields[6],
            )
            if min_level is not None and user.level < min_level:
                raise AccessDenied()

            return user

        if reply.token == DefaultToken.TK_SYS_NAK:
            raise AuthenticationFailed()

        raise self._build_exception_from_reply(reply=reply)

    def get_info(
        self,       # type: Boss
        user_id,    # type: int
    ):
        # type: (...) -> UserDto

        request = str(user_id).encode("utf-8")

        reply = self.message_bus.send_message(
            component="UserControl",
            message=Message(
                token=token.TK_USER_CONTROL_GET_INFO,
                data_type=DataType.param,
                data=request,
            )
        )

        if reply.token == DefaultToken.TK_SYS_ACK and reply.data is not None:
            response = eTree.fromstring(reply.data.decode("utf-8"))
            user = response.find("user")
            return UserDto(
                user_id=user_id,
                level=int(user.get("Level")),
                user_name=user.get("UserName"),
                long_name=user.get("LongName"),
                status=int(user.get("Status")),
                admission_date=user.get("AdmissionDate"),
                termination_date=user.get("TerminationDate"),
            )

        if reply.token == DefaultToken.TK_SYS_NAK:
            raise AuthenticationFailed()

        raise self._build_exception_from_reply(reply=reply)

    def get_users_list(
        self,       # type: Boss
    ):
        # type: (...) -> List[UserDto]

        reply = self.message_bus.send_message(
            component="UserControl",
            message=Message(token=token.TK_USER_CONTROL_GET_USERS_LIST)
        )

        if reply.token == DefaultToken.TK_SYS_ACK and reply.data is not None:
            response = eTree.fromstring(reply.data.decode("utf-8"))
            users = response.findall("user")
            users_list = []
            for user_data in users:
                users_list.append(
                    UserDto(
                        user_id=int(user_data.get("UserId")),
                        level=int(user_data.get("Level")),
                        user_name=user_data.get("UserName"),
                        long_name=user_data.get("LongName"),
                        status=int(user_data.get("Status")),
                        admission_date=user_data.get("AdmissionDate"),
                        termination_date=user_data.get("TerminationDate"),
                    )
                )

            return users_list

        if reply.token == DefaultToken.TK_SYS_NAK:
            raise AuthenticationFailed()

        raise self._build_exception_from_reply(reply=reply)

    def insert_or_update_users(
        self,       # type: Boss
        users,      # type: List[UserDto]
    ):
        # type: (...) -> None

        users_json = []
        for user in users:
            users_json.append(
                {
                    "user_id": user.user_id,
                    "user_name": user.user_name,
                    "long_name": user.long_name,
                    "password": user.password,
                    "level": user.level,
                    "status": user.status,
                    "admission_date": user.admission_date,
                    "termination_date": user.termination_date,
                    "fingerprint_minutiae": user.fingerprint_minutiae,
                    "nfc_uuid": user.nfc_uuid,
                }
            )

        reply = self.message_bus.send_message(
            component="UserControl",
            message=Message(
                token=token.TK_USER_CONTROL_INSERT_OR_UPDATE_USERS,
                data_type=DataType.json,
                data=json.dumps(users_json).encode("utf-8"),
            ),
        )

        if reply.token == DefaultToken.TK_SYS_ACK:
            return

        raise self._build_exception_from_reply(reply=reply)

    def _build_exception_from_reply(
        self,   # type: Boss
        reply,  # type: Message
    ):
        # type: (...) -> Exception

        if reply.token == DefaultToken.TK_SYS_BAD_REQUEST:
            bad_request = BadRequestMessage.build_from_message(message=reply)
            exception = BossException.get_exception(error_code=bad_request.error_code)
            return exception

        elif reply.token == DefaultToken.TK_SYS_APPLICATION_ERROR:
            application_error = ApplicationErrorMessage.build_from_message(message=reply)
            exception = BossException.get_exception(error_code=application_error.error_code)
            exception.error_message = application_error.error_message
            return exception

        return 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))
