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"
        if self.version == ChuniConstants.VER_CHUNITHM_LUMINOUS:
            return "220"

    async 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.server.hostname}:{t_port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
                "matchingUriX": f"http://{self.core_cfg.server.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.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
                "reflectorUri": f"http://{self.core_cfg.server.hostname}:{self.core_cfg.server.port}/SDHD/{self._interal_ver_to_intver()}/ChuniServlet/",
            },
            "isDumpUpload": False,
            "isAou": False,
        }

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

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

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

    async def handle_get_user_map_area_api_request(self, data: Dict) -> Dict:
        user_map_areas = await 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}

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

    async def handle_get_user_preview_api_request(self, data: Dict) -> Dict:
        profile = await self.data.profile.get_profile_preview(data["userId"], self.version)
        if profile is None:
            return None
        profile_character = await 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

    async def handle_cm_get_user_preview_api_request(self, data: Dict) -> Dict:
        p = await 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,
        }

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

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

    async def handle_get_game_gacha_api_request(self, data: Dict) -> Dict:
        """
        returns all current active banners (gachas)
        """
        game_gachas = await 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": [],
        }

    async 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 = await 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": [],
        }

    async def handle_cm_get_user_data_api_request(self, data: Dict) -> Dict:
        p = await 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,
                }
            ],
        }

    async def handle_get_user_gacha_api_request(self, data: Dict) -> Dict:
        user_gachas = await 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,
        }

    async def handle_get_user_printed_card_api_request(self, data: Dict) -> Dict:
        user_print_list = await 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,
        }

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

        user_print_states = await 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,
        }

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

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

    async 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 = await 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 = await 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}

    async 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")

        await 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")

        await 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"]:
                await self.data.item.put_item(user_id, item)

        # add every gamegachaCard to database
        for card in upsert["gameGachaCardList"]:
            await 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 = await 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,
        }

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

    async 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
        await self.data.item.put_user_print_detail(user_id, serial_id, user_print_detail)

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

    async 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"]:
                await self.data.item.put_item(user_id, item)

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

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

    async 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:
            await self.data.item.put_user_print_state(user_id, id=order_id, hasCompleted=True)

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

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

    async def handle_begin_matching_api_request(self, data: Dict) -> Dict:
        room_id = 1
        # check if there is a free matching room
        matching_room = await 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 = await 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
            await self.data.item.put_matching(
                self.version, room_id, [new_member], user_id=new_member["userId"]
            )

            # get the newly created matching room
            matching_room = await 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!
            await 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}

    async def handle_end_matching_api_request(self, data: Dict) -> Dict:
        matching_room = await 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
        ]

        await 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.server.hostname}",
        }

    async 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 = await 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:
                await self.data.item.delete_matching(self.version, room["roomId"])
            else:
                # remove the user from the room
                await self.data.item.put_matching(
                    self.version,
                    room["roomId"],
                    new_members,
                    user_id=room["user"],
                    rest_sec=room["restMSec"],
                )

        return {"returnCode": "1"}

    async def handle_get_matching_state_api_request(self, data: Dict) -> Dict:
        polling_interval = 1
        # get the current active room
        matching_room = await 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

        await 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
        }
