#!/usr/bin/env python3
"""
Minecraft Microsoft Auth - works with current (2025-2026) Microsoft login pages.
Fixes msmcauth which is broken because Microsoft switched from single-quote
to escaped double-quote format in their login page JavaScript config.
"""


def _decode_jwt_payload(token: str) -> dict:
    """Decode JWT payload without verification (just for reading claims)."""
    import base64
    # Remove 't=' prefix if present
    if token.startswith('t='):
        token = token[2:]
    parts = token.split('.')
    if len(parts) != 3:
        return {}
    # Add padding
    payload = parts[1] + '=' * (4 - len(parts[1]) % 4)
    decoded = base64.urlsafe_b64decode(payload)
    return json.loads(decoded)


import re
import time
from dataclasses import dataclass
from typing import Optional

# Constants - use msmcauth URL format (with scope parameter)
AUTHORIZE_URL = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en"

USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0"

XBL_URL = "https://user.auth.xboxlive.com/user/authenticate"
XSTS_URL = "https://xsts.auth.xboxlive.com/xsts/authorize"
MC_LOGIN_URL = "https://api.minecraftservices.com/authentication/login_with_xbox"
MC_PROFILE_URL = "https://api.minecraftservices.com/minecraft/profile"
MC_OWNERSHIP_URL = "https://api.minecraftservices.com/entitlements/mcstore"
MC_CHANGE_USERNAME_URL = "https://api.minecraftservices.com/minecraft/profile/name"

# Regex patterns - constructed to handle escaped quotes in MS login page
# The page format is: sFTTag\":\"<input type=\\\"hidden\\\" ... value=\\\"TOKEN\\\"/>\"
# In raw HTML bytes: backslash(92) + quote(34) = escaped quote
PPFT_REGEX = re.compile(r'value=\\\"([A-Za-z0-9*!\$\-_]+)')
URLPOST_REGEX = re.compile(r'"urlPost":"([^"]+)"')


class AuthError(Exception):
    pass

class InvalidCredentialsError(AuthError):
    pass

class TwoFactorError(AuthError):
    pass

class NoXboxAccountError(AuthError):
    pass

class NoMinecraftError(AuthError):
    pass


@dataclass
class AuthResult:
    access_token: str
    username: str
    uuid: str
    gamertag: str
    xuid: str
    expires_in: int = 1209600


def extract_login_params(html: str) -> tuple[str, str]:
    """Extract PPFT token and POST URL from Microsoft login page."""
    ppft_match = PPFT_REGEX.search(html)
    if not ppft_match:
        raise AuthError("Failed to extract PPFT from login page")
    ppft = ppft_match.group(1)

    url_post_match = URLPOST_REGEX.search(html)
    if not url_post_match:
        raise AuthError("Failed to extract urlPost from login page")
    url_post = url_post_match.group(1)

    return ppft, url_post


