import asyncio
import time
import random
import json
import base64
from collections import defaultdict
from datetime import datetime
from typing import Tuple, Dict, Any, Optional

import aiohttp
import httpx
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from google.protobuf import json_format, message
from google.protobuf.message import Message

from .proto import (
    FreeFire_pb2, main_pb2, AccountPersonalShow_pb2,
    PlayerPersonalShow_pb2, PlayerStats_pb2, PlayerCSStats_pb2,
    MajoRLoGinrEq_pb2, MajoRLoGinrEs_pb2
)

# ========== Constants ==========
MAIN_KEY = base64.b64decode('WWcmdGMlREV1aDYlWmNeOA==')
MAIN_IV = base64.b64decode('Nm95WkRyMjJFM3ljaGpNJQ==')
SECRET_KEY = b'Yg&tc%DEuh6%Zc^8'
SECRET_IV = b'6oyZDr22E3ychjM%'

RELEASEVERSION = "OB52"                     # hardcoded, update manually if needed
USERAGENT = "Dalvik/2.1.0 (Linux; U; Android 13; CPH2095 Build/RKQ1.211119.001)"
SUPPORTED_REGIONS = {"IND", "BR", "US", "SAC", "NA", "SG", "RU", "ID", "TW", "VN", "TH", "ME", "PK", "CIS", "BD", "EU"}

# Token cache
_token_cache: Dict[str, Dict] = {}
_token_lock = asyncio.Lock()

# ========== Helpers ==========
def aes_cbc_encrypt(key: bytes, iv: bytes, plaintext: bytes) -> bytes:
    cipher = AES.new(key, AES.MODE_CBC, iv)
    return cipher.encrypt(pad(plaintext, AES.block_size))

def aes_cbc_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
    cipher = AES.new(key, AES.MODE_CBC, iv)
    return unpad(cipher.decrypt(ciphertext), AES.block_size)

def decode_protobuf(encoded_data: bytes, message_type: Message) -> Message:
    instance = message_type()
    instance.ParseFromString(encoded_data)
    return instance

async def json_to_proto(json_data: str, proto_message: Message) -> bytes:
    json_format.ParseDict(json.loads(json_data), proto_message)
    return proto_message.SerializeToString()

def random_ua():
    """Generate a random GarenaMSDK user-agent string."""
    versions = [
        '4.0.18P6', '4.0.19P7', '4.0.20P1', '4.1.0P3', '4.1.5P2', '4.2.1P8',
        '4.2.3P1', '5.0.1B2', '5.0.2P4', '5.1.0P1', '5.2.0B1', '5.2.5P3',
        '5.3.0B1', '5.3.2P2', '5.4.0P1', '5.4.3B2', '5.5.0P1', '5.5.2P3'
    ]
    models = [
        'SM-A125F', 'SM-A225F', 'SM-A325M', 'SM-A515F', 'SM-A725F', 'SM-M215F', 'SM-M325FV',
        'Redmi 9A', 'Redmi 9C', 'POCO M3', 'POCO M4 Pro', 'RMX2185', 'RMX3085',
        'moto g(9) play', 'CPH2239', 'V2027', 'OnePlus Nord', 'ASUS_Z01QD',
    ]
    android_versions = ['9', '10', '11', '12', '13', '14']
    languages = ['en-US', 'es-MX', 'pt-BR', 'id-ID', 'ru-RU', 'hi-IN']
    countries = ['USA', 'MEX', 'BRA', 'IDN', 'RUS', 'IND']
    version = random.choice(versions)
    model = random.choice(models)
    android = random.choice(android_versions)
    lang = random.choice(languages)
    country = random.choice(countries)
    return f"GarenaMSDK/{version}({model};Android {android};{lang};{country};)"

