"""
A launcher profile module. Contains many useful function to manage minecraft account
INFO: The account data generated by this module is not compatible with official launcher profile!

This module is vibed too!
"""

from __future__ import annotations

import base64
import binascii
import datetime
import json
import logging
import os
from copy import deepcopy
from dataclasses import dataclass, field
from pathlib import Path

from core_lib.common.common import bytes_to_data_url
from core_lib.common.exception import (
    AccountException,
    AccountKeyNotFoundException,
    AccountLoadException,
    AccountSaveException,
)

logger = logging.getLogger("Launcher.CoreLib")


def get_account_sample() -> dict:
    return {"accounts": {}, "activeAccountId": None}


get_accounts_sample = get_account_sample


@dataclass
class Account:
    account_id: str
    username: str
    access_token: str
    refresh_token: str
    access_token_expires_at: str | datetime.datetime
    refresh_token_expires_at: str | datetime.datetime
    minecraft_uuid: str = ""
    minecraft_name: str = ""
    avatar: str = ""
    type: str = "msa"
    user_properties: list = field(default_factory=list)


def _datetime_string(value: str | datetime.datetime, field_name: str) -> str:
    if isinstance(value, str):
        return value
    if isinstance(value, datetime.datetime):
        return value.isoformat().replace("+00:00", "Z")
    raise TypeError(f'"{field_name}" must be a datetime.datetime or str')


def create_account_data_from_object(account: Account) -> dict:
    """
    Convert Account object to dictionary
    :param account:
    :return:
        account_dict: dict
    """
    return {
        "accessToken": account.access_token,
        "accessTokenExpiresAt": _datetime_string(
            account.access_token_expires_at, "access_token_expires_at"
        ),
        "refreshToken": account.refresh_token,
        "refreshTokenExpiresAt": _datetime_string(
            account.refresh_token_expires_at, "refresh_token_expires_at"
        ),
        "avatar": account.avatar,
        "accountID": account.account_id,
        "minecraftProfile": {
            "uuid": account.minecraft_uuid,
            "name": account.minecraft_name,
        },
        "type": account.type,
        "userProperties": deepcopy(account.user_properties),
        "username": account.username,
    }

SUPPORT_DATA_URL_IMAGE_TYPE = [
    "image/apng",
    "image/avif",
    "image/bmp",
    "image/gif",
    "image/jpeg",
    "image/png",
    "image/svg+xml",
    "image/tiff",
    "image/vnd.microsoft.icon",  # .ico
    "image/webp",
]

def is_valid_account_avatar(avatar: str | bytes, support_type: list[str] | None=None) -> bool:
    if isinstance(avatar, bytes):
        try:
            avatar = bytes_to_data_url(avatar)
        except (TypeError, ValueError):
            return False

    if not isinstance(avatar, str):
        return False

    # For empty avatar (not set)
    if isinstance(avatar, str) and not avatar:
        return True

    if support_type is None:
        support_type = SUPPORT_DATA_URL_IMAGE_TYPE

    try:
        header, encoded_data = avatar.split(",", 1)
    except ValueError:
        # For keyword avatar (This need launcher to replace the keyword to mapped image)
        return True

    # Check header
    if not header.startswith("data:image/"):
        return False

    if not header.endswith(";base64"):
        return False

    image_type = header.removeprefix("data:").removesuffix(";base64")

    if image_type not in support_type:
        logger.info(f"Unsupported image type: {image_type}")
        return False

    # Try to decode the avatar
    try:
        base64.b64decode(encoded_data, validate=True)
        return True
    except (binascii.Error, ValueError) as exc:
        logger.info(f"Unable to decode account avatar: {exc}")

    return False

def _check_account(account: dict, account_id: str | None = None, fix_wrong: bool = False) -> bool:
    """
    Check if an account is valid.
    :param account:
    :param account_id:
    :param fix_wrong:
    :return:
        is_valid: bool
    """
    if not isinstance(account, dict):
        return False

    required_strings = (
        "accessToken",
        "accessTokenExpiresAt",
        "refreshToken",
        "refreshTokenExpiresAt",
        "accountID",
        "type",
        "username",
    )

    # Check all required key is exiting
    if any(not isinstance(account.get(key), str) or not account[key] for key in required_strings):
        return False

    # Check account id is match as provided
    if account_id is not None and account["accountID"] != account_id:
        return False

    # Verify profile data (Must contain uuid and name)
    minecraft_profile = account.get("minecraftProfile")
    if not isinstance(minecraft_profile, dict):
        return False
    if any(not isinstance(minecraft_profile.get(key), str) for key in ("uuid", "name")):
        return False

    # User properties (Left from official launcher, maybe this field will put some custom account settings in the feture
    properties = account.get("userProperties")
    if properties is None and fix_wrong:
        account["userProperties"] = []
    elif not isinstance(properties, list):
        return False

    # Account avatar
    avatar = account.get("avatar")
    if avatar is None and fix_wrong:
        account["avatar"] = ""
    elif not isinstance(avatar, str):
        return False

    if not is_valid_account_avatar(avatar):
        return False

    return True