class MicrosoftAuthenticator:
    """Authenticate email+password -> Minecraft access token."""

    def __init__(self, log_fn=None):
        import requests
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": USER_AGENT,
            "Accept": "application/json",
        })
        self.log = log_fn or (lambda *a, **k: None)

    def authenticate(self, email: str, password: str) -> AuthResult:
        """Full auth flow: email+password -> Minecraft access token."""

        # Step 1: Get Microsoft login page
        self.log("  [1/7] Getting Microsoft login page...")
        resp = self.session.get(AUTHORIZE_URL, allow_redirects=True, timeout=15)
        resp.raise_for_status()
        ppft, post_url = extract_login_params(resp.text)
        self.log("  [2/7] PPFT extracted, posting credentials...")

        # Step 2: POST credentials
        from urllib.parse import quote
        post_data = (
            f"login={quote(email)}"
            f"&loginfmt={quote(email)}"
            f"&passwd={quote(password)}"
            f"&PPFT={ppft}"
        )
        resp = self.session.post(
            post_url,
            data=post_data,
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            cookies=resp.cookies,
            allow_redirects=True,
            timeout=15,
        )

        final_url = resp.url
        body = resp.text

        if "access_token" not in final_url:
            if "loginfmt" in body.lower() or ("sign in" in body.lower() and "microsoft" in body.lower()):
                raise InvalidCredentialsError("Invalid email or password")
            if "help us protect" in body.lower() or "two-step" in body.lower() or "verify" in body.lower():
                raise TwoFactorError("2FA enabled - use an app password")
            if post_url in final_url:
                raise InvalidCredentialsError("Login page returned - check credentials")
            raise AuthError(f"Unexpected response from Microsoft. URL: {final_url[:120]}")

        # Step 3: Parse access token from redirect URL hash
        self.log("  [3/7] Microsoft auth OK, extracting token...")
        hash_fragment = final_url.split("#", 1)[1]
        params = dict(p.split("=", 1) for p in hash_fragment.split("&") if "=" in p)
        ms_access_token = params.get("access_token", "")
        if not ms_access_token:
            raise AuthError("No access_token in redirect URL")

        # Step 4: XBL authenticate
        self.log("  [4/7] XBL authentication...")
        xbl_headers = {
            "User-Agent": USER_AGENT,
            "Accept": "application/json",
            "x-xbl-contract-version": "0",
        }
        xbl_payload = {
            "RelyingParty": "http://auth.xboxlive.com",
            "TokenType": "JWT",
            "Properties": {
                "AuthMethod": "RPS",
                "SiteName": "user.auth.xboxlive.com",
                "RpsTicket": ms_access_token,
            },
        }
        xbl_resp = self.session.post(XBL_URL, json=xbl_payload, headers=xbl_headers, timeout=10)
        if xbl_resp.status_code != 200:
            text = xbl_resp.text[:300]
            if "InvalidCredential" in text or xbl_resp.status_code == 401:
                raise InvalidCredentialsError("XBL auth failed - invalid credentials")
            if "NoXboxAccount" in text or "2148916238" in text:
                raise NoXboxAccountError("No Xbox account linked to this Microsoft account")
            raise AuthError(f"XBL auth failed: {xbl_resp.status_code} {text}")
        xbl_data = xbl_resp.json()
        xbl_token = xbl_data["Token"]

        # Xbox DisplayClaims may only have 'uhs' - try JWT payload first
        jwt_claims = _decode_jwt_payload(xbl_token)
        self.log(f"  [DEBUG] JWT keys: {list(jwt_claims.keys())}")

        # Try to get xuid/gamertag from JWT xui claim
        xuid = ""
        gamertag = ""
        user_hash = ""

        xui_from_jwt = jwt_claims.get("xui", [])
        if xui_from_jwt:
            xuid = xui_from_jwt[0].get("xid", "")
            gamertag = xui_from_jwt[0].get("gtg", "")
            user_hash = xui_from_jwt[0].get("uhs", "")

        # Fallback: try JWT sub field (sometimes xuid is there)
        if not xuid:
            xuid = jwt_claims.get("sub", "")
            self.log(f"  [DEBUG] JWT sub: {xuid[:20] if xuid else 'MISSING'}")

        # Fallback: DisplayClaims
        if not xuid:
            dc_xui = xbl_data.get("DisplayClaims", {}).get("xui", [{}])[0]
            xuid = dc_xui.get("xid", "")
            gamertag = dc_xui.get("gtg", "")
            user_hash = dc_xui.get("uhs", user_hash)

        # If we still don't have xuid, use uhs as fallback (it's numeric)
        if not xuid and user_hash:
            xuid = user_hash
            self.log(f"  [DEBUG] Using uhs as xuid fallback: {xuid}")

        if not xuid:
            raise AuthError(f"XBL response missing xid - JWT: {list(jwt_claims.keys())}, DC: {xbl_data.get('DisplayClaims', {})}")

        # Step 5: XSTS authenticate
        self.log("  [5/7] XSTS authentication...")
        xsts_headers = {
            "User-Agent": USER_AGENT,
            "Accept": "application/json",
            "x-xbl-contract-version": "1",
        }
        xsts_payload = {
            "RelyingParty": "rp://api.minecraftservices.com/",
            "TokenType": "JWT",
            "Properties": {
                "SandboxId": "RETAIL",
                "UserTokens": [xbl_token],
            },
        }
        xsts_resp = self.session.post(XSTS_URL, json=xsts_payload, headers=xsts_headers, timeout=10)
        if xsts_resp.status_code != 200:
            raise AuthError(f"XSTS auth failed: {xsts_resp.status_code} {xsts_resp.text[:200]}")
        xsts_data = xsts_resp.json()
        xsts_token = xsts_data["Token"]

        # Step 6: Minecraft login_with_xbox
        self.log("  [6/7] Minecraft login_with_xbox...")
        mc_headers = {
            "Accept": "application/json",
            "User-Agent": USER_AGENT,
        }
        mc_payload = {"identityToken": f"XBL3.0 x={user_hash};{xsts_token}"}
        mc_resp = self.session.post(MC_LOGIN_URL, json=mc_payload, headers=mc_headers, timeout=10)
        if mc_resp.status_code != 200:
            raise AuthError(f"Minecraft login failed: {mc_resp.status_code} {mc_resp.text[:200]}")
        mc_token = mc_resp.json()["access_token"]

        # Step 7: Get MC profile
        self.log("  [7/7] Getting Minecraft profile...")
        profile_headers = {
            "Accept": "application/json",
            "User-Agent": USER_AGENT,
            "Authorization": f"Bearer {mc_token}",
        }
        profile_resp = self.session.get(MC_PROFILE_URL, headers=profile_headers, timeout=10)
        if profile_resp.status_code != 200:
            raise AuthError(f"Profile fetch failed: {profile_resp.status_code} {profile_resp.text[:200]}")
        profile = profile_resp.json()
        username = profile.get("name", "Unknown")
        uuid = profile.get("id", "Unknown")

        self.log(f"  ✅ Authenticated: {username} ({uuid})")

        return AuthResult(
            access_token=mc_token,
            username=username,
            uuid=uuid,
            gamertag=gamertag,
            xuid=xuid,
        )


