import httpx
import jwt
import logging
import msgpack

from typing import Optional, Union
from uuid import uuid4

from ..constants import EMPTY
from ..models import DeviceInfo, SystemInfo
from ..utils import decrypt_and_unpack, encrypt_and_pack

class API:
    game_version_url = "https://game-version.sekai-en.com"
    def __init__(self, raise_on_non_20x: bool = True) -> None:
        self.assetBundleHostHash = None
        self.domain = None
        self.raise_on_non_20x = raise_on_non_20x
        if SystemInfo.app_version and SystemInfo.app_hash:
            self.setup()
    #region General Setup
    def call(self, 
             method: str, 
             endpoint: str, 
             data: Optional[dict] = None, 
             return_response: bool = False, 
             raise_on_non_20x: Optional[bool] = None,
             headers: Optional[dict] = None) -> dict | httpx.Response | None:
        """Custom request wrapper to allow for easier calls

        Args:
            method (str)
            endpoint (str)
            data (dict, optional): Data/body. Defaults to None
            return_response (bool): Whether to return an httpx.Response object or a dict. Defaults to False.
            raise_on_non_20x (bool): Overrides self.raise_on_non_20x. Defaults to None
            headers (dict, optional)

        Returns:
            dict | httpx.Response | None: None if status_code == 204
        """        
        if data != EMPTY and data is not None:
            body = encrypt_and_pack(data)
        else: 
            body = data
        self.client.headers["X-Request-ID"] = str(uuid4())
        r = self.client.request(method, f"https://{self.domain}/{endpoint}", data=body if data else None, headers=headers)
        if r.status_code not in [200, 201, 204, 409]:
            print(endpoint, r.status_code, data)
            try: 
                print(decrypt_and_unpack(r.content))
            except: pass
            if raise_on_non_20x == True or (raise_on_non_20x != False and self.raise_on_non_20x):
                print(self.client.headers.get("X-Session-Token"))
                print(r.headers.get("X-Session-Token"))
                raise Exception
        
        if "X-Session-Token" in r.headers:
            self.client.headers["X-Session-Token"] = r.headers["X-Session-Token"]
        
        if return_response:
            return r
        
        if r.status_code == 204: return
        
        data = decrypt_and_unpack(r.content)
        return data
    
    def setup(self):
        """Call various functions to set up the API"""
        self._test_version()
        self._create_session()
        self.system()
    
    def _test_version(self) -> bool:
        """Sends a request to the game version server (default: game-version.sekai-en.com) to verify that the version and appHash are correct

        Returns:
            bool: True if version and appHash are correct, else False
        """
        r = httpx.get(f"{self.game_version_url}/{SystemInfo.app_version}/{SystemInfo.app_hash}")
        if r.status_code != 200:
            return False
        try:
            data = decrypt_and_unpack(r.content)
        except msgpack.exceptions.ExtraData:
            return False
        
        self.profile = data["profile"] # should always be production
        self.assetbundleHostHash = data["assetbundleHostHash"]
        self.domain = data["domain"]
        return True
    
    def _create_session(self, device_info: Optional[DeviceInfo] = None):
        if device_info is not None and not isinstance(device_info, DeviceInfo): raise TypeError
        """Create an httpx.client with the necessary headers"""
        if device_info is None:
            device_info = DeviceInfo
        self.client = httpx.Client()
        self.client.headers = {k: v for k, v in {
            "Accept": "application/octet-stream",
            "Accept-Encoding": "deflate, gzip",
            "Content-Type": "application/octet-stream",
            "User-Agent": "UnityPlayer/2022.3.52f1 (UnityWebRequest/1.0, libcurl/8.10.1-DEV)",
            "X-AI": "f8defa742a470b57b3c4d57de49dc08c",
            "X-App-Version": SystemInfo.app_version,
            "X-App-Hash": SystemInfo.app_hash,
            "X-Asset-Version": SystemInfo.asset_version,
            "X-Data-Version": SystemInfo.data_version,
            "X-DeviceModel": device_info.device_model,
            "X-Install-Id": str(device_info.install_id),
            "X-Platform": device_info.platform,
            "X-Request-ID": str(uuid4()),
            "X-OperatingSystem": device_info.operating_system,
            "X-Unity-Version": "2022.3.52f1",
        }.items() if v is not None} # unholy. disgraceful.
    
    def _update_version_headers(self) -> None:
        self.client.headers.update({k: v for k, v in {
            "X-App-Version": SystemInfo.app_version,
            "X-App-Hash": SystemInfo.app_hash,
            "X-Asset-Version": SystemInfo.asset_version,
            "X-Data-Version": SystemInfo.data_version,
        }.items() if v is not None})
    #endregion
    #region Internal Calls
    def system(self) -> None:
        """Calls the /api/system endpoint and updates system_info with relevant data"""
        session_token = self.client.headers.get("X-Session-Token", None)
        temp_headers = self.client.headers.copy()
        temp_headers.pop("X-Session-Token", None)
        self.client.headers = temp_headers
        r = self.call(
            "GET",
            "api/system",
            return_response=True,
            raise_on_non_20x=False
        )
        data = decrypt_and_unpack(r.content)
        match r.status_code:
            case 200: pass
            case 426:
                logging.error(f"App version and/or hash is outdated, please update the app.")
                exit(1)
            case _: 
                logging.error(f"Calling /api/system returned a non-200 status code ({r.status_code}).")
                print(data)
                exit(1)
        if data.get("appVersions")[-1]["appVersion"] != SystemInfo.app_version: 
            logging.error(f"App version is outdated. Expected {SystemInfo.app_version}, got {data.get('appVersions')[-1]['appVersion']}")
        SystemInfo.update(data)
        self._create_session()
        if session_token:
            self.client.headers["X-Session-Token"] = session_token
    
    def ageinfo(self, user_id: int) -> dict:
        """Calls the /api/user/na/{user_id}/ageinfo endpoint to get the stored "age info" (birth year, month and day that the user entered their age/registered, and approximate age)"""
        return self.call(f"api/user/na/{user_id}/ageinfo")
    
    def update_section(self, user_id: int, section: str, data: dict) -> dict:
        return self.call(
            "PATCH", 
            f"api/user/{user_id}", 
            data={section: data}
        )
    
    def suite(self, user_id: int) -> dict:
        return self.call(
            "GET", 
            f"api/suite/user/{user_id}?isForceAllReload=true"
        )
    
    def get_maintenance(self, module: str) -> bool:
        self.client.headers["X-Request-ID"] = str(uuid4())
        r = self.client.request("GET", f"https://{self.domain}/api/module-maintenance/{module}", data=None)
        if r.status_code != 200:
            logging.warning(f"Endpoint module-maintenance responded with a {r.status_code} status code. This is unexpected! Assuming there is no maintenance.\nPlease forward this information to Taggie on GitHub. https://github.com/tailhaver")
            return False
        data = decrypt_and_unpack(r.content)
        return data.get("isOngoing", False)
        
    #endregion
    #region Account Creation + Transfer
    def create_user(self, device_info: Optional[DeviceInfo] = None) -> dict:
        """Calls the /api/user endpoint to create a user

        Args:
            device_info (DeviceInfo, optional), Defaults to None.

        Returns:
            dict: All user account data from the API
        """
        if device_info is None:
            device_info = DeviceInfo
        body = {
            "platform": device_info.platform,
            "deviceModel": device_info.device_model,
            "operatingSystem": device_info.operating_system
        }
        return self.call("POST", "api/user", data=body)
    
    def login_from_token(self, user_id: int, token: str, device_id: str) -> dict[str, str]:
        """Attempts to log the user in using their known session token

        Args:
            user_id (int)
            token (str)
            device_id (str)

        Returns:
            dict: sessionToken, deviceId
        """
        body = {
            "credential": token,
            "deviceId": device_id
        }
        data = self.call("PUT", f"api/user/{user_id}/auth?refreshUpdatedResources=False", data=body)
        self.client.headers["X-Session-Token"] = data["sessionToken"]

        # "interesting" way to update dataVer assetVer etc
        SystemInfo.direct_update({k: v for k, v in data.items() if "Version" in k})
        self._update_version_headers()
        return {
            "sessionToken": data["sessionToken"],
            "deviceId": data["deviceId"] if "deviceId" in data else device_id
        }

    def _transfer_id(self, transfer_id: str, password: str, login: bool = False, return_response: bool = False) -> httpx.Response:
        """Base function for calling the inherit API

        Args:
            transfer_id (str): Transfer ID
            password (str): Transfer password
            login (bool, optional): True to log in, False to get user data from the ID Defaults to False.
            return_response (bool, optional)

        Returns:
            httpx.Response: API response
        """
        jwt_key = "uYf0cGqbgapejhc8bhba6G1cf5BBznOZeDz9NyFWZOgiiYsfUVNLT3wRUpCH6iDe1umsreAYuo35s8TP"
        payload = {
            "inheritId": transfer_id,
            "password": password
        }
        headers = {
            "X-Inherit-Id-Verify-Token": jwt.encode(payload, jwt_key, algorithm="HS256"),
        }
        r = self.call(
            "POST", 
            f"api/inherit/user/{transfer_id}?isExecuteInherit={login}&isAdult=True&tAge=18", 
            EMPTY, 
            return_response=return_response,
            raise_on_non_20x=False,
            headers=headers, 
        )
        
        match r.status_code:
            case 200:
                pass
            case 403:
                logging.error("Transfer ID inherit failed with code 403. This is likely due to an incorrect password or the transfer ID not existing.")
                raise
            case _:
                logging.error(f"Transfer ID inherit responded with response code {r.status_code}. This is not expected!\nPlease forward this information to Taggie on GitHub. https://github.com/tailhaver")
                raise Exception
        
        return r
    
    def check_transfer_id(self, transfer_id: str, password: str) -> dict[str, dict]:
        """Calls the inherit API to check an ID and returns the user data (if found)

        Args:
            transfer_id (str): Transfer ID
            password (str): Transfer password

        Returns:
            dict[str, dict]
        """
        r = self._transfer_id(transfer_id, password, login=False, return_response=True)
        response = decrypt_and_unpack(r.content)
        return response

    def login_from_transfer_id(self, transfer_id: str, password: str) -> dict:
        """Attempts to log in using the transfer ID

        Args:
            transfer_id (str): Transfer ID
            password (str): Transfer password

        Returns:
            dict: user data
        """
        return decrypt_and_unpack(
            self._transfer_id(transfer_id, password, login=True, return_response=True).content
        )
    
    def create_transfer_id(self, user_id: int, password: str) -> dict:
        data = self.call(
            "PUT",
            f"api/user/{user_id}/inherit",
            data={"password": password}
        )
        return {
            "transferId": data["userInherit"]["inheritId"],
            "password": password
        }
    #endregion
    #region Profile
    def set_name(self, user_id: int, name: str) -> ...:
        return self.update_section(user_id, "userGamedata", {"name": name})
    
    def update_profile(self, 
                       user_id: int, 
                       bio: str, 
                       honorIDs: list[int, int, int],
                       twitterId: str) -> ...:
        return self.call(
            "PUT",
            f"api/user/{user_id}/profile",
            data={
                "userId": user_id,
                "word": bio,
                "honorId1": honorIDs[0] or None,
                "honorId2": honorIDs[1] or None,
                "honorId3": honorIDs[2] or None,
                "twitterId": twitterId,
                "profileImageType": "leader",
                "profileImageId": None
            }
        )
    #endregion
    #region Refreshes
    def check_login_bonus(self, user_id: int) -> list[dict]:
        r = self.call(
            "PUT",
            f"api/user/{user_id}/home/refresh",
            {"refreshableTypes": ["login_bonus"], "isAdult": True}
        )
        return r.get("userLoginBonuses", {})

    def check_friend_requests(self, user_id: int):
        return self.call(
            "PUT",
            f"api/user/{user_id}/home/refresh",
            {"refreshableTypes": ["new_pending_friend_request"], "isAdult": True}
        ).get("newPendingUserFriends")
    
    def list_presents(self, user_id) -> dict:
        return self.call(
            "PUT",
            f"api/user/{user_id}/home/refresh", 
            {"refreshableTypes": ["login_bonus"], "isAdult": True}
        ).get("updatedResources", {}).get("userPresents", {})
    
    def open_presents(self, user_id: int, present_ids: list[int]) -> dict:
        return self.call(
            "POST",
            f"api/user/{user_id}/present",
            data={"presentIds": present_ids}
        )
    
    def check_invitations(self, user_id: int) -> dict:
        return self.call(
            "PUT",
            f"api/user/{user_id}/invitation"
        )
    #endregion
    #region Inventory
    def restore_energy(self, user_id: int, items: list[dict]) -> ...: 
        """
        Args:
            user_id (int)
            items (list[dict]): list of dict with items {resourceId: int, resourceType: str, quantity: int}
        """
        raise NotImplementedError
    
    def claim_mission(self, user_id: int, mission_type: str, mission_ids: list[int]) -> dict:
        return self.call(
            "PUT",
            f"api/user/{user_id}/mission/{mission_type}",
            data={"missionIds": mission_ids}
        )
    
    def set_favorited_stamps(self, user_id: int) -> dict:
        raise NotImplementedError
    #endregion
    #region Cards
    def train_card(self, 
                   user_id: int, 
                   card_id: int,
                   scores: list[int, int]) -> dict: 
        """Increase a cards level using practice scores

        Args:
            user_id (int)
            card_id (int)
            scores (list[int, int]): [Score ID, Count]
        
        Common Score IDs:
            1 - Practice score (Beginner)
            2 - Practice score (Intermediate)
            3 - Practice score (Advanced)

        Returns:
            dict: _description_
        """
        data = {
            'costs': [
                [{'resourceId': score[0], 'resourceType': 'practice_ticket', 'resourceLevel': 0, 'quantity': score[1]} for score in scores if len(score) == 2],
            ]
        }
        return self.call(
            "POST",
            f"api/user/{user_id}/card/{card_id}/practice-ticket",
            data=data
        )
    def increase_mastery_rank(self, user_id: int, card_id: int, mastery_levels: list[int]) -> int:
        return self.call(
            "POST",
            f"api/user/{user_id}/card/{card_id}/master-lesson",
            data={"masterLessonCostIds": mastery_levels},
            return_response=True
        ).status_code
    #endregion
    #region Character Info
    def claim_character_rank_stamps(self, user_id: int, character_id: int) -> int:
        return self.call(
            "PUT",
            f"api/user/{user_id}/character/{character_id}/mission",
            data=EMPTY,
            return_response=True
        ).status_code
    def set_costume(self,
                    user_id: int, 
                    character_id: int,
                    head_costume_id: int,
                    body_costume_id: int,
                    hair_costume_id: int) -> None:
        raise NotImplementedError
        return self.call(
            "PUT",
            f"api/user/{user_id}/character-costume-3d/character/{character_id}/unit/{UNIT_ID}",
            data={'headCostume3dId': head_costume_id, 'bodyCostume3dId': body_costume_id, 'hairCostume3dId': hair_costume_id}
        )
    #endregion
    #region Reading/Stories
    def read_area_conversation(self, user_id: int, conversation_id: int,) -> int:
        return self.call(
            "PUT",
            f"api/user/{user_id}/action-set/{conversation_id}",
            data=EMPTY,
            return_response=True
        ).status_code
    
    def _read_story(self,
                    url: str,
                    skip: bool,
                    num_pages: int) -> httpx.Response:
        self.call(
            "POST",
            url,
            data=EMPTY
        )
        return self.call(
            "POST",
            f"{url}/log",
            data={
                "noSkip": not skip,
                "useSkip": skip,
                "autoFinish": False, 
                "useAuto": False, 
                "fastForward": False, 
                "voice": False, 
                "numPages": num_pages, 
                "continuousPlayStart": False, 
                "playMusicVideo": False, 
                "musicVocalId": 0, 
                "musicCategoryName": 
                "mv", 
                "musicVideoNoSkip": False, 
                "userStoryMusicPlays": []
            },
            return_response=True,
            raise_on_non_20x=False
        ).status_code
    
    def read_unit_story(self, 
                         user_id: int, 
                         story_id: int, 
                         skip: bool = True,
                         num_pages: int = 0) -> int:
        return self._read_story(
            f"api/user/{user_id}/story/unit_story/episode/{story_id}",
            skip,
            num_pages
        )
    
    def read_archive_event_story(self, 
                                 user_id: int, 
                                 story_id: int, 
                                 skip: bool = True,
                                 num_pages: int = 0) -> int:
        return self._read_story(
            f"api/user/{user_id}/story/archive_event_story/episode/{story_id}",
            skip,
            num_pages
        )
    def read_special_story(self, 
                                 user_id: int, 
                                 story_id: int) -> int:
        return self.call(
            "POST",
            f"api/user/{user_id}/story/special_story/episode/{story_id}",
            data=EMPTY
        )
    #endregion
    #region Live Shows
    def start_live_show(self,
                        user_id: int,
                        song_id: int,
                        difficulty_id: int,
                        vocal_id: int,
                        deck_id: int,
                        boost_count: int,
                        is_auto: bool,
                        music_category_name: str,
                        score: int,
                        accuracyCount: list[int, int, int, int, int],
                        maxCombo: int,
                        life: int,
                        tapCount: int,
                        ingameCutinCharacterArchiveVoiceGroupIds: list[int],
                        liveResultCharacterArchiveVoiceGroupId: int) -> int:
        raise NotImplementedError
    #endregion
    #region Virtual Shows
    def attend_virtual_show(self, user_id: int) -> dict:
        # i have no clue how im gonna do this.
        raise NotImplementedError
    #endregion
    #region Events
    def get_event_ranking_raw(self, user_id: int, event_id: int) -> dict:
        return self.call(
            "GET",
            f"api/user/{user_id}/event/{event_id}/ranking?rankingViewType=user_myself"
        )
    
    def get_event_participation(self, user_id: int, event_id: int) -> dict:
        user_status = self.get_event_ranking_raw(user_id, event_id).get('userRankingStatus')
        return not (user_status is None or user_status == 'non_participation')
    
    def get_event_rank(self, user_id: int, event_id: int) -> int | None:
        rankings = self.get_event_ranking_raw(user_id, event_id).get('rankings')
        if rankings is None: return None
        user_ranking = [rank for rank in rankings if rank.get('userId') == user_id]
        if len(user_ranking) == 0: return None
        return user_ranking[0].get('rank', 0)
    
    def get_event_score(self, user_id: int, event_id: int) -> int | None:
        rankings = self.get_event_ranking_raw(user_id, event_id).get('rankings')
        if rankings is None: return None
        user_ranking = [rank for rank in rankings if rank.get('userId') == user_id]
        if len(user_ranking) == 0: return None
        return user_ranking[0].get('score', 0)
    
    def exchange_for_event_item(self, user_id: int, item_id: int, count: int) -> dict:
        return self.call(
            "PUT",
            f"api/user/{user_id}/event-exchange/{item_id}?count={count}"
        )
    #endregion
    #region Green Room
    def claim_green_room(self, user_id: int, cards: list[dict]) -> dict:
        assert isinstance(cards, list)
        assert len(cards) != 0
        return self.call(
            "PUT",
            f"api/user/{user_id}/card/?behavior=exchange",
            data={"userCards": cards}
        )
    #endregion
    #region Gachas
    def pull_gacha(self,
                   user_id: int,
                   gacha_id: int,
                   gacha_behavior_id: int,
                   use_paid_crystals: bool = False) -> dict:
        raise NotImplementedError
    #endregion
    #region Items / Shops
    def _purchase_item(self, user_id: int, shop_id: int, item_id: int) -> ...:
        return self.call(
            "POST",
            f"api/user/{user_id}/shop/{shop_id}/item/{item_id}",
            data=EMPTY
        )
    
    def purchase_costume(self, user_id: int, costume_id: int) -> dict:
        return self.call(
            "POST",
            f"api/user/{user_id}/costume-3d-shop/{costume_id}",
            data=EMPTY
        )
    
    def purchase_stamp(self, user_id: int, stamp_id: int) -> dict:
        return self._purchase_item(user_id, 12, stamp_id)

    def purchase_song(self, user_id: int, song_id: int) -> dict:
        # shop id 2
        raise NotImplementedError
    
    def purchase_cover(self, user_id: int, cover_id: int) -> dict:
        # shop id 4
        raise NotImplementedError
    
    def purchase_area_item(self, user_id: int, group: int, item_id: int) -> dict:
        raise NotImplementedError
    
    #endregion
    #region Other Users
    def search_user(self, other_user_id: int) -> dict:
        """Returns a user's profile info, given by searching for their ID. Contains minimal info."""
        return self.call(
            "GET",
            f"api/user/{other_user_id}/profile"
        )
    
    def send_friend_request(self, 
                            user_id: int, 
                            other_user_id: int, 
                            message: str = "Hi there! Nice to meet you!") -> int:
        return self.call(
            "POST",
            f"api/user/{user_id}/friend/{other_user_id}",
            data={'message': message, 'friendRequestSentLocation': 'id_search'}
        )
    #endregion