import json
import uuid
from datetime import datetime, timedelta
from io import BytesIO
from re import compile
import qrcode
from qrcode.image.styledpil import StyledPilImage
from qrcode.image.styles.moduledrawers import RoundedModuleDrawer

from Matching.PreferenceTyps import ProjectPreference, AgeGroupPreference, InterestsPreferences
from Models import MatchModel, UserModel, ChatModel, ChannelModel, ImageModel, CardModel, QuestionsModel, \
    InfoNuggetDataClass, DescriptionPanelModel, ImagePanelModel, InterestsPanelModel, InfoNuggetsPanelModel, \
    QuestionPanelModel, TextPanelModel, InvalidIDException
from Rest.Answer import Answer, MessageCodes, try_catch_error_answer
from Rest.UserRegex import get_display_name_regex, get_username_regex
from app import db, cache

ENCODING = "utf-8"

username_re = compile('^' + get_username_regex() + '$')
display_name_re = compile('^' + get_display_name_regex() + '$')
with open("StaticData/TU_course_of_study.json", 'r') as f:
    course_of_study_options = json.loads(f.read())


class MatchinitModuleDrawer(RoundedModuleDrawer):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def initialize(self, styledPilImage, image):
        self.corner_width = int(styledPilImage.box_size / 2)
        self.image = image
        self.setup_corners(
            styledPilImage.mode,
            (255, 255, 255),
            (53, 0, 91))


def preference_to_enum(preference_string):
    split_preference = preference_string.split('.')
    front, back = split_preference[0], split_preference[1]

    front_dict = {"ProjectPreference": ProjectPreference, "AgeGroupPreference": AgeGroupPreference,
                  "InterestsPreferences": InterestsPreferences}
    enum_type = front_dict[front]

    if back == "SAME":
        return enum_type.SAME
    if back == "DIFFERENT":
        return enum_type.DIFFERENT
    if back == "HIGHER":
        return enum_type.HIGHER
    if back == "LOWER":
        return enum_type.LOWER
    if back == "DONT_CARE":
        return enum_type.DONT_CARE


def add_other_user_to_chat(chat, user):
    chat.other_user = chat.get_other_user(user)


