import logging
from datetime import datetime, timedelta
from random import randint
from typing import Dict

import pytz
from core.config import CoreConfig
from core.utils import Utils
from titles.chuni.const import ChuniConstants
from titles.chuni.database import ChuniData
from titles.chuni.base import ChuniBase
from titles.chuni.config import ChuniConfig

class ChuniNew(ChuniBase):
    ITEM_TYPE = {"character": 20, "story": 21, "card": 22}

    def __init__(self, core_cfg: CoreConfig, game_cfg: ChuniConfig) -> None:
        self.core_cfg = core_cfg
        self.game_cfg = game_cfg
        self.data = ChuniData(core_cfg)
        self.date_time_format = "%Y-%m-%d %H:%M:%S"
        self.logger = logging.getLogger("chuni")
        self.game = ChuniConstants.GAME_CODE
        self.version = ChuniConstants.VER_CHUNITHM_NEW
    
    def _interal_ver_to_intver(self) -> str:
        if self.version == ChuniConstants.VER_CHUNITHM_NEW:
            return "200"
        if self.version == ChuniConstants.VER_CHUNITHM_NEW_PLUS:
            return "205"
        if self.version == ChuniConstants.VER_CHUNITHM_SUN:
            return "210"
        if self.version == ChuniConstants.VER_CHUNITHM_SUN_PLUS:
            return "215"

    def handle_get_game_setting_api_request(self, data: Dict) -> Dict:
        # use UTC time and convert it to JST time by adding +9
        # matching therefore starts one hour before and lasts for 8 hours
        match_start = datetime.strftime(
            datetime.utcnow() + timedelta(hours=8), self.date_time_format
        )
        match_end = datetime.strftime(
            datetime.utcnow() + timedelta(hours=16), self.date_time_format
        )
        # if reboot start/end time is not defined use the default behavior of being a few hours ago
        if self.core_cfg.title.reboot_start_time == "" or self.core_cfg.title.reboot_end_time == "":
            reboot_start = datetime.strftime(
                datetime.utcnow() + timedelta(hours=6), self.date_time_format
            )
            reboot_end = datetime.strftime(
                datetime.utcnow() + timedelta(hours=7), self.date_time_format
            )
        else:
            # get current datetime in JST
            current_jst = datetime.now(pytz.timezone('Asia/Tokyo')).date()

            # parse config start/end times into datetime
            reboot_start_time = datetime.strptime(self.core_cfg.title.reboot_start_time, "%H:%M")
            reboot_end_time = datetime.strptime(self.core_cfg.title.reboot_end_time, "%H:%M")

            # offset datetimes with current date/time
            reboot_start_time = reboot_start_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))
            reboot_end_time = reboot_end_time.replace(year=current_jst.year, month=current_jst.month, day=current_jst.day, tzinfo=pytz.timezone('Asia/Tokyo'))

            # create strings for use in gameSetting
            reboot_start = reboot_start_time.strftime(self.date_time_format)
            reboot_end = reboot_end_time.strftime(self.date_time_format)
        t_port = Utils.get_title_port(self.core_cfg)
        return {
            "gameSetting": {
                "isMaintenance": False,
                "requestInterval": 10,
                "rebootStartTime": reboot_start,
                "rebootEndTime": reboot_end,
                "isBackgroundDistribute": False,
                "maxCountCharacter": 300,
                "maxCountItem": 300,
                "maxCountMusic": 300,
                "matchStartTime": match_start,
                "matchEndTime": match_end,
                "matchTimeLimit": self.game_cfg.matching.match_time_limit,
                "matchErrorLimit": self.game_cfg.matching.match_error_limit,
                "romVersion": self.game_cfg.version.version(self.version)["rom"],
                "dataVersion": self.game_cfg.version.version(self.version)["data"],
                "matchingUri": f"http://{self.core_cfg.title.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
                "matchingUriX": f"http://{self.core_cfg.title.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
                # might be really important for online battle to connect the cabs via UDP port 50201
                "udpHolePunchUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
                "reflectorUri": f"http://{self.core_cfg.title.hostname}:{self.core_cfg.title.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
            },
            "isDumpUpload": False,
            "isAou": False,
        }

    def handle_remove_token_api_request(self, data: Dict) -> Dict:
        return {"returnCode": "1"}

    def handle_delete_token_api_request(self, data: Dict) -> Dict:
        return {"returnCode": "1"}

    def handle_create_token_api_request(self, data: Dict) -> Dict:
        return {"returnCode": "1"}

    def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
        user_map_areas = self.data.item.get_map_areas(data["userId"])

        map_areas = []
        for map_area in user_map_areas:
            tmp = map_area._asdict()
            tmp.pop("id")
            tmp.pop("user")
            map_areas.append(tmp)

        return {"userId": data["userId"], "userMapAreaList": map_areas}

    def handle_get_user_symbol_chat_setting_api_request(self, data: Dict) -> Dict:
        return {"userId": data["userId"], "symbolCharInfoList": []}

    def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
        profile = self.data.profile.get_profile_preview(data["userId"], self.version)
        if profile is None:
            return None
        profile_character = self.data.item.get_character(
            data["userId"], profile["characterId"]
        )

        if profile_character is None:
            chara = {}
        else:
            chara = profile_character._asdict()
            chara.pop("id")
            chara.pop("user")

        data1 = {
            "userId": data["userId"],
            # Current Login State
            "isLogin": False,
            "lastLoginDate": profile["lastPlayDate"],
            # User Profile
            "userName": profile["userName"],
            "reincarnationNum": profile["reincarnationNum"],
            "level": profile["level"],
            "exp": profile["exp"],
            "playerRating": profile["playerRating"],
            "lastGameId": profile["lastGameId"],
            "lastRomVersion": profile["lastRomVersion"],
            "lastDataVersion": profile["lastDataVersion"],
            "lastPlayDate": profile["lastPlayDate"],
            "emoneyBrandId": 0,
            "trophyId": profile["trophyId"],
            # Current Selected Character
            "userCharacter": chara,
            # User Game Options
            "playerLevel": profile["playerLevel"],
            "rating": profile["rating"],
            "headphone": profile["headphone"],
            # Enables favorites and teams
            "chargeState": 1,
            "userNameEx": "",
            "banState": 0,
            "classEmblemMedal": profile["classEmblemMedal"],
            "classEmblemBase": profile["classEmblemBase"],
            "battleRankId": profile["battleRankId"],
        }
        return data1

    def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
        p = self.data.profile.get_profile_data(data["userId"], self.version)
        if p is None:
            return {}

        return {
            "userName": p["userName"],
            "level": p["level"],
            "medal": p["medal"],
            "lastDataVersion": "2.00.00",
            "isLogin": False,
        }

    def handle_printer_login_api_request(self, data: Dict) -> Dict:
        return {"returnCode": 1}

    def handle_printer_logout_api_request(self, data: Dict) -> Dict:
        return {"returnCode": 1}

    def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
        """
        returns all current active banners (gachas)
        """
        game_gachas = self.data.static.get_gachas(self.version)

        # clean the database rows
        game_gacha_list = []
        for gacha in game_gachas:
            tmp = gacha._asdict()
            tmp.pop("id")
            tmp.pop("version")
            tmp["startDate"] = datetime.strftime(tmp["startDate"], "%Y-%m-%d %H:%M:%S")
            tmp["endDate"] = datetime.strftime(tmp["endDate"], "%Y-%m-%d %H:%M:%S")
            tmp["noticeStartDate"] = datetime.strftime(
                tmp["noticeStartDate"], "%Y-%m-%d %H:%M:%S"
            )
            tmp["noticeEndDate"] = datetime.strftime(
                tmp["noticeEndDate"], "%Y-%m-%d %H:%M:%S"
            )

            game_gacha_list.append(tmp)

        return {
            "length": len(game_gacha_list),
            "gameGachaList": game_gacha_list,
            # no clue
            "registIdList": [],
        }

    def handle_get_game_gacha_card_by_id_api_request(self, data: Dict) -> Dict:
        """
        returns all valid cards for a given gachaId
        """
        game_gacha_cards = self.data.static.get_gacha_cards(data["gachaId"])

        game_gacha_card_list = []
        for gacha_card in game_gacha_cards:
            tmp = gacha_card._asdict()
            tmp.pop("id")
            game_gacha_card_list.append(tmp)

        return {
            "gachaId": data["gachaId"],
            "length": len(game_gacha_card_list),
            # check isPickup from the chuni_static_gachas?
            "isPickup": False,
            "gameGachaCardList": game_gacha_card_list,
            # again no clue
            "emissionList": [],
            "afterCalcList": [],
            "ssrBookCalcList": [],
        }

    def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
        p = self.data.profile.get_profile_data(data["userId"], self.version)
        if p is None:
            return {}

        profile = p._asdict()
        profile.pop("id")
        profile.pop("user")
        profile.pop("version")

        return {
            "userId": data["userId"],
            "userData": profile,
            "userEmoney": [
                {
                    "type": 0,
                    "emoneyCredit": 100,
                    "emoneyBrand": 1,
                    "ext1": 0,
                    "ext2": 0,
                    "ext3": 0,
                }
            ],
        }

    def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
        user_gachas = self.data.item.get_user_gachas(data["userId"])
        if user_gachas is None:
            return {"userId": data["userId"], "length": 0, "userGachaList": []}

        user_gacha_list = []
        for gacha in user_gachas:
            tmp = gacha._asdict()
            tmp.pop("id")
            tmp.pop("user")
            tmp["dailyGachaDate"] = datetime.strftime(tmp["dailyGachaDate"], "%Y-%m-%d")
            user_gacha_list.append(tmp)

        return {
            "userId": data["userId"],
            "length": len(user_gacha_list),
            "userGachaList": user_gacha_list,
        }

    def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
        user_print_list = self.data.item.get_user_print_states(
            data["userId"], has_completed=True
        )
        if user_print_list is None:
            return {
                "userId": data["userId"],
                "length": 0,
                "nextIndex": -1,
                "userPrintedCardList": [],
            }

        print_list = []
        next_idx = int(data["nextIndex"])
        max_ct = int(data["maxCount"])

        for x in range(next_idx, len(user_print_list)):
            tmp = user_print_list[x]._asdict()
            print_list.append(tmp["cardId"])

            if len(print_list) >= max_ct:
                break

        if len(print_list) >= max_ct:
            next_idx = next_idx + max_ct
        else:
            next_idx = -1

        return {
            "userId": data["userId"],
            "length": len(print_list),
            "nextIndex": next_idx,
            "userPrintedCardList": print_list,
        }

    def handle_get_user_card_print_error_api_request(self, data: Dict) -> Dict:
        user_id = data["userId"]

        user_print_states = self.data.item.get_user_print_states(
            user_id, has_completed=False
        )

        card_print_state_list = []
        for card in user_print_states:
            tmp = card._asdict()
            tmp["orderId"] = tmp["id"]
            tmp.pop("user")
            tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")

            card_print_state_list.append(tmp)

        return {
            "userId": user_id,
            "length": len(card_print_state_list),
            "userCardPrintStateList": card_print_state_list,
        }

    def handle_cm_get_user_character_api_request(self, data: Dict) -> Dict:
        return super().handle_get_user_character_api_request(data)

    def handle_cm_get_user_item_api_request(self, data: Dict) -> Dict:
        return super().handle_get_user_item_api_request(data)

    def handle_roll_gacha_api_request(self, data: Dict) -> Dict:
        """
        Handle a gacha roll API request, with:
        gachaId: the gachaId where the cards should be pulled from
        times: the number of gacha rolls
        characterId: the character which the user wants
        """
        gacha_id = data["gachaId"]
        num_rolls = data["times"]
        chara_id = data["characterId"]

        rolled_cards = []

        # characterId is set after 10 rolls, where the user can select a card
        # from all gameGachaCards, therefore the correct cardId for a given
        # characterId should be returned
        if chara_id != -1:
            # get the
            card = self.data.static.get_gacha_card_by_character(gacha_id, chara_id)

            tmp = card._asdict()
            tmp.pop("id")

            rolled_cards.append(tmp)
        else:
            gacha_cards = self.data.static.get_gacha_cards(gacha_id)

            # get the card id for each roll
            for _ in range(num_rolls):
                # get the index from all possible cards
                card_idx = randint(0, len(gacha_cards) - 1)
                # remove the index from the cards so it wont get pulled again
                card = gacha_cards.pop(card_idx)

                # remove the "id" fronm the card
                tmp = card._asdict()
                tmp.pop("id")

                rolled_cards.append(tmp)

        return {"length": len(rolled_cards), "gameGachaCardList": rolled_cards}

    def handle_cm_upsert_user_gacha_api_request(self, data: Dict) -> Dict:
        upsert = data["cmUpsertUserGacha"]
        user_id = data["userId"]
        place_id = data["placeId"]

        # save the user data
        user_data = upsert["userData"]
        user_data.pop("rankUpChallengeResults")
        user_data.pop("userEmoney")

        self.data.profile.put_profile_data(user_id, self.version, user_data)

        # save the user gacha
        user_gacha = upsert["userGacha"]
        gacha_id = user_gacha["gachaId"]
        user_gacha.pop("gachaId")
        user_gacha.pop("dailyGachaDate")

        self.data.item.put_user_gacha(user_id, gacha_id, user_gacha)

        # save all user items
        if "userItemList" in upsert:
            for item in upsert["userItemList"]:
                self.data.item.put_item(user_id, item)

        # add every gamegachaCard to database
        for card in upsert["gameGachaCardList"]:
            self.data.item.put_user_print_state(
                user_id,
                hasCompleted=False,
                placeId=place_id,
                cardId=card["cardId"],
                gachaId=card["gachaId"],
            )

        # retrieve every game gacha card which has been added in order to get
        # the orderId for the next request
        user_print_states = self.data.item.get_user_print_states_by_gacha(
            user_id, gacha_id, has_completed=False
        )
        card_print_state_list = []
        for card in user_print_states:
            tmp = card._asdict()
            tmp["orderId"] = tmp["id"]
            tmp.pop("user")
            tmp["limitDate"] = datetime.strftime(tmp["limitDate"], "%Y-%m-%d")

            card_print_state_list.append(tmp)

        return {
            "returnCode": "1",
            "apiName": "CMUpsertUserGachaApi",
            "userCardPrintStateList": card_print_state_list,
        }

    def handle_cm_upsert_user_printlog_api_request(self, data: Dict) -> Dict:
        return {
            "returnCode": 1,
            "orderId": 0,
            "serialId": "11111111111111111111",
            "apiName": "CMUpsertUserPrintlogApi",
        }

    def handle_cm_upsert_user_print_api_request(self, data: Dict) -> Dict:
        user_print_detail = data["userPrintDetail"]
        user_id = data["userId"]

        # generate random serial id
        serial_id = "".join([str(randint(0, 9)) for _ in range(20)])

        # not needed because are either zero or unset
        user_print_detail.pop("orderId")
        user_print_detail.pop("printNumber")
        user_print_detail.pop("serialId")
        user_print_detail["printDate"] = datetime.strptime(
            user_print_detail["printDate"], "%Y-%m-%d"
        )

        # add the entry to the user print table with the random serialId
        self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)

        return {
            "returnCode": 1,
            "orderId": 0,
            "serialId": serial_id,
            "apiName": "CMUpsertUserPrintApi",
        }

    def handle_cm_upsert_user_print_subtract_api_request(self, data: Dict) -> Dict:
        upsert = data["userCardPrintState"]
        user_id = data["userId"]
        place_id = data["placeId"]

        # save all user items
        if "userItemList" in data:
            for item in data["userItemList"]:
                self.data.item.put_item(user_id, item)

        # set the card print state to success and use the orderId as the key
        self.data.item.put_user_print_state(
            user_id, id=upsert["orderId"], hasCompleted=True
        )

        return {"returnCode": "1", "apiName": "CMUpsertUserPrintSubtractApi"}

    def handle_cm_upsert_user_print_cancel_api_request(self, data: Dict) -> Dict:
        order_ids = data["orderIdList"]
        user_id = data["userId"]

        # set the card print state to success and use the orderId as the key
        for order_id in order_ids:
            self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)

        return {"returnCode": "1", "apiName": "CMUpsertUserPrintCancelApi"}

    def handle_ping_request(self, data: Dict) -> Dict:
        # matchmaking ping request
        return {"returnCode": "1"}

    def handle_begin_matching_api_request(self, data: Dict) -> Dict:
        room_id = 1
        # check if there is a free matching room
        matching_room = self.data.item.get_oldest_free_matching(self.version)

        if matching_room is None:
            # grab the latest roomId and add 1 for the new room
            newest_matching = self.data.item.get_newest_matching(self.version)
            if newest_matching is not None:
                room_id = newest_matching["roomId"] + 1

            # fix userName WTF8
            new_member = data["matchingMemberInfo"]
            new_member["userName"] = self.read_wtf8(new_member["userName"])

            # create the new room with room_id and the current user id (host)
            # user id is required for the countdown later on
            self.data.item.put_matching(
                self.version, room_id, [new_member], user_id=new_member["userId"]
            )

            # get the newly created matching room
            matching_room = self.data.item.get_matching(self.version, room_id)
        else:
            # a room already exists, so just add the new member to it
            matching_member_list = matching_room["matchingMemberInfoList"]
            # fix userName WTF8
            new_member = data["matchingMemberInfo"]
            new_member["userName"] = self.read_wtf8(new_member["userName"])
            matching_member_list.append(new_member)

            # add the updated room to the database, make sure to set isFull correctly!
            self.data.item.put_matching(
                self.version,
                matching_room["roomId"],
                matching_member_list,
                user_id=matching_room["user"],
                is_full=True if len(matching_member_list) >= 4 else False,
            )

        matching_wait = {
            "isFinish": False,
            "restMSec": matching_room["restMSec"],  # in sec
            "pollingInterval": 1,  # in sec
            "matchingMemberInfoList": matching_room["matchingMemberInfoList"],
        }

        return {"roomId": 1, "matchingWaitState": matching_wait}

    def handle_end_matching_api_request(self, data: Dict) -> Dict:
        matching_room = self.data.item.get_matching(self.version, data["roomId"])
        members = matching_room["matchingMemberInfoList"]

        # only set the host user to role 1 every other to 0?
        role_list = [
            {"role": 1} if m["userId"] == matching_room["user"] else {"role": 0}
            for m in members
        ]

        self.data.item.put_matching(
            self.version,
            matching_room["roomId"],
            members,
            user_id=matching_room["user"],
            rest_sec=0,  # make sure to always set 0
            is_full=True,  # and full, so no one can join
        )

        return {
            "matchingResult": 1,  # needs to be 1 for successful matching
            "matchingMemberInfoList": members,
            # no idea, maybe to differentiate between CPUs and real players?
            "matchingMemberRoleList": role_list,
            # TCP/UDP connection?
            "reflectorUri": f"{self.core_cfg.title.hostname}",
        }

    def handle_remove_matching_member_api_request(self, data: Dict) -> Dict:
        # get all matching rooms, because Chuni only returns the userId
        # not the actual roomId
        matching_rooms = self.data.item.get_all_matchings(self.version)
        if matching_rooms is None:
            return {"returnCode": "1"}

        for room in matching_rooms:
            old_members = room["matchingMemberInfoList"]
            new_members = [m for m in old_members if m["userId"] != data["userId"]]

            # if nothing changed go to the next room
            if len(old_members) == len(new_members):
                continue

            # if the last user got removed, delete the matching room
            if len(new_members) <= 0:
                self.data.item.delete_matching(self.version, room["roomId"])
            else:
                # remove the user from the room
                self.data.item.put_matching(
                    self.version,
                    room["roomId"],
                    new_members,
                    user_id=room["user"],
                    rest_sec=room["restMSec"],
                )

        return {"returnCode": "1"}

    def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
        polling_interval = 1
        # get the current active room
        matching_room = self.data.item.get_matching(self.version, data["roomId"])
        members = matching_room["matchingMemberInfoList"]
        rest_sec = matching_room["restMSec"]

        # grab the current member
        current_member = data["matchingMemberInfo"]

        # only the host user can decrease the countdown
        if matching_room["user"] == int(current_member["userId"]):
            # cap the restMSec to 0
            if rest_sec > 0:
                rest_sec -= polling_interval
            else:
                rest_sec = 0

        # update the members in order to recieve messages
        for i, member in enumerate(members):
            if member["userId"] == current_member["userId"]:
                # replace the old user data with the current user data,
                # also parse WTF-8 everytime
                current_member["userName"] = self.read_wtf8(current_member["userName"])
                members[i] = current_member

        self.data.item.put_matching(
            self.version,
            data["roomId"],
            members,
            rest_sec=rest_sec,
            user_id=matching_room["user"],
        )

        # only add the other members to the list
        diff_members = [m for m in members if m["userId"] != current_member["userId"]]

        matching_wait = {
            # makes no difference? Always use False?
            "isFinish": True if rest_sec == 0 else False,
            "restMSec": rest_sec,
            "pollingInterval": polling_interval,
            # the current user needs to be the first one?
            "matchingMemberInfoList": [current_member] + diff_members,
        }

        return {
            "roomId": data["roomId"],
            "matchingWaitState": matching_wait
        }