# ========== Account Credentials (hardcoded) ==========
def get_account_credentials(region: str) -> Tuple[int, str]:
    r = region.upper()
    if r == "IND":
        return 4553603190, "jaduu_F8NMV_BY_MAHU_APIS_XG2MN"
    elif r == "BD":
        return 4566383442, "BLACK_MYLGF_BY_MAHU_APIS_TE49G"
    elif r in {"BR", "US", "SAC", "NA"}:
        return 4566388023, "BLACK_HSDPL_BY_MAHU_APIS_2NPV0"
    else:
        return 4553603201, "jaduu_23MKB_BY_MAHU_APIS_1F3WW"

# ========== Garena OAuth Token ==========
async def get_access_token(uid: int, password: str) -> Tuple[Optional[str], Optional[str]]:
    url = "https://100067.connect.garena.com/oauth/guest/token/grant"
    headers = {
        "Host": "100067.connect.garena.com",
        "User-Agent": random_ua(),
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept-Encoding": "gzip, deflate, br",
        "Connection": "close"
    }
    data = {
        "uid": uid,
        "password": password,
        "response_type": "token",
        "client_type": "2",
        "client_secret": "2ee44819e9b4598845141067b281621874d0d5d7af9d8f7e00c1e54715b7d1e3",
        "client_id": "100067"
    }
    try:
        async with aiohttp.ClientSession() as session:
            async with session.post(url, headers=headers, data=data) as resp:
                if resp.status != 200:
                    return None, None
                resp_data = await resp.json()
                open_id = resp_data.get("open_id")
                access_token = resp_data.get("access_token")
                return open_id, access_token
    except Exception:
        return None, None

async def build_major_login(open_id: str, access_token: str) -> bytes:
    major = MajoRLoGinrEq_pb2.MajorLogin()
    major.event_time = str(datetime.now())[:-7]
    major.game_name = "free fire"
    major.platform_id = 1
    major.client_version = "1.120.1"
    major.system_software = "Android OS 9 / API-28 (PQ3B.190801.10101846/G9650ZHU2ARC6)"
    major.system_hardware = "Handheld"
    major.telecom_operator = "Verizon"
    major.network_type = "WIFI"
    major.screen_width = 1920
    major.screen_height = 1080
    major.screen_dpi = "280"
    major.processor_details = "ARM64 FP ASIMD AES VMH | 2865 | 4"
    major.memory = 3003
    major.gpu_renderer = "Adreno (TM) 640"
    major.gpu_version = "OpenGL ES 3.1 v1.46"
    major.unique_device_id = "Google|34a7dcdf-a7d5-4cb6-8d7e-3b0e448a0c57"
    major.client_ip = "223.191.51.89"
    major.language = "en"
    major.open_id = open_id
    major.open_id_type = "4"
    major.device_type = "Handheld"
    major.memory_available.version = 55
    major.memory_available.hidden_value = 81
    major.access_token = access_token
    major.platform_sdk_id = 1
    major.network_operator_a = "Verizon"
    major.network_type_a = "WIFI"
    major.client_using_version = "7428b253defc164018c604a1ebbfebdf"
    major.external_storage_total = 36235
    major.external_storage_available = 31335
    major.internal_storage_total = 2519
    major.internal_storage_available = 703
    major.game_disk_storage_available = 25010
    major.game_disk_storage_total = 26628
    major.external_sdcard_avail_storage = 32992
    major.external_sdcard_total_storage = 36235
    major.login_by = 3
    major.library_path = "/data/app/com.dts.freefireth-YPKM8jHEwAJlhpmhDhv5MQ==/lib/arm64"
    major.reg_avatar = 1
    major.library_token = "5b892aaabd688e571f688053118a162b|/data/app/com.dts.freefireth-YPKM8jHEwAJlhpmhDhv5MQ==/base.apk"
    major.channel_type = 3
    major.cpu_type = 2
    major.cpu_architecture = "64"
    major.client_version_code = "2019118695"
    major.graphics_api = "OpenGLES2"
    major.supported_astc_bitset = 16383
    major.login_open_id_type = 4
    major.analytics_detail = b"FwQVTgUPX1UaUllDDwcWCRBpWAUOUgsvA1snWlBaO1kFYg=="
    major.loading_time = 13564
    major.release_channel = "android"
    major.extra_info = "KqsHTymw5/5GB23YGniUYN2/q47GATrq7eFeRatf0NkwLKEMQ0PK5BKEk72dPflAxUlEBir6Vtey83XqF593qsl8hwY="
    major.android_engine_init_flag = 110009
    major.if_push = 1
    major.is_vpn = 1
    major.origin_platform_type = "4"
    major.primary_platform_type = "4"
    serialized = major.SerializeToString()
    return aes_cbc_encrypt(SECRET_KEY, SECRET_IV, serialized)