def is_account_valid(account_data: dict, fix_wrong: bool = False) -> bool:
    return _check_account(account_data, fix_wrong=fix_wrong)


def is_accounts_valid(accounts: dict, fix_wrong: bool = False, ignore_invalid_account: bool = False) -> bool:
    """
    Check if accountData (contain multiple account and some settings) is valid.
    :param accounts:
    :param fix_wrong:
    :param ignore_invalid_account:
    :return:
        is_valid: bool
    """
    if not isinstance(accounts, dict) or not isinstance(accounts.get("accounts"), dict):
        return False

    account_map = accounts["accounts"]
    active_id = accounts.get("activeAccountId")

    if active_id is not None and not isinstance(active_id, str):
        return False

    invalid_ids = [
        account_id
        for account_id, account in account_map.items()
        if not isinstance(account_id, str)
        or not _check_account(account, account_id=account_id, fix_wrong=fix_wrong)
    ]

    if invalid_ids and not ignore_invalid_account:
        return False

    for account_id in invalid_ids:
        account_map.pop(account_id, None)

    if active_id is not None and active_id not in account_map:
        if fix_wrong or ignore_invalid_account:
            accounts["activeAccountId"] = next(iter(account_map), None)
        else:
            return False

    return True


def list_accounts(accounts: dict) -> dict:
    """
    Get account map from accountData.
    :param accounts:
    :return:
    """
    value = accounts.get("accounts", {}) if isinstance(accounts, dict) else {}
    return value if isinstance(value, dict) else {}


def is_account_exists(accounts: dict, account_id: str) -> bool:
    """
    Check if an account exists in accountData.
    :param accounts:
    :param account_id:
    :return:
    """
    return account_id in list_accounts(accounts)


def get_account(accounts: dict, account_id: str) -> dict:
    """
    Get an account from accountData.
    :param accounts:
    :param account_id:
    :return:
        account_dict: dict
    """
    try:
        return list_accounts(accounts)[account_id]
    except KeyError as exc:
        raise AccountKeyNotFoundException(f"Specified account id {account_id} not found.") from exc


def get_current_account_id(accounts: dict) -> str | None:
    """
    Get current account id from accountData.
    :param accounts:
    :return:
        account_dict: dict
    """
    return accounts.get("activeAccountId") if isinstance(accounts, dict) else None


get_active_account_id = get_current_account_id


def get_current_account(accounts: dict) -> dict | None:
    """
    Get current account from accountData.
    :param accounts:
    :return:
        account_dict: dict
    """
    account_id = get_current_account_id(accounts)

    return get_account(accounts, account_id) if account_id is not None else None


get_active_account = get_current_account


def set_current_account_id(accounts: dict, account_id: str | None) -> None:
    """
    Set current account id
    :param accounts:
    :param account_id:
    :return:
    """
    if account_id is not None and not is_account_exists(accounts, account_id):
        raise AccountKeyNotFoundException(f"Specified account id {account_id} not found.")

    accounts["activeAccountId"] = account_id


set_active_account_id = set_current_account_id


def add_account(accounts: dict, account: dict, *, overwrite: bool = False) -> tuple[dict, str]:
    """
    Add an account to accountData.
    :param accounts:
    :param account:
    :param overwrite:
    :return:
        new_account_data: dict
        new_account_id: str
    """
    if not is_account_valid(account):
        raise AccountException("Provided account data is not valid.")

    account_id = account["accountID"]
    account_map = list_accounts(accounts)

    if account_id in account_map and not overwrite:
        raise AccountException(f"Account id {account_id} already exists.")

    account_map[account_id] = account

    return accounts, account_id


def create_account(account: Account, accounts: dict | None = None) -> tuple[dict, str]:
    """
    Create new account
    :param account:
    :param accounts:
    :return:
        account_dict: dict
        new_account_id: str
    """
    if accounts is None:
        accounts = get_account_sample()

    account_data = create_account_data_from_object(account)
    _, account_id = add_account(accounts, account_data)

    return account_data, account_id


def update_account(accounts: dict, account_id: str, account_data: dict, fix_wrong: bool = False) -> tuple[dict, str]:
    """
    Update target account data value
    :param accounts:
    :param account_id:
    :param account_data:
    :param fix_wrong:
    :return:
        updated_account_data: dict
        updated_account_id: str
    """
    if not is_account_exists(accounts, account_id):
        raise AccountKeyNotFoundException(f"Specified account id {account_id} not found.")

    if not _check_account(account_data, account_id=account_id, fix_wrong=fix_wrong):
        raise AccountException("Provided account data is not valid.")

    accounts["accounts"][account_id] = account_data

    return accounts, account_id