class UserREST:
    @staticmethod
    def get_self(user):
        answer = Answer()
        return answer.add_message(MessageCodes.WORKED).add_result(user)

    @staticmethod
    def post_self(user, json_args):
        answer = Answer()

        # simple user attributes
        keys = json_args.keys()
        if "display_name" in keys:
            display_name = json_args["display_name"]
            if bool(display_name_re.match(display_name)):
                user.display_name = display_name
            else:
                db.session.rollback()
                return answer.add_message(MessageCodes.USER_INVALID_USERNAME).add_result(user)

        if "username" in keys:
            username = json_args["username"].lower()
            if bool(username_re.match(username)) and not UserModel.query.filter(UserModel.username == username).first():
                user.username = username
            else:
                db.session.rollback()
                return answer.add_message(MessageCodes.USER_INVALID_USER_TAG).add_result(user)
        if "profile_pic_id" in keys:
            pic = ImageModel.query.get(json_args["profile_pic_id"])
            if pic.uploader == user:
                user.profile_pic = pic
            else:
                db.session.rollback()
                return answer.add_message(MessageCodes.USER_INVALID_PROFILE_PIC).add_result(user)
        if "theme" in keys:
            user.theme = json_args["theme"]
        # complex user attributes
        if "matching_preferences" in keys:
            preferences = json_args["matching_preferences"]
            preferences_keys = preferences.keys()
            if "project_pref" in preferences_keys:
                user.matching_preferences.project_pref = preference_to_enum(preferences["project_pref"])
            if "age_group_pref" in preferences_keys:
                user.matching_preferences.age_group_pref = preference_to_enum(preferences["age_group_pref"])
            if "interests_pref" in preferences_keys:
                user.matching_preferences.interests_pref = preference_to_enum(preferences["interests_pref"])
        if "description" in keys:
            user.personal_information.description = json_args["description"]
        if "course_of_study" in keys:
            user.personal_information.course_of_study = json_args["course_of_study"]
        if "interest_list" in keys:
            user.personal_information.set_interests(json_args["interest_list"])
        if "matriculation_date" in keys:
            user.personal_information.matriculation_date = datetime.fromtimestamp(int(json_args["matriculation_date"]))

        if "privacy_settings" in keys:
            privacy = json_args["privacy_settings"]
            privacy_keys = privacy.keys()
            if "show_profile_picture" in privacy_keys:
                user.privacy_settings.show_profile_picture = privacy["show_profile_picture"]
            if "show_matriculation_date" in privacy_keys:
                user.privacy_settings.show_matriculation_date = privacy["show_matriculation_date"]
            if "show_project_groups" in privacy_keys:
                user.privacy_settings.show_project_groups = privacy["show_project_groups"]
            if "show_email" in privacy_keys:
                user.privacy_settings.show_email = privacy["show_email"]
            if "show_description" in privacy_keys:
                user.privacy_settings.show_description = privacy["show_description"]
            if "show_course_of_study" in privacy_keys:
                user.privacy_settings.show_course_of_study = privacy["show_course_of_study"]
            if "show_matching_preferences" in privacy_keys:
                user.privacy_settings.show_matching_preferences = privacy["show_matching_preferences"]
            if "accept_direct_messages" in privacy_keys:
                user.privacy_settings.accept_direct_messages = privacy["accept_direct_messages"]
            if "show_rank" in privacy_keys:
                user.privacy_settings.show_rank = privacy["show_rank"]

        # allow developers to set own moderation flags
        if user.moderation_flags.is_developer and "moderation_flags" in keys:
            moderation = json_args["moderation_flags"]
            moderation_keys = moderation.keys()
            flags = ["can_post", "can_delete", "can_ban", "can_match", "message_anyone"]
            for flag in flags:
                if flag in moderation_keys:
                    setattr(user.moderation_flags, flag, moderation[flag])

        db.session.add(user)
        db.session.commit()

        return answer.add_message(MessageCodes.WORKED).add_result(user)

    @staticmethod
    def get_card(user):
        answer = Answer()
        card = None
        for c in user.cards:
            if c.type == "user":
                card = c
        if not card:
            return answer.add_message(MessageCodes.USER_NO_CARD)
        return answer.add_message(MessageCodes.WORKED).add_result(card)

    @staticmethod
    def post_card(user, json_args):
        answer = Answer()
        keys = json_args.keys()
        if "image_id" not in keys:
            return answer.add_message(MessageCodes.CARD_NO_IMAGE)
        image = ImageModel.query.get(json_args["image_id"])
        if not image:
            return answer.add_message(MessageCodes.IMAGE_DOES_NOT_EXIST)

        for card in CardModel.query.filter(CardModel.user == user, CardModel.type == "user").all():
            db.session.delete(card)

        card = CardModel(user=user, image=image, type="user")
        db.session.add(card)

        if 'panels' in keys:
            pos = 0
            for panel in json_args["panels"]:
                match panel["type"]:
                    case "description_panel":
                        db.session.add(DescriptionPanelModel(card=card, position=pos))

                    case "interests_panel":
                        db.session.add(InterestsPanelModel(card=card, position=pos))

                    case "image_panel":
                        if "image_id" not in panel.keys():
                            return answer.add_message(MessageCodes.CARD_NO_IMAGE)
                        image = ImageModel.query.get(panel["image_id"])
                        if not image:
                            return answer.add_message(MessageCodes.IMAGE_DOES_NOT_EXIST)
                        db.session.add(ImagePanelModel(card=card, image=image, position=pos))

                    case "info_nuggets_panel":
                        if "bubbles" not in panel.keys():
                            return answer.add_message(MessageCodes.CARD_NO_NUGGETS)
                        nuggets = []
                        for nugget in panel["bubbles"]:
                            try:
                                nuggets.append(InfoNuggetDataClass(nugget["id"], nugget["text"]))
                            except InvalidIDException:
                                return answer.add_message(MessageCodes.CARD_NO_NUGGETS)
                        np = InfoNuggetsPanelModel(card=card, position=pos)
                        np.set_nuggets(nuggets)
                        db.session.add(np)

                    case "question_panel":
                        if "answer" not in panel.keys():
                            return answer.add_message(MessageCodes.CARD_NO_ANSWER)
                        if "question_id" not in panel.keys():
                            if "question" not in panel.keys():
                                return answer.add_message(MessageCodes.CARD_NO_QUESTION)
                            question = QuestionsModel.from_string(panel["question"])
                            if not question and panel["question"]:
                                question = QuestionsModel(
                                    question_en=panel["question"],
                                    question_de=panel["question"],
                                    placeholder_en="",
                                    placeholder_de="",
                                    official=False)
                        else:
                            question = QuestionsModel.query.get(panel["question_id"])
                        if not question:
                            return answer.add_message(MessageCodes.CARD_NO_QUESTION)
                        db.session.add(
                            QuestionPanelModel(card=card, position=pos, question=question, answer=panel["answer"]))

                    case "text_panel":
                        if "text" not in panel.keys():
                            return answer.add_message(MessageCodes.CARD_NO_TEXT)
                        db.session.add(TextPanelModel(card=card, position=pos, text=panel["text"]))

                    case _:
                        return answer.add_message(MessageCodes.CARD_INVALID_CARD_TYPE)
                pos += 1

        db.session.commit()
        return answer.add_message(MessageCodes.WORKED).add_result(card)

    @staticmethod
    def get_own_chats(user):
        answer = Answer()

        chats = [chat for chat in user.chats if chat.channel.posts]  # no empty chats
        for chat in chats:
            add_other_user_to_chat(chat, user)

        chats = [chat for chat in chats if
                 user not in chat.other_user.blocked_user and chat.other_user not in user.blocked_user]

        return answer.add_result(chats).add_message(MessageCodes.WORKED)

    @staticmethod
    def get_own_chat(user, other_user_id):
        answer = Answer()

        other_user = UserModel.query.get(other_user_id)
        # check if chat exists
        possible_chat = [chat for chat in user.chats if other_user in chat.users]
        if not possible_chat:
            if other_user not in user.get_matched_users():
                # check if other user allows random chats or someone with message anyone rights is messaging
                if not other_user.privacy_settings.accept_direct_messages and not user.moderation_flags.message_anyone:
                    return answer.add_message(MessageCodes.USER_NO_RANDOM_CHATS)
            chat = ChatModel(channel=ChannelModel())
            chat.users.append(user)
            chat.users.append(other_user)
            db.session.add(chat)
            db.session.commit()
            possible_chat = [chat for chat in user.chats if other_user in chat.users]
        return answer.add_result(possible_chat[0]).add_message(MessageCodes.WORKED)

    @staticmethod
    def get_user(user, user_to_get_id):
        answer = Answer()

        user = UserModel.query.get(user_to_get_id)
        return answer.add_message(MessageCodes.WORKED).add_result(user)

    @staticmethod
    def post_mute_user(user, user_to_mute_id, duration=timedelta(minutes=10)):
        answer = Answer()

        # check if user has moderator rights
        if not user.moderation_flags.is_moderator:
            return answer.add_message(MessageCodes.USER_NOT_A_MODERATOR)

        # prevent mutes for more than a week
        if duration > timedelta(days=7):
            duration = timedelta(days=7)
        # mute user
        user_to_mute = UserModel.query.get(user_to_mute_id)
        user_to_mute.moderation_flags.muted_till = datetime.utcnow() + duration
        return answer.add_message(MessageCodes.USER_MUTED_SUCCESS)

    @staticmethod
    def post_unmute_user(user, user_to_unmute_id):
        answer = Answer()

        # check if user has moderator rights
        if not user.moderation_flags.is_moderator:
            return answer.add_message(MessageCodes.USER_NOT_A_MODERATOR)

        # check if user is muted
        user_to_unmute = UserModel.query.get(user_to_unmute_id)
        if not user_to_unmute.moderation_flags.is_muted():
            return answer.add_message(MessageCodes.USER_IS_NOT_MUTED)

        # unmute user
        user_to_unmute.moderation_flags.muted_till = datetime.utcnow()
        return answer.add_message(MessageCodes.USER_UNMUTED_SUCCESS)

    @staticmethod
    def post_ban_user(user, user_to_ban_id):
        answer = Answer()

        user_to_ban = UserModel.query.get(user_to_ban_id)
        # check if user can ban
        if not user.moderation_flags.can_ban:
            return answer.add_message(MessageCodes.USER_NO_BAN_RIGHTS)
        if not user_to_ban:
            return answer.add_message(MessageCodes.USER_NO_SUCH_USER)
        # set banned flag for user
        user_to_ban.moderation_flags.banned_till = datetime.max
        user_to_ban.login_id = uuid.uuid4()
        db.session.add(user_to_ban)
        db.session.commit()
        return answer.add_message(MessageCodes.WORKED)

    @staticmethod
    def post_unban_user(user, user_to_unban_id):
        answer = Answer()

        user_to_unban = UserModel.query.get(user_to_unban_id)
        # check if user can unban
        if not user.moderation_flags.can_ban:
            return answer.add_message(MessageCodes.USER_NO_BAN_RIGHTS)
        if not user_to_unban:
            return answer.add_message(MessageCodes.USER_NO_SUCH_USER)
        # unset banned flag for user
        user_to_unban.moderation_flags.banned_till = datetime.utcnow()
        db.session.add(user_to_unban)
        db.session.commit()
        return answer.add_message(MessageCodes.WORKED)

    @staticmethod
    def post_block_user(user, user_to_block_id):
        answer = Answer()

        user_to_block = UserModel.query.get(user_to_block_id)
        if user_to_block in user.blocked_user:
            return answer.add_message(MessageCodes.USER_ALREADY_BLOCKED)

        user.blocked_user.append(user_to_block)
        db.session.commit()
        return answer.add_message(MessageCodes.WORKED)

    @staticmethod
    def post_unblock_user(user, user_to_unblock_id):
        answer = Answer()

        user_to_unblock = UserModel.query.get(user_to_unblock_id)
        if user_to_unblock not in user.blocked_user:
            return answer.add_message(MessageCodes.USER_NOT_BLOCKED)

        user.blocked_user.remove(user_to_unblock)
        db.session.commit()
        return answer.add_message(MessageCodes.WORKED)

    @staticmethod
    def delete_self(user, json_args):
        answer = Answer()

        if not user.check_password(json_args['password']):
            return answer.add_message(MessageCodes.USER_WRONG_PASSWORD)
        user.delete()

        return answer.add_message(MessageCodes.WORKED)

    @staticmethod
    def get_qr_code(user):
        answer = Answer()

        code = str(uuid.uuid4())
        cached_data = cache.get(str(user.id))
        if cached_data:
            cached_data = json.loads(str(cached_data, encoding=ENCODING))
            if cached_data["purpose"] == "qrcode":
                code = cached_data["code"]

        data = json.dumps({"user": str(user.id), "code": code})

        qr = qrcode.QRCode(
            version=1,
            error_correction=qrcode.constants.ERROR_CORRECT_H,
            box_size=10,
            border=4,
        )
        qr.add_data(data)
        qr.make(fit=True)

        img = qr.make_image(image_factory=StyledPilImage,
                            module_drawer=MatchinitModuleDrawer(),
                            eye_drawer=MatchinitModuleDrawer(),
                            embeded_image_path="Rest/logo.png"
                            )

        img_io = BytesIO()
        img.save(img_io, 'PNG')
        img_io.seek(0)
        answer.image = ("qrcode", img_io.read())

        code_data = json.dumps({"code": code, "purpose": "qrcode"})
        cache.set(str(user.id), bytes(code_data, encoding=ENCODING), timeout=30 * 60)

        return answer.add_message(MessageCodes.WORKED)

    @staticmethod
    def post_qr_code(user, json_args):
        answer = Answer()

        if not json_args["code"] or not json_args["user"]:
            return answer.add_message(MessageCodes.USER_INVALID_QRCODE)
        friend = UserModel.query.get(json_args["user"])
        if not friend:
            return answer.add_message(MessageCodes.USER_INVALID_QRCODE)
        if user == friend:
            return answer.add_message(MessageCodes.USER_CANT_MATCH_YOURSELF)
        cached_data = cache.get(json_args["user"])
        if not cached_data:
            return answer.add_message(MessageCodes.USER_INVALID_QRCODE)
        cached_data = json.loads(str(cached_data, encoding=ENCODING))
        if cached_data["purpose"] != "qrcode" or cached_data["code"] != json_args["code"]:
            return answer.add_message(MessageCodes.USER_INVALID_QRCODE)

        if friend in user.get_matched_users():
            return answer.add_message(MessageCodes.USER_MATCH_ALREADY_EXISTS)

        match = MatchModel(users=[user, friend])
        chat = ChatModel.query.filter(ChatModel.users.contains(user),
                                      ChatModel.users.contains(friend)).first()
        db.session.add(match)
        if not chat:
            chat = ChatModel(channel=ChannelModel())
            chat.users.extend(match.users)
            db.session.add(chat)
        db.session.commit()
        return answer.add_message(MessageCodes.WORKED)