async def create_jwt(region: str):
    """Obtain a new token for the given region and store it in cache."""
    uid, password = get_account_credentials(region)
    open_id, access_token = await get_access_token(uid, password)
    if not open_id or not access_token:
        return

    payload = await build_major_login(open_id, access_token)

    # Choose login URL based on region
    if region.upper() == "ME":
        base_url = "https://loginbp.common.ggbluefox.com/"
    else:
        base_url = "https://loginbp.ggblueshark.com/"
    url = f"{base_url}MajorLogin"

    headers = {
        'User-Agent': random_ua(),
        'Connection': "Keep-Alive",
        'Accept-Encoding': "gzip",
        'Content-Type': "application/x-www-form-urlencoded",
        'Expect': "100-continue",
        'X-Unity-Version': "2018.4.11f1",
        'X-GA': "v1 1",
        'ReleaseVersion': RELEASEVERSION
    }

    async with httpx.AsyncClient(timeout=15.0) as client:
        resp = await client.post(url, data=payload, headers=headers)
        resp.raise_for_status()
        login_res = MajoRLoGinrEs_pb2.MajorLoginRes()
        login_res.ParseFromString(resp.content)

        async with _token_lock:
            _token_cache[region] = {
                'token': f"Bearer {login_res.token}",
                'region': getattr(login_res, 'lockRegion', region),
                'server_url': login_res.url,
                'expires_at': time.time() + 25200   # 7 hours
            }

async def get_token_info(region: str) -> Tuple[str, str, str]:
    """Return (token, region, server_url) for the region, refreshing if needed."""
    async with _token_lock:
        info = _token_cache.get(region)
        if info and time.time() < info['expires_at']:
            return info['token'], info['region'], info['server_url']

    # Refresh
    await create_jwt(region)
    async with _token_lock:
        info = _token_cache[region]
        return info['token'], info['region'], info['server_url']

# ========== Basic Info from topup.pk ==========
async def get_player_basic_info(uid: str) -> Optional[Tuple[str, int, str]]:
    url = "https://topup.pk/api/auth/player_id_login"
    headers = {
        "Accept": "application/json, text/plain, */*",
        "Accept-Encoding": "gzip, deflate, br",
        "Content-Type": "application/json",
        "Origin": "https://topup.pk",
        "Referer": "https://topup.pk/",
        "User-Agent": "Mozilla/5.0 (Linux; Android 15; RMX5070 Build/UKQ1.231108.001) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.7204.157 Mobile Safari/537.36",
        "X-Requested-With": "mark.via.gp"
    }
    payload = {"app_id": 100067, "login_id": str(uid)}
    try:
        async with aiohttp.ClientSession() as session:
            async with session.post(url, headers=headers, json=payload, timeout=15) as resp:
                if resp.status != 200:
                    return None
                data = await resp.json()
                if data.get("error") == "error_player_id_invalid":
                    return None
                nickname = data.get("nickname")
                region = data.get("region")
                if not region or not nickname:
                    return None
                return region, int(uid), nickname
    except Exception:
        return None