def change_username(mc_access_token: str, new_name: str, log_fn=None) -> bool:
    """Change Minecraft username. Returns True if successful."""
    import requests
    log = log_fn or (lambda *a, **k: None)

    headers = {
        "Authorization": f"Bearer {mc_access_token}",
        "Content-Type": "application/json",
        "Accept": "application/json",
        "User-Agent": USER_AGENT,
    }

    log(f"  Changing username to '{new_name}'...")
    resp = requests.patch(
        MC_CHANGE_USERNAME_URL,
        headers=headers,
        json={"name": new_name},
        timeout=10,
    )

    if resp.status_code == 204:
        log(f"  ✅ Username changed to '{new_name}'!")
        return True
    elif resp.status_code == 429:
        retry_after = resp.headers.get("Retry-After", "60")
        log(f"  ⚠ Rate limited. Retry after {retry_after}s")
        return False
    else:
        log(f"  ❌ Failed: {resp.status_code} {resp.text[:200]}")
        return False


if __name__ == "__main__":
    import sys
    if len(sys.argv) < 3:
        print("Usage: python microsoft_auth.py <email> <password>")
        sys.exit(1)

    auth = MicrosoftAuthenticator(log_fn=lambda *a, **k: print(*a))
    result = auth.authenticate(sys.argv[1], sys.argv[2])
    print(f"\n✅ Success!")
    print(f"  Username: {result.username}")
    print(f"  UUID:     {result.uuid}")
    print(f"  Gamertag: {result.gamertag}")
    print(f"  Token:    {result.access_token[:40]}...")