def remove_account(accounts: dict, account_id: str) -> None:
    """
    Remove account from accountData.
    INFO: If the active account ID is same as the account that provided. Active account will be set to None.
    :param accounts:
    :param account_id:
    :return:
    """
    account_map = list_accounts(accounts)

    try:
        del account_map[account_id]
    except KeyError as exc:
        raise AccountKeyNotFoundException(f"Specified account id {account_id} not found.") from exc

    if get_current_account_id(accounts) == account_id:
        accounts["activeAccountId"] = next(iter(account_map), None)


def convert_account_data_to_object(accounts: dict, account_id: str) -> Account:
    """
    Convert account data to Account
    :param accounts:
    :param account_id:
    :return:
        account_object: Account
    """
    data = get_account(accounts, account_id)
    minecraft_profile = data.get("minecraftProfile", {})
    return Account(
        account_id=data.get("accountID", account_id),
        username=data.get("username", ""),
        access_token=data.get("accessToken", ""),
        refresh_token=data.get("refreshToken", ""),
        access_token_expires_at=data.get("accessTokenExpiresAt", ""),
        refresh_token_expires_at=data.get("refreshTokenExpiresAt", ""),
        minecraft_uuid=minecraft_profile.get("uuid", ""),
        minecraft_name=minecraft_profile.get("name", ""),
        avatar=data.get("avatar", ""),
        type=data.get("type", "msa"),
        user_properties=deepcopy(data.get("userProperties", [])),
    )


def read_account_file(account_filepath: str | Path) -> dict:
    """
    Read account file
    :param account_filepath:
    :return:
        accountData: dict
    """
    path = Path(account_filepath)
    if not path.exists():
        raise AccountLoadException(f"Account file does not exist: {path}")
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, UnicodeError, json.JSONDecodeError) as exc:
        raise AccountLoadException(f"Unable to read account file: {exc}") from exc
    if not is_accounts_valid(data):
        raise AccountLoadException("Account file contains invalid account data.")
    return data


read_accounts_file = read_account_file


def create_or_save_account_file(accounts: dict, account_filepath: str | Path,
                                account_file_bak_filepath: str | Path | None = None,
                                overwrite: bool = True,
                                create_backup: bool = True) -> None:
    """
    Create or save account file
    :param accounts:
    :param account_filepath:
    :param account_file_bak_filepath:
    :param overwrite:
    :param create_backup:
    :return:
    """
    if not is_accounts_valid(accounts):
        raise AccountSaveException("Refusing to save invalid account data.")

    path = Path(account_filepath)
    if path.exists() and not overwrite:
        raise AccountSaveException(f"Account file already exists: {path}")
    backup = (
        Path(account_file_bak_filepath)
        if account_file_bak_filepath is not None
        else path.with_suffix(path.suffix + ".bak")
    )
    temporary = path.with_suffix(path.suffix + ".tmp")

    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        temporary.write_text(
            json.dumps(accounts, indent=4, ensure_ascii=False), encoding="utf-8"
        )
        if create_backup and path.exists():
            if backup.exists():
                backup.unlink()
            os.replace(path, backup)
        os.replace(temporary, path)
    except OSError as exc:
        if create_backup and backup.exists() and not path.exists():
            os.replace(backup, path)
        try:
            temporary.unlink(missing_ok=True)
        except OSError:
            pass
        raise AccountSaveException(f"Unable to save account file: {exc}") from exc

def get_account_avatar(accounts: dict, account_id: str) -> str | None:
    """
    Get account avatar fron raw data
    :param accounts:
    :param account_id:
    :return:
        avatar_data_url: str | None
    """
    account = get_account(accounts, account_id)

    if "avatar" not in account:
        raise AccountKeyNotFoundException(f"Account {account_id} missing 'avatar' field")

    return account.get("avatar", None)

def save_account_avatar(accounts: dict, account_id: str, avatar_raw: bytes | str) -> None:
    """
    Save account avatar to account data
    :param accounts:
    :param account_id:
    :param avatar_raw: Set it to empty string as default
    :return:
    """
    if not is_account_exists(accounts, account_id):
        raise AccountKeyNotFoundException(f"Specified account id {account_id} not found.")

    if isinstance(avatar_raw, bytes):
        try:
            avatar_raw = bytes_to_data_url(avatar_raw)
        except (TypeError, ValueError) as exc:
            raise AccountSaveException("Invalid account avatar bytes.") from exc

    if not is_valid_account_avatar(avatar_raw):
        raise AccountSaveException(f"Invalid account avatar: {avatar_raw}")

    accounts["accounts"][account_id]["avatar"] = avatar_raw

    return None

create_or_save_accounts_file = create_or_save_account_file