# ========== Full Account Info (Gallery etc.) ==========
async def get_full_account_info(uid: int, region: str, call_sign_src: int = 7, need_gallery_info: bool = True) -> dict:
    token, _, server = await get_token_info(region)
    request_data = {
        "accountId": uid,
        "callSignSrc": call_sign_src,
        "needGalleryInfo": need_gallery_info
    }
    payload = await json_to_proto(json.dumps(request_data), PlayerPersonalShow_pb2.request())
    encrypted_payload = aes_cbc_encrypt(MAIN_KEY, MAIN_IV, payload)

    headers = {
        'User-Agent': USERAGENT,
        'Connection': "Keep-Alive",
        'Accept-Encoding': "gzip",
        'Content-Type': "application/octet-stream",
        'Expect': "100-continue",
        'Authorization': token,
        'X-Unity-Version': "2018.4.11f1",
        'X-GA': "v1 1",
        'ReleaseVersion': RELEASEVERSION
    }

    async with httpx.AsyncClient() as client:
        resp = await client.post(server + "/GetPlayerPersonalShow", data=encrypted_payload, headers=headers)
        resp.raise_for_status()
        info_msg = decode_protobuf(resp.content, PlayerPersonalShow_pb2.response)
        return json.loads(json_format.MessageToJson(info_msg))

# ========== Player Stats ==========
async def get_player_stats(uid: int, region: str, mode: str, match_type: str = "CAREER") -> dict:
    token, _, server = await get_token_info(region)

    if mode == "br":
        type_mapping = {"CAREER": 0, "NORMAL": 1, "RANKED": 2}
        url = f"{server}/GetPlayerStats"
        proto_module = PlayerStats_pb2
        request_type = proto_module.request
        response_type = proto_module.response
        payload_data = {"accountid": uid, "matchmode": type_mapping[match_type]}
    else:  # cs
        type_mapping = {"CAREER": 0, "NORMAL": 1, "RANKED": 6}
        url = f"{server}/GetPlayerTCStats"
        proto_module = PlayerCSStats_pb2
        request_type = proto_module.request
        response_type = proto_module.response
        payload_data = {"accountid": uid, "gamemode": 15, "matchmode": type_mapping[match_type]}

    payload = await json_to_proto(json.dumps(payload_data), request_type())
    encrypted_payload = aes_cbc_encrypt(MAIN_KEY, MAIN_IV, payload)

    headers = {
        'User-Agent': USERAGENT,
        'Connection': "Keep-Alive",
        'Accept-Encoding': "gzip",
        'Content-Type': "application/octet-stream",
        'Expect': "100-continue",
        'Authorization': token,
        'X-Unity-Version': "2018.4.11f1",
        'X-GA': "v1 1",
        'ReleaseVersion': RELEASEVERSION
    }

    async with httpx.AsyncClient() as client:
        resp = await client.post(url, data=encrypted_payload, headers=headers, timeout=30)
        resp.raise_for_status()
        stats_msg = decode_protobuf(resp.content, response_type)
        return json.loads(json_format.MessageToJson(stats_msg))

# ========== Ban Status ==========
async def fetch_ban_status(uid: str) -> dict:
    url = f"https://ff.garena.com/api/antihack/check_banned?lang=en&uid={uid}"
    headers = {
        "Accept": "application/json, text/plain, */*",
        "Accept-Encoding": "gzip, deflate, br",
        "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8",
        "User-Agent": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36",
        "Referer": "https://ff.garena.com/en/support/",
        "Origin": "https://ff.garena.com",
        "X-Requested-With": "B6FksShzIgjfrYImLpTsadjS86sddhFH"
    }
    cookies = {
        "_ga_KE3SY7MRSD": "GS2.1.s1772179297$o1$g1$t1772179297$j60$l0$h0",
        "_ga": "GA1.1.1848723043.1772179297",
        "_ga_RF9R6YT614": "GS2.1.s1772179298$o1$g0$t1772179298$j60$l0$h0"
    }
    try:
        async with aiohttp.ClientSession() as session:
            session.headers.update(headers)
            session.cookie_jar.update_cookies(cookies)
            async with session.get(url, timeout=10) as resp:
                if resp.status != 200:
                    return {"error": f"HTTP {resp.status}"}
                return await resp.json()
    except Exception as e:
        return {"error": str(e)}

# ========== Timestamp Converter ==========
def convert_timestamps(obj: Any, key: str = None) -> Any:
    if isinstance(obj, dict):
        return {k: convert_timestamps(v, k) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [convert_timestamps(item, None) for item in obj]
    else:
        if key and key.lower().endswith(('at', 'time')):
            try:
                if isinstance(obj, str) and obj.isdigit():
                    num = int(obj)
                elif isinstance(obj, (int, float)):
                    num = obj
                else:
                    return obj
                if num > 1e8:
                    dt = datetime.fromtimestamp(num)
                    return dt.strftime('%Y-%m-%d %H:%M:%S')
                else:
                    return obj
            except (ValueError, TypeError, OSError, OverflowError):
                return obj
        else:
            return obj

# ========== Main Public Function ==========
async def fetch_player_info(uid: str) -> dict:
    """
    Fetch all available information for a Free Fire player.

    Args:
        uid: Player's UID (string)

    Returns:
        Dictionary containing BanInfo, basic account info, playerStats,
        creditScoreInfo, and galleryInfo.

    Raises:
        ValueError: If UID is invalid or region unsupported.
        Exception: For network/protobuf errors.
    """
    # 1. Basic info from topup.pk
    basic = await get_player_basic_info(uid)
    if not basic:
        raise ValueError("Invalid UID or player not found")
    region, uid_int, nickname = basic
    if region not in SUPPORTED_REGIONS:
        raise ValueError(f"Unsupported region: {region}")

    # 2. Full account info (including gallery)
    account_info = await get_full_account_info(uid_int, region)
    account_info = convert_timestamps(account_info)

    # 3. Ban status
    ban_data = await fetch_ban_status(uid)
    ban_info = {}
    if "error" in ban_data:
        ban_info = {"error": ban_data["error"]}
    else:
        is_banned = ban_data.get("data", {}).get("is_banned", 0) == 1
        period = ban_data.get("data", {}).get("period", 0)
        if is_banned:
            last_login = account_info.get("basicInfo", {}).get("lastLoginAt")
            ban_time = convert_timestamps(last_login, "lastLoginAt") if last_login else "unknown"
            ban_info = {"banned": True, "period": period, "ban_time": ban_time}
        else:
            ban_info = {"banned": False, "period": period, "ban_time": "not banned"}

    # 4. Stats for BR and CS
    stats = {"br": {}, "cs": {}}
    for mode in ["br", "cs"]:
        for match_type in ["CAREER", "NORMAL", "RANKED"]:
            try:
                mode_stats = await get_player_stats(uid_int, region, mode, match_type)
                mode_stats = convert_timestamps(mode_stats)
                stats[mode][match_type] = mode_stats
            except Exception as e:
                stats[mode][match_type] = {"error": str(e)}

    # 5. Separate gallery fields
    standard_keys = {
        'basicInfo', 'profileInfo', 'clanBasicInfo', 'captainBasicInfo',
        'petInfo', 'socialInfo', 'diamondCostRes', 'creditScoreInfo'
    }
    gallery_info = {}
    for key in list(account_info.keys()):
        if key not in standard_keys:
            gallery_info[key] = account_info.pop(key)

    # 6. Assemble final response
    response = {"BanInfo": ban_info}
    response.update(account_info)
    response["playerStats"] = stats
    credit_info = response.pop("creditScoreInfo", {})
    response["creditScoreInfo"] = credit_info
    if gallery_info:
        response["galleryInfo"] = gallery_info

    return response