#!/usr/bin/env python3
"""
Minecraft Username Sniper GUI v12 - NUCLEAR++ PACKAGE
- Multiple OAuth tokens with connection pooling
- NAMEMC (windowed) + LABYNAMES (exact) timing
- Pre-warmed HTTP/2 connections
- Smart retry logic with exponential backoff
- Instant success detection + auto-stop
- Sound + desktop alerts on success
- Token health validation
- DNS pre-resolution
- Adaptive thread control
- NTP clock sync enforcement (4 public servers)
- Offset calibration v2 (10 probes, TLS-aware, P95)
  - Integrated droptime fetcher (playerdb.co + Mojang API)
- Mojang API health checker (4 endpoints + minecraftstatus.com)
 - Name history lookup (playerdb.co ownership timeline)
- Name pattern generator ({letter}, {digit}, {alphanum})
- Auto account type detection (GC/MS/Mojang)
- Token cache with TTL + auto-expiry
- Batch token validation pre-snipe
- Dry-run simulation mode
- Fire-and-forget background mode
- Scheduled snipes with auto-start
- Telegram bot notifications
- Pushover notifications
- Name availability cache
- Per-token stats dashboard
- Multi-region routing
- Auto-update checker
- Real-time latency graph
- Snipe session replay
- Plugin system (on_snipe_start/success/fail hooks)
- DNS over HTTPS (Cloudflare/Google/Quad9)
- Voice alerts (platform-native TTS)
"""

import sys
import os
import time
import httpx
import threading
from concurrent.futures import ThreadPoolExecutor
import json
import gc
import socket
import platform
import subprocess
import re
from datetime import datetime, timezone
from pathlib import Path
from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QTabWidget, QLabel, QLineEdit, QTextEdit, QPushButton, QSpinBox,
    QGroupBox, QFormLayout, QMessageBox, QCheckBox, QFileDialog, QFrame,
    QProgressBar, QComboBox, QSlider, QTableWidget, QTableWidgetItem,
    QHeaderView, QSplitter, QAbstractItemView, QSystemTrayIcon, QMenu, QAction
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread
from PyQt5.QtGui import QFont, QColor, QIcon, QPixmap, QScreen, QPainter, QPainterPath

import concurrent.futures
import collections
import csv
import io
import uuid as uuid_lib
import hashlib
import sqlite3
import zipfile
import tempfile
import wave
import struct
import math as math_lib

# === Disable garbage collection for precision timing ===
gc.disable()

# === Pre-resolve DNS for speed ===
MINECRAFT_API_HOST = "api.minecraftservices.com"
MOJANG_SECURITY_HOST = "api.mojang.com"
MINECRAFT_API_IP = None

# === Account Health Tracking ===
ACCOUNT_TRACKING_FILE = "account_history.json"
NAME_CHANGE_COOLDOWN_DAYS = 90

# === FEATURE #6: FINGERPRINT ROTATION ===
# Pool of realistic browser User-Agent strings
USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Edge/120.0.0.0",
]

# Track per-endpoint rate limit cooldowns
endpoint_cooldowns = {}  # endpoint -> (expires_at_timestamp, reason)

def get_random_user_agent():
    """Get a random User-Agent from the pool"""
    import random
    return random.choice(USER_AGENTS)

def is_endpoint_cooldown(endpoint):
    """Check if endpoint is in cooldown due to 429 Retry-After"""
    if endpoint in endpoint_cooldowns:
        expires_at, reason = endpoint_cooldowns[endpoint]
        if time.time() < expires_at:
            remaining = int(expires_at - time.time())
            return True, remaining, reason
    return False, 0, None

def set_endpoint_cooldown(endpoint, retry_after_seconds, reason="429"):
    """Set a cooldown for an endpoint based on Retry-After header"""
    expires_at = time.time() + retry_after_seconds
    endpoint_cooldowns[endpoint] = (expires_at, reason)

# === FEATURE #8: TOKEN ROTATION ON RATE LIMIT ===
# Track which tokens have been rate limited to avoid reusing them
token_rate_limits = {}  # token_hash -> (expires_at, count)

def is_token_rate_limited(token):
    """Check if a specific token has been rate limited"""
    token_hash = hash(token) % 1000000
    if token_hash in token_rate_limits:
        expires_at, count = token_rate_limits[token_hash]
        if time.time() < expires_at:
            remaining = int(expires_at - time.time())
            return True, remaining, count
    return False, 0, 0

def set_token_rate_limit(token, duration=60):
    """Mark a token as rate limited for a duration"""
    token_hash = hash(token) % 1000000
    expires_at = time.time() + duration
    current_count = token_rate_limits.get(token_hash, (0, 0))[1]
    token_rate_limits[token_hash] = (expires_at, current_count + 1)

def get_usable_tokens(tokens, account_history=None):
    """Filter out rate-limited, expired, and cooldown tokens. Return usable ones sorted by priority."""
    import base64
    
    usable = []
    skipped = 0
    skipped_reasons = {"rate_limited": 0, "expired": 0, "cooldown": 0, "blacklisted": 0}
    
    for token in tokens:
        # Check rate limit
        is_limited, remaining, count = is_token_rate_limited(token)
        if is_limited:
            skipped += 1
            skipped_reasons["rate_limited"] += 1
            continue
        
        # Check JWT expiry
        try:
            # JWT is 3 parts: header.payload.signature
            payload = token.split('.')[1]
            # Add padding if needed
            padding = 4 - len(payload) % 4
            if padding != 4:
                payload += '=' * padding
            decoded = json.loads(base64.urlsafe_b64decode(payload))
            exp = decoded.get('exp', 0)
            if exp and time.time() > exp:
                skipped += 1
                skipped_reasons["expired"] += 1
                continue
        except Exception:
            pass  # Not a JWT or decode failed, skip check
        
        # Check name change cooldown from account history
        token_key = hash(token) % 1000000
        if account_history:
            acc = account_history.get("accounts", {}).get(token_key)
            if acc:
                cooldown_until = acc.get("name_change_cooldown_until")
                if cooldown_until:
                    try:
                        cooldown_ts = datetime.fromisoformat(cooldown_until).timestamp()
                        if time.time() < cooldown_ts:
                            skipped += 1
                            skipped_reasons["cooldown"] += 1
                            continue
                    except Exception:
                        pass
        
        usable.append(token)
    
    return usable, skipped, skipped_reasons

def pre_resolve_dns():
    """Resolve DNS before drop time for speed - pin to fastest IP"""
    global MINECRAFT_API_IP
    try:
        # Resolve ALL IPs and pick the fastest
        addrs = socket.getaddrinfo(MINECRAFT_API_HOST, 443, socket.AF_INET, socket.SOCK_STREAM)
        ips = list(set(addr[4][0] for addr in addrs))
        
        if len(ips) == 1:
            MINECRAFT_API_IP = ips[0]
            return True, MINECRAFT_API_IP
        
        # Test each IP and pick fastest
        fastest_ip = ips[0]
        fastest_time = float('inf')
        
        for ip in ips:
            start = time.time()
            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.settimeout(1)
                sock.connect((ip, 443))
                elapsed = time.time() - start
                sock.close()
                if elapsed < fastest_time:
                    fastest_time = elapsed
                    fastest_ip = ip
            except:
                pass
        
        MINECRAFT_API_IP = fastest_ip
        return True, f"{fastest_ip} ({fastest_time*1000:.0f}ms, {len(ips)} IPs tested)"
    except Exception as e:
        return False, str(e)


# ============================================================================
# FEATURE #9: ACCOUNT LOADING HELPERS
# ============================================================================

def load_accounts_from_file(filepath: str) -> list:
    """
    Load accounts from file (email:password or bearer tokens)
    Format: One account per line
    - email:password (triggers OAuth flow)
    - eyJ... (raw bearer token, length > 200)
    """
    accounts = []
    try:
        with open(filepath, 'r') as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                
                # Detect bearer token (MCsniperGO pattern)
                if len(line) > 200 and ':' not in line and line.startswith('eyJ'):
                    accounts.append({'type': 'token', 'token': line})
                elif ':' in line:
                    # Email:password format
                    parts = line.split(':', 1)
                    if len(parts) == 2 and parts[0] and parts[1]:
                        accounts.append({'type': 'credential', 'email': parts[0], 'password': parts[1]})
                else:
                    # Treat as plain bearer token
                    accounts.append({'type': 'token', 'token': line})
    except Exception as e:
        print(f"[ERROR] Failed to load accounts from {filepath}: {e}")
    return accounts


def authenticate_accounts(accounts: list, log_callback=None) -> dict:
    """
    Authenticate a list of accounts and return bearer tokens
    Returns: {email_or_hash: token} dict
    """
    log = log_callback or print
    tokens = {}
    
    for idx, account in enumerate(accounts):
        if account['type'] == 'token':
            # Already a bearer token
            token_hash = account['token'][:8] + '...'
            tokens[f"token_{idx}"] = account['token']
            log(f"[AUTH] Loaded token {token_hash} (skipping OAuth)")
        else:
            # Email:bearer_token - validate
            email = account.get('email', '')
            token = account.get('token', '')
            
            try:
                auth = BearerTokenAuth(token, log)
                info = auth.validate()
                tokens[email or f"token_{idx}"] = token
                log(f"[AUTH] ✓ Validated {email or 'token'}: {info['username']}")
            except Exception as e:
                log(f"[AUTH] ✗ Failed for {email or 'token'}: {e}")
    
    return tokens


def save_tokens_to_file(tokens_dict: dict, filepath: str):
    """Save authenticated tokens to file (one per line)"""
    try:
        with open(filepath, 'w') as f:
            for token in tokens_dict.values():
                f.write(token + '\n')
    except Exception as e:
        print(f"[ERROR] Failed to save tokens: {e}")


# ============================================================================
# FEATURE #9: AUTO-AUTH SYSTEM (Microsoft OAuth Device Flow)
# ============================================================================
# Authenticates email:password → generates Minecraft bearer tokens
# Based on MCsniperGO's flow: Microsoft OAuth → XBL → XSTS → Minecraft
# ============================================================================

MICROSOFT_CLIENT_ID = "00000000441cc96b"  # Minecraft for Nintendo Switch
MICROSOFT_CLIENT_SECRET = None  # Not needed for public client

# ============================================================================
# FULL AUTH CHAIN: email:password → Minecraft Bearer Token
# ============================================================================
# Flow: Microsoft OAuth → XBL → XSTS → Minecraft Access Token
# ============================================================================

def microsoft_oauth_login(email: str, password: str, log_callback=None) -> dict:
    """
    Step 1: Microsoft login via HTML form scraping (proven method).
    Matches MCsniperGO + Buckshot approach:
    1. GET oauth20_authorize.srf → extract PPFT + URL_POST
    2. POST credentials to URL_POST → get access_token from redirect
    Returns Microsoft access token.
    """
    log = log_callback or (lambda *a: None)
    import requests
    import re

    # Use the proven client ID from MCsniperGO/Buckshot
    client_id = "000000004C12AE6F"  # Desktop app client ID

    authorize_url = (
        f"https://login.live.com/oauth20_authorize.srf"
        f"?client_id={client_id}"
        f"&redirect_uri=https://login.live.com/oauth20_desktop.srf"
        f"&scope=service::user.auth.xboxlive.com::MBI_SSL"
        f"&display=touch&response_type=token&locale=en"
    )

    log(f"    [AUTH Step 1] Fetching Microsoft login page for {email}...")

    session = requests.Session()
    session.headers.update({
        "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",
    })

    # Step 1: GET the authorize page to extract PPFT + URL_POST
    resp = session.get(authorize_url, timeout=15)
    html = resp.text

    # Extract PPFT (protection token) — matches Buckshot regex
    ppft_match = re.search(r'value="(.+?)"', html)
    if not ppft_match:
        # Try alternate regex (MCsniperGO pattern)
        ppft_match = re.search(r'value=\\\\"(.+?)\\\\"', html)
    if not ppft_match:
        raise Exception("Failed to extract PPFT from Microsoft login page")
    ppft = ppft_match.group(1)

    # Extract URL_POST — try multiple patterns
    url_post_match = re.search(r"urlPost:'(.+?)'", html)
    if not url_post_match:
        url_post_match = re.search(r'urlPost":"(.+?)"', html)
    if not url_post_match:
        url_post_match = re.search(r'urlPost:"(.+?)"', html)
    if not url_post_match:
        raise Exception("Failed to extract URL_POST from Microsoft login page")
    url_post = url_post_match.group(1)

    log(f"    [AUTH Step 1] Extracted PPFT + URL_POST, signing in...")

    # Step 2: POST credentials
    import urllib.parse
    body = {
        "login": email,
        "loginfmt": email,
        "passwd": password,
        "PPFT": ppft,
    }

    resp = session.post(
        url_post,
        data=body,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        timeout=15,
        allow_redirects=True,
    )

    final_url = str(resp.url)
    text = resp.text

    # Check for errors
    if "Sign in to" in text or "help us protect" in text.lower():
        raise Exception("Invalid credentials")
    if "2FA is enabled" in text or "Help us protect your account" in text:
        return {"needs_2fa": True, "error": "2FA is enabled — use device code flow or disable 2FA"}
    if "unusual activity" in text.lower():
        raise Exception("Unusual activity alert — visit https://account.live.com/activity to dismiss")

    # Extract access_token from URL fragment (#access_token=...)
    if "#" not in final_url or "access_token" not in final_url:
        raise Exception(
            f"Microsoft login failed — no access_token in redirect. "
            f"URL: {final_url[:200]}"
        )

    fragment = final_url.split("#", 1)[1]
    params = {}
    for pair in fragment.split("&"):
        if "=" in pair:
            k, v = pair.split("=", 1)
            params[k] = urllib.parse.unquote(v)

    access_token = params.get("access_token", "")
    if not access_token:
        raise Exception("No access_token in redirect fragment")

    log(f"    [AUTH Step 1] ✓ Microsoft access_token obtained")
    return {"access_token": access_token}


def microsoft_oauth_2fa(email: str, password: str, session_key: str, code: str, log_callback=None) -> dict:
    """
    Complete 2FA challenge with the verification code.
    """
    log = log_callback or (lambda *a: None)
    import requests

    url = "https://login.live.com/oauth20_token.srf"
    data = {
        "grant_type": "password",
        "client_id": MICROSOFT_CLIENT_ID,
        "resource": "https://api.minecraftservices.com",
        "username": email,
        "password": password,
        "scope": "Service::UserAuth::MCS",
        "session_key": session_key,
        "code": code,
    }

    log(f"    [AUTH 2FA] Completing 2FA challenge for {email}...")
    resp = requests.post(url, data=data, timeout=15)

    if resp.status_code != 200:
        raise Exception(f"2FA challenge failed (HTTP {resp.status_code}): {resp.text[:200]}")

    token_data = resp.json()
    log(f"    [AUTH 2FA] ✓ 2FA completed successfully")
    return {"access_token": token_data.get("access_token", ""), "refresh_token": token_data.get("refresh_token", "")}


def microsoft_app_password_login(email: str, app_password: str, log_callback=None) -> dict:
    """
    Login using a Microsoft App Password (16-char password for 2FA-enabled accounts).
    App passwords bypass 2FA entirely.
    """
    log = log_callback or (lambda *a: None)
    import requests

    # App passwords only work with the live.com endpoint
    url = "https://login.live.com/oauth20_token.srf"
    data = {
        "grant_type": "password",
        "client_id": MICROSOFT_CLIENT_ID,
        "resource": "https://api.minecraftservices.com",
        "username": email,
        "password": app_password,
        "scope": "Service::UserAuth::MCS",
    }

    log(f"    [AUTH AppPW] Microsoft App Password login for {email}...")
    resp = requests.post(url, data=data, timeout=15)

    if resp.status_code != 200:
        raise Exception(f"App Password login failed (HTTP {resp.status_code}): {resp.text[:200]}")

    token_data = resp.json()
    log(f"    [AUTH AppPW] ✓ App Password login successful")
    return {"access_token": token_data.get("access_token", ""), "refresh_token": token_data.get("refresh_token", "")}


def xbox_live_auth(ms_token: str, log_callback=None) -> dict:
    """
    Step 2: Xbox Live authentication (user.auth.xboxlive.com).
    Matches MCsniperGO/Buckshot: POST with RPS ticket, NO Authorization header.
    """
    log = log_callback or (lambda *a: None)
    import requests

    url = "https://user.auth.xboxlive.com/user/authenticate"
    body = {
        "Properties": {
            "AuthMethod": "RPS",
            "SiteName": "user.auth.xboxlive.com",
            "RpsTicket": f"d={ms_token}",
        },
        "RelyingParty": "http://auth.xboxlive.com",
        "TokenType": "JWT",
    }

    log(f"    [AUTH Step 2] Xbox Live auth...")
    resp = requests.post(
        url, json=body,
        headers={"Content-Type": "application/json", "Accept": "application/json", "x-xbl-contract-version": "1"},
        timeout=15,
    )

    if resp.status_code == 401:
        try:
            err = resp.json()
            xerr = err.get("XErr", 0)
            if xerr == 2148916238:
                raise Exception("Account belongs to someone under 18 — add to a Microsoft family group first")
            if xerr == 2148916233:
                raise Exception("No Xbox account linked — sign up at https://account.xbox.com")
            raise Exception(f"XBL auth error XErr={xerr}: {err.get('Message', '')}")
        except Exception as e:
            if "under 18" in str(e) or "No Xbox" in str(e):
                raise
            raise Exception(f"XBL auth failed (HTTP {resp.status_code}): {resp.text[:200]}")
    elif resp.status_code != 200:
        raise Exception(f"XBL auth failed (HTTP {resp.status_code}): {resp.text[:200]}")

    data = resp.json()
    tokens = data.get("AuthenticationToken", "")
    user_host = data.get("UserHosts", "")
    display_claims = data.get("DisplayClaims", "")
    xuid = ""
    if display_claims:
        import jwt as pyjwt
        try:
            decoded = pyjwt.decode(display_claims, options={"verify_signature": False})
            xuid = decoded.get("xui", [{}])[0].get("xid", "")
        except:
            pass

    log(f"    [AUTH Step 2] ✓ XBL authenticated, XUID: {xuid}")
    return {
        "tokens": tokens,
        "user_hosts": user_host,
        "display_claims": display_claims,
        "xuid": xuid,
    }


def xsts_auth(xbl_tokens: str, display_claims: str, log_callback=None) -> dict:
    """
    Step 3: XSTS authorization with Minecraft title.
    Returns XSTS token with Minecraft entitlements.
    """
    log = log_callback or (lambda *a: None)
    import requests

    url = "https://xsts.auth.xboxlive.com/xsts/authorize"
    headers = {
        "Authorization": f"XLX.A {xbl_tokens}",
        "Content-Type": "application/json",
    }
    body = {
        "Properties": {
            "SandboxId": "RETAIL",
            "UserTokens": [xbl_tokens],
        },
        "RelyingParty": "rp://api.minecraftservices.com/",
        "TokenType": "JWT",
    }

    log(f"    [AUTH Step 3] XSTS authorization...")
    resp = requests.post(url, json=body, headers=headers, timeout=15)

    if resp.status_code == 401:
        try:
            err = resp.json()
            xerr = err.get("XErr", 0)
            if xerr == 2148916238:
                raise Exception("Account belongs to someone under 18 — add to a Microsoft family group first")
            if xerr == 2148916233:
                raise Exception("No Xbox account linked — sign up at https://account.xbox.com")
            raise Exception(f"XSTS auth error XErr={xerr}: {err.get('Message', '')}")
        except Exception as e:
            if "under 18" in str(e) or "No Xbox" in str(e):
                raise
            raise Exception(f"XSTS auth failed (HTTP {resp.status_code}): {resp.text[:200]}")
    elif resp.status_code != 200:
        raise Exception(f"XSTS auth failed (HTTP {resp.status_code}): {resp.text[:200]}")

    data = resp.json()
    xsts_token = data.get("Token", "")
    xsts_user = data.get("DisplayClaims", "")

    log(f"    [AUTH Step 3] ✓ XSTS authorized for Minecraft")
    return {"token": xsts_token, "display_claims": xsts_user}


def minecraft_access_token(xsts_token: str, log_callback=None) -> str:
    """
    Step 4: Exchange XSTS token for Minecraft access token.
    Returns the final Bearer token for name changes.
    """
    log = log_callback or (lambda *a: None)
    import requests

    url = "https://api.minecraftservices.com/authentication/login_with_xbox"
    headers = {"Content-Type": "application/json"}
    body = {
        "identity_token": f"XBL3.0 x={xsts_token.split(' ')[0] if ' ' in xsts_token else ''}.{xsts_token}",
    }

    log(f"    [AUTH Step 4] Minecraft access token exchange...")
    resp = requests.post(url, json=body, headers=headers, timeout=15)

    if resp.status_code != 200:
        raise Exception(f"Minecraft auth failed (HTTP {resp.status_code}): {resp.text[:200]}")

    data = resp.json()
    mc_token = data.get("access_token", "")

    log(f"    [AUTH Step 4] ✓ Minecraft bearer token obtained")
    return mc_token

    log(f"    [AUTH Step 4] ✓ Minecraft bearer token obtained")
    return mc_token


# ============================================================================
# NAME AVAILABILITY CHECKER
# ============================================================================

def check_name_available(name: str, bearer_token: str = None, log_callback=None) -> dict:
    """
    Check if a Minecraft username is available for claiming.
    Uses the security endpoint to check name status.
    
    Returns dict with:
      - available: bool
      - status: str (available, taken, invalid, rate_limited)
      - current_owner: str or None
      - cooldown_until: str or None
    """
    log = log_callback or (lambda *a: None)
    import requests

    result = {"available": False, "status": "unknown", "current_owner": None, "cooldown_until": None}

    # Method 1: Try to get profile by username (returns 404 if not taken)
    try:
        resp = requests.get(
            f"https://api.minecraftservices.com/minecraft/profile/{name}",
            timeout=5,
        )
        if resp.status_code == 404:
            result["available"] = True
            result["status"] = "available"
            log(f"    [CHECK] '{name}' is AVAILABLE")
            return result
        elif resp.status_code == 200:
            data = resp.json()
            result["current_owner"] = data.get("name")
            result["status"] = "taken"
            log(f"    [CHECK] '{name}' is TAKEN by {result['current_owner']}")
            return result
        elif resp.status_code == 429:
            result["status"] = "rate_limited"
            log(f"    [CHECK] Rate limited checking '{name}'")
            return result
    except Exception as e:
        log(f"    [CHECK] Error checking '{name}': {e}")

    # Method 2: Use the security endpoint
    try:
        resp = requests.get(
            f"https://api.mojang.com/security/username",
            params={"username": name},
            timeout=5,
        )
        if resp.status_code == 200:
            # Security endpoint returns the UUID if name exists
            result["status"] = "taken"
            log(f"    [CHECK] '{name}' confirmed TAKEN via security endpoint")
        elif resp.status_code == 404:
            result["available"] = True
            result["status"] = "available"
            log(f"    [CHECK] '{name}' confirmed AVAILABLE via security endpoint")
    except:
        pass

    return result


def check_name_change_cooldown(bearer_token: str, log_callback=None) -> dict:
    """
    Check remaining name changes and cooldown for an authenticated account.
    """
    log = log_callback or (lambda *a: None)
    import requests

    result = {"changes_remaining": 0, "cooldown_until": None, "error": None}

    try:
        resp = requests.get(
            "https://api.minecraftservices.com/minecraft/profile",
            headers={"Authorization": f"Bearer {bearer_token}"},
            timeout=10,
        )
        if resp.status_code == 200:
            data = resp.json()
            # The profile endpoint doesn't directly expose remaining changes
            # We need to check the changes endpoint
            changes_resp = requests.get(
                "https://api.minecraftservices.com/minecraft/profile/changes",
                headers={"Authorization": f"Bearer {bearer_token}"},
                timeout=10,
            )
            if changes_resp.status_code == 200:
                changes_data = changes_resp.json()
                # Parse name change history
                result["changes_remaining"] = 2  # Default 2 changes per 30 days
                result["cooldown_until"] = changes_data.get("cooldownUntil")
            else:
                result["error"] = f"Changes endpoint: HTTP {changes_resp.status_code}"
        else:
            result["error"] = f"Profile endpoint: HTTP {resp.status_code}"
    except Exception as e:
        result["error"] = str(e)

    return result


def authenticate_email_password(email: str, password: str, log_callback=None) -> dict:
    """
    Full auth chain: email:password → Minecraft bearer token.
    Returns dict with bearer token and account info.
    """
    log = log_callback or (lambda *a: None)
    import requests

    log(f"  [🔑] Starting full auth chain for {email}")

    # Step 1: Microsoft OAuth
    ms_result = microsoft_oauth_login(email, password, log)

    # Step 2: Xbox Live
    xbl_result = xbox_live_auth(ms_result["access_token"], log)

    # Step 3: XSTS
    xsts_result = xsts_auth(xbl_result["tokens"], xbl_result["display_claims"], log)

    # Step 4: Minecraft token
    mc_token = minecraft_access_token(xsts_result["token"], log)

    # Validate the token
    try:
        profile_resp = requests.get(
            "https://api.minecraftservices.com/minecraft/profile",
            headers={"Authorization": f"Bearer {mc_token}"},
            timeout=10,
        )
        if profile_resp.status_code == 200:
            profile = profile_resp.json()
            username = profile.get("name", "unknown")
            uuid = profile.get("id", "unknown")
            log(f"  [✅] Full chain success: {username} ({uuid})")
        else:
            username = "unknown"
            uuid = "unknown"
    except:
        username = "unknown"
        uuid = "unknown"

    return {
        "bearer_token": mc_token,
        "refresh_token": ms_result.get("refresh_token", ""),
        "username": username,
        "uuid": uuid,
        "xuid": xbl_result.get("xuid", ""),
    }


def refresh_minecraft_token(email: str, password: str, log_callback=None) -> str:
    """
    Refresh an expired Minecraft bearer token by re-running the full auth chain.
    The HTML form method doesn't return refresh tokens, so we re-auth.
    Uses app password if it looks like one (16 chars, no spaces).
    """
    log = log_callback or (lambda *a: None)
    log(f"    [REFRESH] Re-authenticating {email}...")

    # Use app password flow if password looks like an app password
    if len(password) == 16 and ' ' not in password:
        ms_result = microsoft_oauth_app_password(email, password, log)
    else:
        ms_result = microsoft_oauth_login(email, password, log)

    ms_token = ms_result["access_token"]

    # Re-run XBL → XSTS → Minecraft chain
    xbl_result = xbox_live_auth(ms_token, log)
    xsts_result = xsts_auth(xbl_result["tokens"], xbl_result["display_claims"], log)
    mc_token = minecraft_access_token(xsts_result["token"], log)

    log(f"    [REFRESH] ✓ Token refreshed successfully")
    return mc_token


# ============================================================================
# FEATURE #9: AUTO-AUTH SYSTEM (v2 - consolidated)
# ============================================================================
# (load_accounts_from_file and save_tokens_to_file already defined above)


# === Bulk credential parsing ===

def parse_credential_file(filepath: str) -> list:
    """
    Parse a credential file into list of (email, bearer_token) tuples.
    Supports multiple formats:
      - email:bearer_token  (colon-separated)
      - email|bearer_token  (pipe-separated)
      - bearer_token        (token only, one per line)
      - JSON lines: {"email":"x","token":"***"} or {"email":"x","bearer":"***"}
    
    Bearer tokens are obtained by logging into minecraft.net and copying
    the 'clienttoken' cookie from browser DevTools.
    """
    accounts = []
    path = Path(filepath)
    try:
        file_content = path.read_text(encoding='utf-8', errors='replace')
    except Exception:
        return accounts

    for line in file_content.splitlines():
        line = line.strip()
        if not line or line.startswith('#'):
            continue

        # Try JSON format
        if line.startswith('{'):
            try:
                obj = json.loads(line)
                email = obj.get('email', obj.get('username', ''))
                token = obj.get('token', obj.get('bearer', obj.get('bearer_token', '')))
                if email and token:
                    accounts.append((email, token))
                continue
            except json.JSONDecodeError:
                pass

        # Try email:token or email|token format
        for sep in [':', '|']:
            if sep in line:
                parts = line.split(sep, 1)
                if '@' in parts[0]:
                    email = parts[0].strip()
                    token = parts[1].strip()
                    if token:
                        accounts.append((email, token))
                    break
        else:
            # Token only (no email prefix)
            if len(line) > 20:
                accounts.append(('', line))

    return accounts


def load_credentials_from_folder(folder_path: str) -> list:
    """Scan a folder recursively for token files and parse all accounts."""
    accounts = []
    seen = set()
    folder = Path(folder_path)
    if not folder.is_dir():
        return accounts
    
    known_exts = {'.txt', '.csv', '.json', '.list', '.acc', '.accounts', '.log', '.dat', '.cfg', '.conf'}
    
    for filepath in folder.rglob('*'):
        if not filepath.is_file():
            continue
        if filepath.suffix.lower() in known_exts or filepath.suffix == '':
            parsed = parse_credential_file(filepath)
            for email, token in parsed:
                key = email.lower().strip() or token[:20]
                if key and key not in seen:
                    seen.add(key)
                    accounts.append((email.strip(), token.strip()))
    
    return accounts


class BearerTokenAuth:
    """
    Bearer token authentication for Minecraft username sniping.
    
    Users get bearer tokens by:
    1. Logging into https://minecraft.net in their browser
    2. Opening DevTools (F12) -> Application -> Cookies -> minecraft.net
    3. Copying the 'clienttoken' value
    
    The token is used directly as Bearer auth with api.minecraftservices.com.
    No Microsoft OAuth flow needed — faster and more reliable.
    """

    def __init__(self, token: str, log_callback=None):
        self.token = token
        self.log_callback = log_callback or (lambda *a: None)

    def log(self, msg):
        self.log_callback(msg)

    def validate(self):
        """Validate a bearer token and return account info."""
        import requests
        
        self.log("    Validating bearer token...")
        
        resp = requests.get(
            "https://api.minecraftservices.com/minecraft/profile",
            headers={"Authorization": f"Bearer {self.token}"},
            timeout=10
        )
        
        if resp.status_code == 200:
            profile = resp.json()
            username = profile.get("name", "unknown")
            uuid = profile.get("id", "unknown")
            self.log(f"    ✓ Token valid — {username} ({uuid})")
            return {
                "access_token": self.token,
                "username": username,
                "uuid": uuid,
                "xuid": uuid,
                "gamertag": username,
            }
        else:
            raise Exception(f"Token invalid (HTTP {resp.status_code}): {resp.text[:100]}")


    





# ============================================================================
# F3: PROXY ROTATION SYSTEM
# ============================================================================

class ProxyPool:
    """Manages a pool of proxies, rotating on 429 rate limits."""
    
    def __init__(self, proxies=None):
        self.proxies = proxies or []
        self.current_index = 0
        self.failed_proxies = set()  # proxies that returned 429
        self._lock = threading.Lock()
    
    def add_proxy(self, proxy_str: str):
        """Add a proxy: 'http://ip:port' or 'socks5://user:pass@ip:port'"""
        if proxy_str and proxy_str not in self.proxies:
            self.proxies.append(proxy_str)
    
    def add_proxies(self, proxy_list: list):
        for p in proxy_list:
            self.add_proxy(p)
    
    def get_next(self) -> dict:
        """Get next working proxy in rotation."""
        with self._lock:
            if not self.proxies:
                return {}
            for _ in range(len(self.proxies)):
                idx = self.current_index % len(self.proxies)
                self.current_index += 1
                proxy = self.proxies[idx]
                if proxy not in self.failed_proxies:
                    return {"http": proxy, "https": proxy}
            # All failed — clear failures and retry
            self.failed_proxies.clear()
            idx = self.current_index % len(self.proxies)
            self.current_index += 1
            return {"http": self.proxies[idx], "https": self.proxies[idx]}
    
    def mark_failed(self, proxy_str: str):
        """Mark a proxy as rate-limited."""
        with self._lock:
            self.failed_proxies.add(proxy_str)
    
    def clear_failures(self):
        with self._lock:
            self.failed_proxies.clear()
    
    @property
    def available_count(self):
        return len(self.proxies) - len(self.failed_proxies)
    
    @property
    def total_count(self):
        return len(self.proxies)


# Global proxy pool instance
proxy_pool = ProxyPool()


def load_proxy_file(filepath: str) -> int:
    """Load proxies from file (one per line). Returns count loaded."""
    import os
    if not os.path.exists(filepath):
        return 0
    with open(filepath) as f:
        proxies = [line.strip() for line in f if line.strip() and not line.startswith('#')]
    proxy_pool.add_proxies(proxies)
    return len(proxies)


def rotate_proxy_on_429() -> dict:
    """Called when 429 received — rotates to next proxy."""
    current = proxy_pool.get_next()
    if proxy_pool.total_count > 1:
        proxy_pool.clear_failures()  # Give others a chance
    return current

# ============================================================================
# F4: NAME DROP PREDICTION
# ============================================================================

def predict_name_drop(name: str, log_callback=None) -> dict:
    """Predict when a Minecraft username will become available."""
    import requests
    from datetime import datetime, timedelta, timezone
    log = log_callback or (lambda *a: None)
    result = {"uuid": None, "estimated_drop": None, "days_remaining": None, "confidence": "low"}
    try:
        resp = requests.get(f"https://api.minecraftservices.com/minecraft/profile/{name}", timeout=10)
        if resp.status_code == 404:
            return {"available_now": True, "message": f"'{name}' is currently available!"}
        elif resp.status_code != 200:
            return {"error": f"Profile check failed: HTTP {resp.status_code}"}
        data = resp.json()
        result["uuid"] = data.get("id")
        result["confidence"] = "medium"
        log(f"    [DROP] '{name}' is taken (UUID: {result['uuid']})")
        # Estimate 30 days from last claim
        estimated = datetime.now(timezone.utc) + timedelta(days=30)
        result["estimated_drop"] = estimated.isoformat()
        result["days_remaining"] = 30
    except Exception as e:
        result["error"] = str(e)
    return result


def get_name_change_allowed(bearer_token: str, log_callback=None) -> dict:
    """Check if account can change name now."""
    import requests
    log = log_callback or (lambda *a: None)
    result = {"can_change": False, "cooldown_until": None, "error": None}
    try:
        resp = requests.get("https://api.minecraftservices.com/minecraft/profile",
            headers={"Authorization": f"Bearer {bearer_token}"}, timeout=10)
        if resp.status_code == 200:
            data = resp.json()
            result["can_change"] = data.get("nameChangeAllowed", False)
            result["cooldown_until"] = data.get("nameChangeCooldownUntil")
            result["current_name"] = data.get("name")
            log(f"    [COOLDOWN] Can change: {result['can_change']}, Current: {result.get('current_name')}")
        else:
            result["error"] = f"HTTP {resp.status_code}"
    except Exception as e:
        result["error"] = str(e)
    return result


class NameAvailabilityWorker(QThread):
    """Background thread that polls name availability."""
    status_signal = pyqtSignal(str, bool, str)  # name, available, reason
    log_signal = pyqtSignal(str)

    def __init__(self, name: str, interval_seconds: int = 5):
        super().__init__()
        self.name = name
        self.interval = interval_seconds
        self._stop = False

    def stop(self):
        self._stop = True

    def run(self):
        self.log_signal.emit(f"[*] Polling availability for '{self.name}' every {self.interval}s...")
        while not self._stop:
            try:
                result = check_name_available(self.name, log_callback=lambda m: self.log_signal.emit(m))
                self.status_signal.emit(self.name, result["available"], result["status"])
            except Exception as e:
                self.log_signal.emit(f"[!] Availability check error: {e}")
            for _ in range(self.interval * 10):
                if self._stop:
                    break
                self.msleep(100)
        self.log_signal.emit(f"[*] Stopped polling for '{self.name}'")



# ============================================================================
# F5: LIVE NAME MONITOR + F6: COOLDOWN TRACKER
# ============================================================================
# (NameAvailabilityWorker already defined above — single canonical definition)


class NameMonitorWorker(QThread):
    """F5: Watches a name and alerts the instant it becomes available."""
    available_signal = pyqtSignal(str)
    log_signal = pyqtSignal(str)
    poll_signal = pyqtSignal(str, int)
    def __init__(self, name: str, interval_ms: int = 200):
        super().__init__()
        self.name = name
        self.interval_ms = interval_ms
        self._stop = False
    def stop(self):
        self._stop = True
    def run(self):
        import requests
        self.log_signal.emit(f"[👁] Live monitoring '{self.name}' every {self.interval_ms}ms...")
        consec = 0
        while not self._stop:
            try:
                t0 = time.time()
                resp = requests.get(f"https://api.minecraftservices.com/minecraft/profile/{self.name}", timeout=3)
                lat = int((time.time() - t0) * 1000)
                if resp.status_code == 404:
                    consec += 1
                    if consec >= 2:
                        self.available_signal.emit(self.name)
                        self.log_signal.emit(f"[🔔] '{self.name}' IS NOW AVAILABLE!")
                        break
                else:
                    consec = 0
                    self.poll_signal.emit(self.name, lat)
            except:
                pass
            for _ in range(self.interval_ms // 10):
                if self._stop: break
                self.msleep(10)
        self.log_signal.emit(f"[*] Stopped monitoring '{self.name}'")


class CooldownTrackerWorker(QThread):
    """F6: Tracks account cooldown and shows countdown."""
    update_signal = pyqtSignal(int, str, bool)  # seconds_remaining, current_name, can_change
    log_signal = pyqtSignal(str)
    def __init__(self, bearer_token: str):
        super().__init__()
        self.token = bearer_token
        self._stop = False
    def stop(self):
        self._stop = True
    def run(self):
        import requests
        from datetime import datetime, timezone
        while not self._stop:
            try:
                resp = requests.get("https://api.minecraftservices.com/minecraft/profile",
                    headers={"Authorization": f"Bearer {self.token}"}, timeout=10)
                if resp.status_code == 200:
                    data = resp.json()
                    can_change = data.get("nameChangeAllowed", False)
                    current = data.get("name", "?")
                    cooldown_until = data.get("nameChangeCooldownUntil")
                    secs = 0
                    if cooldown_until:
                        try:
                            dt = datetime.fromisoformat(cooldown_until.replace("Z", "+00:00"))
                            secs = max(0, int((dt - datetime.now(timezone.utc)).total_seconds()))
                        except:
                            secs = -1
                    self.update_signal.emit(secs, current, can_change)
                else:
                    self.log_signal.emit(f"[COOLDOWN] Token expired (HTTP {resp.status_code})")
                    break
            except Exception as e:
                self.log_signal.emit(f"[COOLDOWN] Error: {e}")
            for _ in range(100):
                if self._stop: break
                self.msleep(1000)


class BulkAuthWorker(QThread):
    """Background thread for bulk credential authentication."""
    log_signal = pyqtSignal(str)
    progress_signal = pyqtSignal(int, int)  # current, total
    account_result_signal = pyqtSignal(str, str, str, str)  # email, status, gamertag, xuid
    finished_signal = pyqtSignal(int, int, int)  # success, failed, skipped

    def __init__(self, accounts: list, token_cache_path: str = None, use_playwright: bool = False):
        super().__init__()
        self.accounts = accounts
        self.token_cache_path = token_cache_path or str(Path.home() / ".hermes" / "minecraft-sniper" / "tokens.json")
        self.tokens = {}  # email -> token dict
        self._stop = False
        self.use_playwright = use_playwright

    def __del__(self):
        self.wait()

    def run(self):
        cache = self._load_cache()
        total = len(self.accounts)
        success = 0
        failed = 0
        skipped = 0

        for idx, (email, token) in enumerate(self.accounts):
            if self._stop:
                break

            self.progress_signal.emit(idx + 1, total)

            label = email or f"token_{idx}"
            
            # Check cache first
            cached = cache.get(email.lower() if email else token[:20])
            if cached and cached.get("token"):
                self.log_signal.emit(f"  ⚡ Cached for {label}")
                self.account_result_signal.emit(label, "cached", cached.get("username", ""), "")
                self.tokens[label] = cached["token"]
                skipped += 1
                continue

            # Check if this looks like a password or a bearer token
            # Bearer tokens are long JWTs (contain dots, 100+ chars)
            # Passwords are shorter and don't look like JWTs
            is_password = not (len(token) > 100 and '.' in token)

            if is_password and email:
                # Run full auth chain: email:password → bearer token
                try:
                    self.log_signal.emit(f"  🔑 Full auth chain for {label}...")
                    result = authenticate_email_password(
                        email, token,
                        lambda msg: self.log_signal.emit(f"    {msg}")
                    )
                    new_token = result["bearer_token"]
                    username = result.get("username", "unknown")
                    uuid = result.get("uuid", "")

                    # Cache the generated bearer token
                    cache_key = email.lower()
                    cache[cache_key] = {
                        "token": new_token,
                        "email": email,
                        "password": token,  # Store the password for re-auth
                        "username": username,
                        "uuid": uuid,
                        "validated_at": datetime.now().isoformat(),
                        "auth_method": "email_password",
                    }
                    self.tokens[label] = new_token
                    self.account_result_signal.emit(label, "success", username, result.get("xuid", ""))
                    self.log_signal.emit(f"  ✅ {label} → {username} (auth chain)")
                    success += 1
                except Exception as e:
                    error_msg = str(e)
                    self.account_result_signal.emit(label, "failed", "", "")
                    self.log_signal.emit(f"  ❌ {label} (auth chain): {error_msg[:100]}")
                    failed += 1
            else:
                # Validate existing bearer token
                try:
                    self.log_signal.emit(f"  🔑 Validating {label}...")

                    auth = BearerTokenAuth(token, lambda msg: self.log_signal.emit(f"    {msg}"))
                    info = auth.validate()

                    self.tokens[label] = token
                    cache_key = email.lower() if email else token[:20]
                    cache[cache_key] = {
                        "token": token,
                        "username": info.get("username", ""),
                        "uuid": info.get("uuid", ""),
                        "validated_at": datetime.now().isoformat()
                    }

                    username = info.get("username", "unknown")
                    self.account_result_signal.emit(label, "success", username, "")
                    self.log_signal.emit(f"  ✅ {label} → {username}")
                    success += 1

                except Exception as e:
                    error_msg = str(e)
                    self.account_result_signal.emit(label, "failed", "", "")
                    self.log_signal.emit(f"  ❌ {label}: {error_msg[:80]}")
                    failed += 1

            # Small delay to avoid rate limiting
            if idx < total - 1:
                self.msleep(300)

        # Save cache
        self._save_cache(cache)

        total_done = success + failed + skipped
        self.finished_signal.emit(success, failed, skipped)
        self.log_signal.emit(f"\n📊 Bulk auth complete: ✅ {success} new | ❌ {failed} failed | ⚡ {skipped} cached")

    def stop(self):
        self._stop = True

    def _load_cache(self) -> dict:
        try:
            path = Path(self.token_cache_path)
            if path.exists():
                return json.loads(path.read_text())
        except Exception:
            pass
        return {}

    def _save_cache(self, cache: dict):
        try:
            path = Path(self.token_cache_path)
            path.parent.mkdir(parents=True, exist_ok=True)
            path.write_text(json.dumps(cache, indent=2))
        except Exception:
            pass
    

class SingleAuthTestWorker(QThread):
    """Test a single account authentication in a background thread."""
    log_signal = pyqtSignal(str)
    finished_signal = pyqtSignal(str, bool, str, str)  # email, success, gamertag, error

    def __init__(self, email: str, password: str, use_playwright: bool = False):
        super().__init__()
        self.email = email
        self.password = password
        self.use_playwright = use_playwright

    def __del__(self):
        self.wait()

    def run(self):
        """Validate bearer token against Minecraft API."""
        try:
            auth = BearerTokenAuth(self.password, lambda m: self.log_signal.emit(m))
            try:
                info = auth.validate()
                self.finished_signal.emit(self.email, True, info.get('gamertag', info.get('username', '')), "")
            except Exception as e:
                self.finished_signal.emit(self.email, False, "", str(e))
        except Exception as e:
            self.finished_signal.emit(self.email, False, "", f"Worker error: {e}")


def play_success_sound():
    """Play sound alert on success"""
    try:
        system = platform.system()
        if system == "Linux":
            # Try multiple sound files
            for sound_file in ["/usr/share/sounds/freedesktop/stereo/complete.oga",
                             "/usr/share/sounds/ubuntu-submitted/stereo/alarm-clock.oga",
                             "/usr/share/sounds/freedesktop/stereo/message.oga"]:
                try:
                    if subprocess.call(["which", "aplay"], stdout=subprocess.DEVNULL) == 0:
                        subprocess.run(["aplay", "-q", sound_file], timeout=2)
                        return
                    else:
                        subprocess.run(["play", sound_file], timeout=2)
                        return
                except:
                    continue
        elif system == "Darwin":  # macOS
            subprocess.run(["afplay", "/System/Library/Sounds/Glass.aiff"], timeout=2)
        elif system == "Windows":
            subprocess.run(["powershell", "-Command", "[System.Media.SystemSounds]::Beep.Play()"], timeout=2)
    except Exception as e:
        pass  # Silent fail if sound not available


def send_desktop_notification(title, message):
    """Send desktop notification on success"""
    try:
        system = platform.system()
        if system == "Linux":
            subprocess.run(["notify-send", "-u", "critical", "-t", "10000", title, message], timeout=2)
        elif system == "Darwin":  # macOS
            subprocess.run(["osascript", "-e", f'display notification "{message}" with title "{title}"'], timeout=2)
        elif system == "Windows":
            # Windows notification would require a library, skip for now
            pass
    except Exception as e:
        pass  # Silent fail

# === FEATURE: SCREENSHOT ON SUCCESS ===
def take_success_screenshot(username):
    """Take a screenshot of the screen when a name is successfully claimed"""
    try:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        screenshots_dir = Path("screenshots")
        screenshots_dir.mkdir(exist_ok=True)
        filename = screenshots_dir / f"snipe_{username}_{timestamp}.png"
        
        system = platform.system()
        if system == "Linux":
            # Try multiple screenshot methods
            for cmd in [
                ["gnome-screenshot", "-f", str(filename)],
                ["import", "-window", "root", str(filename)],  # ImageMagick
                ["scrot", str(filename)],
            ]:
                try:
                    result = subprocess.run(cmd, timeout=5, capture_output=True)
                    if result.returncode == 0 and filename.exists():
                        return str(filename)
                except (FileNotFoundError, subprocess.TimeoutExpired):
                    continue
            # Fallback: use PyQt5 screen capture
            app = QApplication.instance()
            if app:
                screen = QScreen.primaryScreen
                if screen:
                    pixmap = screen.grabWindow(0)
                    pixmap.save(str(filename))
                    return str(filename)
        elif system == "Darwin":  # macOS
            subprocess.run(["screencapture", str(filename)], timeout=5)
            if filename.exists():
                return str(filename)
        elif system == "Windows":
            # Use PowerShell for Windows screenshot
            ps_cmd = f'(Add-Type -AssemblyName System.Windows.Forms -Passthru)[System.Windows.Forms.Screen]::PrimaryScreen.Bounds | Out-Null; $bmp = New-Object System.Drawing.Bitmap([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Width, [System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Height); $g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen([System.Windows.Forms.Screen]::PrimaryScreen.Bounds.Location, [System.Drawing.Point]::Empty, $bmp.Size); $bmp.Save("{filename}"); $g.Dispose(); $bmp.Dispose()'
            subprocess.run(["powershell", "-Command", ps_cmd], timeout=5)
            if filename.exists():
                return str(filename)
    except Exception as e:
        pass  # Silent fail
    return None


class SniperWorker(QThread):
    """Worker thread for firing concurrent requests"""
    log_signal = pyqtSignal(str)
    progress_signal = pyqtSignal(int)
    stats_signal = pyqtSignal(dict)  # Real-time stats: {fired, success, failed, rps, competition, avg_response_ms, active_tokens, time_remaining}
    
    def __init__(self, names, tokens, drop_time_utc, threads_count, timing_mode="exact", priority_mode="fresh_first", warmup_enabled=True, warmup_duration=30, adaptive_enabled=True, aggressiveness=7, fingerprint_rotation=True, token_rotation=True, temporal_jitter=True, response_analysis=True, multi_name_mode="sequential", stagger_seconds=0, offset_calibration_enabled=True, webhook_url=None, auto_restart_enabled=False, auto_restart_max=3, skin_upload_enabled=False, skin_file_path=None, skin_model="classic", queue_mode=False, queue_callback=None, desktop_notifications_enabled=False, fire_and_forget=False, dry_run=False, pre_validate_tokens=True, auto_detect_account_type=True, ntp_sync_enabled=True):
        super().__init__()
        self.names = names if isinstance(names, list) else [names]  # Support single or multiple names
        self.tokens = tokens
        self._token_cache = self._load_token_cache()  # Load cache for refresh tokens
        self.drop_time_utc = drop_time_utc
        # Queue mode: each name has its own drop time
        self.queue_mode = queue_mode
        self.queue_callback = queue_callback  # callback(idx, status)
        self.threads_count = threads_count
        self.timing_mode = timing_mode
        self.priority_mode = priority_mode
        self.warmup_enabled = warmup_enabled
        self.warmup_duration = warmup_duration  # seconds before drop to start warming
        self.stop_event = threading.Event()
        self.success_event = threading.Event()
        self.requests_fired = 0
        self.requests_success = 0
        self.requests_failed = 0
        self.connection_warmed = False
        self.warmup_requests_sent = 0
        self.warmup_success = 0
        self.warmup_failures = 0
        
        # === FEATURE #17: AUTO-OFFSET CALIBRATION ===
        # Measure actual network latency to MC API and adjust fire time
        # Only 5 probe requests, 30s apart, runs BEFORE drop window
        self.offset_calibration_enabled = offset_calibration_enabled
        self.calibrated_offset_ms = 0  # ms to fire EARLIER than nominal drop
        
        # === MULTI-NAME MODE ===
        self.multi_name_mode = multi_name_mode  # "sequential", "parallel", or "staggered"
        self.stagger_seconds = stagger_seconds  # seconds between names (for staggered mode)
        self.name_results = {}  # Track results per name: {name: {"success": bool, "message": str}}
        self.current_name_index = 0
        self.names_completed = 0
        self.total_names = len(self.names)
        
        # === AVAILABILITY CHECKER (FEATURE #5) ===
        # Poll Mojang API to confirm name is actually available before wasting snipe attempts
        self.availability_check_enabled = True
        self.namemc_available = None  # None = unknown, True = available, False = taken
        self.namemc_last_check = 0
        self.NAMEMC_CHECK_INTERVAL = 10  # seconds between availability checks
        self.MOJANG_PROFILE_URL = "https://api.mojang.com/users/profiles/minecraft/{}"
        self.PLAYERDB_URL = "https://playerdb.co/api/player/minecraft/{}"
        self.namemc_confirmed_available = False  # did we ever see it become available?
        self.auto_refresh_tokens = False  # Will be set by GUI if auto-refresh enabled
        self.token_refresh_window = 300  # 5 minutes before drop (default, can be overridden)
        
        # === FEATURE #5: ADAPTIVE THROTTLING SETTINGS ===
        self.adaptive_enabled = adaptive_enabled
        self.aggressiveness = aggressiveness  # 1-10 scale, affects how aggressively we adapt
        
        # === FEATURE #6: FINGERPRINT ROTATION ===
        # Rotate User-Agent per request to avoid fingerprint-based rate limiting
        self.fingerprint_rotation = fingerprint_rotation
        self.current_user_agent = get_random_user_agent()
        self.fingerprint_rotations = 0  # track how many times we've rotated
        
        # === FEATURE #4: ERROR LEARNING (AUTO-BLACKLIST) ===
        # Track errors per token and blacklist consistently failing ones
        self.token_error_counts = {}  # token_hash -> error_count
        self.token_error_types = {}   # token_hash -> {status_code: count}
        self.blacklisted_tokens = set()  # tokens that exceeded error threshold
        self.ERROR_THRESHOLD = 5  # blacklist after 5 consecutive errors
        self.BLACKLIST_THRESHOLD = 0.8  # blacklist if >80% of errors are same type
        
        # === FEATURE #8: TOKEN ROTATION ON RATE LIMIT ===
        # Rotate to different tokens when one gets rate limited
        self.token_rotation_enabled = token_rotation
        self.token_rotation_count = 0
        self.rate_limited_tokens = set()  # tokens currently rate limited
        self.token_rotation_duration = 60  # seconds to skip a rate-limited token
        
        # === IP-level rate limit detection ===
        # If multiple tokens get 429'd within a short window, it's an IP-level ban, not per-token
        self.ip_rate_limit_window = collections.deque(maxlen=20)  # timestamp of recent 429s
        self.ip_rate_limit_cooldown_until = 0  # unix timestamp when IP cooldown expires
        self.IP_429_THRESHOLD = 5  # if 5+ tokens get 429 within 10s, assume IP-level
        self.IP_429_WINDOW = 10.0  # seconds to watch for pattern
        self.IP_429_COOLDOWN = 30  # pause all firing for 30s on IP-level detection
        self.ip_cooldown_triggered = False
        
        # === FEATURE #13: TEMPORAL JITTER ===
        # Add microsecond-level randomization to evade detection
        self.temporal_jitter_enabled = temporal_jitter
        self.jitter_range_ms = 3  # spread requests over ±3ms window
        self.jitter_applied_count = 0
        
        # === PHASE 3: MULTI-ACCOUNT ROTATION ===
        # Round-robin + success-weighted token selection
        self.token_rotation_index = 0  # round-robin pointer
        self.token_success_counts = {}  # token_hash -> success count (for weighting)
        self.token_total_counts = {}    # token_hash -> total request count
        self.account_history = None     # loaded from account_history.json
        
        # === FEATURE #15: RESPONSE ANALYSIS & EARLY ABORT ===
        # Monitor responses in real-time and abort if outcome determined
        self.response_analysis_enabled = response_analysis
        self.early_abort_on_success = True  # Always abort on success
        self.failure_patterns = ["name taken", "already registered", "invalid token", "authentication failed", "token invalid"]
        self.consecutive_failures = 0
        self.success_indicators = ["success", "created", "ok", "claimed"]
        self.failure_count = 0
        self.success_count = 0
        self.max_consecutive_failures = 50  # abort after 50 same-type failures
        
        # === FEATURE #5: ADAPTIVE THROTTLING ===
        # Dynamically adjust request rate based on competition and error rates
        self.competition_level = "LOW"  # LOW, MEDIUM, HIGH, EXTREME
        self.last_100_requests = []  # track recent request outcomes
        self.rate_limit_spike = False  # detect sudden 429 increase
        self.adaptive_threads = self.threads_count  # current thread count (can be reduced)
        self.adaptive_delay = 0.0  # current delay between bursts (can be increased)
        self.COMPETITION_WINDOW = 100  # analyze last 100 requests
        self.RATE_LIMIT_THRESHOLD = 0.3  # >30% 429s = rate limit spike
        self.COMPETITION_HIGH_THRESHOLD = 0.4  # >40% failures = high competition
        self.COMPETITION_EXTREME_THRESHOLD = 0.6  # >60% failures = extreme competition
        self.MIN_THREADS = max(1, self.threads_count // 4)  # never go below 25%
        self.MAX_DELAY = 2.0  # max delay between bursts (seconds)
        
        # === PHASE 4: REAL-TIME STATS TRACKING ===
        self.stats_enabled = True
        self._stats_start_time = 0  # when firing started
        self._request_timestamps = collections.deque(maxlen=200)  # track request timings for RPS
        self._response_times_ms = collections.deque(maxlen=200)  # track response times
        self._stats_last_fired = 0  # for RPS calculation
        self._target_timestamp = 0  # target drop time for countdown
        self._stats_thread = None  # background stats emitter thread
        
        # === FEATURE: AUTO-RESTART ON FAILURE ===
        self.auto_restart_enabled = auto_restart_enabled
        self.auto_restart_max = auto_restart_max
        self.auto_restart_count = 0
        self._sniper_config = {  # Store config for restart
            "names": names, "tokens": tokens, "drop_time_utc": drop_time_utc,
            "threads_count": threads_count, "timing_mode": timing_mode,
            "priority_mode": priority_mode, "warmup_enabled": warmup_enabled,
            "warmup_duration": warmup_duration, "adaptive_enabled": adaptive_enabled,
            "aggressiveness": aggressiveness, "fingerprint_rotation": fingerprint_rotation,
            "token_rotation": token_rotation, "temporal_jitter": temporal_jitter,
            "response_analysis": response_analysis, "multi_name_mode": multi_name_mode,
            "stagger_seconds": stagger_seconds, "offset_calibration_enabled": offset_calibration_enabled,
            "webhook_url": webhook_url,
        }
        
        # === FEATURE: AUTO SKIN UPLOAD ON SNIPE ===
        # Automatically upload a skin after successfully claiming a name
        # This verifies the snipe worked and sets your identity immediately
        self.skin_upload_enabled = skin_upload_enabled
        self.skin_file_path = skin_file_path
        self.skin_model = skin_model  # "classic" or "slim"
        self.skin_uploaded = False  # Prevent double upload

        # === FEATURE: DESKTOP NOTIFICATIONS ===
        self.desktop_notifications_enabled = desktop_notifications_enabled

        # === FEATURE: THREAD-LOCAL HTTP CLIENT POOL ===
        self._thread_clients = threading.local()
        self.fire_and_forget = fire_and_forget  # Non-blocking request mode
        
        # === QUICK WIN: DRY-RUN / SIMULATION MODE ===
        self.dry_run = dry_run
        self.dry_run_stats = {"simulated_fires": 0, "simulated_success": 0, "simulated_failures": 0}
        
        # === QUICK WIN: PRE-SNIPE TOKEN VALIDATION ===
        self.pre_validate_tokens = pre_validate_tokens
        
        # === QUICK WIN: AUTO-DETECT ACCOUNT TYPE ===
        self.auto_detect_account_type = auto_detect_account_type
        self.token_account_types = {}  # token_hash -> "GC" | "MS" | "Mojang" | "unknown"
        
       # === QUICK WIN: TOKEN CACHE WITH TTL ===
        self.token_cache = {}  # token -> {"expires_at": float, "type": str, "username": str}
        self.TOKEN_CACHE_TTL = 5400  # 90 minutes (tokens typically last ~1h, give buffer)

        # === FEATURE: NTP CLOCK SYNC ===
        self.ntp_sync_enabled = ntp_sync_enabled

    def _get_thread_client(self, token=None):
        """Get or create a thread-local HTTP client for connection reuse"""
        if not hasattr(self._thread_clients, 'client') or self._thread_clients.client is None:
            self._thread_clients.client = httpx.Client(
                transport=httpx.HTTPTransport(pool=connection_pool),
                http2=True,
                timeout=httpx.Timeout(3.0, connect=3.0),
                headers={
                    "Content-Type": "application/json",
                    "User-Agent": get_random_user_agent(),
                },
            )
        return self._thread_clients.client

    def _close_thread_clients(self):
        """Close all thread-local HTTP clients"""
        if hasattr(self._thread_clients, 'client') and self._thread_clients.client is not None:
            self._thread_clients.client.close()
            self._thread_clients.client = None

    def _load_token_cache(self) -> dict:
        """Load token cache for refresh token lookups"""
        cache_path = str(Path.home() / ".hermes" / "minecraft-sniper" / "tokens.json")
        try:
            path = Path(cache_path)
            if path.exists():
                return json.loads(path.read_text())
        except Exception:
            pass
        return {}

    # ====================================================================
    # QUICK WIN: AUTO-DETECT ACCOUNT TYPE
    # ====================================================================

    def _detect_account_type(self, token):
        """Detect if a token is Game Center (GC), Microsoft (MS), or legacy Mojang.
        
        Returns: "GC", "MS", "Mojang", or "unknown"
        
        Detection heuristics:
        - GC tokens: typically start with specific prefixes or have JWT structure with GC claims
        - MS tokens: Microsoft OAuth2 bearer tokens (long base64 strings)
        - Mojang: legacy tokens (shorter, different format)
        - Fallback: hit the MC API and infer from response headers
        """
        token_key = hash(token) % 1000000
        
        # Check cache first
        if token_key in self.token_account_types:
            return self.token_account_types[token_key]
        
        account_type = "unknown"
        
        try:
            # Heuristic 1: Token format analysis
            if token.startswith("eyJ"):
                # JWT token - could be GC or MS
                try:
                    # Decode JWT header without verification
                    import base64
                    header_b64 = token.split('.')[0]
                    # Add padding if needed
                    header_b64 += '=' * (4 - len(header_b64) % 4)
                    header = json.loads(base64.urlsafe_b64decode(header_b64))
                    issuer = header.get('iss', '').lower()
                    if 'apple' in issuer or 'gc' in issuer:
                        account_type = "GC"
                    elif 'microsoft' in issuer or 'live.com' in issuer:
                        account_type = "MS"
                    else:
                        account_type = "MS"  # Default JWT to MS
                except Exception:
                    account_type = "MS"  # Default for JWT format
            elif len(token) > 200:
                # Very long tokens are typically Microsoft OAuth2
                account_type = "MS"
            elif len(token) < 64:
                # Short tokens might be legacy Mojang
                account_type = "Mojang"
            
            # Heuristic 2: API call to confirm
            client = self._get_thread_client(token)
            try:
                r = client.get("https://api.minecraftservices.com/minecraft/profile", 
                              headers={"Authorization": f"Bearer {token}"}, timeout=3)
                if r.status_code == 200:
                    # Valid token - check response headers for clues
                    x_ms_cx = r.headers.get('x-ms-cx', '')
                    if 'apple' in x_ms_cx.lower() or 'gamecenter' in x_ms_cx.lower():
                        account_type = "GC"
                    elif account_type == "unknown":
                        account_type = "MS"  # Most valid modern tokens are MS
                elif r.status_code == 401:
                    account_type = "invalid"
            except Exception:
                pass  # Keep heuristic-based guess
                
        except Exception:
            pass
        
        # Cache the result
        self.token_account_types[token_key] = account_type
        return account_type

    def _batch_detect_account_types(self, tokens):
        """Detect account types for all tokens and log summary."""
        counts = {"GC": 0, "MS": 0, "Mojang": 0, "unknown": 0, "invalid": 0}
        for token in tokens:
            acc_type = self._detect_account_type(token)
            counts[acc_type] = counts.get(acc_type, 0) + 1
        
        summary_parts = []
        for acc_type, count in counts.items():
            if count > 0:
                emoji = {"GC": "🍎", "MS": "🔵", "Mojang": "🟫", "unknown": "❓", "invalid": "❌"}.get(acc_type, "?")
                summary_parts.append(f"{emoji} {acc_type}: {count}")
        
        self.log_signal.emit(f"[🔍] Account types: {', '.join(summary_parts)}")
        return counts

    # ====================================================================
    # QUICK WIN: PRE-SNIPE TOKEN VALIDATION
    # ====================================================================

    def _pre_validate_tokens(self, tokens):
        """Validate all tokens against MC API before snipe starts.
        
        Returns list of valid tokens, logs invalid ones.
        """
        self.log_signal.emit(f"[🔎] Pre-validating {len(tokens)} tokens before snipe...")
        valid_tokens = []
        invalid_count = 0
        
        for i, token in enumerate(tokens):
            token_key = hash(token) % 10000
            try:
                client = self._get_thread_client(token)
                r = client.get("https://api.minecraftservices.com/minecraft/profile",
                              headers={"Authorization": f"Bearer {token}"}, timeout=3)
                if r.status_code == 200:
                    profile = r.json()
                    username = profile.get('name', 'unknown')
                    valid_tokens.append(token)
                    # Cache token info with TTL
                    self.token_cache[token_key] = {
                        "expires_at": time.time() + self.TOKEN_CACHE_TTL,
                        "type": self._detect_account_type(token),
                        "username": username,
                    }
                else:
                    invalid_count += 1
                    self.log_signal.emit(f"  ❌ Token {token_key}: HTTP {r.status_code} — excluded")
            except Exception as e:
                invalid_count += 1
                self.log_signal.emit(f"  ❌ Token {token_key}: {str(e)[:40]} — excluded")
            
            # Progress every 10 tokens
            if (i + 1) % 10 == 0:
                self.log_signal.emit(f"  ... validated {i+1}/{len(tokens)} tokens ({len(valid_tokens)} valid, {invalid_count} invalid)")
        
        self.log_signal.emit(f"[✅] Pre-validation complete: {len(valid_tokens)} valid, {invalid_count} invalid out of {len(tokens)} tokens")
        
        if invalid_count > 0:
            self.log_signal.emit(f"  ⚠️  {invalid_count} token(s) will NOT be used for this snipe")
        
        if len(valid_tokens) == 0 and len(tokens) > 0:
            self.log_signal.emit(f"[🚨] WARNING: No valid tokens! Snipe will fail.")
        
        return valid_tokens

    # ====================================================================
    # QUICK WIN: TOKEN CACHE WITH TTL
    # ====================================================================

    def _get_cached_token_info(self, token):
        """Get cached info for a token if not expired."""
        token_key = hash(token) % 10000
        cache_entry = self.token_cache.get(token_key)
        if cache_entry and cache_entry["expires_at"] > time.time():
            return cache_entry
        # Expired - remove
        self.token_cache.pop(token_key, None)
        return None

    def _is_token_expired(self, token):
        """Check if a token's cache entry has expired."""
        return self._get_cached_token_info(token) is None

    # ====================================================================
    # FEATURE: NTP CLOCK SYNC — Measure local clock drift
    # ====================================================================

    def _ntp_sync_check(self):
        """Query NTP servers to measure local clock drift.

        Uses the Simple Network Time Protocol (NTP) via UDP to query public
        NTP servers and calculate how much the local system clock differs
        from atomic time.

        Returns:
            dict with keys:
                - drift_ms: clock drift in milliseconds (positive = local is ahead)
                - offset_ms: round-trip offset in milliseconds
                - server: NTP server used
                - ok: bool, True if drift is within acceptable bounds
                - warning: str or None
        """
        NTP_SERVERS = [
            "time.google.com",
            "time.apple.com",
            "pool.ntp.org",
            "time.nist.gov",
        ]
        NTP_PORT = 123
        NTP_DELTA = 2208988800 - 1900  # Unix epoch - NTP epoch
        DRIFT_WARN_MS = 50   # Warn if drift > 50ms
        DRIFT_CRITICAL_MS = 200  # Critical if drift > 200ms

        self.log_signal.emit("[🕐] NTP CLOCK SYNC: Checking local clock accuracy...")

        results = []
        for server in NTP_SERVERS:
            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.settimeout(3.0)

                # NTP packet: 48 bytes, mode=3 (client), version=3
                ntp_packet = b'\x1b' + 47 * b'\0'

                t1 = time.time()  # Transmit timestamp (local)
                sock.sendto(ntp_packet, (server, NTP_PORT))
                response, _ = sock.recvfrom(1024)
                t4 = time.time()  # Receive timestamp (local)

                sock.close()

                if len(response) < 48:
                    continue

                # Extract timestamps from NTP response
                # t2 = Originate Timestamp (server sent)
                # t3 = Transmit Timestamp (server sent)
                t2_raw = int.from_bytes(response[32:36], 'big')
                t3_raw = int.from_bytes(response[40:44], 'big')

                t2 = (t2_raw - NTP_DELTA) / 65536.0
                t3 = (t3_raw - NTP_DELTA) / 65536.0

                # Calculate drift and offset per RFC 1305
                # drift = ((t2 - t1) + (t3 - t4)) / 2
                # offset (round-trip) = (t4 - t1) - (t3 - t2)
                drift = ((t2 - t1) + (t3 - t4)) / 2
                rtt = (t4 - t1) - (t3 - t2)

                drift_ms = drift * 1000
                rtt_ms = rtt * 1000

                if rtt_ms < 0 or rtt_ms > 5000:
                    continue  # Skip obviously bad results

                results.append({
                    "server": server,
                    "drift_ms": drift_ms,
                    "rtt_ms": rtt_ms,
                })
                self.log_signal.emit(
                    f"  [🕐] {server}: drift={drift_ms:+.1f}ms, RTT={rtt_ms:.1f}ms"
                )

            except (socket.timeout, socket.error, OSError) as e:
                self.log_signal.emit(f"  [🕐] {server}: failed ({e})")
                continue

        if not results:
            self.log_signal.emit("[🕐] ⚠️  All NTP servers unreachable — skipping clock sync")
            return {"drift_ms": 0, "ok": True, "warning": "NTP unavailable"}

        # Use the result with lowest RTT (most reliable)
        best = min(results, key=lambda r: r["rtt_ms"])
        drift_ms = best["drift_ms"]
        abs_drift = abs(drift_ms)

        warning = None
        ok = True
        if abs_drift > DRIFT_CRITICAL_MS:
            warning = f"CRITICAL: Clock drift {drift_ms:+.1f}ms — snipe timing will be off!"
            ok = False
        elif abs_drift > DRIFT_WARN_MS:
            warning = f"WARNING: Clock drift {drift_ms:+.1f}ms — consider syncing your system clock"

        self.log_signal.emit(
            f"[🕐] Best result from {best['server']}: "
            f"drift={drift_ms:+.1f}ms, RTT={best['rtt_ms']:.1f}ms"
        )

        if warning:
            self.log_signal.emit(f"[🕐] {warning}")
        else:
            self.log_signal.emit(f"[🕐] ✓ Clock accuracy OK (drift {abs_drift:.1f}ms)")

        return {
            "drift_ms": drift_ms,
            "offset_ms": best["rtt_ms"],
            "server": best["server"],
            "ok": ok,
            "warning": warning,
        }

    # ====================================================================
    # FEATURE: INTEGRATED DROPTIME FROM NAMEMC + 3NAME.XYZ
    # ====================================================================

    def _fetch_droptime_ashcon(self, username):
        """
        Fetch name droptime from ashcon.app API (Mojang proxy).

        ashcon.app provides name change history with timestamps.
        Cooldown: 60 days (Mojang policy).

        Returns: ISO timestamp string or None
        """
        from datetime import datetime, timedelta, timezone
        url = f"https://api.ashcon.app/mojang/v2/user/{username}"
        try:
            resp = requests.get(url, timeout=5)
            if resp.status_code == 200:
                data = resp.json()
                history = data.get("username_history", [])
                if len(history) > 1:
                    # Has name changes — use last change timestamp
                    # ashcon doesn't include timestamps in username_history,
                    # so we need playerdb for that
                    pass
                # created_at gives account creation date (not name change)
                return None
        except Exception:
            pass
        self.log_signal.emit(f"  [📊] ashcon.app: no droptime data for '{username}'")
        return None

    def _fetch_droptime_playerdb(self, username):
        """
        Fetch name droptime from playerdb.co API.

        playerdb.co provides name_history with changedToAt timestamps.
        Cooldown: 60 days (Mojang policy).

        Returns: ISO timestamp string or None
        """
        from datetime import datetime, timedelta, timezone
        url = f"https://playerdb.co/api/player/minecraft/{username}"
        try:
            resp = requests.get(url, timeout=5)
            if resp.status_code == 200:
                data = resp.json()
                if data.get("success"):
                    history = data["data"]["player"].get("name_history", [])
                    if history:
                        # Get the most recent name change
                        last_entry = history[-1]
                        changed_to_at = last_entry.get("changedToAt")
                        if changed_to_at:
                            # changedToAt is in milliseconds
                            last_dt = datetime.fromtimestamp(changed_to_at / 1000, tz=timezone.utc)
                            drop = last_dt + timedelta(days=60)
                            now = datetime.now(timezone.utc)
                            if drop > now:
                                remaining = drop - now
                                self.log_signal.emit(
                                    f"  [📊] playerdb: '{username}' last changed {last_dt.strftime('%Y-%m-%d %H:%M UTC')} "
                                    f"→ drops ~{drop.strftime('%Y-%m-%d %H:%M UTC')} "
                                    f"(in {remaining.days}d {remaining.seconds//3600}h {(remaining.seconds%3600)//60}m)"
                                )
                            else:
                                self.log_signal.emit(
                                    f"  [📊] playerdb: '{username}' cooldown expired — name may be available"
                                )
                            return drop.isoformat()
        except Exception as e:
            self.log_signal.emit(f"  [📊] playerdb request failed: {str(e)[:60]}")
        self.log_signal.emit(f"  [📊] playerdb: no droptime data for '{username}'")
        return None

    def _fetch_droptime_mojang_check(self, username):
        """
        Check if a name is currently available via Mojang API.

        GET /users/profiles/minecraft/{name} returns 404 if the name
        is not currently assigned to any account (i.e., available).

        Returns: True if available, False if taken, None on error
        """
        url = f"https://api.mojang.com/users/profiles/minecraft/{username}"
        try:
            resp = requests.get(url, timeout=5)
            if resp.status_code == 200:
                self.log_signal.emit(f"  [📊] Mojang: '{username}' is currently TAKEN")
                return False
            elif resp.status_code == 404:
                self.log_signal.emit(f"  [📊] Mojang: '{username}' is AVAILABLE (not assigned)")
                return True
            else:
                self.log_signal.emit(f"  [📊] Mojang: unexpected status {resp.status_code}")
                return None
        except Exception as e:
            self.log_signal.emit(f"  [📊] Mojang check failed: {str(e)[:60]}")
            return None

    def _fetch_droptime(self, username):
        """
        Fetch droptime for a username from multiple sources.

        Tries playerdb.co (name history + 60-day cooldown) →
        Mojang availability check → ashcon.app validation.

        Returns: ISO timestamp string or None
        """
        self.log_signal.emit(f"[*] Fetching droptime for '{username}'...")

        # Source 1: playerdb.co — best source for name change history
        playerdb_time = self._fetch_droptime_playerdb(username)
        if playerdb_time:
            self.log_signal.emit(f"[✓] Using droptime from playerdb.co")
            return playerdb_time

        # Source 2: Check if name is even assigned
        available = self._fetch_droptime_mojang_check(username)
        if available is True:
            self.log_signal.emit(f"[✓] '{username}' appears available — try sniping now!")
            return None
        elif available is False:
            self.log_signal.emit(f"[*] '{username}' is taken but no change history found — manual drop time needed")

        self.log_signal.emit(f"[*] No droptime data found for '{username}' — manual entry needed")
        return None

    # ====================================================================
    # FEATURE: MOJANG API STATUS CHECKER
    # ====================================================================

    def _check_mojang_status(self):
        """
        Check health of all critical Mojang endpoints before snipe.

        Probes:
        - api.mojang.com/profile/minecraft (name lookup)
        - api.mojang.com/users/profiles/minecraft (UUID lookup)
        - sessionserver.mojang.com (auth)
        - auth.xbox.com (Xbox live)
        - minecraftstatus.com (community status page)

        Returns dict with endpoint statuses and overall health score.
        """
        self.log_signal.emit("[*] Checking Mojang API health...")

        endpoints = {
            "Profile API": "https://api.mojang.com/profile/minecraft/Steve",
            "UUID API": "https://api.mojang.com/users/profiles/minecraft/Steve",
            "Session Server": "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=Steve&uuid=0",
            "Xbox Auth": "https://xl.start.microsoft.com/xl/2.0/",
        }

        results = {}
        healthy = 0
        total = len(endpoints)

        for name, url in endpoints.items():
            try:
                start = time.monotonic()
                resp = requests.get(url, timeout=5)
                rtt = (time.monotonic() - start) * 1000

                if resp.status_code in (200, 204, 404, 403):
                    status = "healthy"
                    healthy += 1
                    icon = "🟢"
                elif resp.status_code == 429:
                    status = "rate_limited"
                    icon = "🟡"
                elif resp.status_code >= 500:
                    status = "error"
                    icon = "🔴"
                else:
                    status = f"http_{resp.status_code}"
                    icon = "🟡"

                results[name] = {"status": status, "rtt_ms": rtt, "code": resp.status_code}
                self.log_signal.emit(f"  [{icon}] {name}: {status} ({rtt:.0f}ms)")
            except Exception as e:
                results[name] = {"status": "unreachable", "rtt_ms": 0, "error": str(e)}
                self.log_signal.emit(f"  [🔴] {name}: unreachable ({str(e)[:40]})")

        # Also check minecraftstatus.com for community reports
        try:
            resp = requests.get("https://minecraftstatus.com/api/v2/status", timeout=5)
            if resp.status_code == 200:
                data = resp.json()
                # Parse status from response
                self.log_signal.emit(f"  [📊] minecraftstatus.com: API reachable")
        except:
            pass

        health_pct = (healthy / total * 100) if total else 0
        if health_pct >= 75:
            self.log_signal.emit(f"[✓] Mojang API health: {health_pct:.0f}% ({healthy}/{total} endpoints healthy)")
        elif health_pct >= 50:
            self.log_signal.emit(f"[⚠️] Mojang API partially degraded: {health_pct:.0f}% ({healthy}/{total})")
        else:
            self.log_signal.emit(f"[🚨] Mojang API severely degraded: {health_pct:.0f}% ({healthy}/{total}) — consider waiting")

        return {
            "endpoints": results,
            "healthy": healthy,
            "total": total,
            "health_pct": health_pct,
        }

    # ====================================================================
    # FEATURE: NAME HISTORY LOOKUP
    # ====================================================================

    def _lookup_name_history(self, username):
        """
        Look up the ownership history of a Minecraft username.

        Uses playerdb.co API to get full name change history, showing
        who owned the name previously and when it will drop.

        Returns list of history entries or None.
        """
        self.log_signal.emit(f"[*] Looking up history for '{username}'...")

        url = f"https://playerdb.co/api/player/minecraft/{username}"
        try:
            resp = requests.get(url, timeout=5)
            if resp.status_code == 200:
                data = resp.json()
                if data.get("success"):
                    history = data["data"]["player"].get("name_history", [])
                    if history:
                        self.log_signal.emit(f"  [📜] '{username}' name change history ({len(history)} changes):")
                        for i, entry in enumerate(history, 1):
                            from_name = entry.get("name", "?")
                            changed_at = entry.get("changedToAt", "?")
                            if changed_at != "?":
                                from datetime import datetime, timezone
                                dt = datetime.fromtimestamp(changed_at / 1000, tz=timezone.utc)
                                changed_str = dt.strftime("%Y-%m-%d %H:%M UTC")
                            else:
                                changed_str = "?"
                            self.log_signal.emit(f"    {i}. Changed to '{from_name}' ({changed_str})")

                        # Calculate estimated next drop (60-day cooldown)
                        last_entry = history[-1]
                        if last_entry.get("changedToAt"):
                            from datetime import datetime, timedelta, timezone
                            last_dt = datetime.fromtimestamp(last_entry["changedToAt"] / 1000, tz=timezone.utc)
                            drop = last_dt + timedelta(days=60)
                            now = datetime.now(timezone.utc)
                            if drop > now:
                                delta = drop - now
                                self.log_signal.emit(
                                    f"  [⏰] Estimated drop: {drop.strftime('%Y-%m-%d %H:%M UTC')} "
                                    f"(in {delta.days}d {delta.seconds//3600}h {(delta.seconds%3600)//60}m)"
                                )
                            else:
                                self.log_signal.emit(f"  [⏰] Name cooldown expired — may be available")
                        return history
                    else:
                        self.log_signal.emit(f"  [📜] No name change history for '{username}'")
                        return None
        except Exception as e:
            self.log_signal.emit(f"  [!] Could not fetch history: {str(e)[:60]}")
        self.log_signal.emit(f"  [!] Could not fetch history for '{username}'")
        return None

    # ====================================================================
    # FEATURE: NAME PATTERN GENERATOR
    # ====================================================================

    def _generate_name_patterns(self, pattern):
        """
        Expand a name pattern into candidate usernames.

        Supported syntax:
        - {letter} or {a-z} → any lowercase letter
        - {LETTER} or {A-Z} → any uppercase letter
        - {digit} or {0-9} → any digit 0-9
        - {alphanum} → any letter or digit
        - {n} → repeat n times (e.g., {letter}{3} = aaa, aab, ...)
        - {{ }} → literal braces

        Examples:
        - "x{letter}x" → axx, bxx, cxx, ... zxx
        - "pro{digit}{digit}" → pro00, pro01, ... pro99
        - "{letter}{letter}gaming" → aagaming, aagaming, ... zzgaming

        Returns: list of generated names (max 10000)
        """
        import string
        import itertools

        # Parse the pattern into segments
        segments = []
        i = 0
        while i < len(pattern):
            if pattern[i] == '{':
                # Find closing brace
                j = pattern.find('}', i + 1)
                if j == -1:
                    segments.append(pattern[i:])
                    break

                token = pattern[i+1:j]

                if token == '{':
                    # Escaped opening brace
                    segments.append(['{'])
                    i = j + 1
                    continue
                elif token == '}':
                    # Escaped closing brace
                    segments.append(['}'])
                    i = j + 1
                    continue
                elif token in ('letter', 'a-z'):
                    segments.append(list(string.ascii_lowercase))
                elif token in ('LETTER', 'A-Z'):
                    segments.append(list(string.ascii_uppercase))
                elif token in ('digit', '0-9'):
                    segments.append(list(string.digits))
                elif token == 'alphanum':
                    segments.append(list(string.ascii_letters + string.digits))
                else:
                    # Unknown token, treat as literal
                    segments.append([pattern[i:j+1]])

                i = j + 1
            elif pattern[i] == '}':
                segments.append(['}'])
                i += 1
            else:
                segments.append([pattern[i]])
                i += 1

        if not segments:
            return []

        # Generate cartesian product (with limit)
        max_results = 10000
        names = []
        for combo in itertools.product(*segments):
            names.append(''.join(combo))
            if len(names) >= max_results:
                break

        return names


    # ====================================================================
    # FEATURE: IMPROVED OFFSET CALIBRATION v2
    # ====================================================================

    def _calibrate_offset(self, target_ts):
        """
        Measure network latency to MC API and calculate optimal fire offset.

        v2 improvements:
        - 10 probes instead of 5 (more data points)
        - 12s interval instead of 30s (fits within 120s window)
        - Separate TLS handshake time from pure request time
        - Measure jitter (latency variance) to assess stability
        - Use P95 latency instead of median for safer offset
        - TLS-aware: first probe measures full handshake, rest measure TTFB

        Returns: offset in milliseconds (positive = fire this many ms EARLIER)
        """
        if not self.offset_calibration_enabled:
            return 0

        num_probes = 10
        probe_interval = 12  # seconds between probes (10 * 12 = 120s total)
        calibration_start = target_ts - (num_probes * probe_interval) - 10  # buffer

        self.log_signal.emit(f"[📐] AUTO-OFFSET CALIBRATION v2: ENABLED")
        self.log_signal.emit(
            f"[📐] {num_probes} probes × {probe_interval}s apart, "
            f"TLS handshake + TTFB measurement"
        )
        self.log_signal.emit("[📐] Harmless GET requests — NOT snipe attempts")

        # Wait until calibration start time
        while not self.stop_event.is_set():
            now = time.time()
            delta = calibration_start - now
            if delta <= 0:
                break
            elif delta > 5:
                time.sleep(5)
            else:
                time.sleep(0.5)

        if self.stop_event.is_set():
            return 0

        self.log_signal.emit(f"[📐] 🔬 CALIBRATION PHASE STARTED")

        latencies = []       # Total round-trip latency (ms)
        tls_times = []       # TLS handshake time (ms)
        ttfb_times = []      # Time to first byte after handshake (ms)

        probe_url = "https://api.minecraftservices.com/minecraft/profile"
        token = self.tokens[0]

        # Keep a persistent client across probes to measure warm vs cold
        client = httpx.Client(
            transport=httpx.HTTPTransport(pool=connection_pool),
            http2=True,
            timeout=httpx.Timeout(5.0, connect=3.0),
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
                "User-Agent": get_random_user_agent(),
            },
        )

        try:
            for i in range(num_probes):
                if self.stop_event.is_set():
                    break

                remaining = int(target_ts - time.time())
                self.log_signal.emit(f"[📐] Probe {i+1}/{num_probes} — {remaining}s until drop")

                try:
                    # High-precision timing with perf_counter
                    t_start = time.perf_counter()

                    # Measure DNS + TLS handshake separately on first probe
                    if i == 0:
                        # Cold connection — full handshake
                        r = client.get(probe_url, timeout=4.0)
                        t_after_request = time.perf_counter()
                        total_ms = (t_after_request - t_start) * 1000
                        tls_times.append(total_ms)  # First probe ≈ full handshake
                    else:
                        # Warm connection — reused HTTP/2 stream
                        r = client.get(probe_url, timeout=4.0)
                        t_after_request = time.perf_counter()
                        total_ms = (t_after_request - t_start) * 1000

                    latencies.append(total_ms)
                    ttfb_times.append(total_ms)

                    self.log_signal.emit(
                        f"[📐]   → {r.status_code} in {total_ms:.1f}ms"
                        f"{' (cold TLS)' if i == 0 else ' (warm)'}"
                    )

                except httpx.TimeoutException:
                    self.log_signal.emit("[📐]   → TIMEOUT")
                except Exception as e:
                    self.log_signal.emit(f"[📐]   → ERROR: {str(e)[:40]}")

                # Wait between probes
                if i < num_probes - 1 and not self.stop_event.is_set():
                    for _ in range(probe_interval * 10):
                        if self.stop_event.is_set():
                            break
                        time.sleep(0.1)

        finally:
            client.close()

        if len(latencies) < 3:
            self.log_signal.emit(
                f"[📐] ⚠️  Insufficient probe data ({len(latencies)}/{num_probes}) — skipping"
            )
            return 0

        # === Statistical analysis ===
        sorted_lat = sorted(latencies)
        n = len(sorted_lat)

        median_ms = sorted_lat[n // 2]
        p95_idx = min(int(n * 0.95), n - 1)
        p95_ms = sorted_lat[p95_idx]
        min_ms = sorted_lat[0]
        max_ms = sorted_lat[-1]
        avg_ms = sum(latencies) / n

        # Jitter = standard deviation of latencies
        variance = sum((x - avg_ms) ** 2 for x in latencies) / n
        jitter_ms = variance ** 0.5

        # Cold TLS overhead (first probe - average warm probe)
        tls_overhead = 0
        if tls_times and len(ttfb_times) > 1:
            warm_avg = sum(ttfb_times[1:]) / len(ttfb_times[1:])
            tls_overhead = tls_times[0] - warm_avg

        # Use P95 for offset (safer than median — accounts for latency spikes)
        # Add half the jitter as buffer
        # Subtract server processing time
        SERVER_PROCESSING_MS = 100
        offset_ms = p95_ms + (jitter_ms * 0.5) - SERVER_PROCESSING_MS
        offset_ms = max(0, offset_ms)  # Clamp to 0

        self.calibrated_offset_ms = offset_ms

        self.log_signal.emit(f"\n[📐] === CALIBRATION RESULTS v2 ===")
        self.log_signal.emit(f"[📐] Probes: {n}/{num_probes} successful")
        self.log_signal.emit(
            f"[📐] Range: {min_ms:.1f}ms — {max_ms:.1f}ms"
        )
        self.log_signal.emit(f"[📐] Avg: {avg_ms:.1f}ms | Median: {median_ms:.1f}ms | P95: {p95_ms:.1f}ms")
        self.log_signal.emit(f"[📐] Jitter (σ): {jitter_ms:.1f}ms")
        if tls_overhead > 0:
            self.log_signal.emit(f"[📐] TLS overhead: {tls_overhead:.1f}ms (cold vs warm)")
        self.log_signal.emit(f"[📐] Server processing: -{SERVER_PROCESSING_MS}ms")
        self.log_signal.emit(
            f"[📐] Calibrated offset: +{offset_ms:.1f}ms (fire EARLIER)"
        )
        self.log_signal.emit(f"[📐] ============================\n")

        return offset_ms

    def run(self):
        """Main sniper logic - supports multi-name mode"""
        # === PHASE 3: LOAD ACCOUNT HISTORY ===
        self._load_account_history()
        
        # === FEATURE #7: MULTI-NAME MODE ===
        if len(self.names) > 1:
            self.log_signal.emit(f"[*] Starting multi-name sniper: {len(self.names)} name(s)")
            self.log_signal.emit(f"[*] Mode: {self.multi_name_mode}, Stagger: {self.stagger_seconds}s")
            self._run_multi_name_mode()
        else:
            # Single name mode (backward compatible)
            self.log_signal.emit(f"[*] Starting sniper for: {self.names[0]}")
            self.log_signal.emit(f"[*] Mode: {self.timing_mode}, Tokens: {len(self.tokens)}, Threads: {self.threads_count}")
            
            # Pre-resolve DNS
            self.log_signal.emit("[*] Pre-resolving DNS...")
            dns_success, dns_result = pre_resolve_dns()
            if dns_success:
                self.log_signal.emit(f"[*] DNS resolved: {MINECRAFT_API_HOST} → {dns_result}")
            else:
                self.log_signal.emit(f"[!] DNS warning: {dns_result}")
            
            # Validate tokens
            self.log_signal.emit("[*] Validating tokens...")
            valid_tokens = self._validate_tokens()
            if len(valid_tokens) == 0:
                self.log_signal.emit("[ERROR] No valid tokens found!")
                return
            self.tokens = valid_tokens
            self.log_signal.emit(f"[*] {len(valid_tokens)} valid tokens ready")

            # === FEATURE: NTP CLOCK SYNC CHECK ===
            if self.ntp_sync_enabled:
                ntp_result = self._ntp_sync_check()

            # === FEATURE: MOJANG API STATUS CHECK ===
            status_result = self._check_mojang_status()

            # === QUICK WIN: PRE-SNIPE TOKEN VALIDATION ===
            if self.pre_validate_tokens:
                valid_tokens = self._pre_validate_tokens(self.tokens)
                if len(valid_tokens) == 0:
                    self.log_signal.emit("[🚨] No valid tokens after pre-validation! Aborting snipe.")
                    return
                self.tokens = valid_tokens
            
            # === QUICK WIN: AUTO-DETECT ACCOUNT TYPE ===
            if self.auto_detect_account_type:
                self._batch_detect_account_types(self.tokens)
            
            # === QUICK WIN: DRY-RUN MODE BANNER ===
            if self.dry_run:
                self.log_signal.emit("=" * 60)
                self.log_signal.emit("[🧪] DRY-RUN / SIMULATION MODE ENABLED")
                self.log_signal.emit("[🧪] No actual name change requests will be sent!")
                self.log_signal.emit("[🧪] This tests timing, token validity, and network conditions.")
                self.log_signal.emit("=" * 60)
            
            # Pre-warm connections
            self.log_signal.emit("[*] Pre-warming HTTP/2 connections...")
            self._warm_connections()
            self.log_signal.emit("[✓] Connections warmed and ready!")
            
            if self.timing_mode == "exact":
                self._exact_timing_snipe()
            else:
                self._windowed_timing_snipe()
            
            # Stop stats emitter
            self._stop_stats_emitter()
            
            # === QUICK WIN: DRY-RUN SUMMARY ===
            if self.dry_run:
                self.log_signal.emit("=" * 60)
                self.log_signal.emit("[🧪] DRY-RUN SUMMARY")
                self.log_signal.emit(f"  Simulated fires: {self.dry_run_stats['simulated_fires']}")
                self.log_signal.emit(f"  Simulated successes: {self.dry_run_stats['simulated_success']}")
                self.log_signal.emit(f"  Simulated failures: {self.dry_run_stats['simulated_failures']}")
                self.log_signal.emit(f"  No actual name change requests were sent!")
                self.log_signal.emit("=" * 60)
    
    def _run_multi_name_mode(self):
        """Handle multiple usernames with staggered/sequential/parallel execution"""
        self.log_signal.emit(f"[*] Multi-name queue: {', '.join(self.names)}")
        
        for idx, name in enumerate(self.names):
            if self.stop_event.is_set():
                break

            # Queue callback: mark as sniping
            if self.queue_callback:
                self.queue_callback(idx, "sniping")

            self.log_signal.emit(f"\n{'='*60}")
            self.log_signal.emit(f"[*] Processing name {idx+1}/{len(self.names)}: {name}")
            self.log_signal.emit(f"{'='*60}\n")

            # Set current name
            self.name = name

            # Queue mode: use per-name drop time
            if self.queue_mode and isinstance(self.drop_time_utc, list):
                self.drop_time_utc = self.drop_time_utc[idx]

            # === FEATURE #9: AUTO-REFRESH TOKENS=***
            if self.auto_refresh_tokens:
                self.log_signal.emit(f"🔄 Auto-refresh: Authenticating new tokens for {name}")
                self._auto_authenticate_tokens()

            # Pre-resolve DNS
            dns_success, dns_result = pre_resolve_dns()
            if dns_success:
                self.log_signal.emit(f"[*] DNS resolved: {MINECRAFT_API_HOST} → {dns_result}")

            # Validate tokens
            valid_tokens = self._validate_tokens()
            if len(valid_tokens) == 0:
                self.log_signal.emit(f"[ERROR] No valid tokens for {name}!")
                self.name_results[name] = {"success": False, "reason": "No valid tokens"}
                if self.queue_callback:
                    self.queue_callback(idx, "failed")
                continue
            self.tokens = valid_tokens

            # Pre-warm connections
            self._warm_connections()

            # Snipe this name
            if self.timing_mode == "exact":
                self._exact_timing_snipe()
            else:
                self._windowed_timing_snipe()

            # Update queue status based on result
            if self.queue_callback and name in self.name_results:
                if self.name_results[name].get("success"):
                    self.queue_callback(idx, "success")
                else:
                    self.queue_callback(idx, "failed")

            # Stagger delay
            if self.multi_name_mode == "staggered" and idx < len(self.names) - 1:
                self.log_signal.emit(f"[*] Stagger delay: {self.stagger_seconds}s before next name")
                for _ in range(self.stagger_seconds):
                    if self.stop_event.is_set():
                        break
                    time.sleep(1)
        
        # Print summary
        if len(self.names) > 1:
            self.log_signal.emit(f"\n{'='*60}")
            self.log_signal.emit(f"📊 MULTI-NAME SUMMARY:")
            for name, result in self.name_results.items():
                status = "✅ SUCCESS" if result.get("success") else "❌ FAILED"
                self.log_signal.emit(f"  {name}: {status}")
            self.log_signal.emit(f"{'='*60}\n")
    
    def _validate_tokens(self):
        """Check which tokens are valid before drop time, re-auth from cache if expired"""
        valid_tokens = []
        invalid_tokens = []
        test_url = "https://api.minecraftservices.com/minecraft/profile"
        
        # Load cache for potential re-auth
        cache_path = Path.home() / ".hermes" / "minecraft-sniper" / "tokens.json"
        cache = {}
        if cache_path.exists():
            try:
                cache = json.loads(cache_path.read_text())
            except Exception:
                pass

        for i, token in enumerate(self.tokens):
            if self.stop_event.is_set():
                break
            
            client = httpx.Client(
                transport=httpx.HTTPTransport(pool=connection_pool),
                http2=True,
                timeout=httpx.Timeout(3.0, connect=1.5),
                headers={
                    "Authorization": f"Bearer {token}",
                    "Content-Type": "application/json"
                },
            )
            
            try:
                r = client.get(test_url, timeout=2)
                if r.status_code in [200, 201, 204]:
                    valid_tokens.append(token)
                    self.log_signal.emit(f"[✓] Token {i+1} validated")
                else:
                    self.log_signal.emit(f"[✗] Token {i+1} invalid (status {r.status_code}) — trying re-auth...")
                    invalid_tokens.append((i, token))
            except Exception as e:
                self.log_signal.emit(f"[✗] Token {i+1} failed: {str(e)[:30]} — trying re-auth...")
                invalid_tokens.append((i, token))
            finally:
                client.close()
            
            time.sleep(0.1)
        
        # Try to re-auth invalid tokens from cached credentials
        if invalid_tokens and cache:
            # Bearer tokens don't expire (they're session cookies from minecraft.net)
            # No re-auth needed — just report invalid tokens
            if len(invalid_tokens):
                self.log_signal.emit(f"⚠️ {len(invalid_tokens)} tokens were invalid — they may have been logged out on minecraft.net")
                self.log_signal.emit("   → Refresh by logging into minecraft.net and copying the clienttoken cookie again")

            # Save updated cache
            if cache:
                cache_path.write_text(json.dumps(cache, indent=2))
        
        return valid_tokens
    
    def _load_account_history(self):
        """Load account history from disk for cooldown-aware token selection"""
        try:
            with open(ACCOUNT_TRACKING_FILE, 'r') as f:
                self.account_history = json.load(f)
            self.log_signal.emit(f"[*] Loaded account history: {len(self.account_history.get('accounts', {}))} accounts tracked")
        except (FileNotFoundError, json.JSONDecodeError):
            self.account_history = {"accounts": {}}
    
    def _get_smart_token(self, tokens):
        """
        Select the best token using round-robin + cooldown awareness + success weighting.
        Returns the best token from the pool.
        """
        usable, skipped, reasons = get_usable_tokens(tokens, self.account_history)
        
        if skipped:
            reason_parts = []
            for k, v in reasons.items():
                if v:
                    reason_parts.append(f"{v} {k}")
            self.log_signal.emit(f"[*] Filtered {skipped} unusable tokens ({', '.join(reason_parts)})")
        
        if not usable:
            return None
        
        # Round-robin selection with success weighting
        # Prefer tokens that haven't been used yet this cycle
        if len(usable) > 1:
            # Rotate: start from next position
            self.token_rotation_index = self.token_rotation_index % len(usable)
            token = usable[self.token_rotation_index]
            self.token_rotation_index += 1
        else:
            token = usable[0]
        
        return token
    
    def _warm_connections(self):
        """Pre-warm HTTP/2 connections for instant firing - REUSE clients for sniping"""
        warm_url = "https://api.minecraftservices.com/minecraft/profile"
        
        # Warm the MAIN thread-local client that will be used for sniping
        # This ensures the actual sniping requests use pre-warmed connections
        client = self._get_thread_client()
        
        for i in range(min(3, len(self.tokens))):
            if self.stop_event.is_set():
                break
            
            token = self.tokens[i]
            client.headers["Authorization"] = f"Bearer {token}"
            
            try:
                client.get(warm_url, timeout=2)
                time.sleep(0.05)
            except:
                pass
            # DO NOT close - keep this client alive for sniping
        
        self.connection_warmed = True
    
    def _token_warmup_request(self, token, username=None):
        """
        Send a harmless profile request to keep token fresh in Minecraft's cache.
        Uses thread-local client for connection reuse.
        Returns: (success: bool, status_code: int, response_time_ms: float)
        """
        import random
        endpoint_type = random.choice(["profile", "profile", "profile", "sketch"])
        
        if endpoint_type == "profile":
            url = "https://api.minecraftservices.com/minecraft/profile"
        else:
            url = "https://api.minecraftservices.com/minecraft/profile/skin"
        
        # Use thread-local client for connection reuse
        client = self._get_thread_client(token)
        start_time = time.time()
        try:
            r = client.get(url, timeout=1.5)
            response_time = (time.time() - start_time) * 1000
            if r.status_code in [200, 201, 204]:
                return True, r.status_code, response_time
            else:
                return False, r.status_code, response_time
        except httpx.TimeoutException:
            return False, 0, (time.time() - start_time) * 1000
        except Exception as e:
            return False, 0, (time.time() - start_time) * 1000
        # DO NOT close - keep connection alive for sniping
    
    def _run_token_warmup_phase(self, target_ts):
        """
        FEATURE #1: Token Warm-Up System
        Run continuous harmless requests starting warmup_duration seconds before drop time.
        This keeps tokens "fresh" in Minecraft's session cache, reducing first-request latency.
        """
        if not self.warmup_enabled:
            self.log_signal.emit("[*] Token warm-up: DISABLED")
            return
        
        warmup_start_ts = target_ts - self.warmup_duration
        self.log_signal.emit(f"[🔥] TOKEN WARM-UP SYSTEM: ENABLED")
        self.log_signal.emit(f"[🔥] Duration: {self.warmup_duration}s before drop (starts in {self.warmup_duration}s)")
        self.log_signal.emit(f"[🔥] Strategy: Continuous profile requests to keep cache fresh")
        
        # Wait until warmup start time
        wait_start = time.time()
        while not self.stop_event.is_set():
            now = time.time()
            delta_to_warmup = warmup_start_ts - now
            
            if delta_to_warmup <= 0:
                break
            elif delta_to_warmup > 5:
                time.sleep(5)
            else:
                time.sleep(0.5)
            
            # Update progress bar during wait
            delta_to_drop = target_ts - now
            if delta_to_drop > 0:
                progress = min(99, int((1 - delta_to_drop / (target_ts - wait_start)) * 100))
                self.progress_signal.emit(max(0, progress))
        
        if self.stop_event.is_set():
            return
        
        # === WARM-UP PHASE STARTED ===
        self.log_signal.emit(f"[🔥] 🔥🔥🔥 TOKEN WARM-UP PHASE STARTED! 🔥🔥🔥")
        self.log_signal.emit(f"[🔥] Dropping in {self.warmup_duration}s - firing harmless requests now...")
        self.log_signal.emit(f"[🔥] Rate-limit protection: auto-backoff on 429/403 responses")
        
        warmup_cycle = 0
        last_status_update = time.time()
        
        # Rate-limit backoff state (shared across tokens)
        rate_limit_window = collections.deque(maxlen=10)  # Track last 10 responses
        backoff_multiplier = 1.0  # Current backoff multiplier
        consecutive_rate_limits = 0
        
        # Per-token cooldown tracking (seconds since last request per token)
        token_last_request = {}
        
        # Continue until drop time
        while not self.stop_event.is_set():
            now = time.time()
            if now >= target_ts:
                break
            
            # Fire warmup requests with all tokens
            import random
            for token in self.tokens:
                if self.stop_event.is_set() or now >= target_ts:
                    break
                
                # Enforce per-token cooldown (minimum 5s between requests to same token)
                token_key = token[:16]
                if token_key in token_last_request:
                    time_since = now - token_last_request[token_key]
                    if time_since < 5.0:
                        continue  # Skip this token, not enough time passed
                token_last_request[token_key] = now
                
                success, status, resp_time = self._token_warmup_request(token)
                self.warmup_requests_sent += 1
                rate_limit_window.append(status)
                
                if success:
                    self.warmup_success += 1
                    consecutive_rate_limits = 0  # Reset on success
                else:
                    self.warmup_failures += 1
                    if status in [401]:
                        self.log_signal.emit(f"[⚠️] Token {token_key}... returned 401 - invalid/expired!")
                    elif status in [403, 429]:
                        consecutive_rate_limits += 1
                        self.log_signal.emit(f"[⚠️] Rate-limited ({status})! Consecutive: {consecutive_rate_limits}")
                
                # Tiny delay between tokens to avoid thundering herd
                time.sleep(0.02)
            
            warmup_cycle += 1
            
            # Status update every 10 seconds
            if time.time() - last_status_update > 10:
                remaining = int(target_ts - time.time())
                success_rate = self.warmup_success / max(1, self.warmup_requests_sent) * 100
                self.log_signal.emit(f"[🔥] Warm-up: {self.warmup_requests_sent} reqs, {self.warmup_success}✓ {self.warmup_failures}✗ ({success_rate:.0f}%) | {remaining}s to drop | backoff x{backoff_multiplier:.1f}")
                last_status_update = time.time()
            
            # Adaptive wait time based on rate-limit detection
            base_wait = random.uniform(2.0, 4.0)
            
            # Exponential backoff on consecutive rate limits
            if consecutive_rate_limits >= 3:
                backoff_multiplier = min(8.0, 2 ** consecutive_rate_limits)
                self.log_signal.emit(f"[🔥] Heavy rate-limiting detected! Backing off x{backoff_multiplier:.1f} (wait ~{backoff_multiplier * base_wait:.0f}s)")
            elif consecutive_rate_limits >= 1:
                backoff_multiplier = min(4.0, 1.5 ** consecutive_rate_limits)
            else:
                backoff_multiplier = max(1.0, backoff_multiplier * 0.8)  # Gradually reduce backoff
            
            wait_time = base_wait * backoff_multiplier
            time.sleep(min(wait_time, target_ts - time.time()))
        
        # === WARM-UP PHASE COMPLETE ===
        self.log_signal.emit(f"[🔥] ✅ TOKEN WARM-UP COMPLETE!")
        self.log_signal.emit(f"[🔥] Stats: {self.warmup_requests_sent} requests | {self.warmup_success} success | {self.warmup_failures} failed")
        
        if self.warmup_failures > self.warmup_success * 0.3:  # More than 30% failure rate
            self.log_signal.emit(f"[⚠️] High warm-up failure rate detected! Some tokens may be cold or invalid.")
        else:
            self.log_signal.emit(f"[✓] All tokens warmed and ready for snipe!")
    
    # ============================================================================
    # FEATURE #17: AUTO-OFFSET CALIBRATION
    # ============================================================================
    # Measures actual latency to MC API from this machine and adjusts fire time
    # to compensate. Runs 120s before drop, fires 5 harmless GET requests
    # 30s apart to build a latency profile. NOT spammy — only 5 requests total.
    # ============================================================================

    def _exact_timing_snipe(self):
        """Fire at exact drop time, then continue firing every 2-5 seconds"""
        try:
            target = datetime.fromisoformat(self.drop_time_utc.replace("Z", "+00:00"))
            target_ts = target.timestamp()
            
            self.log_signal.emit(f"[*] Waiting for exact drop time: {self.drop_time_utc} UTC")
            
            # === RUN AUTO-OFFSET CALIBRATION (FEATURE #17) ===
            # Runs 120s before drop, 5 probes at 30s spacing
            self._calibrate_offset(target_ts)
            
            # Adjust target time by calibrated offset
            adjusted_target_ts = target_ts
            if self.calibrated_offset_ms > 0:
                adjusted_target_ts = target_ts - (self.calibrated_offset_ms / 1000.0)
                self.log_signal.emit(f"[⏱️]  Adjusted fire time: {self.calibrated_offset_ms:.1f}ms EARLIER than nominal drop")
            
            # === RUN TOKEN WARM-UP PHASE ===
            self._run_token_warmup_phase(adjusted_target_ts)
            
            if self.stop_event.is_set():
                self.log_signal.emit("[!] Sniper stopped by user")
                return
            
            # === PHASE 4: START REAL-TIME STATS EMITTER ===
            self._start_stats_emitter(adjusted_target_ts)
            
            # === FEATURE #5: START NAMEMC MONITOR THREAD ===
            namemc_thread = threading.Thread(
                target=self._run_namemc_monitor,
                args=(self.name, adjusted_target_ts),
                daemon=True,
            )
            namemc_thread.start()
            self.log_signal.emit(f"[📡] NAMEMC availability monitor started")
            
            self.progress_signal.emit(100)
            if self.calibrated_offset_ms > 0:
                self.log_signal.emit(f"[!] 🎯🎯🎯 ADJUSTED FIRE TIME ({self.calibrated_offset_ms:.0f}ms early)! FIRING NOW! 🎯🎯🎯")
            else:
                self.log_signal.emit(f"[!] 🎯🎯🎯 EXACT DROP TIME! FIRING NOW! 🎯🎯🎯")
            
            # === FEATURE #2: NUCLEAR MODE - FIRST BURST ===
            # Fire immediately at exact time with ZERO DELAY (nuclear mode)
            self.log_signal.emit(f"[!] ⚛️ ACTIVATING NUCLEAR MODE FOR INITIAL BURST!")
            self._fire_all_tokens_burst(nuclear_mode=True)
            
            # Check if we succeeded on the first nuclear burst
            if self.success_event.is_set():
                return
            
            self.log_signal.emit(f"[!] ⚠️ Nuclear burst complete, switching to controlled bursts...")
            self.log_signal.emit(f"[!] ⚠️ Continuing to fire every 2-5 seconds until success...")
            self.log_signal.emit(f"[!] ⚠️ Press STOP to quit anytime!")
            
            # Continue firing periodically (2-5 seconds) after the exact time
            import random
            burst_count = 1
            
            while not self.stop_event.is_set():
                if self.success_event.is_set():
                    self.log_signal.emit("[✓] Success detected! Stopping exact mode")
                    break
                
                burst_count += 1
                wait_time = random.uniform(2.0, 5.0)
                
                self.log_signal.emit(f"[*] ⏱️ Waiting {wait_time:.1f}s before next burst...")
                
                # Check stop event during wait
                for _ in range(int(wait_time * 10)):
                    if self.stop_event.is_set() or self.success_event.is_set():
                        break
                    time.sleep(0.1)
                
                if self.stop_event.is_set() or self.success_event.is_set():
                    break
                
                self.log_signal.emit(f"[!] 🔥 Follow-up Burst #{burst_count} firing...")
                self._fire_all_tokens_burst()
            
            if not self.success_event.is_set():
                self.log_signal.emit("[!] Sniper stopped by user")
            
        except Exception as e:
            self.log_signal.emit(f"[ERROR] Timing error: {e}")
    
    def _windowed_timing_snipe(self):
        """Fire requests continuously every 1-3 seconds starting at drop time (NO END)"""
        try:
            target = datetime.fromisoformat(self.drop_time_utc.replace("Z", "+00:00"))
            target_ts = target.timestamp()
            
            self.log_signal.emit(f"[*] Waiting for drop time: {self.drop_time_utc} UTC")
            
            # === RUN AUTO-OFFSET CALIBRATION (FEATURE #17) ===
            self._calibrate_offset(target_ts)
            
            # Adjust target time
            adjusted_target_ts = target_ts
            if self.calibrated_offset_ms > 0:
                adjusted_target_ts = target_ts - (self.calibrated_offset_ms / 1000.0)
                self.log_signal.emit(f"[⏱️]  Adjusted fire time: {self.calibrated_offset_ms:.1f}ms EARLIER than nominal drop")
            
            # === PHASE 4: START REAL-TIME STATS EMITTER ===
            self._start_stats_emitter(adjusted_target_ts)
            
            # === FEATURE #5: START NAMEMC MONITOR THREAD ===
            namemc_thread = threading.Thread(
                target=self._run_namemc_monitor,
                args=(self.name, adjusted_target_ts),
                daemon=True,
            )
            namemc_thread.start()
            self.log_signal.emit(f"[📡] NAMEMC availability monitor started")
            
            # Wait until the adjusted drop time
            while not self.stop_event.is_set():
                now = time.time()
                delta = adjusted_target_ts - now
                
                if delta <= 0:
                    break
                elif delta > 1:
                    time.sleep(delta - 1)
                else:
                    time.sleep(0.1)
            
            if self.stop_event.is_set():
                self.log_signal.emit("[!] Sniper stopped by user")
                return
            
            self.log_signal.emit(f"[!] 🎯🎯🎯 DROP TIME REACHED! Firing continuously every 1-3 seconds...")
            self.log_signal.emit(f"[!] ⚠️ This will run until success or manual stop!")
            
            # Fire continuously every 1-3 seconds (NO TIME LIMIT)
            import random
            burst_count = 0
            
            while not self.stop_event.is_set():
                if self.success_event.is_set():
                    self.log_signal.emit("[✓] Success detected! Stopping windowed mode")
                    break
                
                burst_count += 1
                
                # === FEATURE #2: NUCLEAR MODE - FIRST BURST ONLY ===
                if burst_count == 1:
                    self.log_signal.emit(f"[!] ⚛️ ACTIVATING NUCLEAR MODE FOR INITIAL BURST!")
                    self._fire_all_tokens_burst(nuclear_mode=True)
                else:
                    self.log_signal.emit(f"[!] 🔥 Burst #{burst_count} firing...")
                    self._fire_all_tokens_burst()
                
                # Adaptive delay based on success rate (1-3 seconds)
                if self.requests_fired > 0:
                    success_rate = self.requests_success / self.requests_fired
                    if success_rate > 0.3:  # High success rate, slow down
                        wait_time = random.uniform(2.0, 3.0)
                    else:
                        wait_time = random.uniform(1.0, 2.5)
                else:
                    wait_time = random.uniform(1.0, 3.0)
                
                self.log_signal.emit(f"[*] ⏱️ Next burst in {wait_time:.1f}s... (Press STOP to quit)")
                
                # Check stop event during wait
                for _ in range(int(wait_time * 10)):
                    if self.stop_event.is_set() or self.success_event.is_set():
                        break
                    time.sleep(0.1)
            
            if not self.success_event.is_set():
                self.log_signal.emit("[!] Sniper stopped by user")
            
        except Exception as e:
            self.log_signal.emit(f"[ERROR] Windowed timing error: {e}")
    
    def _fire_all_tokens_burst(self, nuclear_mode=False):
        """Fire concurrent requests with all tokens in controlled bursts
        
        Args:
            nuclear_mode: If True, fires ALL threads simultaneously with 0ms delay (maximum aggression)
        """
        global success_achieved
        
        # Sort tokens by priority
        tokens_to_use = self.tokens.copy()
        if self.priority_mode == "fresh_first":
            # Assume newer tokens (later in list) are less likely rate-limited
            tokens_to_use = tokens_to_use[::-1]
        
        # === FEATURE #4: FILTER OUT BLACKLISTED TOKENS=***
        active_tokens = [t for t in tokens_to_use if not self._is_token_blacklisted(t)]
        
        # === PHASE 3: FILTER OUT EXPIRED, COOLDOWN, RATE-LIMITED TOKENS ===
        usable, skipped, reasons = get_usable_tokens(active_tokens, self.account_history)
        if skipped:
            reason_parts = []
            for k, v in reasons.items():
                if v:
                    reason_parts.append(f"{v} {k}")
            self.log_signal.emit(f"[*] Skipped {skipped} tokens ({', '.join(reason_parts)})")
        active_tokens = usable
        
        filtered_count = len(tokens_to_use) - len(active_tokens)
        if filtered_count > 0:
            self.log_signal.emit(f"[⚫] Filtering {filtered_count} unusable tokens. Active: {len(active_tokens)}/{len(tokens_to_use)}")
        
        # === IP-LEVEL RATE LIMIT COOLDOWN CHECK ===
        # If multiple tokens got 429'd recently, pause ALL firing to let IP cooldown expire
        self._check_ip_rate_limit_cooldown()
        
        # === FEATURE #5: USE ADAPTIVE THREAD COUNT ===
        threads_to_use = self.adaptive_threads if not nuclear_mode else self.threads_count
        
        # === STAGGERED BURST: limit concurrent tokens per batch to avoid IP-level rate limits ===
        # Firing all tokens at once triggers IP-level 429s. Instead, split into batches of
        # MAX_CONCURRENT_TOKENS with a short delay between batches. Still fast (~100ms gap)
        # but keeps request rate under Minecraft's per-IP threshold.
        MAX_CONCURRENT_TOKENS = 4  # max tokens per batch
        BATCH_DELAY = 0.1  # 100ms between batches
        
        num_batches = math.ceil(len(active_tokens) / MAX_CONCURRENT_TOKENS) if active_tokens else 0
        
        threads_list = []
        burst_size = len(active_tokens) * threads_to_use
        
        if nuclear_mode:
            self.log_signal.emit(f"[!] ⚛️⚛️⚛️ NUCLEAR BURST! {burst_size} requests in {num_batches} batch(es) ⚛️⚛️⚛️")
        else:
            self.log_signal.emit(f"[!] Firing {burst_size} requests across {len(active_tokens)} tokens in {num_batches} batch(es) (Threads: {threads_to_use}, Competition: {self.competition_level})")
        
        if nuclear_mode:
            # === NUCLEAR MODE: BATCHED BURST ===
            # Fire in batches of MAX_CONCURRENT_TOKENS with minimal delay
            for batch_idx in range(num_batches):
                if self.success_event.is_set() or self.stop_event.is_set():
                    break
                # F2: Stop if another thread already won
                if hasattr(self, '_snipe_won') and self._snipe_won.is_set():
                    self.log_signal.emit("[⏹] Stopping — another account already won!")
                    break
                
                batch_start = batch_idx * MAX_CONCURRENT_TOKENS
                batch_end = min(batch_start + MAX_CONCURRENT_TOKENS, len(active_tokens))
                batch_tokens = active_tokens[batch_start:batch_end]
                
                for token in batch_tokens:
                    if self.success_event.is_set() or self.stop_event.is_set():
                        break
                    
                    for j in range(self.threads_count):
                        if self.success_event.is_set():
                            break
                        
                        # === FEATURE #13: APPLY TEMPORAL JITTER ===
                        if self.temporal_jitter_enabled:
                            jitter_ms = random.uniform(-self.jitter_range_ms, self.jitter_range_ms)
                            jitter_sec = jitter_ms / 1000.0
                            time.sleep(max(0, jitter_sec))
                            self.jitter_applied_count += 1
                        
                        t = threading.Thread(
                            target=self._send_request_with_retry,
                            args=(token,),
                            daemon=True
                        )
                        t.start()
                        threads_list.append(t)
                
                # Small delay between batches (not on last batch)
                if batch_idx < num_batches - 1:
                    time.sleep(BATCH_DELAY)
        else:
            # === STANDARD MODE: BATCHED CONTROLLED BURST ===
            for batch_idx in range(num_batches):
                if self.success_event.is_set() or self.stop_event.is_set():
                    break
                # F2: Stop if another thread already won
                if hasattr(self, '_snipe_won') and self._snipe_won.is_set():
                    self.log_signal.emit("[⏹] Stopping — another account already won!")
                    break
                
                batch_start = batch_idx * MAX_CONCURRENT_TOKENS
                batch_end = min(batch_start + MAX_CONCURRENT_TOKENS, len(active_tokens))
                batch_tokens = active_tokens[batch_start:batch_end]
                
                for token in batch_tokens:
                    if self.success_event.is_set() or self.stop_event.is_set():
                        break
                    
                    for j in range(threads_to_use):
                        if self.success_event.is_set():
                            break
                        
                        # === FEATURE #13: APPLY TEMPORAL JITTER ===
                        if self.temporal_jitter_enabled:
                            jitter_ms = random.uniform(-self.jitter_range_ms, self.jitter_range_ms)
                            jitter_sec = jitter_ms / 1000.0
                            time.sleep(max(0, jitter_sec))
                            self.jitter_applied_count += 1
                        
                        t = threading.Thread(
                            target=self._send_request_with_retry,
                            args=(token,)
                        )
                        t.start()
                        threads_list.append(t)
                    
                    # Small stagger between tokens within batch
                    time.sleep(0.01)
                
                # Delay between batches
                if batch_idx < num_batches - 1:
                    time.sleep(BATCH_DELAY)
        
        # === H3: FIRE-AND-FORGET MODE ===
        # In fire-and-forget mode, don't block waiting for thread completion.
        # Threads run in background and update stats via signals.
        if getattr(self, 'fire_and_forget', False):
            self.log_signal.emit(f"[⚡] H3 FIRE-AND-FORGET: {len(threads_list)} threads running in background (non-blocking)")
            # Brief pause to let most threads start, but don't block
            time.sleep(0.2)
        else:
            # Wait for all threads to complete
            for t in threads_list:
                t.join(timeout=5.0)
        
         # === FEATURE #13: LOG JITTER STATS ===
        if self.temporal_jitter_enabled and self.jitter_applied_count > 0:
            self.log_signal.emit(f"[⚡] Temporal jitter applied to {self.jitter_applied_count} requests (±{self.jitter_range_ms}ms)")
        
        # === FEATURE #15: LOG RESPONSE ANALYSIS STATS ===
        if self.response_analysis_enabled:
            self.log_signal.emit(f"[📊] Response analysis: {self.success_count} successes, {self.failure_count} failures detected")
            if self.consecutive_failures > 0:
                self.log_signal.emit(f"[📊] Consecutive failure streak: {self.consecutive_failures}")
        
        # Check if we achieved success
        if self.success_event.is_set():
            self.log_signal.emit(f"[🏆] SUCCESS ACHIEVED! Total requests: {self.requests_fired}")
            self._trigger_success_alerts()
    
    def _send_request_with_retry(self, token):
        """Send request with smart retry logic, fingerprint rotation, and Retry-After detection"""
        global success_achieved

        # === H1: PRIMARY ENDPOINT + POST-SUCCCESS VERIFICATION ===
        # Only one endpoint actually claims names: api.minecraftservices.com
        url = f"https://api.minecraftservices.com/minecraft/profile/name/{self.name}"
        # Mojang profile lookup (read-only — used to verify claim after success)
        mojang_url = f"https://api.mojang.com/users/profiles/minecraft/{self.name}"
        endpoint = "minecraft/profile/name"
        
        # === FEATURE: THREAD-LOCAL CLIENT REUSE ===
        # Reuse HTTP client across requests in the same thread for connection pooling
        client = self._get_thread_client(token)
        
        # === FEATURE #3: FAST-FAIL TIMEOUT ===
        # Fast-fail threshold: if request takes > 3 seconds, abort immediately
        FAST_FAIL_TIMEOUT = 3.0  # seconds
        
        max_retries = 3
        retry_delays = [0.1, 0.3, 0.6]  # Exponential backoff
        
        for attempt in range(max_retries):
            if self.success_event.is_set() or self.stop_event.is_set():
                return
            
            # === FEATURE #6: CHECK ENDPOINT COOLDOWN ===
            # Respect Retry-After headers from previous 429 responses
            in_cooldown, remaining, reason = is_endpoint_cooldown(endpoint)
            if in_cooldown:
                self.log_signal.emit(f"[🚫] {endpoint} in cooldown ({remaining}s remaining) - {reason}")
                return  # Abort this request, endpoint is on cooldown
            
            # === FEATURE #6: ROTATE FINGERPRINT ===
            # Get a fresh User-Agent for this request to avoid fingerprint-based rate limiting
            if self.fingerprint_rotation:
                self.current_user_agent = get_random_user_agent()
                self.fingerprint_rotations += 1
                # Update client headers with new UA
                client.headers["User-Agent"] = self.current_user_agent
            
            # Update auth header for this token
            client.headers["Authorization"] = f"Bearer {token}"
            
            timestamp = datetime.now(timezone.utc).isoformat()
            
            try:
                # === FEATURE #3: FAST-FAIL WITH TIMER ===
                start_time = time.time()
                
                # === QUICK WIN: DRY-RUN MODE ===
                if self.dry_run:
                    # Simulate request timing without actually sending the PUT
                    import random
                    simulated_latency = random.uniform(0.05, 0.3)  # Simulate 50-300ms network latency
                    time.sleep(simulated_latency)
                    elapsed = time.time() - start_time
                    self.dry_run_stats["simulated_fires"] += 1
                    
                    # Simulate outcome based on name availability check
                    simulated_success = self.namemc_available == True  # Only "succeed" if name is available
                    if simulated_success:
                        self.dry_run_stats["simulated_success"] += 1
                        self.log_signal.emit(
                            f"[🧪 DRY-RUN] Simulated SUCCESS ({elapsed:.3f}s) - Token {hash(token) % 10000} "
                            f"| Would have claimed {self.name}"
                        )
                    else:
                        self.dry_run_stats["simulated_failures"] += 1
                        self.log_signal.emit(
                            f"[🧪 DRY-RUN] Simulated FAIL ({elapsed:.3f}s) - Token {hash(token) % 10000} "
                            f"| Name {self.name} appears taken"
                        )
                    
                    self.requests_fired += 1
                    if simulated_success:
                        self.requests_success += 1
                    else:
                        self.requests_failed += 1
                    continue  # Skip to next iteration, don't process real response
                
                r = client.put(url, json={})
                elapsed = time.time() - start_time
                
                # Fast-fail check: if request took too long, log and abort
                if elapsed > FAST_FAIL_TIMEOUT:
                    self.requests_failed += 1
                    self.log_signal.emit(f"[⚠️] FAST-FAIL: Request took {elapsed:.2f}s (> {FAST_FAIL_TIMEOUT}s) - Token {hash(token) % 10000}")
                    return  # Abort this token immediately
                
                try:
                    body = r.json()
                except:
                    body = r.text
                
                # Update stats
                self.requests_fired += 1
                
                # Check for success
                if r.status_code in [200, 201, 204]:
                    self.requests_success += 1
                    self.log_signal.emit(f"[🏆🏆🏆 SUCCESS! {r.status_code} ({elapsed:.3f}s) - Token {hash(token) % 10000}]")
                    
                    # === H1: Verify claim on Mojang (read-only confirmation) ===
                    try:
                        mojang_r = client.get(f"https://api.mojang.com/users/profiles/minecraft/{self.name}", timeout=3)
                        if mojang_r.status_code == 200:
                            mojang_data = mojang_r.json()
                            self.log_signal.emit(f"[✅] Mojang confirms: {self.name} → UUID {mojang_data.get('id', 'unknown')}")
                        else:
                            self.log_signal.emit(f"[⚠️] Mojang check returned {mojang_r.status_code} (may need a moment to propagate)")
                    except Exception as e:
                        self.log_signal.emit(f"[⚠️] Mojang verification failed: {str(e)[:50]}")
                    self.success_event.set()
                    success_achieved.set()
                    # F2: Stop all other threads — we won
                    if hasattr(self, '_snipe_won'):
                        self._snipe_won.set()

                    # === FEATURE #15: COUNT SUCCESS ===
                    self.success_count += 1

                    # === FEATURE #5: TRACK OUTCOME ===
                    self._track_request_outcome("SUCCESS", elapsed)

                   # === Record name change for cooldown tracking ===
                    self._record_name_change(token, self.name, True)

                    # === PHASE 5: Record in history DB ===
                    self._record_snipe_history("success", r.status_code, token)

                    # === PHASE 5: Discord webhook alert ===
                    self._send_webhook_alert("success", self.name, r.status_code, token)

                    # === FEATURE: Auto skin upload on success ===
                    self._upload_skin_on_success(token)

                    # === FEATURE: Desktop notification on success ===
                    self._send_desktop_notification(
                        f"🏆 Sniped: {self.name}",
                        "Username successfully claimed!"
                    )

                    return

                # Check for duplicate (you already own it!)
                if r.status_code == 403 and isinstance(body, dict) and body.get('details', {}).get('status') == 'DUPLICATE':
                    self.requests_success += 1
                    self.log_signal.emit(f"[🏆 ALREADY OWNED! 403 DUPLICATE - You have the name!]")
                    self.success_event.set()
                    success_achieved.set()

                    # === FEATURE #15: COUNT SUCCESS ===
                    self.success_count += 1

                    # === FEATURE #5: TRACK OUTCOME ===
                    self._track_request_outcome("DUPLICATE", elapsed)

                    # === PHASE 5: Record in history DB ===
                    self._record_snipe_history("owned", r.status_code, token)
                    
                    return
                
                # === FEATURE #6: HANDLE 429 WITH RETRY-AFTER DETECTION ===
                if r.status_code == 429:
                    # F3: Rotate proxy on 429
                    if proxy_pool.total_count > 0:
                        rotate_proxy_on_429()
                        self.log_signal.emit(f"[🔄] Proxy rotated on 429 — {proxy_pool.available_count}/{proxy_pool.total_count} available")

                    # === Track this 429 for IP-level detection ===
                    self.ip_rate_limit_window.append(time.time())
                    # Check for Retry-After header (seconds until we can retry)
                    retry_after = r.headers.get("Retry-After")
                    
                    # === FEATURE #8: MARK TOKEN AS RATE LIMITED ===
                    if self.token_rotation_enabled:
                        set_token_rate_limit(token, self.token_rotation_duration)
                        self.rate_limited_tokens.add(token)
                        self.token_rotation_count += 1
                        usable, skipped = get_usable_tokens(self.tokens)
                        self.log_signal.emit(f"[🔄] Token rate limited - Rotated #{self.token_rotation_count} | Usable: {len(usable)}/{len(self.tokens)} | Token {hash(token) % 10000}")
                    
                    if retry_after:
                        try:
                            retry_after_seconds = int(retry_after)
                            # Set endpoint cooldown to respect the server's rate limit
                            set_endpoint_cooldown(endpoint, retry_after_seconds, "429")
                            self.log_signal.emit(f"[🚫] RATE LIMITED (429) - Cooldown: {retry_after_seconds}s - {endpoint}")
                            
                            # Track this as a rate limit event for adaptive throttling
                            self._track_request_outcome("RATE_LIMITED", elapsed)
                            
                            return  # Abort all requests to this endpoint until cooldown expires
                        except ValueError:
                            # Retry-After might be a date, fall through to standard retry
                            pass
                    
                    # No Retry-After or couldn't parse it, use standard retry logic
                    if attempt < max_retries - 1:
                        wait_time = retry_delays[attempt]
                        self.log_signal.emit(f"[⏱️] Rate limited (429), retrying in {wait_time}s...")
                        time.sleep(wait_time)
                        continue
                    
                    # Max retries exceeded
                    self.requests_failed += 1
                    self._track_request_outcome("FAILURE", elapsed)
                    self.log_signal.emit(f"[{timestamp}] ❌ 429 MAX RETRIES - Token {hash(token) % 10000}")
                    return
                
                # === AUTO-REFRESH: Try refreshing expired token on 401 ===
                if r.status_code == 401 and attempt == 0 and not self.fire_and_forget:
                    try:
                        # Look up email:password in cache (stored by BulkAuthWorker)
                        email_pw = None
                        for label, cache_data in self._token_cache.items():
                            if isinstance(cache_data, dict) and cache_data.get("token") == token:
                                email_pw = (cache_data.get("email"), cache_data.get("password"))
                                break
                        
                        if email_pw and email_pw[0] and email_pw[1]:
                            self.log_signal.emit(f"  🔄 Token expired - re-auth for Token {token_hash}...")
                            new_token = refresh_minecraft_token(email_pw[0], email_pw[1], lambda msg: self.log_signal.emit(f"    {msg}"))
                            if new_token:
                                for k, v in list(self.tokens.items()):
                                    if v == token:
                                        self.tokens[k] = new_token
                                        break
                                self.log_signal.emit(f"  ✅ Re-auth successful! Retrying...")
                                client.close()
                                client = self._get_thread_client(token=new_token)
                                continue
                    except Exception as refresh_err:
                        self.log_signal.emit(f"  ⚠️ Re-auth failed: {refresh_err}")
                
                # Failed request
                self.requests_failed += 1
                
                # === FEATURE #15: RESPONSE ANALYSIS ===
                if self.response_analysis_enabled:
                    # Check response body for failure patterns
                    response_text = str(body).lower() if body else ""
                    matched_pattern = None
                    for pattern in self.failure_patterns:
                        if pattern in response_text:
                            matched_pattern = pattern
                            self.failure_count += 1
                            self.consecutive_failures += 1
                            break
                    else:
                        self.consecutive_failures = 0
                    
                    # Check for success indicators in body
                    if any(indicator in response_text for indicator in self.success_indicators):
                        self.success_count += 1
                    
                    # Early abort: too many consecutive same-type failures
                    if self.consecutive_failures >= self.max_consecutive_failures:
                        self.log_signal.emit(f"[🛑] Early abort triggered: {self.consecutive_failures} consecutive '{matched_pattern}' responses")
                        self.stop_event.set()
                    
                    # Early abort: success already achieved (redundancy check)
                    if self.early_abort_on_success and self.success_count > 0:
                        self.stop_event.set()
                
                # === FEATURE #4: ERROR LEARNING - TRACK ERRORS ===
                token_hash = hash(token) % 10000
                self._track_error(token_hash, r.status_code)
                
                # === FEATURE #5: TRACK OUTCOME ===
                self._track_request_outcome("FAILURE", elapsed)

                # === PHASE 5: Record in history DB (only final attempt, not retries) ===
                if attempt >= max_retries - 1:
                    self._record_snipe_history("failed", r.status_code, token)

                # Decode error message
                error_detail = self._decode_error(r.status_code, body)
                status = "❌"
                self.log_signal.emit(f"[{timestamp}] {status} {r.status_code} - Token {token_hash} - {error_detail}")
                
            except httpx.TimeoutException:
                self.requests_failed += 1
                
                # === FEATURE #4: TRACK TIMEOUT ERRORS ===
                token_hash = hash(token) % 10000
                self._track_error(token_hash, "TIMEOUT")
                
                # === FEATURE #5: TRACK OUTCOME ===
                self._track_request_outcome("TIMEOUT", elapsed)
                
                if attempt < max_retries - 1:
                    wait_time = retry_delays[attempt]
                    self.log_signal.emit(f"[⏱️] TIMEOUT, retrying in {wait_time}s...")
                    time.sleep(wait_time)
                    continue
                else:
                    self.log_signal.emit(f"[{timestamp}] TIMEOUT - Token {token_hash}")
                    
            except Exception as e:
                self.requests_failed += 1
                
                # === FEATURE #4: TRACK EXCEPTION ERRORS ===
                token_hash = hash(token) % 10000
                self._track_error(token_hash, "EXCEPTION")
                
                # === FEATURE #5: TRACK OUTCOME ===
                self._track_request_outcome("EXCEPTION", elapsed)
                
                self.log_signal.emit(f"[{timestamp}] ERROR - {str(e)[:50]}")
                
            finally:
                client.close()
            
            # If we got here, either succeeded or max retries reached
            if self.success_event.is_set():
                return
    
    def _track_error(self, token_hash, error_type):
        """Track errors per token and blacklist consistently failing ones"""
        # Initialize tracking for this token if needed
        if token_hash not in self.token_error_counts:
            self.token_error_counts[token_hash] = 0
            self.token_error_types[token_hash] = {}
        
        # Increment error count
        self.token_error_counts[token_hash] += 1
        
        # Track error type distribution
        if error_type not in self.token_error_types[token_hash]:
            self.token_error_types[token_hash][error_type] = 0
        self.token_error_types[token_hash][error_type] += 1
        
        # Check if token should be blacklisted
        self._check_blacklist(token_hash)
    
    def _check_blacklist(self, token_hash):
        """Check if token should be blacklisted based on error patterns"""
        if token_hash in self.blacklisted_tokens:
            return  # Already blacklisted
        
        error_count = self.token_error_counts.get(token_hash, 0)
        
        # Check if error count exceeds threshold
        if error_count >= self.ERROR_THRESHOLD:
            error_types = self.token_error_types.get(token_hash, {})
            total_errors = sum(error_types.values())
            
            # Find most common error type
            if total_errors > 0:
                most_common_error = max(error_types.items(), key=lambda x: x[1])
                dominant_error_rate = most_common_error[1] / total_errors
                
                # Blacklist if dominant error rate exceeds threshold
                if dominant_error_rate >= self.BLACKLIST_THRESHOLD:
                    self.blacklisted_tokens.add(token_hash)
                    self.log_signal.emit(f"[⚫] BLACKLISTED: Token {token_hash} ({error_count} errors, {most_common_error[0]}: {most_common_error[1]})")
                    self.log_signal.emit(f"   Pattern: {most_common_error[1]}/{total_errors} ({dominant_error_rate*100:.0f}%) were {most_common_error[0]}")
    
    def _is_token_blacklisted(self, token):
        """Check if a token is blacklisted"""
        return hash(token) % 10000 in self.blacklisted_tokens
    
    def _record_name_change(self, token, username, success):
        """Record a name change for cooldown tracking (persists to account_history.json)"""
        token_key = hash(token) % 1000000
        try:
            history_path = ACCOUNT_TRACKING_FILE
            try:
                with open(history_path, 'r') as f:
                    history = json.load(f)
            except (FileNotFoundError, json.JSONDecodeError):
                history = {"accounts": {}}
            
            if "accounts" not in history:
                history["accounts"] = {}
            
            now = datetime.now(timezone.utc).isoformat()
            
            if token_key not in history["accounts"]:
                history["accounts"][token_key] = {
                    "token_hash": token_key,
                    "username": None,
                    "last_checked": None,
                    "last_name_change": None,
                    "name_change_cooldown_until": None,
                    "total_attempts": 0,
                    "successful_changes": 0,
                    "status": "unknown"
                }
            
            acc = history["accounts"][token_key]
            acc["total_attempts"] += 1
            acc["username"] = username
            
            if success:
                acc["successful_changes"] += 1
                acc["last_name_change"] = now
                cooldown_until_ts = datetime.now(timezone.utc).timestamp() + (NAME_CHANGE_COOLDOWN_DAYS * 86400)
                acc["name_change_cooldown_until"] = datetime.fromtimestamp(cooldown_until_ts, tz=timezone.utc).isoformat()
                acc["status"] = "cooldown"
                self.log_signal.emit(f"📝 Recorded name change: {username} → 90-day cooldown set for Token {token_key}")
            
            with open(history_path, 'w') as f:
                json.dump(history, f, indent=2)
        except Exception as e:
            self.log_signal.emit(f"[!] Failed to record name change: {e}")
    
    def _decode_error(self, status_code, body):
        """Decode Minecraft API error response into human-readable message"""
        if status_code == 400:
            return "Bad Request - invalid username format"
        elif status_code == 401:
            msg = "Expired/Invalid Token"
            if isinstance(body, dict):
                detail = body.get("errorMessage", body.get("message", ""))
                if "expired" in str(detail).lower():
                    msg = "Token Expired - needs refresh"
                elif "invalid" in str(detail).lower():
                    msg = "Token Invalid"
            return msg
        elif status_code == 403:
            if isinstance(body, dict):
                details = body.get("details", {})
                status = details.get("status", "")
                if status == "DUPLICATE":
                    return "Already owns this name"
                elif status == "NAME_CHANGE_COOLDOWN":
                    return "90-day name change cooldown active"
                message = body.get("errorMessage", body.get("message", ""))
                if "cooldown" in str(message).lower():
                    return "Name change on cooldown"
            return "Forbidden - check account permissions"
        elif status_code == 404:
            return "Name not available (not yet dropped)"
        elif status_code == 429:
            return "Rate limited (too many requests)"
        elif status_code == 500:
            return "Minecraft server error (try again)"
        elif status_code == 502:
            return "Minecraft server bad gateway"
        elif status_code == 503:
            return "Minecraft service unavailable"
        elif status_code >= 500:
            return f"Minecraft server error ({status_code})"
        else:
            return f"Unknown error ({status_code})"

    # ========== PHASE 5: History DB Integration ==========

    def _record_snipe_history(self, status, response_code, token):
        """Record snipe attempt in SQLite history database"""
        try:
            import sniper_history
            db = sniper_history.SnipeHistoryDB()
            account_email = f"token_{hash(token) % 10000}" if token else "unknown"
            db.record_attempt(
                target_name=self.name,
                status=status,
                response_status=response_code,
                response_body=str(response_code),
                account_email=account_email
            )
        except Exception:
            pass  # Don't break sniping for DB errors

    def _send_webhook_alert(self, event_type, name, status_code, token):
        """Send Discord webhook alert if configured"""
        if not hasattr(self, 'webhook_url') or not self.webhook_url:
            return
        try:
            import sniper_webhook
            alert = sniper_webhook.DiscordWebhookAlert(self.webhook_url)
            if event_type == "success":
                alert.alert("🎯 SNIP SUCCESS!", f"Claimed **{name}** (HTTP {status_code})", color=0x00ff00)
            elif event_type == "owned":
                alert.alert("🏆 ALREADY OWNED", f"Account already has **{name}**", color=0xffa500)
            elif event_type == "auth":
                alert.alert("🔑 Auth Event", f"Account {name}: {status_code}", color=0x58a6ff)
        except Exception:
            pass  # Don't break sniping for webhook errors

    # ========== END PHASE 5 ==========

    # ========== FEATURE: AUTO SKIN UPLOAD ==========

    def _upload_skin_on_success(self, token):
        """
        Upload a skin after successfully sniping a name.
        This serves two purposes:
        1. Verifies the name change actually took effect (double-check)
        2. Sets your identity immediately so you own the visual identity too
        
        Mojang API: PUT /minecraft/profile/skin
        Body: multipart/form-data with 'slug', 'model', and skin file
        """
        if not self.skin_upload_enabled or self.skin_uploaded:
            return
        
        if not self.skin_file_path or not Path(self.skin_file_path).exists():
            self.log_signal.emit(f"[⚠️] Skin upload: File not found at {self.skin_file_path}")
            return
        
        self.log_signal.emit(f"[*] 🎨 Uploading skin: {Path(self.skin_file_path).name} (model: {self.skin_model})")
        
        try:
            skin_data = Path(self.skin_file_path).read_bytes()
            
            # Validate it's a reasonable PNG
            if not skin_data.startswith(b'\x89PNG'):
                self.log_signal.emit(f"[⚠️] Skin upload: File is not a valid PNG image")
                return
            
            file_size_kb = len(skin_data) / 1024
            if file_size_kb > 1024:  # 1MB limit
                self.log_signal.emit(f"[⚠️] Skin upload: File too large ({file_size_kb:.0f}KB, max 1MB)")
                return
            
            url = "https://api.minecraftservices.com/minecraft/profile/skin"
            
            client = httpx.Client(
                transport=httpx.HTTPTransport(pool=connection_pool),
                http2=True,
                timeout=httpx.Timeout(10.0, connect=5.0),
                headers={"Authorization": f"Bearer {token}"},
            )
            
            start_time = time.time()
            r = client.put(url, data={
                "slug": "skin",
                "model": self.skin_model,
            }, files={
                "file": ("skin.png", skin_data, "image/png")
            })
            elapsed = time.time() - start_time
            
            if r.status_code in [200, 204]:
                self.skin_uploaded = True
                self.log_signal.emit(f"[✅] Skin uploaded successfully! ({elapsed:.2f}s)")
                
                # Also verify the current name by fetching profile
                self._verify_skin_and_name(token)
            else:
                try:
                    err_body = r.json()
                except:
                    err_body = r.text
                self.log_signal.emit(f"[⚠️] Skin upload failed: HTTP {r.status_code} - {err_body}")
                
        except Exception as e:
            self.log_signal.emit(f"[⚠️] Skin upload error: {e}")

    def _verify_skin_and_name(self, token):
        """After skin upload, verify the account actually has the new name."""
        try:
            client = httpx.Client(
                transport=httpx.HTTPTransport(pool=connection_pool),
                http2=True,
                timeout=httpx.Timeout(5.0),
                headers={"Authorization": f"Bearer {token}"},
            )
            r = client.get("https://api.minecraftservices.com/minecraft/profile")
            if r.status_code == 200:
                profile = r.json()
                actual_name = profile.get("name", "unknown")
                if actual_name == self.name:
                    self.log_signal.emit(f"[✅] VERIFIED: Account name confirmed as '{actual_name}'")
                else:
                    self.log_signal.emit(f"[⚠️] Name mismatch! Expected '{self.name}' but profile shows '{actual_name}'")
            else:
                self.log_signal.emit(f"[⚠️] Could not verify profile (HTTP {r.status_code})")
        except Exception as e:
            self.log_signal.emit(f"[⚠️] Profile verification error: {e}")

    # ========== END AUTO SKIN UPLOAD ==========

    # ========== FEATURE: DESKTOP NOTIFICATIONS ==========

    def _send_desktop_notification(self, title, message):
        """Send OS desktop notification (Windows toast, Linux notify-send, macOS terminal-notifier)"""
        if not self.desktop_notifications_enabled:
            return
        try:
            import platform
            system = platform.system()
            if system == "Windows":
                self._notify_windows(title, message)
            elif system == "Linux":
                self._notify_linux(title, message)
            elif system == "Darwin":
                self._notify_macos(title, message)
        except Exception as e:
            self.log_signal.emit(f"[⚠️] Notification failed: {e}")

    def _notify_windows(self, title, message):
        """Windows toast notification via ctypes (no pip install needed)"""
        try:
            # Try win10toast first (commonly installed)
            from win10toast import ToastNotifier
            toaster = ToastNotifier()
            toaster.show_toast(title, message, duration=10, threaded=True)
        except ImportError:
            # Fallback: use subprocess with Windows native notification via PowerShell
            import subprocess, tempfile, os
            script = f'''
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
$root = $template.GetElementsByTagName("text")
$root[0].AppendChild($template.CreateTextNode("{title}")) | Out-Null
$root[1].AppendChild($template.CreateTextNode("{message}")) | Out-Null
$toast = [Windows.UI.Notifications.ToastNotification]::new($template)
$toast.Tag = "MCSniper"
$toast.Group = "MCSniper"
$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Minecraft Sniper")
$notifier.Show($toast)
'''
            ps_file = tempfile.NamedTemporaryFile(suffix='.ps1', delete=False, mode='w')
            ps_file.write(script)
            ps_file.close()
            subprocess.Popen(["powershell", "-ExecutionPolicy", "Bypass", "-File", ps_file.name], creationflags=subprocess.CREATE_NO_WINDOW)
            os.unlink(ps_file.name)

    def _notify_linux(self, title, message):
        """Linux desktop notification via notify-send"""
        import subprocess
        subprocess.Popen(["notify-send", title, message], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    def _notify_macos(self, title, message):
        """macOS notification via terminal-notifier or osascript"""
        import subprocess
        try:
            subprocess.Popen(["terminal-notifier", "-title", title, "-message", message],
                           stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        except FileNotFoundError:
            script = f'display notification "{message}" with title "{title}"'
            subprocess.Popen(["osascript", "-e", script], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    # ========== END DESKTOP NOTIFICATIONS ==========

    def _check_ip_rate_limit_cooldown(self):
        """
        Detect IP-level rate limiting and enforce a global cooldown.
        
        If 5+ tokens get 429'd within 10 seconds, it's almost certainly an IP-level
        rate limit (not per-token). Pause ALL firing for 30s to let it expire.
        """
        now = time.time()
        
        # If already in cooldown, check if it's expired
        if self.ip_rate_limit_cooldown_until > now:
            remaining = int(self.ip_rate_limit_cooldown_until - now)
            self.log_signal.emit(f"🛡️ IP cooldown active — pausing all firing for {remaining}s")
            time.sleep(min(remaining, 5))  # Sleep in 5s chunks to allow stop event
            return
        
        # Count 429s in the detection window
        window_start = now - self.IP_429_WINDOW
        recent_429s = sum(1 for ts in self.ip_rate_limit_window if ts > window_start)
        
        if recent_429s >= self.IP_429_THRESHOLD:
            # IP-level rate limit detected!
            self.ip_rate_limit_cooldown_until = now + self.IP_429_COOLDOWN
            self.ip_cooldown_triggered = True
            self.log_signal.emit(f"")
            self.log_signal.emit(f"🛡️🛡️🛡️ IP-LEVEL RATE LIMIT DETECTED! 🛡️🛡️🛡️")
            self.log_signal.emit(f"   {recent_429s} tokens got 429'd in {self.IP_429_WINDOW}s — this is an IP ban, not per-token")
            self.log_signal.emit(f"   Pausing ALL firing for {self.IP_429_COOLDOWN}s to let cooldown expire")
            self.log_signal.emit(f"")
            
            # Wait out the cooldown (in chunks so stop_event can interrupt)
            while time.time() < self.ip_rate_limit_cooldown_until and not self.stop_event.is_set() and not self.success_event.is_set():
                sleep_time = min(5, self.ip_rate_limit_cooldown_until - time.time())
                time.sleep(sleep_time)
    
    def _check_namemc_availability(self, username):
        """
        Check if a username is available via Mojang API.

        GET /users/profiles/minecraft/{name} returns 404 if the name
        is not assigned to any account (i.e., available).

        Returns: True = available, False = taken, None = error
        """
        url = self.MOJANG_PROFILE_URL.format(username)
        
        client = httpx.Client(
            timeout=httpx.Timeout(5.0, connect=3.0),
            headers={"Accept": "application/json"},
        )
        
        try:
            r = client.get(url)
            
            if r.status_code == 404:
                # Not found = name is available!
                return True
            
            if r.status_code == 200:
                # Has profile data = name is taken
                return False
            
            # Other status (403, 500, etc.) — can't determine
            return None
        except Exception as e:
            self.log_signal.emit(f"[⚠️] Mojang availability check failed: {str(e)[:60]}")
            return None
        finally:
            client.close()
    
    def _run_namemc_monitor(self, username, target_ts):
        """
        Monitor Mojang API in the background during the wait phase.
        
        Runs as a separate thread that polls every NAMEMC_CHECK_INTERVAL seconds.
        Sets self.namemc_confirmed_available = True when it detects the name is free.
        """
        while not self.stop_event.is_set() and not self.success_event.is_set():
            now = time.time()
            
            # Don't start checking until within 2 minutes of drop
            if target_ts - now > 120:
                time.sleep(10)
                continue
            
            # Respect check interval
            if now - self.namemc_last_check < self.NAMEMC_CHECK_INTERVAL:
                time.sleep(2)
                continue
            
            self.namemc_last_check = now
            remaining = max(0, int(target_ts - now))
            
            available = self._check_namemc_availability(username)
            
            if available is True:
                if not self.namemc_confirmed_available:
                    self.namemc_confirmed_available = True
                    self.log_signal.emit(f"")
                    self.log_signal.emit(f"🟢🟢🟢 Mojang CONFIRMS: '{username}' IS AVAILABLE! 🟢🟢🟢")
                    self.log_signal.emit(f"   Name just dropped — snipe will fire at drop time ({remaining}s)")
                    self.log_signal.emit(f"")
                else:
                    self.log_signal.emit(f"[🟢] Mojang: '{username}' still available | {remaining}s to drop")
            elif available is False:
                if self.namemc_confirmed_available:
                    # Was available, now taken — someone else got it!
                    self.log_signal.emit(f"")
                    self.log_signal.emit(f"🔴🔴🔴 Mojang: '{username}' WAS TAKEN! Someone else sniped it! 🔴🔴🔴")
                    self.log_signal.emit(f"   Continuing snipe in case MC API is behind...")
                    self.log_signal.emit(f"")
                    self._send_desktop_notification(
                        f"⚠️ Lost: {username}",
                        "Someone else sniped this name!"
                    )
                else:
                    self.log_signal.emit(f"[🔴] Mojang: '{username}' still taken | {remaining}s to drop")
            else:
                self.log_signal.emit(f"[⚪] Mojang: unable to determine '{username}' status | {remaining}s to drop")
            
            # Check less frequently as we get closer to drop
            if remaining < 30:
                time.sleep(3)
            elif remaining < 60:
                time.sleep(5)
            else:
                time.sleep(min(self.NAMEMC_CHECK_INTERVAL, remaining - 30))
    
    # === FEATURE #5: ADAPTIVE THROTTLING METHODS ===
    
    def _track_request_outcome(self, outcome, elapsed_time):
        """Track request outcome for adaptive analysis"""
        self.last_100_requests.append({
            "outcome": outcome,
            "elapsed": elapsed_time,
            "timestamp": time.time()
        })
        
        # Keep only last N requests
        if len(self.last_100_requests) > self.COMPETITION_WINDOW:
            self.last_100_requests.pop(0)
        
        # Analyze and adapt if we have enough data
        if len(self.last_100_requests) >= 20:
            self._analyze_and_adapt()
    
    def _analyze_and_adapt(self):
        """Analyze recent requests and adapt strategy"""
        if len(self.last_100_requests) < 20:
            return
        
        # Calculate metrics
        total = len(self.last_100_requests)
        successes = sum(1 for r in self.last_100_requests if r["outcome"] in ["SUCCESS", "DUPLICATE"])
        failures = sum(1 for r in self.last_100_requests if r["outcome"] in ["FAILURE", "TIMEOUT", "EXCEPTION"])
        rate_limits = sum(1 for r in self.last_100_requests if r["outcome"] == "FAILURE" and hasattr(self, 'last_status') and self.last_status == 429)
        
        failure_rate = failures / total
        avg_response_time = sum(r["elapsed"] for r in self.last_100_requests) / total
        
        # Detect rate limit spike
        recent_429s = sum(1 for r in self.last_100_requests[-10:] if r["outcome"] == "FAILURE")
        current_rate_limit_spike = recent_429s / 10 > self.RATE_LIMIT_THRESHOLD
        
        # Determine competition level
        old_competition = self.competition_level
        
        if failure_rate > self.COMPETITION_EXTREME_THRESHOLD:
            self.competition_level = "EXTREME"
        elif failure_rate > self.COMPETITION_HIGH_THRESHOLD:
            self.competition_level = "HIGH"
        elif failure_rate > 0.2:
            self.competition_level = "MEDIUM"
        else:
            self.competition_level = "LOW"
        
        # Adapt threads and delay based on competition
        old_threads = self.adaptive_threads
        old_delay = self.adaptive_delay
        
        if self.competition_level == "EXTREME":
            # Slow down significantly
            self.adaptive_threads = max(self.MIN_THREADS, self.threads_count // 2)
            self.adaptive_delay = min(self.MAX_DELAY, self.adaptive_delay + 0.5)
            self.rate_limit_spike = True
            
        elif self.competition_level == "HIGH":
            # Moderate slowdown
            self.adaptive_threads = max(self.MIN_THREADS, self.threads_count * 3 // 4)
            self.adaptive_delay = min(self.MAX_DELAY, self.adaptive_delay + 0.3)
            self.rate_limit_spike = current_rate_limit_spike
            
        elif self.competition_level == "MEDIUM":
            # Slight adjustment
            self.adaptive_threads = max(self.MIN_THREADS, self.threads_count * 7 // 8)
            self.adaptive_delay = min(0.5, self.adaptive_delay + 0.1)
            self.rate_limit_spike = False
            
        else:  # LOW
            # Ramp back up to full power
            self.adaptive_threads = min(self.threads_count, self.adaptive_threads + 2)
            self.adaptive_delay = max(0.0, self.adaptive_delay - 0.1)
            self.rate_limit_spike = False
        
        # Log changes
        if old_competition != self.competition_level or old_threads != self.adaptive_threads or abs(old_delay - self.adaptive_delay) > 0.01:
            emoji = {"LOW": "🟢", "MEDIUM": "🟡", "HIGH": "🟠", "EXTREME": "🔴"}.get(self.competition_level, "⚪")
            self.log_signal.emit(f"\\n{emoji} [ADAPTIVE] Competition: {self.competition_level} | Threads: {old_threads}→{self.adaptive_threads} | Delay: {old_delay:.2f}s→{self.adaptive_delay:.2f}s")
            self.log_signal.emit(f"   Stats: {successes} success / {failures} failures ({failure_rate*100:.0f}%) | Avg response: {avg_response_time*1000:.0f}ms")
            if self.rate_limit_spike:
                self.log_signal.emit(f"   ⚠️ RATE LIMIT SPIKE DETECTED - Backing off!")
    
    def _trigger_success_alerts(self):
        """Trigger all success alerts"""
        self.log_signal.emit(f"\n{'='*60}")
        self.log_signal.emit(f"[🎉🎉🎉 USERNAME SECURED: {self.name} 🎉🎉🎉]")
        self.log_signal.emit(f"{'='*60}\n")
        
        # Play sound
        play_success_sound()
        
        # Send desktop notification
        send_desktop_notification(
            "🏆 MINECRAFT NAME SECURED!",
            f"Username '{self.name}' has been claimed successfully!"
        )
        
        # === FEATURE: SCREENSHOT ON SUCCESS ===
        screenshot_path = take_success_screenshot(self.name)
        if screenshot_path:
            self.log_signal.emit(f"📸 Screenshot saved: {screenshot_path}")
        else:
            self.log_signal.emit("⚠️ Screenshot capture failed (no screenshot tool available)")
        
        # Additional log
        self.log_signal.emit(f"[✓] Total requests fired: {self.requests_fired}")
        self.log_signal.emit(f"[✓] Successful: {self.requests_success}")
        self.log_signal.emit(f"[✓] Failed: {self.requests_failed}")
        
        # === FEATURE #4: BLACKLIST SUMMARY ===
        if self.blacklisted_tokens:
            self.log_signal.emit(f"\n[⚫] BLACKLIST SUMMARY: {len(self.blacklisted_tokens)} tokens blacklisted")
            for token_hash in sorted(self.blacklisted_tokens):
                error_count = self.token_error_counts.get(token_hash, 0)
                error_types = self.token_error_types.get(token_hash, {})
                if error_types:
                    most_common = max(error_types.items(), key=lambda x: x[1])
                    self.log_signal.emit(f"   Token {token_hash}: {error_count} errors ({most_common[0]}: {most_common[1]})")
        else:
            self.log_signal.emit(f"[✓] No tokens blacklisted during this run")
        
        # === FEATURE #5: ADAPTIVE SUMMARY ===
        self.log_signal.emit(f"\n[📊] ADAPTIVE THROTTLING SUMMARY:")
        self.log_signal.emit(f"   Final competition level: {self.competition_level}")
        self.log_signal.emit(f"   Thread count: {self.threads_count} → {self.adaptive_threads}")
        self.log_signal.emit(f"   Final delay between bursts: {self.adaptive_delay:.2f}s")
        if self.last_100_requests:
            total = len(self.last_100_requests)
            avg_time = sum(r["elapsed"] for r in self.last_100_requests) / total
            self.log_signal.emit(f"   Avg response time: {avg_time*1000:.0f}ms")
    
    # === PHASE 4: REAL-TIME STATS EMITTER ===
    
    def _start_stats_emitter(self, target_ts):
        """Start background thread that emits stats_signal every second"""
        self._target_timestamp = target_ts
        self._stats_start_time = time.time()
        self._stats_last_fired = self.requests_fired
        
        def _emit_stats():
            while not self.stop_event.is_set() and not self.success_event.is_set():
                elapsed = time.time() - self._stats_start_time
                if elapsed < 1:
                    time.sleep(0.5)
                    continue
                
                now = time.time()
                current_fired = self.requests_fired
                rps = (current_fired - self._stats_last_fired) / 1.0  # per second
                self._stats_last_fired = current_fired
                
                total_elapsed = now - self._stats_start_time
                overall_rps = current_fired / total_elapsed if total_elapsed > 0 else 0
                
                avg_ms = 0
                if self.last_100_requests:
                    avg_ms = sum(r["elapsed"] for r in self.last_100_requests[-20:]) / min(len(self.last_100_requests), 20) * 1000
                
                success_rate = 0
                if current_fired > 0:
                    success_rate = self.requests_success / current_fired * 100
                
                time_remaining = 0
                if target_ts:
                    time_remaining = max(0, target_ts - now)
                
                self.stats_signal.emit({
                    "fired": current_fired,
                    "success": self.requests_success,
                    "failed": self.requests_failed,
                    "rps": round(overall_rps, 1),
                    "instant_rps": round(rps, 1),
                    "competition": self.competition_level,
                    "avg_response_ms": round(avg_ms, 0),
                    "success_rate": round(success_rate, 1),
                    "active_threads": self.adaptive_threads,
                    "time_remaining": round(time_remaining, 1),
                    "elapsed": round(total_elapsed, 1),
                })
                
                time.sleep(1)
        
        self._stats_thread = threading.Thread(target=_emit_stats, daemon=True)
        self._stats_thread.start()
    
    def _stop_stats_emitter(self):
        """Stop the stats emitter (called when snipe ends)"""
        self.stop_event.set()  # will be cleared if not actually stopping
        if self._stats_thread:
            self._stats_thread.join(timeout=2)
            self._stats_thread = None


# === FEATURE: AUTO-RESTART WATCHDOG ===
class SniperWatchdog(QThread):
    """Monitors the sniper process and auto-restarts if it crashes or gets rate-limited"""
    log_signal = pyqtSignal(str)
    restart_signal = pyqtSignal(dict)  # Emits config dict to restart sniper
    status_signal = pyqtSignal(str)    # "watching", "restarting", "stopped"
    
    def __init__(self, max_restarts=3, restart_delay=10):
        super().__init__()
        self.max_restarts = max_restarts
        self.restart_delay = restart_delay
        self.restart_count = 0
        self.running = False
        self.monitoring = False
        self._stop_event = threading.Event()
    
    def start_monitoring(self, config):
        """Start monitoring with the given sniper config"""
        self.restart_count = 0
        self.running = True
        self.monitoring = True
        self._config = config
        self._stop_event.clear()
        self.start()
        self.status_signal.emit("watching")
    
    def stop_monitoring(self):
        """Stop the watchdog"""
        self.running = False
        self.monitoring = False
        self._stop_event.set()
        self.wait(3000)
        self.status_signal.emit("stopped")
    
    def run(self):
        """Monitor loop — watch for crash indicators and auto-restart"""
        while self.running and not self._stop_event.is_set():
            try:
                # Check if rate limit cooldown has persisted too long (indicates crash/stuck)
                import time as _time
                _time.sleep(5)  # Check every 5 seconds
                
                if self._stop_event.is_set():
                    break
                    
            except Exception as e:
                self.log_signal.emit(f"[Watchdog] Monitor error: {e}")
        
        self.monitoring = False
    
    def trigger_restart(self):
        """Manually trigger a restart (called from GUI when crash detected)"""
        if not self.running or self.restart_count >= self.max_restarts:
            self.log_signal.emit(f"[Watchdog] Cannot restart: {'max retries reached' if self.restart_count >= self.max_restarts else 'not running'}")
            return False
        
        self.restart_count += 1
        self.status_signal.emit("restarting")
        self.log_signal.emit(f"[Watchdog] Auto-restart #{self.restart_count}/{self.max_restarts} in {self.restart_delay}s...")
        
        # Delay before restart to let connections cool down
        for i in range(self.restart_delay):
            if self._stop_event.is_set():
                return False
            import time as _time
            _time.sleep(1)
        
        if self._stop_event.is_set():
            return False
        
        self.restart_signal.emit(self._config)
        self.log_signal.emit(f"[Watchdog] Restart #{self.restart_count} triggered")
        return True




class NameMCSearchWorker(QThread):
    """Worker thread for searching Minecraft names via NameMC API"""
    log_signal = pyqtSignal(str)
    # Use JSON string for dict — PyQt5 can't pass dict through signals
    result_signal = pyqtSignal(str, str)  # name, json_string
    
    def __init__(self, names):
        super().__init__()
        self.names = names
    
    def run(self):
        """Query NameMC API for each name"""
        import json as _json
        import requests as _requests
        import time as _time
        import uuid as _uuid
        from datetime import datetime as _dt, timedelta as _td

        # Try to import cloudscraper for Cloudflare-protected sites like NameMC
        cloudscraper_avail = False
        _cs = None
        try:
            import cloudscraper as _cs_mod
            _cs = _cs_mod.CloudScraper()
            cloudscraper_avail = True
        except ImportError:
            pass

        # Fallback: regular session with browser headers
        session = _requests.Session()
        browser_headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
            "Accept": "application/json, text/plain, */*",
            "Accept-Language": "en-US,en;q=0.9",
            "Referer": "https://namemc.com/",
        }
        session.headers.update(browser_headers)

        def query_namemc(username):
            """Try multiple methods to get name history data"""
            # Method 1: playerdb.co (most reliable for name history)
            try:
                resp = session.get(f"https://playerdb.co/api/player/minecraft/{username}", timeout=8)
                if resp.status_code == 200:
                    data = resp.json()
                    if data.get("success"):
                        player = data["data"]["player"]
                        history = player.get("name_history", [])
                        last_change = None
                        if history:
                            last_change = history[-1].get("changedToAt")
                        return {
                            "username": player.get("username", username),
                            "id": player.get("id", ""),
                            "created": None,
                            "nameChanged": last_change,
                            "nameHistory": history,
                        }
            except Exception:
                pass

            # Method 2: ashcon.app API (unofficial, no Cloudflare)
            try:
                resp = session.get(f"https://api.ashcon.app/mojang/v2/user/{username}", timeout=8)
                if resp.status_code == 200:
                    data = resp.json()
                    return {
                        "username": data.get("username", username),
                        "id": data.get("id", ""),
                        "created": None,
                        "nameChanged": None,
                        "nameHistory": [],
                    }
            except Exception:
                pass

            return None

        for name in self.names:
            try:
                # Step 1: Get UUID from Mojang (always works)
                mojang_url = f"https://api.mojang.com/users/profiles/minecraft/{name}"
                mojang_resp = session.get(mojang_url, timeout=10)

                if mojang_resp.status_code in (404, 204):
                    result = _json.dumps({
                        "available": True, "uuid": "N/A",
                        "first_seen": "N/A", "last_seen": "N/A",
                        "est_expiry": "N/A", "time_remaining": "N/A",
                    })
                    self.result_signal.emit(name, result)
                    self.log_signal.emit(f"[NameMC] {name}: AVAILABLE!")
                    _time.sleep(0.35)
                    continue

                elif mojang_resp.status_code == 429:
                    retry_after = int(mojang_resp.headers.get("Retry-After", 5))
                    self.log_signal.emit(f"[NameMC] Rate limited, waiting {retry_after}s...")
                    _time.sleep(retry_after)
                    mojang_resp = session.get(mojang_url, timeout=10)
                    if mojang_resp.status_code != 200:
                        self.log_signal.emit(f"[NameMC] Error querying '{name}': HTTP {mojang_resp.status_code}")
                        _time.sleep(0.35)
                        continue

                elif mojang_resp.status_code != 200:
                    self.log_signal.emit(f"[NameMC] Error querying '{name}': HTTP {mojang_resp.status_code}")
                    _time.sleep(0.35)
                    continue

                data = mojang_resp.json()
                uuid_raw = data.get("id", "")
                uuid_formatted = f"{uuid_raw[:8]}-{uuid_raw[8:12]}-{uuid_raw[12:16]}-{uuid_raw[16:20]}-{uuid_raw[20:]}" if len(uuid_raw) == 32 else uuid_raw

                # Step 2: Get name change history for expiry date
                nm_data = query_namemc(name)

                name_changed_ts = None
                created_ts = None

                if nm_data:
                    created_ts = nm_data.get("created", None)
                    name_changed_ts = nm_data.get("nameChanged", None)

                # Parse dates
                first_seen = "N/A"
                last_changed = "N/A"
                est_expiry = "N/A"
                time_remaining = "N/A"

                if created_ts:
                    try:
                        first_seen = _dt.utcfromtimestamp(created_ts / 1000).strftime("%Y-%m-%d")
                    except Exception:
                        pass

                if name_changed_ts:
                    try:
                        changed_dt = _dt.utcfromtimestamp(name_changed_ts / 1000)
                        last_changed = changed_dt.strftime("%Y-%m-%d")
                        cooldown_expiry = changed_dt + _td(days=60)
                        est_expiry = cooldown_expiry.strftime("%Y-%m-%d")
                        now = _dt.utcnow()
                        diff = cooldown_expiry - now
                        if diff.total_seconds() > 0:
                            days = diff.days
                            hours = diff.seconds // 3600
                            time_remaining = f"{days}d {hours}h" if days > 0 else f"{hours}h"
                            if days < 7:
                                self.log_signal.emit(f"[NameMC] ⚠️ '{name}' expires in {time_remaining}!")
                        else:
                            time_remaining = "Expired (available!)"
                    except Exception:
                        pass

                result = _json.dumps({
                    "available": False, "uuid": uuid_formatted,
                    "first_seen": first_seen, "last_seen": last_changed,
                    "est_expiry": est_expiry, "time_remaining": time_remaining,
                })
                self.result_signal.emit(name, result)

                if est_expiry != "N/A":
                    self.log_signal.emit(f"[NameMC] {name}: Taken | UUID: {uuid_formatted[:8]}... | Changed: {last_changed} | Drops: {est_expiry} ({time_remaining})")
                else:
                    self.log_signal.emit(f"[NameMC] {name}: Taken | UUID: {uuid_formatted[:8]}... | (no change history found)")

                _time.sleep(0.35)

            except Exception as e:
                self.log_signal.emit(f"[NameMC] Error querying '{name}': {e}")




class SniperGUI(QMainWindow):
    """Main GUI application"""
    
    def __init__(self):
        super().__init__()
        self.workers = []
        self.init_ui()
        self._setup_system_tray()
    
    def _setup_system_tray(self):
        """Setup system tray icon for minimize-to-tray behavior"""
        try:
            # Create a simple icon (colored square with "S" letter)
            icon_pixmap = QPixmap(64, 64)
            icon_pixmap.fill(Qt.transparent)
            
            # We'll use a text-based icon approach
            from PyQt5.QtGui import QPainter, QColor as QtColor
            painter = QPainter(icon_pixmap)
            painter.setRenderHint(QPainter.Antialiasing)
            
            # Background circle
            painter.setBrush(QtColor("#3a7bd5"))
            painter.setPen(QtColor("#3a7bd5"))
            painter.drawEllipse(2, 2, 60, 60)
            
            # "S" text
            painter.setPen(QtColor("white"))
            font = QFont("Segoe UI", 28, QFont.Bold)
            painter.setFont(font)
            painter.drawText(icon_pixmap.rect(), Qt.AlignCenter, "S")
            painter.end()
            
            tray_icon = QIcon(icon_pixmap)
            
            # Create tray icon
            self.tray_icon = QSystemTrayIcon(tray_icon, self)
            self.tray_icon.setToolTip("SNJELR Name Sniper — Right-click for options")
            
            # Create tray menu
            tray_menu = QMenu(self)
            
            show_action = QAction("🖥️ Show Window", self)
            show_action.triggered.connect(self.showNormal)
            tray_menu.addAction(show_action)
            
            tray_menu.addSeparator()
            
            quit_action = QAction("❌ Quit", self)
            quit_action.triggered.connect(self._quit_app)
            tray_menu.addAction(quit_action)
            
            self.tray_icon.setContextMenu(tray_menu)
            self.tray_icon.activated.connect(self._on_tray_activated)
            self.tray_icon.show()
            
        except Exception as e:
            pass  # Silent fail if tray not available
    
    def _on_tray_activated(self, reason):
        """Handle tray icon clicks"""
        if reason == QSystemTrayIcon.DoubleClick:
            self.showNormal()
            self.activateWindow()
    
    def _quit_app(self):
        """Quit the application (called from tray menu)"""
        self.stop_sniper()
        QApplication.quit()
    
    def closeEvent(self, event):
        """Minimize to tray instead of closing"""
        event.ignore()
        self.hide()
        if hasattr(self, 'tray_icon') and self.tray_icon.isVisible():
            self.tray_icon.showMessage(
                "SNJELR Name Sniper",
                "Minimized to system tray. Right-click to quit.",
                QSystemTrayIcon.Information,
                2000
            )
    
    def init_ui(self):
        """Initialize the user interface"""
        self.setWindowTitle("Minecraft Username Sniper")
        self.setGeometry(100, 100, 1200, 950)
        
        # Main widget and layout
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)
        
        # Clean dark theme stylesheet
        self.setStyleSheet("""
            QMainWindow {
                background-color: #1e1e1e;
            }
            QWidget {
                background-color: #1e1e1e;
                color: #cccccc;
                font-family: 'Segoe UI', 'SF Pro Text', 'Inter', Arial, sans-serif;
                font-size: 12px;
            }
            QLabel {
                color: #cccccc;
            }
            QLineEdit {
                background-color: #2d2d2d;
                border: 1px solid #3e3e3e;
                border-radius: 6px;
                padding: 8px 12px;
                color: #e0e0e0;
                selection-background-color: #3a7bd5;
            }
            QLineEdit:focus {
                border: 1px solid #3a7bd5;
            }
            QTextEdit {
                background-color: #2d2d2d;
                border: 1px solid #3e3e3e;
                border-radius: 6px;
                padding: 8px 12px;
                color: #e0e0e0;
                selection-background-color: #3a7bd5;
            }
            QTextEdit:focus {
                border: 1px solid #3a7bd5;
            }
            QSpinBox, QComboBox {
                background-color: #2d2d2d;
                border: 1px solid #3e3e3e;
                border-radius: 6px;
                padding: 6px 10px;
                color: #e0e0e0;
                selection-background-color: #3a7bd5;
            }
            QSpinBox:focus, QComboBox:focus {
                border: 1px solid #3a7bd5;
            }
            QGroupBox {
                font-weight: 600;
                font-size: 11px;
                color: #999999;
                border: none;
                margin-top: 10px;
                padding-top: 28px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                left: 0px;
                padding: 0 4px 6px 0;
                color: #999999;
                font-size: 10px;
                font-weight: 700;
                text-transform: uppercase;
                letter-spacing: 0.5px;
            }
            QPushButton {
                background-color: #2d2d2d;
                color: #cccccc;
                border: 1px solid #3e3e3e;
                border-radius: 6px;
                padding: 8px 16px;
                font-weight: 600;
                font-size: 11px;
                min-width: 60px;
            }
            QPushButton:hover {
                background-color: #383838;
                border-color: #555555;
            }
            QPushButton:pressed {
                background-color: #2a2a2a;
                border-color: #3e3e3e;
            }
            QCheckBox {
                color: #cccccc;
                spacing: 8px;
                font-size: 11px;
            }
            QCheckBox::indicator {
                width: 16px;
                height: 16px;
                border: 1.5px solid #555555;
                border-radius: 4px;
                background-color: #2d2d2d;
            }
            QCheckBox::indicator:checked {
                background-color: #3a7bd5;
                border-color: #3a7bd5;
                image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDEyIDEyIj48cGF0aCBkPSJNMSA2bDMgMyA2LTYiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==);
            }
            QSpinBox::up-button, QSpinBox::down-button {
                background-color: #383838;
                width: 22px;
                border: none;
                border-radius: 3px;
            }
            QComboBox::drop-down {
                border: none;
                width: 24px;
            }
            QComboBox::down-arrow {
                width: 10px;
                height: 10px;
            }
            QComboBox QAbstractItemView {
                background-color: #2d2d2d;
                border: 1px solid #3e3e3e;
                selection-background-color: #3a7bd5;
                color: #e0e0e0;
                outline: none;
            }
            QTabWidget::pane {
                border: none;
                background-color: transparent;
                top: -1px;
            }
            QTabBar {
            }
            QTabBar::tab {
                background: transparent;
                padding: 10px 20px;
                margin-right: 2px;
                border: none;
                border-radius: 8px;
                font-weight: 600;
                font-size: 12px;
                color: #777777;
                min-width: 80px;
            }
            QTabBar::tab:hover {
                background: #2d2d2d;
                color: #bbbbbb;
            }
            QTabBar::tab:selected {
                background: #3a7bd5;
                color: white;
            }
            QTextEdit#log_output {
                background-color: #1a1a1a;
                color: #888888;
                border: 1px solid #2d2d2d;
                border-radius: 8px;
                padding: 10px 12px;
                font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
                font-size: 11px;
            }
            QProgressBar {
                border: none;
                border-radius: 4px;
                text-align: center;
                background-color: #2d2d2d;
                height: 8px;
            }
            QProgressBar::chunk {
                background-color: #3a7bd5;
                border-radius: 4px;
            }
            QSlider::groove:horizontal {
                border: none;
                height: 4px;
                background-color: #3e3e3e;
                border-radius: 2px;
            }
            QSlider::handle:horizontal {
                background-color: #3a7bd5;
                border: none;
                width: 14px;
                height: 14px;
                margin: -5px 0;
                border-radius: 7px;
            }
            QSlider::handle:horizontal:hover {
                background-color: #5a9bf5;
            }
            QTableWidget {
                background-color: #2d2d2d;
                border: 1px solid #3e3e3e;
                border-radius: 8px;
                gridline-color: #3e3e3e;
                selection-background-color: #3a7bd5;
                color: #cccccc;
            }
            QTableWidget::item {
                padding: 4px;
            }
            QHeaderView::section {
                background-color: #252525;
                border: none;
                border-bottom: 1px solid #3e3e3e;
                padding: 8px 12px;
                font-weight: 600;
                font-size: 10px;
                color: #888888;
                text-transform: uppercase;
                letter-spacing: 0.5px;
            }
            QScrollBar:vertical {
                background-color: #1e1e1e;
                width: 8px;
                border: none;
            }
            QScrollBar::handle:vertical {
                background-color: #444444;
                border-radius: 4px;
                min-height: 30px;
            }
            QScrollBar::handle:vertical:hover {
                background-color: #555555;
            }
            QScrollBar::add-line, QScrollBar::sub-line {
                border: none;
                background: none;
            }
            QScrollArea {
                border: none;
                background: transparent;
            }
        """)
        
        # === Header Bar ===
        header = QWidget()
        header.setObjectName("header")
        header.setStyleSheet("""
            QWidget#header {
                background-color: #1e1e1e;
                border-bottom: 1px solid #2d2d2d;
                padding: 16px 24px 12px 24px;
            }
        """)
        header_layout = QHBoxLayout(header)
        header_layout.setContentsMargins(24, 16, 24, 12)
        
        # Title
        title_label = QLabel("Minecraft Username Sniper")
        title_label.setFont(QFont("Segoe UI", 18, QFont.Bold))
        title_label.setStyleSheet("color: #e0e0e0;")
        header_layout.addWidget(title_label)
        header_layout.addStretch()
        
        # Status pill
        self.status_label = QLabel("Ready")
        self.status_label.setFont(QFont("Segoe UI", 11, QFont.Medium))
        self.status_label.setStyleSheet("""
            QLabel {
                color: #4caf50;
                background-color: #1b3a1b;
                padding: 4px 14px;
                border-radius: 12px;
                font-size: 11px;
            }
        """)
        header_layout.addWidget(self.status_label)
        
        main_layout.addWidget(header)
        
        # === M1: COUNTDOWN TIMER ===
        self._create_countdown_panel(main_layout)
        
        # === REAL-TIME STATS PANEL ===
        self._create_stats_panel(main_layout)
        
        # === Tabs ===
        tabs = QTabWidget()
        main_layout.addWidget(tabs, stretch=1)
        
        namemc_tab = self._create_sniper_tab("NAMEMC (Windowed Mode)", timing_mode="windowed")
        tabs.addTab(namemc_tab, "NAMEMC")
        
        labynames_tab = self._create_sniper_tab("LabyNames (Exact Mode)", timing_mode="exact")
        tabs.addTab(labynames_tab, "LabyNames")
        
        health_tab = self._create_health_tab()
        tabs.addTab(health_tab, "Health")

        accounts_tab = self._create_accounts_tab()
        tabs.addTab(accounts_tab, "Accounts")

        power_tab = self._create_power_features_tab()
        tabs.addTab(power_tab, "Power")

        # === NEW QoL TABS ===
        preflight_tab = self._create_preflight_tab()
        tabs.addTab(preflight_tab, "🛡️ Pre-Flight")

        history_tab = self._create_history_dashboard_tab()
        tabs.addTab(history_tab, "📜 History")

        health_tab = self._create_account_health_tab()
        tabs.addTab(health_tab, "💊 Accounts")

        presets_tab = self._create_presets_tab()
        tabs.addTab(presets_tab, "⚙️ Presets")
        
        # === Log Output (footer) ===
        log_footer = QWidget()
        log_footer.setObjectName("log_footer")
        log_footer.setStyleSheet("QWidget#log_footer { background-color: #1e1e1e; border-top: 1px solid #2d2d2d; }")
        log_layout = QVBoxLayout(log_footer)
        log_layout.setContentsMargins(24, 12, 24, 16)
        log_layout.setSpacing(8)
        
        # Log header row
        log_header = QHBoxLayout()
        log_header.setSpacing(12)
        
        log_title = QLabel("Log")
        log_title.setFont(QFont("Segoe UI", 10, QFont.Bold))
        log_title.setStyleSheet("color: #888888;")
        log_header.addWidget(log_title)
        log_header.addStretch()
        
        clear_btn = QPushButton("Clear")
        clear_btn.setObjectName("log_clear_btn")
        clear_btn.setStyleSheet("""
            QPushButton#log_clear_btn {
                background-color: transparent;
                border: 1px solid #3e3e3e;
                color: #888888;
                padding: 4px 12px;
                border-radius: 4px;
                font-size: 10px;
            }
            QPushButton#log_clear_btn:hover {
                background-color: #2d2d2d;
                color: #cccccc;
            }
        """)
        clear_btn.clicked.connect(lambda: self.log_output.clear())
        log_header.addWidget(clear_btn)
        
        save_btn = QPushButton("Save")
        save_btn.setObjectName("log_save_btn")
        save_btn.setStyleSheet("""
            QPushButton#log_save_btn {
                background-color: transparent;
                border: 1px solid #3e3e3e;
                color: #888888;
                padding: 4px 12px;
                border-radius: 4px;
                font-size: 10px;
            }
            QPushButton#log_save_btn:hover {
                background-color: #2d2d2d;
                color: #cccccc;
            }
        """)
        save_btn.clicked.connect(self.save_log)
        log_header.addWidget(save_btn)
        
        log_header.addLayout(log_header) if False else None
        log_layout.addLayout(log_header)
        
        self.log_output = QTextEdit()
        self.log_output.setObjectName("log_output")
        self.log_output.setReadOnly(True)
        self.log_output.setFont(QFont("JetBrains Mono", 10))
        self.log_output.setLineWrapMode(QTextEdit.NoWrap)
        self.log_output.setMaximumHeight(180)
        log_layout.addWidget(self.log_output)
        
        main_layout.addWidget(log_footer)
    
    def _create_stats_panel(self, main_layout):
        """Create real-time stats display panel (Phase 4)"""
        stats_container = QWidget()
        stats_container.setObjectName("stats_bar")
        stats_container.setStyleSheet("""
            QWidget#stats_bar {
                background-color: #1e1e1e;
                border-bottom: 1px solid #2d2d2d;
                padding: 10px 24px;
            }
        """)
        stats_layout = QHBoxLayout(stats_container)
        stats_layout.setContentsMargins(24, 10, 24, 10)
        stats_layout.setSpacing(12)
        
        # Stats pills - minimal label + value
        self.stats_cards = {}
        card_configs = [
            ("Fired", "stats_fired"),
            ("Success", "stats_success"),
            ("Failed", "stats_failed"),
            ("RPS", "stats_rps"),
            ("Rate", "stats_rate"),
            ("Avg", "stats_avg_ms"),
            ("Competition", "stats_competition"),
            ("Threads", "stats_threads"),
            ("Remaining", "stats_remaining"),
        ]
        
        for label_text, attr_name in card_configs:
            card = QWidget()
            card.setObjectName(f"stat_{attr_name}")
            card.setStyleSheet("""
                QWidget {
                    background-color: #252525;
                    border-radius: 6px;
                    padding: 6px 10px;
                }
            """)
            card_layout = QHBoxLayout(card)
            card_layout.setContentsMargins(8, 4, 8, 4)
            card_layout.setSpacing(6)
            
            lbl = QLabel(label_text)
            lbl.setFont(QFont("Segoe UI", 10, QFont.DemiBold))
            lbl.setStyleSheet("color: #666666;")
            card_layout.addWidget(lbl)
            
            sep = QLabel("/")
            sep.setStyleSheet("color: #444444;")
            card_layout.addWidget(sep)
            
            value = QLabel("---")
            value.setFont(QFont("Segoe UI", 10, QFont.Bold))
            value.setStyleSheet("color: #cccccc;")
            card_layout.addWidget(value)
            
            setattr(self, attr_name, value)
            self.stats_cards[attr_name] = value
            stats_layout.addWidget(card, stretch=0)
        
        stats_layout.addStretch()
        main_layout.addWidget(stats_container, stretch=0)
    
    def _update_stats_display(self, stats):
        """Update the stats panel with new values"""
        self.stats_fired.setText(str(stats.get("fired", 0)))
        self.stats_success.setText(str(stats.get("success", 0)))
        self.stats_failed.setText(str(stats.get("failed", 0)))
        self.stats_rps.setText(f"{stats.get('rps', 0)}")
        
        rate = stats.get("success_rate", 0)
        self.stats_rate.setText(f"{rate:.1f}%")
        
        avg_ms = stats.get("avg_response_ms", 0)
        self.stats_avg_ms.setText(f"{avg_ms:.0f}ms")
        
        competition = stats.get("competition", "---")
        emoji = {"LOW": "🟢", "MEDIUM": "🟡", "HIGH": "🟠", "EXTREME": "🔴"}.get(competition, "⚪")
        self.stats_competition.setText(f"{emoji} {competition}")
        
        self.stats_threads.setText(str(stats.get("active_threads", 0)))
        
        remaining = stats.get("time_remaining", 0)
        if remaining > 0:
            mins = int(remaining) // 60
            secs = int(remaining) % 60
            self.stats_remaining.setText(f"{mins}:{secs:02d}")
        else:
            elapsed = stats.get("elapsed", 0)
            mins = int(elapsed) // 60
            secs = int(elapsed) % 60
            self.stats_remaining.setText(f"FIRING {mins}:{secs:02d}")
    
    def _create_sniper_tab(self, title, timing_mode):
        """Create a sniper configuration tab"""
        tab = QWidget()
        layout = QVBoxLayout(tab)
        layout.setContentsMargins(24, 20, 24, 24)
        layout.setSpacing(20)
        
        # Input fields
        inputs_group = QGroupBox("Target")
        inputs_layout = QVBoxLayout()
        inputs_layout.setSpacing(12)
        
        # Username input
        username_layout = QVBoxLayout()
        username_label = QLabel("Username to snipe")
        username_label.setStyleSheet("color: #999999; font-size: 11px; margin-bottom: 4px;")
        username_layout.addWidget(username_label)
        self.username_input = QLineEdit()
        self.username_input.setPlaceholderText("Enter username (e.g., Eternal)")
        username_layout.addWidget(self.username_input)

        # Tool buttons row
        tools_row = QHBoxLayout()
        tools_row.setSpacing(6)

        lookup_btn = QPushButton("📜 History")
        lookup_btn.setToolTip("Look up name ownership history on NameMC")
        lookup_btn.setMaximumWidth(90)
        lookup_btn.clicked.connect(self._lookup_history_for_name)
        tools_row.addWidget(lookup_btn)

        pattern_btn = QPushButton("🔤 Expand Pattern")
        pattern_btn.setToolTip("Expand pattern like x{letter}x → axx, bxx, cxx...")
        pattern_btn.setMaximumWidth(110)
        pattern_btn.clicked.connect(self._expand_name_pattern)
        tools_row.addWidget(pattern_btn)

        status_btn = QPushButton("🌐 Check API")
        status_btn.setToolTip("Check Mojang API health before snipe")
        status_btn.setMaximumWidth(90)
        status_btn.clicked.connect(self._check_api_status)
        tools_row.addWidget(status_btn)

        username_layout.addLayout(tools_row)
        inputs_layout.addLayout(username_layout)
        
        # Drop time input
        time_layout = QVBoxLayout()
        time_label = QLabel("Drop time (UTC)")
        time_label.setStyleSheet("color: #999999; font-size: 11px; margin-bottom: 4px;")
        time_layout.addWidget(time_label)
        self.drop_time_input = QLineEdit()
        self.drop_time_input.setPlaceholderText("YYYY-MM-DDTHH:MM:SS.000Z (auto-filled below)")
        time_layout.addWidget(self.drop_time_input)

        # Timezone selector
        tz_label = QLabel("Your Timezone")
        tz_label.setStyleSheet("color: #999999; font-size: 11px; margin-bottom: 4px;")
        time_layout.addWidget(tz_label)

        self.tz_select = QComboBox()
        tz_options = [
            "CEST (Central European Summer, UTC+2)",
            "CET (Central European, UTC+1)",
            "EST (Eastern, UTC-5)",
            "EDT (Eastern Daylight, UTC-4)",
            "CST (Central, UTC-6)",
            "CDT (Central Daylight, UTC-5)",
            "PST (Pacific, UTC-8)",
            "PDT (Pacific Daylight, UTC-7)",
            "MST (Mountain, UTC-7)",
            "MDT (Mountain Daylight, UTC-6)",
            "GMT/BST (UTC+0 / UTC+1)",
            "JST (Japan, UTC+9)",
            "AEST (Australia East, UTC+10)",
            "IST (India, UTC+5:30)",
            "UTC",
        ]
        self.tz_select.addItems(tz_options)
        # Auto-detect system timezone
        try:
            import time as _time_mod
            tz_offset_seconds = -_time_mod.timezone if _time_mod.daylight == 0 else -_time_mod.altzone
            tz_offset_hours = tz_offset_seconds / 3600
            tz_name = _time_mod.tzname[1 if _time_mod.daylight and _time_mod.localtime().tm_isdst else 0]
            auto_index = self._match_tz_to_options(tz_offset_hours, tz_name)
            if auto_index >= 0:
                self.tz_select.setCurrentIndex(auto_index)
        except Exception:
            pass  # Fallback to first option (CEST)
        self.tz_select.setStyleSheet("padding: 4px; background: #1e1e1e; color: #ccc; border: 1px solid #444;")
        time_layout.addWidget(self.tz_select)

        # --- Timezone Converter Section ---
        conv_group = QGroupBox("⏱️ Timezone Converter")
        conv_layout = QVBoxLayout()
        conv_layout.setSpacing(6)

        # Input textarea
        conv_input_label = QLabel("Paste drop window text below:")
        conv_input_label.setStyleSheet("color: #999; font-size: 11px;")
        conv_layout.addWidget(conv_input_label)

        self.conv_input = QTextEdit()
        self.conv_input.setPlaceholderText("Drop Window\n6/28/2026 • 8:16:58 PM\n7/3/2026 • 9:19 AM\nTime Remaining\n8:58:22")
        self.conv_input.setFont(QFont("JetBrains Mono", 9))
        self.conv_input.setMaximumHeight(100)
        self.conv_input.setStyleSheet("background: #1a1a1a; color: #ccc; border: 1px solid #444; padding: 6px; border-radius: 4px;")
        conv_layout.addWidget(self.conv_input)

        # Convert button
        convert_btn = QPushButton("⏱️ Convert to UTC")
        convert_btn.setStyleSheet("padding: 8px; background: #3a7bd5; color: white; font-weight: bold; border-radius: 4px;")
        convert_btn.setToolTip("Convert pasted local times to UTC format")
        convert_btn.clicked.connect(self._convert_local_to_utc)
        conv_layout.addWidget(convert_btn)

        # Output textarea
        conv_output_label = QLabel("Converted UTC output:")
        conv_output_label.setStyleSheet("color: #999; font-size: 11px;")
        conv_layout.addWidget(conv_output_label)

        self.conv_output = QTextEdit()
        self.conv_output.setPlaceholderText("Converted times will appear here...")
        self.conv_output.setFont(QFont("JetBrains Mono", 9))
        self.conv_output.setMaximumHeight(100)
        self.conv_output.setReadOnly(True)
        self.conv_output.setStyleSheet("background: #0d1117; color: #58a6ff; border: 1px solid #30363d; padding: 6px; border-radius: 4px;")
        conv_layout.addWidget(self.conv_output)

        # Copy button row
        copy_row = QHBoxLayout()
        copy_btn = QPushButton("📋 Copy First UTC to Clipboard")
        copy_btn.setStyleSheet("padding: 4px 8px; background: #2d2d2d; color: #ccc; border: 1px solid #555; border-radius: 3px;")
        copy_btn.clicked.connect(self._copy_first_utc)
        copy_row.addWidget(copy_btn)

        use_btn = QPushButton("🎯 Use First UTC as Drop Time")
        use_btn.setStyleSheet("padding: 4px 8px; background: #1a6b2a; color: white; border: 1px solid #2d8a3e; border-radius: 3px;")
        use_btn.clicked.connect(self._use_first_utc_as_drop)
        copy_row.addWidget(use_btn)
        conv_layout.addLayout(copy_row)

        conv_group.setLayout(conv_layout)
        inputs_layout.addWidget(conv_group)

        # Also keep a small lookup button
        lookup_btn = QPushButton("🔍 Lookup Name")
        lookup_btn.setToolTip("Look up name history & availability")
        lookup_btn.setStyleSheet("padding: 6px; background: #2d2d2d; color: #ccc; border: 1px solid #555; border-radius: 3px;")
        lookup_btn.clicked.connect(self._fetch_droptime_for_names)
        time_layout.addWidget(lookup_btn)
        inputs_layout.addLayout(time_layout)
        
        inputs_group.setLayout(inputs_layout)
        layout.addWidget(inputs_group)
        
        # Tokens input (multi-line)
        tokens_group = QGroupBox("OAuth Tokens")
        tokens_layout = QVBoxLayout()
        tokens_layout.setSpacing(8)
        self.tokens_input = QTextEdit()
        self.tokens_input.setPlaceholderText("Paste tokens here, one per line...")
        self.tokens_input.setFont(QFont("JetBrains Mono", 9))
        self.tokens_input.setMaximumHeight(120)
        tokens_layout.addWidget(self.tokens_input)
        tokens_group.setLayout(tokens_layout)
        layout.addWidget(tokens_group)
        
        # Auto-Auth section
        autoauth_group = QGroupBox("Auto-Auth")
        autoauth_layout = QVBoxLayout()
        autoauth_layout.setSpacing(10)
        
        self.autoauth_checkbox = QCheckBox("Auto-authenticate from credentials")
        self.autoauth_checkbox.setChecked(False)
        self.autoauth_checkbox.setToolTip("Load email:password from file, authenticate automatically 5 mins before each drop")
        autoauth_layout.addWidget(self.autoauth_checkbox)
        
        account_file_row = QHBoxLayout()
        account_file_row.setSpacing(8)
        self.account_file_input = QLineEdit()
        self.account_file_input.setPlaceholderText("accounts.txt (email:password or bearer tokens)")
        self.account_file_input.setText("accounts.txt")
        account_file_row.addWidget(self.account_file_input, stretch=1)
        browse_btn = QPushButton("Browse")
        browse_btn.clicked.connect(self._browse_account_file)
        account_file_row.addWidget(browse_btn)
        autoauth_layout.addLayout(account_file_row)
        
        self.auto_refresh_checkbox = QCheckBox("Auto-refresh tokens 5min before each drop")
        self.auto_refresh_checkbox.setChecked(False)
        self.auto_refresh_checkbox.setToolTip("Re-authenticate silently before every snipe window")
        autoauth_layout.addWidget(self.auto_refresh_checkbox)
        
        self.save_tokens_checkbox = QCheckBox("Save tokens to file after authentication")
        self.save_tokens_checkbox.setChecked(True)
        self.save_tokens_checkbox.setToolTip("Cache authenticated tokens for reuse (skip OAuth next time)")
        autoauth_layout.addWidget(self.save_tokens_checkbox)
        
        autoauth_group.setLayout(autoauth_layout)
        layout.addWidget(autoauth_group)
        
        # Advanced settings
        advanced_group = QGroupBox("Advanced")
        advanced_layout = QVBoxLayout()
        advanced_layout.setSpacing(12)
        
        # Threads per token
        threads_row = QHBoxLayout()
        threads_row.setSpacing(8)
        threads_label = QLabel("Threads per token")
        threads_label.setStyleSheet("color: #999999; font-size: 11px;")
        threads_row.addWidget(threads_label)
        threads_row.addStretch()
        self.threads_spin = QSpinBox()
        self.threads_spin.setRange(1, 100)
        self.threads_spin.setValue(10)
        threads_row.addWidget(self.threads_spin)
        advanced_layout.addLayout(threads_row)
        
        # Priority mode
        priority_row = QHBoxLayout()
        priority_row.setSpacing(8)
        priority_label = QLabel("Token priority")
        priority_label.setStyleSheet("color: #999999; font-size: 11px;")
        priority_row.addWidget(priority_label)
        priority_row.addStretch()
        self.priority_combo = QComboBox()
        self.priority_combo.addItems(["Fresh Tokens First", "Original Order"])
        priority_row.addWidget(self.priority_combo)
        advanced_layout.addLayout(priority_row)
        
        # Warm-up
        self.warmup_checkbox = QCheckBox("Token Warm-Up (pre-warms 30s before drop)")
        self.warmup_checkbox.setChecked(True)
        self.warmup_checkbox.setToolTip("Pre-warms tokens 30s before drop to keep cache fresh")
        advanced_layout.addWidget(self.warmup_checkbox)
        
        warmup_row = QHBoxLayout()
        warmup_row.setSpacing(8)
        warmup_label = QLabel("Warm-up duration")
        warmup_label.setStyleSheet("color: #999999; font-size: 11px;")
        warmup_row.addWidget(warmup_label)
        warmup_row.addStretch()
        self.warmup_duration_spin = QSpinBox()
        self.warmup_duration_spin.setRange(10, 60)
        self.warmup_duration_spin.setValue(30)
        self.warmup_duration_spin.setSuffix("s")
        warmup_row.addWidget(self.warmup_duration_spin)
        advanced_layout.addLayout(warmup_row)
        
        # Adaptive throttling
        self.adaptive_checkbox = QCheckBox("Adaptive Throttling (auto-adjusts based on competition)")
        self.adaptive_checkbox.setChecked(True)
        self.adaptive_checkbox.setToolTip("Dynamically adjust thread count and delays based on competition and error rates")
        advanced_layout.addWidget(self.adaptive_checkbox)
        
        # Aggressiveness slider
        agg_row = QHBoxLayout()
        agg_row.setSpacing(8)
        agg_label = QLabel("Aggressiveness")
        agg_label.setStyleSheet("color: #999999; font-size: 11px;")
        agg_row.addWidget(agg_label)
        agg_row.addStretch()
        self.aggressiveness_slider = QSlider(Qt.Horizontal)
        self.aggressiveness_slider.setRange(1, 10)
        self.aggressiveness_slider.setValue(7)
        self.aggressiveness_slider.setMinimumWidth(120)
        self.aggressiveness_slider.setToolTip("1 = Conservative, 10 = Extreme")
        agg_row.addWidget(self.aggressiveness_slider)
        self.aggressiveness_label = QLabel("7")
        self.aggressiveness_label.setMinimumWidth(24)
        self.aggressiveness_label.setAlignment(Qt.AlignCenter)
        self.aggressiveness_label.setStyleSheet("color: #3a7bd5; font-weight: 600;")
        agg_row.addWidget(self.aggressiveness_label)
        advanced_layout.addLayout(agg_row)
        self.aggressiveness_slider.valueChanged.connect(lambda v: self.aggressiveness_label.setText(str(v)))
        
        # Feature toggles
        self.fingerprint_checkbox = QCheckBox("Fingerprint Rotation (evades rate limiting)")
        self.fingerprint_checkbox.setChecked(True)
        self.fingerprint_checkbox.setToolTip("Rotate User-Agent per request")
        advanced_layout.addWidget(self.fingerprint_checkbox)
        
        self.token_rotation_checkbox = QCheckBox("Token Rotation on 429 (auto-skips poisoned tokens)")
        self.token_rotation_checkbox.setChecked(True)
        self.token_rotation_checkbox.setToolTip("Automatically skip rate-limited tokens")
        advanced_layout.addWidget(self.token_rotation_checkbox)
        
        self.temporal_jitter_checkbox = QCheckBox("Temporal Jitter (±3ms randomization)")
        self.temporal_jitter_checkbox.setChecked(True)
        self.temporal_jitter_checkbox.setToolTip("Add microsecond-level timing randomization")
        advanced_layout.addWidget(self.temporal_jitter_checkbox)
        
        self.response_analysis_checkbox = QCheckBox("Response Analysis && Early Abort")
        self.response_analysis_checkbox.setChecked(True)
        self.response_analysis_checkbox.setToolTip("Analyze responses in real-time and abort early if outcome is determined")
        advanced_layout.addWidget(self.response_analysis_checkbox)
        
        self.offset_calibration_checkbox = QCheckBox("📐 Auto-Offset Calibration v2 (10 probes, TLS-aware, P95)")
        self.offset_calibration_checkbox.setChecked(True)
        self.offset_calibration_checkbox.setToolTip("10 probes × 12s apart. Measures cold TLS, warm TTFB, jitter. Uses P95 latency + half-jitter buffer.")
        advanced_layout.addWidget(self.offset_calibration_checkbox)

        # === NTP CLOCK SYNC CHECK ===
        self.ntp_sync_checkbox = QCheckBox("🕐 NTP Clock Sync (verify system clock accuracy)")
        self.ntp_sync_checkbox.setChecked(True)
        self.ntp_sync_checkbox.setToolTip("Query NTP servers before snipe to detect clock drift >50ms")
        self.ntp_sync_checkbox.setStyleSheet("color: #9b59b6; font-weight: 600;")
        advanced_layout.addWidget(self.ntp_sync_checkbox)

        # === FIRE-AND-FORGET MODE ===
        self.fire_and_forget_checkbox = QCheckBox("🔥 Fire-and-Forget (send requests without waiting for response)")
        self.fire_and_forget_checkbox.setChecked(False)
        self.fire_and_forget_checkbox.setToolTip("Fire requests without waiting for responses — faster but no real-time feedback")
        self.fire_and_forget_checkbox.setStyleSheet("color: #e74c3c; font-weight: 600;")
        advanced_layout.addWidget(self.fire_and_forget_checkbox)

        # === QUICK WIN: DRY-RUN / SIMULATION MODE ===
        self.dry_run_checkbox = QCheckBox("🧪 Dry-Run / Simulation Mode (test timing without firing)")
        self.dry_run_checkbox.setChecked(False)
        self.dry_run_checkbox.setToolTip("Simulate the snipe: run all timing logic but DO NOT send the actual name change request")
        self.dry_run_checkbox.setStyleSheet("color: #e67e22; font-weight: 600;")
        advanced_layout.addWidget(self.dry_run_checkbox)

        # === QUICK WIN: PRE-SNIPE TOKEN VALIDATION ===
        self.pre_validate_checkbox = QCheckBox("Pre-Snipe Token Validation (filter invalid tokens before firing)")
        self.pre_validate_checkbox.setChecked(True)
        self.pre_validate_checkbox.setToolTip("Validate all tokens against MC API before snipe starts — removes dead tokens from the pool")
        advanced_layout.addWidget(self.pre_validate_checkbox)

        # === QUICK WIN: AUTO-DETECT ACCOUNT TYPE ===
        self.auto_detect_account_type_checkbox = QCheckBox("Auto-Detect Account Type (GC / MS / Mojang)")
        self.auto_detect_account_type_checkbox.setChecked(True)
        self.auto_detect_account_type_checkbox.setToolTip("Automatically detect whether tokens are Game Center, Microsoft, or legacy Mojang")
        advanced_layout.addWidget(self.auto_detect_account_type_checkbox)

        advanced_group.setLayout(advanced_layout)
        layout.addWidget(advanced_group)
        
        # Progress bar
        self.progress_bar = QProgressBar()
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(True)
        layout.addWidget(self.progress_bar)
        
        # Start/Stop buttons row
        buttons_row = QHBoxLayout()
        buttons_row.setSpacing(8)
        
        self.start_btn = QPushButton("Start Snipe")
        self.start_btn.setObjectName("start_snipe_btn")
        self.start_btn.setStyleSheet("""
            QPushButton#start_snipe_btn {
                background-color: #3a7bd5;
                color: white;
                border: none;
                border-radius: 6px;
                padding: 10px 20px;
                font-weight: 600;
                font-size: 11px;
            }
            QPushButton#start_snipe_btn:hover {
                background-color: #4a8be5;
            }
            QPushButton#start_snipe_btn:pressed {
                background-color: #2d6ab5;
            }
        """)
        self.start_btn.clicked.connect(lambda: self.start_sniper(timing_mode))
        buttons_row.addWidget(self.start_btn)
        
        self.stop_btn = QPushButton("Stop")
        self.stop_btn.setObjectName("stop_snipe_btn")
        self.stop_btn.setStyleSheet("""
            QPushButton#stop_snipe_btn {
                background-color: #dc3545;
                color: white;
                border: none;
                border-radius: 6px;
                padding: 10px 20px;
                font-weight: 600;
                font-size: 11px;
            }
            QPushButton#stop_snipe_btn:hover {
                background-color: #e04a5a;
            }
            QPushButton#stop_snipe_btn:pressed {
                background-color: #b52a38;
            }
        """)
        self.stop_btn.clicked.connect(self.stop_sniper)
        buttons_row.addWidget(self.stop_btn)
        
        load_tokens_btn = QPushButton("Load Tokens")
        load_tokens_btn.clicked.connect(self.load_tokens_from_file)
        buttons_row.addWidget(load_tokens_btn)
        
        buttons_row.addStretch()
        layout.addLayout(buttons_row)
        
        layout.addStretch()
        
        # Store timing mode in tab
        tab.timing_mode = timing_mode
        
        return tab
    
    def _log(self, message):
        """Add message to log output"""
        timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
        
        # Color code important messages
        if "SUCCESS" in message or "🏆" in message:
            self.log_output.append(f"<span style='color: #00ff00; font-weight: bold;'>[{timestamp}] {message}</span>")
        elif "ERROR" in message or "❌" in message:
            self.log_output.append(f"<span style='color: #ff0000;'>[{timestamp}] {message}</span>")
        elif "FIRE" in message or "🎯" in message:
            self.log_output.append(f"<span style='color: #ff9800; font-weight: bold;'>[{timestamp}] {message}</span>")
        elif "AUTH" in message:
            self.log_output.append(f"<span style='color: #9c27b0; font-weight: bold;'>[{timestamp}] {message}</span>")
        elif "📐" in message or "CALIBRATION" in message or "CALIBRATED" in message:
            self.log_output.append(f"<span style='color: #3a7bd5;'>[{timestamp}] {message}</span>")
        else:
            self.log_output.append(f"[{timestamp}] {message}")
        
        self.log_output.verticalScrollBar().setValue(self.log_output.verticalScrollBar().maximum())
        
        # Write to log file
        self._write_log_file(f"[{timestamp}] {message}")
    
    def _write_log_file(self, message):
        """Append message to rotating log file"""
        try:
            log_dir = "logs"
            import os
            if not os.path.exists(log_dir):
                os.makedirs(log_dir)
            
            date_str = datetime.now().strftime("%Y-%m-%d")
            log_file = os.path.join(log_dir, f"sniper_{date_str}.log")
            
            with open(log_file, 'a', encoding='utf-8') as f:
                f.write(message + '\n')
        except Exception:
            pass  # Silent fail for file logging
    
    def _browse_account_file(self):
        """Open file dialog to select accounts file"""
        filepath, _ = QFileDialog.getOpenFileName(self, "Select Accounts File", "", "Text Files (*.txt);;All Files (*)")
        if filepath:
            self.account_file_input.setText(filepath)
    
    def _authenticate_accounts_if_needed(self, tokens_text: str, log_callback=None) -> list:
        """
        Auto-authenticate from email:password if enabled
        Returns: list of bearer tokens
        """
        log = log_callback or self._log
        
        # Check if auto-auth is enabled
        if not self.autoauth_checkbox.isChecked():
            return [t.strip() for t in tokens_text.split('\n') if t.strip()]
        
        # Load accounts from file
        account_file = self.account_file_input.text().strip()
        if not account_file:
            log("⚠️ Auto-auth enabled but no account file specified!")
            QMessageBox.warning(self, "Warning", "Please specify an accounts file for auto-auth!")
            return []
        
        log(f"🤖 Auto-auth: Loading accounts from {account_file}")
        accounts = load_accounts_from_file(account_file)
        
        if not accounts:
            log("❌ No accounts found in file!")
            QMessageBox.critical(self, "Error", f"No valid accounts found in {account_file}")
            return []
        
        log(f"📊 Found {len(accounts)} account(s) to authenticate")
        
        # Authenticate each account
        tokens_dict = authenticate_accounts(accounts, log_callback=log)
        
        if not tokens_dict:
            log("❌ All authentications failed!")
            QMessageBox.critical(self, "Error", "All account authentications failed!")
            return []
        
        # Save tokens if requested
        if self.save_tokens_checkbox.isChecked():
            import os
            tokens_file = account_file.rsplit('.', 1)[0] + '_tokens.txt'
            save_tokens_to_file(tokens_dict, tokens_file)
            log(f"💾 Saved {len(tokens_dict)} authenticated token(s) to {tokens_file}")
        
        # Return token list
        return list(tokens_dict.values())
    
    def start_sniper(self, timing_mode):
        """Start the sniper with current settings"""
        # Get current tab's inputs
        current_tab = self.centralWidget().findChild(QTabWidget).currentWidget()
        
        username = self.username_input.text().strip()
        drop_time = self.drop_time_input.text().strip()
        tokens_text = self.tokens_input.toPlainText()
        threads_count = self.threads_spin.value()
        priority_mode = "fresh_first" if self.priority_combo.currentText() == "Fresh Tokens First" else "original"
        
        # Validate inputs
        if not username:
            QMessageBox.warning(self, "Error", "Please enter a username to snipe!")
            return
        
        if not drop_time:
            QMessageBox.warning(self, "Error", "Please enter a drop time (UTC)!")
            return
        
        # === FEATURE #9: AUTO-AUTH=***
        if self.autoauth_checkbox.isChecked():
            # Authenticate from credentials
            tokens = self._authenticate_accounts_if_needed(tokens_text, log_callback=self._log)
        else:
            # Use manual tokens
            tokens = [t.strip() for t in tokens_text.split('\n') if t.strip()]
        
        if not tokens:
            QMessageBox.warning(self, "Error", "No valid tokens available!")
            return
        
        # Stop any existing workers
        self.stop_sniper()
        
        # Update status
        self.status_label.setText("🔥 SNIPE ACTIVE - GOOD LUCK!")
        self.status_label.setStyleSheet("padding: 5px; background-color: #3a7bd5; color: white; border-radius: 3px;")
        
        # Create and start worker
        self._log(f"🚀 Starting {timing_mode} sniper...")
        
        # Get warm-up settings
        warmup_enabled = self.warmup_checkbox.isChecked()
        warmup_duration = self.warmup_duration_spin.value()
        
        # === FEATURE #5: Get adaptive throttling settings ===
        adaptive_enabled = self.adaptive_checkbox.isChecked()
        aggressiveness = self.aggressiveness_slider.value()
        
        # === FEATURE #6: Get fingerprint rotation settings ===
        fingerprint_rotation = self.fingerprint_checkbox.isChecked()
        
         # === FEATURE #8: Get token rotation settings ===
        token_rotation = self.token_rotation_checkbox.isChecked()
        
        # === FEATURE #13: Get temporal jitter settings ===
        temporal_jitter = self.temporal_jitter_checkbox.isChecked()
        
        # === FEATURE #15: Get response analysis settings ===
        response_analysis = self.response_analysis_checkbox.isChecked()
        
        # === FEATURE #17: Get offset calibration settings ===
        offset_calibration = self.offset_calibration_checkbox.isChecked()
        
        # === FEATURE: Get webhook URL ===
        webhook_url = self.webhook_url_input.text().strip() if hasattr(self, 'webhook_url_input') and self.webhook_enable_checkbox.isChecked() else None
        
        # === FEATURE: Auto-restart watchdog ===
        auto_restart_enabled = getattr(self, 'watchdog_enable_checkbox', None) and self.watchdog_enable_checkbox.isChecked()
        auto_restart_max = self.watchdog_max_restarts_spin.value() if hasattr(self, 'watchdog_max_restarts_spin') else 3
        
        # Support multi-name (comma-separated)
        names = [n.strip() for n in username.split(',') if n.strip()]
        multi_name_mode = "sequential" if len(names) > 1 else "single"
        stagger_seconds = 0.1 if len(names) > 1 else 0
        
        worker = SniperWorker(
            names, tokens, drop_time, threads_count, timing_mode, priority_mode,
            warmup_enabled, warmup_duration, adaptive_enabled, aggressiveness,
            fingerprint_rotation, token_rotation, temporal_jitter, response_analysis,
            multi_name_mode, stagger_seconds, offset_calibration_enabled=offset_calibration,
            webhook_url=webhook_url,
            auto_restart_enabled=auto_restart_enabled,
            auto_restart_max=auto_restart_max,
            skin_enabled=getattr(self, 'skin_enable_checkbox', None) and self.skin_enable_checkbox.isChecked(),
            skin_file_path=getattr(self, 'skin_file_input', None) and self.skin_file_input.text().strip(),
            skin_model=getattr(self, 'skin_model_combo', None) and self.skin_model_combo.currentText(),
            desktop_notifications_enabled=True,
            fire_and_forget=getattr(self, 'fire_and_forget_checkbox', None) and self.fire_and_forget_checkbox.isChecked(),
            dry_run=getattr(self, 'dry_run_checkbox', None) and self.dry_run_checkbox.isChecked(),
            pre_validate_tokens=getattr(self, 'pre_validate_checkbox', None) and self.pre_validate_checkbox.isChecked(),
            auto_detect_account_type=getattr(self, 'auto_detect_account_type_checkbox', None) and self.auto_detect_account_type_checkbox.isChecked(),
            ntp_sync_enabled=getattr(self, 'ntp_sync_checkbox', None) and self.ntp_sync_checkbox.isChecked(),
        )
        worker.log_signal.connect(self._log)
        worker.progress_signal.connect(self.progress_bar.setValue)
        worker.stats_signal.connect(self._update_stats_display)
        worker.finished.connect(lambda: self.on_sniper_finished())
        worker.start()
        self.workers.append(worker)
        
        # === Start watchdog if enabled ===
        if auto_restart_enabled:
            self._start_watchdog(worker)
        
        self._log(f"✅ Sniper initialized! Username: {username}, Tokens: {len(tokens)}, Threads: {threads_count}")
        self._log(f"⏱️ Waiting for drop time: {drop_time}")
        if warmup_enabled:
            self._log(f"🔥 Token warm-up: ENABLED ({warmup_duration}s before drop)")
        else:
            self._log(f"⚠️ Token warm-up: DISABLED")
        if adaptive_enabled:
            self._log(f"📊 Adaptive throttling: ENABLED (Aggressiveness: {aggressiveness}/10)")
        else:
            self._log(f"⚠️ Adaptive throttling: DISABLED")
        if fingerprint_rotation:
            self._log(f"🎭 Fingerprint rotation: ENABLED")
        else:
            self._log(f"⚠️ Fingerprint rotation: DISABLED")
        if token_rotation:
            self._log(f"🔄 Token rotation on rate limit: ENABLED")
        else:
            self._log(f"⚠️ Token rotation on rate limit: DISABLED")
        if temporal_jitter:
            self._log(f"⚡ Temporal jitter: ENABLED (±3ms window)")
        else:
            self._log(f"⚠️ Temporal jitter: DISABLED")
        if response_analysis:
            self._log(f"📊 Response analysis & early abort: ENABLED")
        else:
            self._log(f"⚠️ Response analysis: DISABLED")
        if offset_calibration:
            self._log(f"📐 Auto-offset calibration: ENABLED (5 probes, 30s apart)")
        else:
            self._log(f"⚠️ Auto-offset calibration: DISABLED")
        if auto_restart_enabled:
            self._log(f"🛡️ Auto-restart watchdog: ENABLED (max {auto_restart_max} restarts)")
        else:
            self._log(f"⚠️ Auto-restart watchdog: DISABLED")
        if getattr(self, 'skin_enable_checkbox', None) and self.skin_enable_checkbox.isChecked():
            self._log(f"🎨 Auto skin upload: ENABLED ({self.skin_file_input.text().strip()})")
        else:
            self._log(f"⚠️ Auto skin upload: DISABLED")
    
    def stop_sniper(self):
        """Stop all running snipers"""
        self._log("⏹️ Stopping all snipers...")
        
        # Stop watchdog if running
        if hasattr(self, 'watchdog') and self.watchdog.running:
            self.watchdog.stop_monitoring()
            self.watchdog_status_label.setText("⏸️ Inactive")
            self.watchdog_status_label.setStyleSheet("color: #888; padding: 8px; font-size: 12px;")
        
        for worker in self.workers:
            worker.stop_event.set()
            worker.wait(3000)  # Wait up to 3 seconds
        self.workers = []
        self._log("✅ All snipers stopped")
        self.status_label.setText("Stopped")
        self.status_label.setStyleSheet("padding: 5px; background-color: #dc3545; color: white; border-radius: 3px;")
    
    def on_sniper_finished(self):
        """Called when sniper completes"""
        if self.success_event.is_set() if hasattr(self, 'success_event') else False:
            self.status_label.setText("🏆 SUCCESS! NAME CLAIMED!")
            self.status_label.setStyleSheet("padding: 5px; background-color: #3a7bd5; color: white; border-radius: 3px;")
        else:
            self.status_label.setText("Completed")
            self.status_label.setStyleSheet("padding: 5px; background-color: #3a7bd5; color: white; border-radius: 3px;")

    def _match_tz_to_options(self, offset_hours, tz_name):
        """Match detected system timezone offset + name to a combo box option index."""
        # (offset, name_keywords) -> index in tz_options list
        candidates = [
            (2, ["CEST", "Central European"]),
            (1, ["CET"]),
            (-5, ["EST", "Eastern"]),
            (-4, ["EDT"]),
            (-6, ["CST", "Central"]),
            (-5, ["CDT"]),
            (-8, ["PST", "Pacific"]),
            (-7, ["PDT", "MST"]),
            (-6, ["MDT"]),
            (0, ["GMT", "UTC"]),
            (1, ["BST"]),
            (9, ["JST"]),
            (10, ["AEST"]),
            (5.5, ["IST"]),
            (0, []),  # UTC fallback (last in list)
        ]
        tz_upper = tz_name.upper() if tz_name else ""
        for i, (off, keywords) in enumerate(candidates):
            if off == offset_hours:
                # If keywords match the name, great
                if keywords and any(kw.upper() in tz_upper for kw in keywords):
                    return i
                # Even without name match, offset match is good
                if not keywords or tz_upper == "":
                    return i
        # Fallback: just match offset
        for i, (off, _) in enumerate(candidates):
            if off == offset_hours:
                return i
        return -1

    def _get_tz_offset_hours(self):
        """Get the UTC offset in hours from the selected timezone."""
        tz_text = self.tz_select.currentText()
        # Map timezone names to UTC offset hours
        tz_map = {
            "CEST": 2, "CET": 1,
            "EST": -5, "EDT": -4,
            "CST": -6, "CDT": -5,
            "PST": -8, "PDT": -7,
            "MST": -7, "MDT": -6,
            "GMT": 0, "BST": 1,
            "JST": 9,
            "AEST": 10,
            "IST": 5.5,
            "UTC": 0,
        }
        for key, offset in tz_map.items():
            if key in tz_text:
                return offset
        return 0  # default UTC

    def _convert_local_to_utc(self):
        """Parse drop window text from the converter input box and show UTC output."""
        from datetime import datetime, timedelta, timezone
        import re
        import traceback
        from PyQt5.QtWidgets import QMessageBox

        try:
            # Get text from the converter input box
            raw_text = self.conv_input.toPlainText().strip()

            if not raw_text:
                self.conv_output.setPlainText("⚠️ Paste drop window text in the input box first!")
                try: self._log("[!] Converter input is empty — paste your drop window text first")
                except: pass
                return

            try: self._log(f"[⏱️] Converting (timezone: {self.tz_select.currentText()})...")
            except: pass
            try: self._log(f"[DEBUG] Raw input: {repr(raw_text[:100])}")
            except: pass

            tz_offset = self._get_tz_offset_hours()
            local_tz = timezone(timedelta(hours=tz_offset))
            tz_short = self.tz_select.currentText().split("(")[0].strip()

            # Clean Discord emoji artifacts like <:18:1416478902121009323>
            cleaned = re.sub(r'<:[^>]+>', ':', raw_text)
            cleaned = cleaned.replace('::', ':')
            try: self._log(f"[DEBUG] Cleaned: {repr(cleaned[:100])}")
            except: pass

            # Pattern: "6/28/2026 • 8:16:58 PM" or "7/3/2026 • 9:19 AM"
            pattern = r'(\d{1,2}/\d{1,2}/\d{4})\s*[•·\-]\s*(\d{1,2}:\d{2}(?::\d{2})?\s*[AP]M)'
            matches = re.findall(pattern, cleaned, re.IGNORECASE)
            try: self._log(f"[DEBUG] Pattern 1 matches: {matches}")
            except: pass

            if not matches:
                # Try alternate pattern
                pattern2 = r'(\w+\s+\d{1,2},?\s+\d{4})\s+(?:at\s+)?(\d{1,2}:\d{2}(?::\d{2})?\s*[AP]M)'
                matches = re.findall(pattern2, cleaned, re.IGNORECASE)
                try: self._log(f"[DEBUG] Pattern 2 matches: {matches}")
                except: pass
                if not matches:
                    self.conv_output.setPlainText("❌ Could not parse any datetime\n\nExpected format:\n  6/28/2026 • 8:16:58 PM\n  7/3/2026 • 9:19 AM")
                    try: self._log("[!] Could not parse any datetime from the text")
                    except: pass
                    return

            converted = []
            output_lines = []

            for date_str, time_str in matches:
                try:
                    # Handle times without seconds
                    time_clean = time_str.strip()
                    parts = time_clean.replace("AM", "").replace("PM", "").strip().split(":")
                    if len(parts) == 2:
                        time_clean = time_clean.replace(parts[1], parts[1] + ":00")

                    dt_local = datetime.strptime(f"{date_str} {time_clean}", "%m/%d/%Y %I:%M:%S %p")
                    if dt_local.year < 2024:
                        dt_local = dt_local.replace(year=datetime.now().year)

                    dt_local_tz = dt_local.replace(tzinfo=local_tz)
                    dt_utc = dt_local_tz.astimezone(timezone.utc)
                    utc_str = dt_utc.strftime("%Y-%m-%dT%H:%M:%S.000Z")
                    local_display = dt_local.strftime("%m/%d/%Y %I:%M:%S %p")

                    converted.append((local_display, utc_str, dt_utc))
                    output_lines.append(f"{local_display} {tz_short} → {utc_str}")

                except Exception as e:
                    output_lines.append(f"❌ Failed: '{date_str} {time_str}' — {e}")

            # Build output
            if converted:
                if len(converted) > 1:
                    window_hours = (converted[-1][2] - converted[0][2]).total_seconds() / 3600
                    output_lines.append("")
                    output_lines.append(f"⏱️ Drop window: {window_hours:.1f} hours")

                output_lines.append("")
                output_lines.append(f"📋 First UTC: {converted[0][1]}")

                self.conv_output.setPlainText("\n".join(output_lines))

                # Store for copy/use buttons
                self._converted_utc_list = [c[1] for c in converted]

                # Also auto-fill the drop time
                self.drop_time_input.setText(converted[0][1])

                try:
                    self._log(f"[✅] Converted {len(converted)} time(s), drop time set to: {converted[0][1]}")
                    for line in output_lines:
                        self._log(f"  {line}")
                except: pass
            else:
                self.conv_output.setPlainText("❌ No valid datetimes found")

        except Exception as e:
            error_msg = f"❌ Converter error: {e}\n\n{traceback.format_exc()}"
            self.conv_output.setPlainText(error_msg[:500])
            try:
                self._log(f"[❌] Converter crashed: {e}")
                self._log(traceback.format_exc())
            except: pass
            # Always show a popup so you can't miss it
            QMessageBox.critical(self, "Converter Error", f"The timezone converter failed:\n\n{e}")

    def _copy_first_utc(self):
        """Copy the first converted UTC time to clipboard."""
        clipboard = QApplication.clipboard()
        if hasattr(self, '_converted_utc_list') and self._converted_utc_list:
            clipboard.setText(self._converted_utc_list[0])
            self._log(f"[📋] Copied to clipboard: {self._converted_utc_list[0]}")
        else:
            # Try from drop_time_input as fallback
            txt = self.drop_time_input.text().strip()
            if txt:
                clipboard.setText(txt)
                self._log(f"[📋] Copied to clipboard: {txt}")
            else:
                self._log("[!] Nothing to copy — convert a time first")

    def _use_first_utc_as_drop(self):
        """Set the first converted UTC time as the drop time."""
        if hasattr(self, '_converted_utc_list') and self._converted_utc_list:
            self.drop_time_input.setText(self._converted_utc_list[0])
            self._log(f"[🎯] Drop time set to: {self._converted_utc_list[0]}")
        else:
            self._log("[!] Nothing to use — convert a time first")

    def _fetch_droptime_for_names(self):
        """Fetch droptime from playerdb.co + Mojang for names in the name input."""
        import sys
        import traceback
        try:
            names_text = self.username_input.text().strip()
            if not names_text:
                self._log("[!] Please enter at least one username first")
                self._log("[DEBUG] Button was clicked but no name entered")
                return

            names = [n.strip() for n in names_text.split(',') if n.strip()]
            self._log(f"[*] Fetching droptime for {len(names)} name(s)...")
            self._log(f"[DEBUG] Names parsed: {names}")

            from datetime import datetime, timedelta, timezone

            for i, name in enumerate(names[:5]):  # Limit to first 5 names
                self._log(f"[*] Checking '{name}'...")
                self._log(f"[DEBUG] About to call playerdb for '{name}'")
                QApplication.processEvents()

                # Source 1: playerdb.co — name change history with timestamps
                found_droptime = False
                try:
                    self._log(f"[DEBUG] Starting playerdb request for '{name}'...")
                    sys.stdout.flush()
                    resp = requests.get(f"https://playerdb.co/api/player/minecraft/{name}", timeout=8)
                    self._log(f"[DEBUG] playerdb responded with status {resp.status_code}")
                    if resp.status_code == 200:
                        data = resp.json()
                        if data.get("success"):
                            history = data["data"]["player"].get("name_history", [])
                            if history:
                                last = history[-1]
                                ts = last.get("changedToAt")
                                if ts:
                                    last_dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
                                    drop = last_dt + timedelta(days=60)
                                    now = datetime.now(timezone.utc)
                                    found_droptime = True
                                    if drop > now:
                                        delta = drop - now
                                        self._log(
                                            f"  [📊] '{name}': last changed {last_dt.strftime('%Y-%m-%d %H:%M UTC')} "
                                            f"→ drops ~{drop.strftime('%Y-%m-%d %H:%M UTC')} "
                                            f"(in {delta.days}d {delta.seconds//3600}h {(delta.seconds%3600)//60}m)"
                                        )
                                    else:
                                        self._log(f"  [📊] '{name}': cooldown expired — may be available")
                                    if not self.drop_time_input.text():
                                        self.drop_time_input.setText(drop.strftime("%Y-%m-%dT%H:%M:%S.000Z"))
                            else:
                                self._log(f"  [📊] '{name}': no name history on playerdb")
                        else:
                            self._log(f"  [📊] '{name}': playerdb returned success=False")
                except requests.exceptions.Timeout:
                    self._log(f"  [📊] playerdb TIMEOUT for '{name}' — server may be slow")
                except Exception as e:
                    self._log(f"  [📊] playerdb failed for '{name}': {str(e)[:80]}")
                    self._log(f"[DEBUG] Traceback: {traceback.format_exc()[:200]}")

                # Source 2: Mojang name history API (more reliable for name changes)
                if not found_droptime:
                    try:
                        self._log(f"[DEBUG] Trying Mojang name history for '{name}'...")
                        # First get UUID
                        uuid_resp = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{name}", timeout=5)
                        if uuid_resp.status_code == 200:
                            uuid = uuid_resp.json().get("id", "")
                            if uuid:
                                # Now get name history
                                names_resp = requests.get(f"https://api.mojang.com/user/profiles/{uuid}/names", timeout=5)
                                if names_resp.status_code == 200:
                                    names_list = names_resp.json()
                                    if len(names_list) > 1:
                                        # Has name changes - get the last one with a timestamp
                                        last_change = None
                                        for entry in names_list:
                                            if entry.get("changedToAt"):
                                                last_change = entry
                                        if last_change:
                                            ts = last_change["changedToAt"]
                                            last_dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
                                            drop = last_dt + timedelta(days=60)
                                            now = datetime.now(timezone.utc)
                                            if drop > now:
                                                delta = drop - now
                                                self._log(
                                                    f"  [📊] '{name}': last changed to '{last_change.get('name')}' "
                                                    f"on {last_dt.strftime('%Y-%m-%d %H:%M UTC')} "
                                                    f"→ drops ~{drop.strftime('%Y-%m-%d %H:%M UTC')} "
                                                    f"(in {delta.days}d {delta.seconds//3600}h)"
                                                )
                                            else:
                                                self._log(f"  [📊] '{name}': cooldown EXPIRED — may be available")
                                            if not self.drop_time_input.text():
                                                self.drop_time_input.setText(drop.strftime("%Y-%m-%dT%H:%M:%S.000Z"))
                                        else:
                                            self._log(f"  [📊] '{name}': Mojang has {len(names_list)} names but no timestamps")
                                    else:
                                        self._log(f"  [📊] '{name}': no name changes on Mojang")
                        elif uuid_resp.status_code == 404:
                            self._log(f"  [✅] '{name}' not found on Mojang — name may be available!")
                    except requests.exceptions.Timeout:
                        self._log(f"  [📊] Mojang name history TIMEOUT for '{name}'")
                    except Exception as e:
                        self._log(f"  [📊] Mojang name history failed for '{name}': {str(e)[:80]}")

                # Source 3: Mojang availability check
                try:
                    self._log(f"[DEBUG] Starting Mojang availability check for '{name}'...")
                    resp = requests.get(f"https://api.mojang.com/users/profiles/minecraft/{name}", timeout=5)
                    if resp.status_code == 404:
                        self._log(f"  [✅] Mojang: '{name}' is AVAILABLE")
                    elif resp.status_code == 200:
                        self._log(f"  [❌] Mojang: '{name}' is currently TAKEN")
                    else:
                        self._log(f"  [📊] Mojang: '{name}' status code {resp.status_code}")
                except requests.exceptions.Timeout:
                    self._log(f"  [📊] Mojang TIMEOUT for '{name}'")
                except Exception as e:
                    self._log(f"  [📊] Mojang check failed for '{name}': {str(e)[:80]}")

            self._log("[✅] Droptime fetch complete — scroll up for results")
            self._log("[DEBUG] Method finished successfully")
        except Exception as outer_e:
            self._log(f"[❌] Fetch droptime CRASHED: {outer_e}")
            self._log(f"[DEBUG] {traceback.format_exc()[:300]}")

    def _lookup_history_for_name(self):
        """Look up name history from playerdb.co."""
        name = self.username_input.text().strip()
        if not name:
            self._log("[!] Please enter a username first")
            return
        self._log(f"[*] Looking up history for '{name}'...")
        try:
            resp = requests.get(f"https://playerdb.co/api/player/minecraft/{name}", timeout=5)
            if resp.status_code == 200:
                data = resp.json()
                if data.get("success"):
                    history = data["data"]["player"].get("name_history", [])
                    if history:
                        self._log(f"  [📜] '{name}' name change history ({len(history)} changes):")
                        for i, entry in enumerate(history, 1):
                            n = entry.get("name", "?")
                            ts = entry.get("changedToAt", "?")
                            if ts != "?":
                                from datetime import datetime, timezone
                                dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
                                ts = dt.strftime("%Y-%m-%d %H:%M UTC")
                            self._log(f"    {i}. Changed to '{n}' ({ts})")
                        # Estimate drop (60-day cooldown)
                        last = history[-1]
                        if last.get("changedToAt"):
                            from datetime import datetime, timedelta, timezone
                            last_dt = datetime.fromtimestamp(last["changedToAt"] / 1000, tz=timezone.utc)
                            drop = last_dt + timedelta(days=60)
                            now = datetime.now(timezone.utc)
                            if drop > now:
                                delta = drop - now
                                self._log(f"  [⏰] Estimated drop: {drop.strftime('%Y-%m-%d %H:%M UTC')} (in {delta.days}d {delta.seconds//3600}h {(delta.seconds%3600)//60}m)")
                            else:
                                self._log(f"  [⏰] Name cooldown expired — may be available")
                            if not self.drop_time_input.text():
                                self.drop_time_input.setText(drop.strftime("%Y-%m-%dT%H:%M:%S.000Z"))
                    else:
                        self._log(f"  [📜] No name change history for '{name}'")
                else:
                    self._log(f"  [!] playerdb returned error")
            else:
                self._log(f"  [!] playerdb returned HTTP {resp.status_code}")
        except Exception as e:
            self._log(f"  [!] History lookup failed: {str(e)[:60]}")

    def _expand_name_pattern(self):
        """Expand a name pattern into candidates."""
        pattern = self.username_input.text().strip()
        if not pattern or '{' not in pattern:
            self._log("[!] Please enter a pattern with {letter}, {digit}, {LETTER}, or {alphanum}")
            self._log("[!] Examples: x{letter}x, pro{digit}{digit}, {letter}{letter}gaming")
            return
        try:
            import string
            import itertools
            # Inline pattern expansion (same logic as worker method)
            segments = []
            i = 0
            while i < len(pattern):
                if pattern[i] == '{':
                    j = pattern.find('}', i + 1)
                    if j == -1:
                        segments.append(pattern[i:])
                        break
                    token = pattern[i+1:j]
                    if token in ('letter', 'a-z'):
                        segments.append(list(string.ascii_lowercase))
                    elif token in ('LETTER', 'A-Z'):
                        segments.append(list(string.ascii_uppercase))
                    elif token in ('digit', '0-9'):
                        segments.append(list(string.digits))
                    elif token == 'alphanum':
                        segments.append(list(string.ascii_letters + string.digits))
                    else:
                        segments.append([pattern[i:j+1]])
                    i = j + 1
                else:
                    segments.append([pattern[i]])
                    i += 1
            names = []
            for combo in itertools.product(*segments):
                names.append(''.join(combo))
                if len(names) >= 10000:
                    break
            self._log(f"[*] Pattern '{pattern}' expanded to {len(names)} candidates (showing first 20):")
            for n in names[:20]:
                self._log(f"    {n}")
            if len(names) > 20:
                self._log(f"    ... and {len(names)-20} more")
            # Ask if user wants to load them into the queue
            self._log(f"[*] To use these, copy them and paste into the queue tab")
        except Exception as e:
            self._log(f"  [!] Pattern expansion failed: {str(e)[:60]}")

    def _check_api_status(self):
        """Check Mojang API health."""
        self._log("[*] Checking Mojang API health...")
        endpoints = {
            "Profile API": "https://api.mojang.com/profile/minecraft/Steve",
            "UUID API": "https://api.mojang.com/users/profiles/minecraft/Steve",
            "Session Server": "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=Steve&uuid=0",
            "Xbox Auth": "https://xl.start.microsoft.com/xl/2.0/",
        }
        healthy = 0
        for name, url in endpoints.items():
            try:
                start = time.monotonic()
                resp = requests.get(url, timeout=5)
                rtt = (time.monotonic() - start) * 1000
                if resp.status_code in (200, 204, 404, 403):
                    self._log(f"  [🟢] {name}: OK ({rtt:.0f}ms)")
                    healthy += 1
                elif resp.status_code == 429:
                    self._log(f"  [🟡] {name}: rate limited ({rtt:.0f}ms)")
                else:
                    self._log(f"  [🟡] {name}: HTTP {resp.status_code} ({rtt:.0f}ms)")
            except Exception as e:
                self._log(f"  [🔴] {name}: unreachable ({str(e)[:40]})")
        pct = healthy / len(endpoints) * 100
        if pct >= 75:
            self._log(f"[✓] API health: {pct:.0f}% ({healthy}/{len(endpoints)}) — good to go")
        else:
            self._log(f"[⚠️] API health: {pct:.0f}% ({healthy}/{len(endpoints)}) — consider waiting")

    def load_tokens_from_file(self):
        """Load tokens from a text file"""
        file_path, _ = QFileDialog.getOpenFileName(self, "Load Tokens", "", "Text Files (*.txt)")
        if file_path:
            try:
                with open(file_path, 'r') as f:
                    tokens = f.read()
                self.tokens_input.setText(tokens)
                token_count = len([t for t in tokens.split('\n') if t.strip()])
                self._log(f"✅ Loaded {token_count} tokens from {file_path}")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to load tokens: {e}")
    
    def save_log(self):
        """Save log to file"""
        file_path, _ = QFileDialog.getSaveFileName(self, "Save Log", "sniper_log.txt", "Text Files (*.txt)")
        if file_path:
            try:
                with open(file_path, 'w') as f:
                    f.write(self.log_output.toPlainText())
                self._log(f"✅ Log saved to {file_path}")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to save log: {e}")

    # === Account Health Dashboard ===

    def _load_account_history(self):
        """Load account usage history from file"""
        try:
            with open(ACCOUNT_TRACKING_FILE, 'r') as f:
                return json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            return {"accounts": {}}

    def _save_account_history(self, history):
        """Save account usage history to file"""
        with open(ACCOUNT_TRACKING_FILE, 'w') as f:
            json.dump(history, f, indent=2)

    def _record_name_change(self, token, username, success):
        """Record a name change attempt for cooldown tracking"""
        token_key = hash(token) % 1000000
        history = self._load_account_history()
        now = datetime.now(timezone.utc).isoformat()
        
        if token_key not in history["accounts"]:
            history["accounts"][token_key] = {
                "token_hash": token_key,
                "username": None,
                "last_checked": None,
                "last_name_change": None,
                "name_change_cooldown_until": None,
                "total_attempts": 0,
                "successful_changes": 0,
                "status": "unknown"
            }
        
        acc = history["accounts"][token_key]
        acc["total_attempts"] += 1
        acc["username"] = username
        
        if success:
            acc["successful_changes"] += 1
            acc["last_name_change"] = now
            cooldown_until = datetime.now(timezone.utc).timestamp() + (NAME_CHANGE_COOLDOWN_DAYS * 86400)
            acc["name_change_cooldown_until"] = datetime.fromtimestamp(cooldown_until, tz=timezone.utc).isoformat()
            acc["status"] = "cooldown"
        
        self._save_account_history(history)

    def _is_token_on_cooldown(self, token):
        """Check if a token is on 90-day name change cooldown"""
        token_key = hash(token) % 1000000
        history = self._load_account_history()
        if token_key not in history["accounts"]:
            return False, 0
        acc = history["accounts"][token_key]
        if not acc.get("name_change_cooldown_until"):
            return False, 0
        try:
            cooldown_end = datetime.fromisoformat(acc["name_change_cooldown_until"])
            if datetime.now(timezone.utc) < cooldown_end:
                remaining = (cooldown_end - datetime.now(timezone.utc)).days
                return True, remaining
        except (ValueError, TypeError):
            pass
        return False, 0

    def _create_health_tab(self):
        """Create the Account Health Dashboard tab — upgraded v2"""
        tab = QWidget()
        layout = QVBoxLayout()
        tab.setObjectName("sniper_tab")
        
        # === Readiness Banner (big visual indicator) ===
        self.health_readiness = QLabel("Account Health Dashboard — Load tokens & click 'Scan' to see readiness")
        self.health_readiness.setStyleSheet("""
            QLabel {
                font-size: 14px;
                font-weight: bold;
                color: #cccccc;
                padding: 10px 14px;
                background-color: #2d2d2d;
                border-radius: 8px;
                border: 1px solid #3e3e3e;
            }
        """)
        layout.addWidget(self.health_readiness)
        
        # === Button row: Scan | Auto-Refresh toggle | Clear History ===
        btn_layout = QHBoxLayout()
        
        scan_btn = QPushButton("Scan Tokens")
        scan_btn.setObjectName("start_button")
        scan_btn.setMinimumHeight(36)
        scan_btn.clicked.connect(self._scan_token_health)
        btn_layout.addWidget(scan_btn)
        
        self.auto_refresh_checkbox = QCheckBox("Auto-refresh every 30s")
        self.auto_refresh_checkbox.setStyleSheet("color: #cccccc; spacing: 6px;")
        self.auto_refresh_checkbox.stateChanged.connect(self._toggle_auto_refresh)
        btn_layout.addWidget(self.auto_refresh_checkbox)
        
        btn_layout.addStretch()
        
        clear_history_btn = QPushButton("Clear History")
        clear_history_btn.setObjectName("stop_button")
        clear_history_btn.setMinimumHeight(36)
        clear_history_btn.clicked.connect(self._clear_account_history)
        btn_layout.addWidget(clear_history_btn)
        
        layout.addLayout(btn_layout)
        
        # Progress bar for scanning
        self.health_progress = QProgressBar()
        self.health_progress.setVisible(False)
        layout.addWidget(self.health_progress)
        
        # === Table with 10 columns ===
        self.health_table = QTableWidget()
        self.health_table.setColumnCount(10)
        self.health_table.setHorizontalHeaderLabels([
            "#", "Ready", "Status", "Username", "UUID", "Last Checked",
            "Name Change", "JWT Expiry", "Cooldown", "Snipe-Ready"
        ])
        
        self.health_table.setStyleSheet("""
            QTableWidget {
                background-color: #2d2d2d;
                color: #cccccc;
                gridline-color: #3e3e3e;
                border: 1px solid #3e3e3e;
                border-radius: 6px;
                font-family: 'JetBrains Mono', Consolas;
                font-size: 10px;
            }
            QTableWidget::item {
                padding: 4px;
            }
            QHeaderView::section {
                background-color: #2d2d2d;
                color: #3a7bd5;
                padding: 6px;
                border: 1px solid #3e3e3e;
                font-weight: bold;
                font-size: 11px;
            }
            QTableWidget::item:selected {
                background-color: #2d2d2d;
            }
            QScrollBar:vertical {
                background-color: #2d2d2d;
                width: 10px;
                border-radius: 5px;
            }
            QScrollBar::handle:vertical {
                background-color: #2d2d2d;
                border-radius: 5px;
            }
        """)
        
        # Configure columns
        header = self.health_table.horizontalHeader()
        col_widths = [40, 50, 65, 0, 0, 140, 140, 90, 90, 80]
        for i, w in enumerate(col_widths):
            if w == 0:
                header.setSectionResizeMode(i, QHeaderView.Stretch)
            else:
                header.setSectionResizeMode(i, QHeaderView.Fixed)
                header.resizeSection(i, w)
        
        self.health_table.setSelectionBehavior(QTableWidget.SelectRows)
        self.health_table.setEditTriggers(QTableWidget.NoEditTriggers)
        self.health_table.verticalHeader().setVisible(False)
        self.health_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.health_table.customContextMenuRequested.connect(self._health_table_context_menu)
        
        layout.addWidget(self.health_table)
        
        # === Legend ===
        legend = QLabel(
            "Legend: ✅ Valid & Ready | ⏳ On Cooldown (90d) | ❌ Expired/Invalid | ⚠️ Rate Limited | "
            "Row color: 🟢 Snipe-ready | 🟡 Valid but cooldown | 🔴 Not usable"
        )
        legend.setStyleSheet("""
            QLabel {
                font-size: 10px;
                color: #888;
                padding: 4px;
            }
        """)
        layout.addWidget(legend)
        
        # Auto-refresh timer (started/stopped by checkbox)
        self._health_auto_timer = QTimer()
        self._health_auto_timer.setInterval(30000)  # 30 seconds
        self._health_auto_timer.timeout.connect(self._scan_token_health)
        self._health_auto_timer.setSingleShot(False)
        
        return tab
    
    def _toggle_auto_refresh(self, state):
        """Start/stop the auto-refresh timer"""
        if state == Qt.Checked:
            self._health_auto_timer.start()
            self._scan_token_health()  # immediate first scan
        else:
            self._health_auto_timer.stop()
    
    def _health_table_context_menu(self, position):
        """Right-click context menu on the health table"""
        menu = __import__('PyQt5.QtWidgets', fromlist=['QMenu']).QMenu(self)
        
        row = self.health_table.rowAt(position.y())
        if row < 0:
            return
        
        # Get token from stored results
        if not hasattr(self, '_health_scan_tokens') or row >= len(self._health_scan_tokens):
            return
        token = self._health_scan_tokens[row]
        
        copy_token_act = menu.addAction("📋 Copy Token")
        copy_token_act.triggered.connect(lambda: self._copy_to_clipboard(token))
        
        username_item = self.health_table.item(row, 3)
        username = username_item.text() if username_item else ""
        if username and username != "—":
            copy_user_act = menu.addAction(f"📋 Copy Username ({username})")
            copy_user_act.triggered.connect(lambda: self._copy_to_clipboard(username))
        
        menu.addSeparator()
        remove_act = menu.addAction("🗑️ Remove Token from Input")
        remove_act.triggered.connect(lambda: self._remove_token_from_input(row))
        
        menu.exec_(self.health_table.viewport().mapToGlobal(position))
    
    def _copy_to_clipboard(self, text):
        """Copy text to system clipboard"""
        from PyQt5.QtWidgets import QApplication
        QApplication.clipboard().setText(text)
    
    def _remove_token_from_input(self, row):
        """Remove a token from the tokens text input"""
        if not hasattr(self, '_health_scan_tokens') or row >= len(self._health_scan_tokens):
            return
        token_to_remove = self._health_scan_tokens[row]
        current = self.tokens_input.toPlainText()
        lines = [l.strip() for l in current.split('\n') if l.strip()]
        # Remove first matching token (match by content, not by hash since multiple tokens could hash the same)
        new_lines = []
        removed = False
        for line in lines:
            if line == token_to_remove and not removed:
                removed = True
                continue
            new_lines.append(line)
        self.tokens_input.setPlainText('\n'.join(new_lines))
        if removed:
            self.append_log(f"[HEALTH] Removed token from input ({row+1} tokens remaining)")

    def _scan_token_health(self):
        """Scan all loaded tokens and update the health dashboard"""
        tokens_text = self.tokens_input.toPlainText()
        tokens = [t.strip() for t in tokens_text.split('\n') if t.strip()]
        
        if not tokens:
            QMessageBox.warning(self, "No Tokens", "Please load or paste tokens first!")
            return
        
        self.health_table.setRowCount(0)
        self.health_summary.setText(f"🏥 Scanning {len(tokens)} tokens...")
        self.health_progress.setVisible(True)
        self.health_progress.setMaximum(len(tokens))
        self.health_progress.setValue(0)
        
        # Run scan in a thread to not block UI
        self._health_scan_thread = threading.Thread(
            target=self._run_health_scan, args=(tokens,), daemon=True
        )
        self._health_scan_thread.start()

    def _run_health_scan(self, tokens):
        """Run the actual token health scan (runs in background thread)"""
        results = []
        
        for i, token in enumerate(tokens):
            token_key = hash(token) % 1000000
            
            # Check cooldown status
            on_cooldown, days_left = self._is_token_on_cooldown(token)
            
            # Validate token against Minecraft API
            status = "❓"
            username = "—"
            uuid = "—"
            last_checked = "—"
            name_change = "—"
            cooldown_str = "—"
            jwt_expiry = "—"
            
            # Check JWT expiry from token payload
            try:
                import base64
                payload = token.split('.')[1]
                padding = 4 - len(payload) % 4
                if padding != 4:
                    payload += '=' * padding
                decoded_jwt = json.loads(base64.urlsafe_b64decode(payload))
                exp = decoded_jwt.get('exp', 0)
                if exp:
                    exp_dt = datetime.fromtimestamp(exp, tz=timezone.utc)
                    days_left = (exp_dt - datetime.now(timezone.utc)).days
                    hours_left = int((exp_dt - datetime.now(timezone.utc)).total_seconds() / 3600)
                    jwt_expiry = f"{days_left}d {hours_left % 24}h"
                    if days_left < 0:
                        jwt_expiry = "EXPIRED"
                    elif days_left < 1:
                        jwt_expiry = f"{hours_left}h"
            except Exception:
                pass
            
            try:
                client = httpx.Client(
                    http2=True,
                    timeout=httpx.Timeout(5.0, connect=3.0),
                    headers={
                        "Authorization": f"Bearer {token}",
                        "Content-Type": "application/json"
                    }
                )
                r = client.get("https://api.minecraftservices.com/minecraft/profile")
                client.close()
                
                if r.status_code == 200:
                    data = r.json()
                    username = data.get("name", "—")
                    uuid = data.get("id", "—")
                    # Format UUID with dashes
                    if len(uuid) == 32:
                        uuid = f"{uuid[:8]}-{uuid[8:12]}-{uuid[12:16]}-{uuid[16:20]}-{uuid[20:]}"
                    status = "✅"
                    last_checked = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
                    
                    if on_cooldown:
                        status = "⏳"
                        cooldown_str = f"{days_left}d left"
                    
                elif r.status_code == 401:
                    status = "❌"
                elif r.status_code == 429:
                    status = "⚠️"
                else:
                    status = f"❌ {r.status_code}"
                    
            except Exception as e:
                status = f"❌ {str(e)[:20]}"
            
            # Load history for name change info
            history = self._load_account_history()
            if token_key in history["accounts"]:
                acc = history["accounts"][token_key]
                if acc.get("last_name_change"):
                    name_change = acc["last_name_change"][:16].replace("T", " ")
                if acc.get("total_attempts", 0) > 0:
                    if not name_change or name_change == "—":
                        name_change = f"{acc['total_attempts']} attempts"
            
            # Update cooldown string
            if on_cooldown and status == "✅":
                cooldown_str = f"{days_left}d left"
            elif on_cooldown:
                cooldown_str = f"{days_left}d left"
            elif history.get("accounts", {}).get(token_key, {}).get("name_change_cooldown_until"):
                cooldown_str = "Expired ✓"
            else:
                cooldown_str = "Available"
            
            results.append({
                "index": i + 1,
                "status": status,
                "username": username,
                "uuid": uuid,
                "last_checked": last_checked,
                "name_change": name_change,
                "jwt_expiry": jwt_expiry,
                "cooldown": cooldown_str,
                "on_cooldown": on_cooldown,
                "snipe_ready": status in ("✅",) and not on_cooldown,  # ✅ valid + not on cooldown = go
            })
            
            # Update progress (thread-safe via QTimer)
            QTimer.singleShot(0, lambda p=i+1: self.health_progress.setValue(p))
        
        # Update table on main thread
        QTimer.singleShot(0, lambda: self._populate_health_table(results, tokens))

    def _populate_health_table(self, results, tokens):
        """Populate the health table with scan results (main thread) — upgraded v2"""
        self.health_table.setRowCount(len(results))
        
        # Store tokens for context menu
        self._health_scan_tokens = tokens
        
        # === Compute summary counts ===
        valid = sum(1 for r in results if r["snipe_ready"])
        expired = sum(1 for r in results if r["status"].startswith("❌"))
        cooldown = sum(1 for r in results if r["on_cooldown"])
        rate_limited = sum(1 for r in results if r["status"] == "⚠️")
        total = len(results)
        
        # === Readiness Banner with color ===
        if valid == total and total > 0:
            banner_bg = "#1b3a2a"
            banner_border = "#4CAF50"
        elif valid > 0:
            banner_bg = "#3a2a1b"
            banner_border = "#FF9800"
        else:
            banner_bg = "#3a1b1b"
            banner_border = "#f44336"
        
        self.health_readiness.setText(
            f"🏥 {total} tokens — 🟢 {valid} snipe-ready | ❌ {expired} expired | "
            f"⏳ {cooldown} on cooldown | ⚠️ {rate_limited} rate limited"
        )
        self.health_readiness.setStyleSheet(f"""
            QLabel {{
                font-size: 14px;
                font-weight: bold;
                color: #cccccc;
                padding: 10px 14px;
                background-color: {banner_bg};
                border-radius: 8px;
                border: 2px solid {banner_border};
            }}
        """)
        self.health_progress.setVisible(False)
        
        # === Sort: snipe-ready first, then valid, then everything else ===
        sort_key = lambda r: (0 if r["snipe_ready"] else 1 if r["status"] in ("✅", "⚠️") else 2, r["index"])
        sorted_indices = sorted(range(len(results)), key=sort_key)
        
        bold_font = QFont("Consolas", 10, QFont.Bold)
        
        for display_row, src_idx in enumerate(sorted_indices):
            r = results[src_idx]
            
            # === Row background color ===
            if r["snipe_ready"]:
                row_bg = "#0d2818"  # dark green tint
            elif r["on_cooldown"]:
                row_bg = "#2a1f0d"  # dark amber tint
            else:
                row_bg = "#2a0d0d"  # dark red tint
            
            # Index
            idx_item = QTableWidgetItem(str(display_row + 1))
            idx_item.setTextAlignment(Qt.AlignCenter)
            idx_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 0, idx_item)
            
            # Ready indicator (big emoji)
            ready_icon = "🟢" if r["snipe_ready"] else ("🟡" if r["on_cooldown"] else "🔴")
            ready_item = QTableWidgetItem(ready_icon)
            ready_item.setTextAlignment(Qt.AlignCenter)
            ready_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 1, ready_item)
            
            # Status
            status_item = QTableWidgetItem(r["status"])
            status_item.setTextAlignment(Qt.AlignCenter)
            if r["status"] == "✅":
                status_item.setForeground(QColor("#4CAF50"))
            elif r["status"].startswith("❌"):
                status_item.setForeground(QColor("#f44336"))
            elif r["status"] == "⏳":
                status_item.setForeground(QColor("#FF9800"))
            elif r["status"] == "⚠️":
                status_item.setForeground(QColor("#FFC107"))
            status_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 2, status_item)
            
            # Username
            username_item = QTableWidgetItem(r["username"])
            if r["username"] != "—":
                username_item.setForeground(QColor("#58a6ff"))
            username_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 3, username_item)
            
            # UUID
            uuid_item = QTableWidgetItem(r["uuid"])
            uuid_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 4, uuid_item)
            
            # Last checked
            lc_item = QTableWidgetItem(r["last_checked"])
            lc_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 5, lc_item)
            
            # Name change
            nc_item = QTableWidgetItem(r["name_change"])
            nc_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 6, nc_item)
            
            # JWT Expiry
            jwt_item = QTableWidgetItem(r["jwt_expiry"])
            jwt_item.setTextAlignment(Qt.AlignCenter)
            if r["jwt_expiry"] == "EXPIRED":
                jwt_item.setForeground(QColor("#f44336"))
                jwt_item.setFont(bold_font)
            elif r["jwt_expiry"] != "—":
                jwt_item.setForeground(QColor("#58a6ff"))
            jwt_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 7, jwt_item)
            
            # Cooldown
            cooldown_item = QTableWidgetItem(r["cooldown"])
            cooldown_item.setTextAlignment(Qt.AlignCenter)
            if r["on_cooldown"]:
                cooldown_item.setForeground(QColor("#FF9800"))
            elif r["cooldown"] == "Available":
                cooldown_item.setForeground(QColor("#4CAF50"))
            cooldown_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 8, cooldown_item)
            
            # Snipe-Ready column (combined signal)
            if r["snipe_ready"]:
                sr_text = "✅ GO"
                sr_color = QColor("#4CAF50")
            elif r["on_cooldown"]:
                sr_text = "⏳ WAIT"
                sr_color = QColor("#FF9800")
            elif r["status"].startswith("❌"):
                sr_text = "❌ NO"
                sr_color = QColor("#f44336")
            else:
                sr_text = "❓ ?"
                sr_color = QColor("#888")
            
            sr_item = QTableWidgetItem(sr_text)
            sr_item.setTextAlignment(Qt.AlignCenter)
            sr_item.setForeground(sr_color)
            sr_item.setFont(bold_font)
            sr_item.setBackground(QColor(row_bg))
            self.health_table.setItem(display_row, 9, sr_item)

    def _clear_account_history(self):
        """Clear the account usage history"""
        reply = QMessageBox.question(
            self, "Clear History",
            "Clear all account history (cooldown tracking, usage stats)?\n\nThis won't affect your tokens.",
            QMessageBox.Yes | QMessageBox.No
        )
        if reply == QMessageBox.Yes:
            self._save_account_history({"accounts": {}})
            self.health_summary.setText("🏥 Account history cleared")
            self._log("🗑️ Account history cleared")

    # ========== PHASE 5: POWER FEATURES TAB ==========

    def _create_power_features_tab(self):
        """Create Phase 5 power features tab: webhook, history, autoreclaim, headless"""
        from PyQt5.QtWidgets import QSplitter

        tab = QWidget()
        layout = QVBoxLayout(tab)

        # Sub-tabs within power features
        power_tabs = QTabWidget()
        layout.addWidget(power_tabs)

        # --- Webhook Config ---
        webhook_tab = self._create_webhook_tab()
        power_tabs.addTab(webhook_tab, "🔔 Discord Webhook")

        # --- Snipe History ---
        history_tab = self._create_history_tab()
        power_tabs.addTab(history_tab, "📊 Snipe History")

        # --- Auto-Reclaim ---
        reclaim_tab = self._create_autoreclaim_tab()
        power_tabs.addTab(reclaim_tab, "🔄 Auto-Reclaim")

        # --- Auto-Restart Watchdog ---
        watchdog_tab = self._create_watchdog_tab()
        power_tabs.addTab(watchdog_tab, "🛡️ Auto-Restart")

        # --- NameMC Search ---
        namemc_search_tab = self._create_namemc_search_tab()
        power_tabs.addTab(namemc_search_tab, "🔍 NameMC Search")

        # --- Headless Launcher ---
        headless_tab = self._create_headless_tab()
        power_tabs.addTab(headless_tab, "💻 Headless CLI")

        # --- PST → UTC Time Converter ---
        time_tab = self._create_time_converter_tab()
        power_tabs.addTab(time_tab, "⏰ PST→UTC Converter")

        # --- Auto Skin Upload ---
        skin_tab = self._create_skin_tab()
        power_tabs.addTab(skin_tab, "🎨 Auto Skin")

        # --- Name Queue ---
        queue_tab = self._create_name_queue_tab()
        power_tabs.addTab(queue_tab, "📋 Name Queue")

        # --- Network Latency Monitor ---
        latency_tab = self._create_latency_tab()
        power_tabs.addTab(latency_tab, "📡 Latency")

        # --- Proxy ---
        proxy_tab = self._create_proxy_tab()
        power_tabs.addTab(proxy_tab, "🌐 Proxy")

        # --- Desktop File ---
        desktop_tab = self._create_desktop_file_tab()
        power_tabs.addTab(desktop_tab, "🖥️ Desktop File")

        # --- Scheduled Snipes (m1) ---
        sched_tab = self._create_scheduled_snipes_tab()
        power_tabs.addTab(sched_tab, "⏰ Scheduled")

        # --- Telegram Notifications (m2) ---
        tg_tab = self._create_telegram_tab()
        power_tabs.addTab(tg_tab, "📱 Telegram")

        # --- Pushover Notifications (l6) ---
        push_tab = self._create_pushover_tab()
        power_tabs.addTab(push_tab, "🔔 Pushover")

        # --- Per-Token Stats (m5) ---
        token_stats_tab = self._create_token_stats_tab()
        power_tabs.addTab(token_stats_tab, "📊 Token Stats")

        # --- Latency Graph (l1) ---
        lat_graph_tab = self._create_latency_graph_tab()
        power_tabs.addTab(lat_graph_tab, "📈 Latency Graph")

        # --- Session Replay (l2) ---
        replay_tab = self._create_session_replay_tab()
        power_tabs.addTab(replay_tab, "🎬 Session Replay")

        # --- Plugin System (l3) ---
        plugin_tab = self._create_plugin_tab()
        power_tabs.addTab(plugin_tab, "🧩 Plugins")

        # --- DNS over HTTPS (l4) ---
        doh_tab = self._create_doh_tab()
        power_tabs.addTab(doh_tab, "🔒 DoH")

        # --- Auto-Update (m8) ---
        update_tab = self._create_update_tab()
        power_tabs.addTab(update_tab, "🔄 Updates")

        return tab

    def _create_webhook_tab(self):
        """Discord webhook configuration panel"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🔔 Send snipe results, auth status, and alerts to a Discord webhook.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        form = QFormLayout()
        self.webhook_url_input = QLineEdit()
        self.webhook_url_input.setPlaceholderText("https://discord.com/api/webhooks/...")
        form.addRow("Webhook URL:", self.webhook_url_input)

        self.webhook_enable_checkbox = QCheckBox("Enable webhook alerts (sends on success/failure/auth)")
        form.addRow("", self.webhook_enable_checkbox)

        self.webhook_test_btn = QPushButton("📨 Send Test Ping")
        self.webhook_test_btn.clicked.connect(self._test_webhook)
        form.addRow("", self.webhook_test_btn)

        layout.addLayout(form)
        layout.addStretch()
        return tab

    def _test_webhook(self):
        """Send a test ping to the configured Discord webhook"""
        url = self.webhook_url_input.text().strip()
        if not url:
            QMessageBox.warning(self, "No URL", "Enter a Discord webhook URL first.")
            return
        try:
            import sniper_webhook
            alert = sniper_webhook.DiscordWebhookAlert(url)
            alert.alert("⚡ Sniper GUI test ping", "Webhook is working!", color=0x00ff00)
            self._log("✅ Webhook test ping sent successfully")
            QMessageBox.information(self, "Success", "Test ping sent to Discord!")
        except Exception as e:
            self._log(f"❌ Webhook test failed: {e}")
            QMessageBox.critical(self, "Failed", f"Webhook test failed:\n{e}")

    def _create_history_tab(self):
        """Snipe history viewer with export"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("📊 Persistent snipe attempt history stored in SQLite.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        # Stats row
        stats_row = QHBoxLayout()
        self.history_total_label = QLabel("Total: ---")
        self.history_success_label = QLabel("Success: ---")
        self.history_fail_label = QLabel("Failed: ---")
        for lbl in [self.history_total_label, self.history_success_label, self.history_fail_label]:
            lbl.setFont(QFont("Consolas", 11, QFont.Bold))
            lbl.setStyleSheet("color: #3a7bd5; padding: 4px;")
            stats_row.addWidget(lbl)
        stats_row.addStretch()
        layout.addLayout(stats_row)

        # History table
        self.history_table = QTableWidget()
        self.history_table.setColumnCount(6)
        self.history_table.setHorizontalHeaderLabels(["Time (UTC)", "Name", "Status", "Response", "Account", "ID"])
        self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.history_table.setSelectionBehavior(QTableWidget.SelectRows)
        layout.addWidget(self.history_table)

        # Buttons
        btn_row = QHBoxLayout()
        refresh_hist = QPushButton("🔄 Refresh")
        refresh_hist.clicked.connect(self._refresh_history)
        btn_row.addWidget(refresh_hist)

        export_csv = QPushButton("📤 Export CSV")
        export_csv.clicked.connect(self._export_history_csv)
        btn_row.addWidget(export_csv)

        export_json = QPushButton("📤 Export JSON")
        export_json.clicked.connect(self._export_history_json)
        btn_row.addWidget(export_json)

        btn_row.addStretch()
        clear_hist = QPushButton("🗑️ Clear All")
        clear_hist.clicked.connect(self._clear_history)
        btn_row.addWidget(clear_hist)

        layout.addLayout(btn_row)

        # Load on creation
        self._refresh_history()
        return tab

    def _refresh_history(self):
        """Load snipe history from DB into table"""
        try:
            import sniper_history
            db = sniper_history.SnipeHistoryDB()
            records = db.get_recent(attempts=200)

            self.history_total_label.setText(f"Total: {db.get_stats().get('total_attempts', 0)}")
            self.history_success_label.setText(f"Success: {db.get_stats().get('successful_snipes', 0)}")
            self.history_fail_label.setText(f"Failed: {db.get_stats().get('failed_attempts', 0)}")

            self.history_table.setRowCount(len(records))
            for row_idx, rec in enumerate(records):
                for col, key in enumerate(["timestamp", "target_name", "status", "response_status", "account_email", "id"]):
                    item = QTableWidgetItem(str(rec.get(key, "")))
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                    # Color code status
                    if key == "status":
                        if rec.get("status") == "success":
                            item.setForeground(QColor("#4CAF50"))
                        elif rec.get("status") == "error":
                            item.setForeground(QColor("#f44336"))
                        else:
                            item.setForeground(QColor("#FF9800"))
                    self.history_table.setItem(row_idx, col, item)
        except Exception as e:
            try:
                self._log(f"⚠️ History refresh error: {e}")
            except AttributeError:
                pass  # log_output not yet initialized during startup

    def _export_history_csv(self):
        """Export history to CSV"""
        path, _ = QFileDialog.getSaveFileName(self, "Export History", "", "CSV (*.csv)")
        if path:
            import sniper_history
            db = sniper_history.SnipeHistoryDB()
            db.export_csv(path)
            self._log(f"📤 History exported to {path}")

    def _export_history_json(self):
        """Export history to JSON"""
        path, _ = QFileDialog.getSaveFileName(self, "Export History", "", "JSON (*.json)")
        if path:
            import sniper_history
            db = sniper_history.SnipeHistoryDB()
            db.export_json(path)
            self._log(f"📤 History exported to {path}")

    def _clear_history(self):
        """Clear all history"""
        reply = QMessageBox.question(self, "Clear History",
            "Delete ALL snipe history records? This cannot be undone.",
            QMessageBox.Yes | QMessageBox.No)
        if reply == QMessageBox.Yes:
            import sniper_history
            db = sniper_history.SnipeHistoryDB()
            db.clear_all()
            self._refresh_history()
            self._log("🗑️ Snipe history cleared")

    def _create_autoreclaim_tab(self):
        """Auto-reclaim management panel"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🔄 Monitor previously-owned names and auto-snip if they drop.\nPolls Mojang API to detect availability.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        # Name management
        name_form = QFormLayout()
        self.reclaim_name_input = QLineEdit()
        self.reclaim_name_input.setPlaceholderText("Username to track")
        name_form.addRow("Name:", self.reclaim_name_input)

        name_btns = QHBoxLayout()
        add_name = QPushButton("➕ Add Name")
        add_name.clicked.connect(self._add_reclaim_name)
        name_btns.addWidget(add_name)

        release_name = QPushButton("📢 Release Name (start monitoring)")
        release_name.clicked.connect(self._release_reclaim_name)
        name_btns.addWidget(release_name)

        remove_name = QPushButton("❌ Remove Name")
        remove_name.clicked.connect(self._remove_reclaim_name)
        name_btns.addWidget(remove_name)

        name_btns.addStretch()
        layout.addLayout(name_form)
        layout.addLayout(name_btns)

        # Interval
        interval_form = QFormLayout()
        self.reclaim_interval_spin = QSpinBox()
        self.reclaim_interval_spin.setRange(10, 3600)
        self.reclaim_interval_spin.setValue(60)
        self.reclaim_interval_spin.setSuffix("s")
        interval_form.addRow("Check Interval:", self.reclaim_interval_spin)
        layout.addLayout(interval_form)

        # Status table
        self.reclaim_table = QTableWidget()
        self.reclaim_table.setColumnCount(4)
        self.reclaim_table.setHorizontalHeaderLabels(["Name", "Status", "Available?", "Last Check"])
        self.reclaim_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        layout.addWidget(self.reclaim_table)

        # Controls
        ctrl_row = QHBoxLayout()
        self.reclaim_start_btn = QPushButton("▶️ Start Monitor")
        self.reclaim_start_btn.clicked.connect(self._start_autoreclaim)
        ctrl_row.addWidget(self.reclaim_start_btn)

        self.reclaim_stop_btn = QPushButton("⏹️ Stop Monitor")
        self.reclaim_stop_btn.clicked.connect(self._stop_autoreclaim)
        self.reclaim_stop_btn.setEnabled(False)
        ctrl_row.addWidget(self.reclaim_stop_btn)

        refresh_reclaim = QPushButton("🔄 Refresh List")
        refresh_reclaim.clicked.connect(self._refresh_reclaim_list)
        ctrl_row.addWidget(refresh_reclaim)

        ctrl_row.addStretch()
        layout.addLayout(ctrl_row)

        self._refresh_reclaim_list()
        return tab

    def _add_reclaim_name(self):
        name = self.reclaim_name_input.text().strip()
        if not name:
            return
        try:
            from sniper_history import register_my_name
            register_my_name(name)
        except ImportError:
            try:
                import sqlite3
                from pathlib import Path
                db_dir = Path.home() / ".hermes" / "minecraft-sniper"
                db_dir.mkdir(parents=True, exist_ok=True)
                db_path = db_dir / "snipe_history.db"
                conn = sqlite3.connect(str(db_path))
                conn.execute("CREATE TABLE IF NOT EXISTS my_names (id INTEGER PRIMARY KEY, username TEXT UNIQUE, claimed_date TEXT, active INTEGER DEFAULT 1, notes TEXT)")
                conn.execute("INSERT OR REPLACE INTO my_names (username, claimed_date, active) VALUES (?, datetime('now'), 1)", (name,))
                conn.commit(); conn.close()
            except Exception as e:
                self._log(f"⚠️ Could not add name: {e}")
                return
        self._refresh_reclaim_list()
        self._log(f"➕ Added {name} to auto-reclaim watchlist")

    def _release_reclaim_name(self):
        name = self.reclaim_name_input.text().strip()
        if not name:
            return
        try:
            from sniper_history import release_my_name
            release_my_name(name)
        except ImportError:
            try:
                import sqlite3
                from pathlib import Path
                db_path = Path.home() / ".hermes" / "minecraft-sniper" / "snipe_history.db"
                conn = sqlite3.connect(str(db_path))
                conn.execute("UPDATE my_names SET active = 0, released_date = datetime('now') WHERE username = ?", (name,))
                conn.commit(); conn.close()
            except Exception as e:
                self._log(f"⚠️ Could not release name: {e}")
                return
        self._refresh_reclaim_list()
        self._log(f"📢 Marked {name} as released — monitoring for availability")

    def _remove_reclaim_name(self):
        name = self.reclaim_name_input.text().strip()
        if not name:
            return
        try:
            import sqlite3
            from pathlib import Path
            db_path = Path.home() / ".hermes" / "minecraft-sniper" / "snipe_history.db"
            conn = sqlite3.connect(str(db_path))
            conn.execute("DELETE FROM my_names WHERE username = ?", (name,))
            conn.commit(); conn.close()
        except Exception as e:
            self._log(f"⚠️ Could not remove name: {e}")
            return
        self._refresh_reclaim_list()
        self._log(f"❌ Removed {name} from watchlist")

    def _refresh_reclaim_list(self):
        try:
            import sniper_autoreclaim
            mgr = sniper_autoreclaim.ReclaimManager()
            names = mgr.list_names()
            self.reclaim_table.setRowCount(len(names))
            for row_idx, n in enumerate(names):
                for col, key in enumerate(["name", "status", "available", "last_checked"]):
                    item = QTableWidgetItem(str(n.get(key, "")))
                    item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                    if key == "available" and n.get("available") == True:
                        item.setForeground(QColor("#4CAF50"))
                        item.setText("✅ YES")
                    elif key == "available" and n.get("available") == False:
                        item.setForeground(QColor("#f44336"))
                        item.setText("❌ No")
                    self.reclaim_table.setItem(row_idx, col, item)
        except Exception as e:
            try:
                self._log(f"⚠️ Auto-reclaim refresh error: {e}")
            except AttributeError:
                pass  # log_output not yet initialized during startup

    def _start_autoreclaim(self):
        """Start the auto-reclaim monitor in a background thread"""
        import sniper_autoreclaim

        tokens = self._load_tokens()
        webhook_url = self.webhook_url_input.text().strip() if self.webhook_enable_checkbox.isChecked() else None

        self.reclaim_worker = sniper_autoreclaim.ReclaimWorker(
            tokens=tokens,
            interval=self.reclaim_interval_spin.value(),
            webhook_url=webhook_url
        )
        self.reclaim_worker.log_signal.connect(self._log)
        self.reclaim_worker.alert_signal.connect(lambda name, avail: self._log(f"🔄 {name}: {'✅ AVAILABLE!' if avail else 'still taken'}"))
        self.reclaim_worker.start()

        self.reclaim_start_btn.setEnabled(False)
        self.reclaim_stop_btn.setEnabled(True)
        self._log("▶️ Auto-reclaim monitor started")

    def _stop_autoreclaim(self):
        if hasattr(self, "reclaim_worker"):
            self.reclaim_worker.stop()
            self.reclaim_worker.wait(3000)
            self.reclaim_start_btn.setEnabled(True)
            self.reclaim_stop_btn.setEnabled(False)
            self._log("⏹️ Auto-reclaim monitor stopped")

    # ========== Accounts Manager Tab ==========

    def _create_accounts_tab(self):
        """
        Accounts Manager — load a folder of credentials, auto-auth all accounts,
        and populate the token list. Supports multiple credential formats.
        """
        tab = QWidget()
        layout = QVBoxLayout(tab)

        # Header
        header = QLabel("Accounts Manager\nDrop a folder of token files — validate all bearer tokens and load them into the sniper.")
        header.setWordWrap(True)
        header.setStyleSheet("font-size: 13px; font-weight: bold; padding: 8px; color: #cccccc;")
        layout.addWidget(header)

       # Format info
        fmt_label = QLabel(
            "Supported formats:\n"
            "  - email:bearer_token  (colon-separated)\n"
            "  - email|bearer_token  (pipe-separated)\n"
            "  - bearer_token        (token only, one per line)\n"
            "  - JSON: {\"email\":\"x\",\"token\":\"y\"}\n"
            "  - Any .txt, .csv, .json, .list, .acc file\n"
            "  - Tokens from minecraft.net cookies (F12 -> Application -> Cookies)"
        )
        fmt_label.setWordWrap(True)
        fmt_label.setStyleSheet("color: #888; padding: 6px; background: #2d2d2d; border-radius: 6px; font-size: 11px;")
        layout.addWidget(fmt_label)

        # === Quick Auth Panel: type email & password → get token ===
        quick_group = QGroupBox("⚡ Quick Auth — Paste Email & Bearer Token → Get Token")
        quick_layout = QVBoxLayout(quick_group)
        quick_layout.setSpacing(10)

        quick_info = QLabel("Paste a Minecraft bearer token — validate and load it directly into the sniper.")
        quick_info.setWordWrap(True)
        quick_info.setStyleSheet("color: #888; font-size: 11px; padding: 4px;")
        quick_layout.addWidget(quick_info)

        # Email input
        quick_email_row = QHBoxLayout()
        quick_email_label = QLabel("Email:")
        quick_email_label.setFixedWidth(60)
        quick_email_row.addWidget(quick_email_label)
        self.quick_email_input = QLineEdit()
        self.quick_email_input.setPlaceholderText("optional-label@email.com (or leave blank for token-only)")
        quick_email_row.addWidget(self.quick_email_input, 1)
        quick_layout.addLayout(quick_email_row)

        # Password input
        quick_pass_row = QHBoxLayout()
        quick_pass_label = QLabel("Token:")
        quick_pass_label.setFixedWidth(60)
        quick_pass_row.addWidget(quick_pass_label)
        self.quick_pass_input = QLineEdit()
        self.quick_pass_input.setPlaceholderText("paste bearer token from minecraft.net cookie here")
        self.quick_pass_input.setEchoMode(QLineEdit.Normal)  # Show token for debugging
        quick_pass_row.addWidget(self.quick_pass_input, 1)
        quick_layout.addLayout(quick_pass_row)

        # Quick auth buttons
        quick_btn_row = QHBoxLayout()
        self.quick_auth_btn = QPushButton("🔑 Validate & Load Token")
        self.quick_auth_btn.clicked.connect(self._quick_auth_and_load)
        self.quick_auth_btn.setStyleSheet("padding: 8px 16px; background: #16a34a; color: white; border-radius: 4px; font-weight: bold;")
        quick_btn_row.addWidget(self.quick_auth_btn)

        self.quick_auth_status = QLabel("")
        self.quick_auth_status.setStyleSheet("color: #aaa; font-size: 11px; padding: 4px;")
        quick_btn_row.addWidget(self.quick_auth_status, 1)

        quick_layout.addLayout(quick_btn_row)

        # Separator
        separator = QFrame()
        separator.setFrameShape(QFrame.HLine)
        separator.setFrameShadow(QFrame.Sunken)
        separator.setStyleSheet("color: #444;")
        layout.addWidget(separator)

        # Folder selection
        folder_row = QHBoxLayout()
        self.accounts_folder_input = QLineEdit()
        self.accounts_folder_input.setPlaceholderText("Path to folder with account files (or a single file)")
        folder_btn = QPushButton("📁 Browse...")
        folder_btn.clicked.connect(self._browse_accounts_folder)
        folder_row.addWidget(self.accounts_folder_input, 1)
        folder_row.addWidget(folder_btn)
        layout.addLayout(folder_row)

        # Scan & Auth buttons
        btn_row = QHBoxLayout()
        self.scan_accounts_btn = QPushButton("Scan Folder")
        self.scan_accounts_btn.clicked.connect(self._scan_accounts_folder)
        self.scan_accounts_btn.setStyleSheet("min-width: 120px; padding: 8px;")
        btn_row.addWidget(self.scan_accounts_btn)

        self.auth_all_btn = QPushButton("Auth All Accounts")
        self.auth_all_btn.clicked.connect(self._auth_all_accounts)
        self.auth_all_btn.setEnabled(False)
        self.auth_all_btn.setStyleSheet("min-width: 150px; padding: 8px; background: #3a7bd5; color: white; border-radius: 4px;")
        btn_row.addWidget(self.auth_all_btn)

        self.stop_auth_btn = QPushButton("⏹️ Stop")
        self.stop_auth_btn.clicked.connect(self._stop_bulk_auth)
        self.stop_auth_btn.setEnabled(False)
        self.stop_auth_btn.setStyleSheet("min-width: 80px; padding: 8px;")
        btn_row.addWidget(self.stop_auth_btn)

        self.load_tokens_btn = QPushButton("📥 Load Tokens into Sniper")
        self.load_tokens_btn.clicked.connect(self._load_auth_tokens)
        self.load_tokens_btn.setEnabled(False)
        self.load_tokens_btn.setStyleSheet("min-width: 170px; padding: 8px; background: #16a34a; color: white; border-radius: 4px;")
        btn_row.addWidget(self.load_tokens_btn)

        btn_row.addStretch()
        layout.addLayout(btn_row)

        # Auth method selection
        method_row = QHBoxLayout()
        self.use_playwright_cb = QCheckBox("✅ Bearer token auth — no browser needed")
        self.use_playwright_cb.setChecked(True)
        self.use_playwright_cb.setEnabled(False)  # Always on, no alternative
        self.use_playwright_cb.setToolTip("Bearer tokens from minecraft.net cookies — fastest and most reliable method")
        method_row.addWidget(self.use_playwright_cb)

        self.test_auth_btn = QPushButton("🧪 Test Single Account")
        self.test_auth_btn.clicked.connect(self._test_single_auth)
        self.test_auth_btn.setEnabled(False)
        self.test_auth_btn.setStyleSheet("min-width: 150px; padding: 8px; background: #8b5cf6; color: white; border-radius: 4px;")
        method_row.addWidget(self.test_auth_btn)
        method_row.addStretch()
        layout.addLayout(method_row)

        # Progress
        self.auth_progress = QProgressBar()
        self.auth_progress.setMaximumHeight(18)
        self.auth_progress.setVisible(False)
        layout.addWidget(self.auth_progress)

        self.auth_status_label = QLabel("")
        self.auth_status_label.setStyleSheet("color: #aaa; padding: 4px; font-size: 11px;")
        layout.addWidget(self.auth_status_label)

        # Accounts table
        table_group = QGroupBox("📋 Discovered Accounts")
        table_layout = QVBoxLayout(table_group)
        self.accounts_table = QTableWidget()
        self.accounts_table.setColumnCount(5)
        self.accounts_table.setHorizontalHeaderLabels(["#", "Label", "Token", "Status", "Username"])
        self.accounts_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed)
        self.accounts_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.Fixed)
        self.accounts_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.Stretch)
        self.accounts_table.setColumnWidth(0, 40)
        self.accounts_table.setColumnWidth(3, 80)
        self.accounts_table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.accounts_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        table_layout.addWidget(self.accounts_table)
        layout.addWidget(table_group)

        # Cache management
        cache_row = QHBoxLayout()
        self.clear_cache_btn = QPushButton("🗑️ Clear Token Cache")
        self.clear_cache_btn.clicked.connect(self._clear_token_cache)
        self.clear_cache_btn.setStyleSheet("padding: 6px;")
        cache_row.addWidget(self.clear_cache_btn)

        self.cache_info_label = QLabel("Cache: not loaded")
        self.cache_info_label.setStyleSheet("color: #666; padding: 6px;")
        cache_row.addWidget(self.cache_info_label)
        cache_row.addStretch()
        layout.addLayout(cache_row)

        self._update_cache_info()
        return tab

    def _browse_accounts_folder(self):
        choice = QMessageBox.question(
            self, "Browse for Accounts",
            "Do you want to select a folder or a single file?",
            QMessageBox.StandardButton(QMessageBox.Yes | QMessageBox.No),
            QMessageBox.Yes
        )
        if choice == QMessageBox.Yes:
            folder = QFileDialog.getExistingDirectory(self, "Select Credential Folder")
            if folder:
                self.accounts_folder_input.setText(folder)
        else:
            filepath, _ = QFileDialog.getOpenFileName(self, "Select Credential File", "", "Text Files (*.txt);;All Files (*)")
            if filepath:
                self.accounts_folder_input.setText(filepath)

    def _clean_path(self, path_str):
        """Strip quotes, backticks, and whitespace from pasted paths"""
        return path_str.strip().strip('"').strip("'").strip('`').strip()

    def _scan_accounts_folder(self):
        folder = self.accounts_folder_input.text().strip().strip('"').strip("'").strip()
        if not folder:
            QMessageBox.warning(self, "No Folder", "Please select a credential folder first.")
            return

        from pathlib import Path as _Path
        path = _Path(folder)

        # If it's a file, use its parent directory and also parse the file directly
        if path.is_file():
            self._log(f"[Power] Selected a file, using its folder: {path.parent}")
            folder = str(path.parent)
            path = _Path(folder)

        if not path.is_dir():
            QMessageBox.critical(self, "Invalid Path", f"Not a valid folder:\n{folder}")
            return

        accounts = load_credentials_from_folder(folder)

        # Debug: log what was found
        self._log(f"[Power] Scanned folder: {folder}")
        self._log(f"[Power] Found {len(accounts)} accounts")
        if accounts:
            for email, _ in accounts:
                self._log(f"  ✓ {email}")
        else:
            # Try to list files for debug
            all_files = list(path.rglob('*'))
            file_count = len([f for f in all_files if f.is_file()])
            self._log(f"[Power] Folder contains {file_count} files total")
            for f in all_files[:10]:
                if f.is_file():
                    self._log(f"  file: {f.name} (suffix: {f.suffix})")

        if not accounts:
            QMessageBox.information(self, "No Accounts Found",
                "No accounts found in that folder.\n\nMake sure files contain:\n"
                "  EMAIL: x@y.com\n  PASSWORD: zzzz\n\nor email:password format.\n\n"
                "Check the log for file listing.")
            return

        # Populate table
        self.accounts_table.setRowCount(len(accounts))
        self._discovered_accounts = accounts  # Store for auth

        for idx, (email, password) in enumerate(accounts):
            num_item = QTableWidgetItem(str(idx + 1))
            num_item.setFlags(num_item.flags() & ~Qt.ItemIsEditable)
            self.accounts_table.setItem(idx, 0, num_item)

            email_item = QTableWidgetItem(email)
            email_item.setFlags(email_item.flags() & ~Qt.ItemIsEditable)
            self.accounts_table.setItem(idx, 1, email_item)

            pass_item = QTableWidgetItem("•" * min(len(password), 12))
            pass_item.setFlags(pass_item.flags() & ~Qt.ItemIsEditable)
            pass_item.setForeground(QColor("#666"))
            self.accounts_table.setItem(idx, 2, pass_item)

            status_item = QTableWidgetItem("⏳ Pending")
            status_item.setFlags(status_item.flags() & ~Qt.ItemIsEditable)
            status_item.setForeground(QColor("#f0ad4e"))
            self.accounts_table.setItem(idx, 3, status_item)

            gt_item = QTableWidgetItem("")
            gt_item.setFlags(gt_item.flags() & ~Qt.ItemIsEditable)
            self.accounts_table.setItem(idx, 4, gt_item)

        self.auth_all_btn.setEnabled(True)
        self.test_auth_btn.setEnabled(True)
        self.auth_status_label.setText(f"📋 Found {len(accounts)} accounts ready for authentication")
        self._log(f"🔍 Scanned {folder}: found {len(accounts)} accounts")

    def _auth_all_accounts(self):
        if not hasattr(self, '_discovered_accounts') or not self._discovered_accounts:
            return

        self.auth_worker = BulkAuthWorker(self._discovered_accounts, use_playwright=False)
        self.auth_worker.log_signal.connect(self._log)
        self.auth_worker.progress_signal.connect(self._update_auth_progress)
        self.auth_worker.account_result_signal.connect(self._update_account_result)
        self.auth_worker.finished_signal.connect(self._auth_finished)

        self.auth_progress.setVisible(True)
        self.auth_progress.setMaximum(len(self._discovered_accounts))
        self.auth_progress.setValue(0)
        self.auth_all_btn.setEnabled(False)
        self.stop_auth_btn.setEnabled(True)
        self.scan_accounts_btn.setEnabled(False)
        self.auth_status_label.setText("🔑 Validating bearer tokens...")

        self.auth_worker.start()

    def _update_auth_progress(self, current, total):
        self.auth_progress.setValue(current)

    def _update_account_result(self, email, status, gamertag, xuid):
        """Update the table row for an account result."""
        for row in range(self.accounts_table.rowCount()):
            email_item = self.accounts_table.item(row, 1)
            if email_item and email_item.text() == email:
                status_item = self.accounts_table.item(row, 3)
                gt_item = self.accounts_table.item(row, 4)

                if status == "success":
                    status_item.setText("✅ OK")
                    status_item.setForeground(QColor("#4CAF50"))
                    gt_item.setText(gamertag)
                    gt_item.setForeground(QColor("#4CAF50"))
                elif status == "cached":
                    status_item.setText("⚡ Cached")
                    status_item.setForeground(QColor("#2196F3"))
                    gt_item.setText(gamertag)
                    gt_item.setForeground(QColor("#2196F3"))
                elif status == "failed":
                    status_item.setText("❌ Failed")
                    status_item.setForeground(QColor("#f44336"))
                break

    def _auth_finished(self, success, failed, skipped):
        self.auth_progress.setVisible(False)
        self.auth_all_btn.setEnabled(True)
        self.stop_auth_btn.setEnabled(False)
        self.scan_accounts_btn.setEnabled(True)
        self.load_tokens_btn.setEnabled(True)
        self.auth_status_label.setText(f"📊 Done: ✅ {success} new | ❌ {failed} failed | ⚡ {skipped} cached")
        self._update_cache_info()

    def _stop_bulk_auth(self):
        if hasattr(self, 'auth_worker'):
            self.auth_worker.stop()
            self.stop_auth_btn.setEnabled(False)
            self.auth_status_label.setText("⏹️ Authentication stopped")

    def _test_single_auth(self):
        """Test authenticate the first pending account."""
        if not hasattr(self, '_discovered_accounts') or not self._discovered_accounts:
            return
        
        # Find first pending account
        email = None
        password = None
        for row in range(self.accounts_table.rowCount()):
            status_item = self.accounts_table.item(row, 3)
            if status_item and "Pending" in status_item.text():
                email_item = self.accounts_table.item(row, 1)
                if email_item:
                    email = email_item.text()
                    # Get password from discovered accounts
                    for e, p in self._discovered_accounts:
                        if e == email:
                            password = p
                            break
                break
        
        if not email:
            QMessageBox.information(self, "No Pending Accounts", "All accounts have been tested.")
            return
        
        use_pw = self.use_playwright_cb.isChecked()
        method = "Playwright" if use_pw else "HTTP"
        reply = QMessageBox.question(
            self, "Test Single Account",
            f"Test authenticate {email} using {method} auth?\n\nThis will launch a headless browser and log in to Microsoft.",
            QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes
        )
        if reply != QMessageBox.Yes:
            return
        
        self.test_auth_btn.setEnabled(False)
        self._log(f"\n🧪 Testing single auth: {email} ({method})")
        
        worker = SingleAuthTestWorker(email, password, use_pw)
        worker.log_signal.connect(self._log)
        worker.finished_signal.connect(self._test_auth_finished)
        worker.start()
    
    def _test_auth_finished(self, email, success, gamertag, error):
        """Handle test auth result."""
        self.test_auth_btn.setEnabled(True)
        for row in range(self.accounts_table.rowCount()):
            email_item = self.accounts_table.item(row, 1)
            if email_item and email_item.text() == email:
                status_item = self.accounts_table.item(row, 3)
                gt_item = self.accounts_table.item(row, 4)
                if success:
                    status_item.setText("✅ OK")
                    status_item.setForeground(QColor("#4CAF50"))
                    gt_item.setText(gamertag)
                    gt_item.setForeground(QColor("#4CAF50"))
                else:
                    status_item.setText("❌ Failed")
                    status_item.setForeground(QColor("#f44336"))
                    gt_item.setText(error[:40] if error else "")
                break
    
    def _load_auth_tokens(self):
        """Load authenticated tokens from cache into the sniper's token list."""
        cache_path = Path.home() / ".hermes" / "minecraft-sniper" / "tokens.json"
        if not cache_path.exists():
            QMessageBox.warning(self, "No Cache", "No token cache found. Run authentication first.")
            return

        cache = json.loads(cache_path.read_text())
        tokens = []
        for email, data in cache.items():
            if data.get("token"):
                tokens.append(data["token"])

        if not tokens:
            QMessageBox.information(self, "No Tokens", "No valid tokens found in cache.")
            return

        # Write to token file
        token_file_path = Path("/home/bub/minecraft-sniper-gui/account.txt")
        with open(token_file_path, 'w') as f:
            for token in tokens:
                f.write(f"{token}\n")

        # If there's a token file input in the UI, update it
        if hasattr(self, 'token_file_input'):
            self.token_file_input.setText(str(token_file_path))

        # Reload tokens
        if hasattr(self, '_load_tokens'):
            self._loaded_tokens = self._load_tokens()
            self._log(f"📥 Loaded {len(tokens)} tokens from cache into sniper")

        QMessageBox.information(self, "Tokens Loaded",
            f"Loaded {len(tokens)} tokens from {len(cache)} cached accounts.\n\n"
            f"Token file: {token_file_path}")

    def _clear_token_cache(self):
        cache_path = Path.home() / ".hermes" / "minecraft-sniper" / "tokens.json"
        if cache_path.exists():
            cache_path.unlink()
            self._log("🗑️ Token cache cleared")
            self._update_cache_info()
            QMessageBox.information(self, "Cache Cleared", "Token cache has been cleared.")
        else:
            self._update_cache_info()

    def _update_cache_info(self):
        cache_path = Path.home() / ".hermes" / "minecraft-sniper" / "tokens.json"
        if cache_path.exists():
            try:
                cache = json.loads(cache_path.read_text())
                valid = sum(1 for v in cache.values() if v.get("token"))
                self.cache_info_label.setText(f"Cache: {len(cache)} accounts ({valid} with tokens)")
            except Exception:
                self.cache_info_label.setText("Cache: corrupted")
        else:
            self.cache_info_label.setText("Cache: empty")

    # ========== Quick Auth: email & password → token ==========

    def _quick_auth_and_load(self):
        """Validate a bearer token and load it."""
        email = self.quick_email_input.text().strip()
        token = self.quick_pass_input.text().strip()
        
        if not token:
            self.quick_auth_status.setText("⚠️ Please paste a bearer token")
            return
        
        self.quick_auth_status.setText("🔍 Validating token...")
        self.quick_auth_btn.setEnabled(False)
        
        def _do_validate():
            try:
                auth = BearerTokenAuth(token, lambda m: None)
                info = auth.validate()
                username = info.get('username', 'unknown')
                self.quick_auth_status.setText(f"✅ Valid: {username}")
                self._log(f"✅ Token validated: {username}")
                
                # Load into tokens list
                current = self.tokens_input.toPlainText()
                label = email or username
                entry = f"{label}:{token}"
                if current.strip():
                    self.tokens_input.setPlainText(current.rstrip() + "\n" + entry)
                else:
                    self.tokens_input.setPlainText(entry)
                
                self._log(f"📥 Token loaded into sniper ({label})")
            except Exception as e:
                self.quick_auth_status.setText(f"❌ {e}")
                self._log(f"❌ Token validation failed: {e}")
            finally:
                self.quick_auth_btn.setEnabled(True)
        
        from PyQt5.QtCore import QThread
        class QuickAuthThread(QThread):
            def run(self):
                _do_validate()
        thread = QuickAuthThread()
        thread.start()

    # ========== Name Queue Tab ==========

    def _create_name_queue_tab(self):
        """Name queue management: add/remove/reorder names with individual drop times"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel(
            "📋 Queue multiple names with individual drop times.\n\n"
            "The sniper will automatically process each name in order, \n"
            "moving to the next name after completing the previous attempt.\n\n"
            "Queue is persisted to disk between sessions."
        )
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        # Add name row
        add_row = QHBoxLayout()
        self.queue_name_input = QLineEdit()
        self.queue_name_input.setPlaceholderText("Username")
        self.queue_name_input.setMaximumWidth(160)
        add_row.addWidget(QLabel("Name:"))
        add_row.addWidget(self.queue_name_input)

        self.queue_time_input = QLineEdit()
        self.queue_time_input.setPlaceholderText("2025-09-11T00:18:47.000Z")
        add_row.addWidget(QLabel("Drop time (UTC):"))
        add_row.addWidget(self.queue_time_input)

        self.queue_add_btn = QPushButton("➕ Add")
        self.queue_add_btn.clicked.connect(self._queue_add_name)
        add_row.addWidget(self.queue_add_btn)

        self.queue_start_btn = QPushButton("🚀 Start Queue")
        self.queue_start_btn.clicked.connect(self._queue_start_snipe)
        add_row.addWidget(self.queue_start_btn)

        layout.addLayout(add_row)

        # Queue table
        self.queue_table = QTableWidget()
        self.queue_table.setColumnCount(4)
        self.queue_table.setHorizontalHeaderLabels(["#", "Name", "Drop Time (UTC)", "Status"])
        self.queue_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
        self.queue_table.horizontalHeader().setSectionResizeMode(0, 40)
        self.queue_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
        self.queue_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
        self.queue_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
        layout.addWidget(self.queue_table)

        # Action buttons
        action_row = QHBoxLayout()
        self.queue_remove_btn = QPushButton("❌ Remove Selected")
        self.queue_remove_btn.clicked.connect(self._queue_remove_selected)
        action_row.addWidget(self.queue_remove_btn)

        self.queue_move_up_btn = QPushButton("⬆️ Move Up")
        self.queue_move_up_btn.clicked.connect(self._queue_move_up)
        action_row.addWidget(self.queue_move_up_btn)

        self.queue_move_down_btn = QPushButton("⬇️ Move Down")
        self.queue_move_down_btn.clicked.connect(self._queue_move_down)
        action_row.addWidget(self.queue_move_down_btn)

        self.queue_clear_btn = QPushButton("🗑️ Clear All")
        self.queue_clear_btn.clicked.connect(self._queue_clear_all)
        action_row.addWidget(self.queue_clear_btn)

        self.queue_import_btn = QPushButton("📁 Import List")
        self.queue_import_btn.clicked.connect(self._queue_import_list)
        action_row.addWidget(self.queue_import_btn)

        layout.addLayout(action_row)

        # Queue stats
        self.queue_stats_label = QLabel("Queue: 0 names")
        self.queue_stats_label.setStyleSheet("padding: 4px; color: #aaa;")
        layout.addWidget(self.queue_stats_label)

        # Load saved queue
        self._name_queue = []
        self._load_name_queue()
        self._refresh_queue_table()
        return tab

    def _get_queue_file(self):
        """Get path to queue persistence file"""
        queue_dir = Path.home() / ".hermes" / "minecraft-sniper"
        queue_dir.mkdir(parents=True, exist_ok=True)
        return queue_dir / "name_queue.json"

    def _load_name_queue(self):
        """Load name queue from disk"""
        try:
            queue_file = self._get_queue_file()
            if queue_file.exists():
                self._name_queue = json.loads(queue_file.read_text())
        except Exception:
            self._name_queue = []

    def _save_name_queue(self):
        """Save name queue to disk"""
        try:
            queue_file = self._get_queue_file()
            queue_file.write_text(json.dumps(self._name_queue, indent=2))
        except Exception:
            pass

    def _refresh_queue_table(self):
        """Refresh the queue table display"""
        self.queue_table.setRowCount(len(self._name_queue))
        for i, entry in enumerate(self._name_queue):
            # Index
            index_item = QTableWidgetItem(str(i + 1))
            index_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            self.queue_table.setItem(i, 0, index_item)

            # Name
            name_item = QTableWidgetItem(entry.get("name", ""))
            self.queue_table.setItem(i, 1, name_item)

            # Drop time
            time_item = QTableWidgetItem(entry.get("drop_time", ""))
            self.queue_table.setItem(i, 2, time_item)

            # Status
            status = entry.get("status", "pending")
            status_emoji = {"pending": "⏳", "ready": "🟡", "sniping": "🔴", "success": "✅", "failed": "❌"}.get(status, "⏳")
            status_item = QTableWidgetItem(f"{status_emoji} {status}")
            status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
            if status == "success":
                status_item.setForeground(QColor("#4CAF50"))
            elif status == "failed":
                status_item.setForeground(QColor("#f44336"))
            elif status == "sniping":
                status_item.setForeground(QColor("#FF9800"))
            self.queue_table.setItem(i, 3, status_item)

        self.queue_stats_label.setText(f"Queue: {len(self._name_queue)} name(s)")

    def _queue_add_name(self):
        """Add a name to the queue"""
        name = self.queue_name_input.text().strip()
        drop_time = self.queue_time_input.text().strip()

        if not name:
            QMessageBox.warning(self, "Error", "Please enter a username!")
            return
        if not drop_time:
            QMessageBox.warning(self, "Error", "Please enter a drop time!")
            return

        self._name_queue.append({
            "name": name,
            "drop_time": drop_time,
            "status": "pending"
        })
        self._save_name_queue()
        self._refresh_queue_table()
        self.queue_name_input.clear()
        self.queue_time_input.clear()

    def _queue_remove_selected(self):
        """Remove selected names from queue"""
        rows = sorted(set(item.row() for item in self.queue_table.selectedItems()), reverse=True)
        for row in rows:
            self._name_queue.pop(row)
        self._save_name_queue()
        self._refresh_queue_table()

    def _queue_move_up(self):
        """Move selected name up in queue"""
        row = self.queue_table.currentRow()
        if row > 0:
            self._name_queue[row], self._name_queue[row - 1] = self._name_queue[row - 1], self._name_queue[row]
            self._save_name_queue()
            self._refresh_queue_table()
            self.queue_table.selectRow(row - 1)

    def _queue_move_down(self):
        """Move selected name down in queue"""
        row = self.queue_table.currentRow()
        if 0 <= row < len(self._name_queue) - 1:
            self._name_queue[row], self._name_queue[row + 1] = self._name_queue[row + 1], self._name_queue[row]
            self._save_name_queue()
            self._refresh_queue_table()
            self.queue_table.selectRow(row + 1)

    def _queue_clear_all(self):
        """Clear entire queue"""
        if QMessageBox.question(self, "Clear Queue", "Remove all names from the queue?") == QMessageBox.StandardButton.Yes:
            self._name_queue = []
            self._save_name_queue()
            self._refresh_queue_table()

    def _queue_import_list(self):
        """Import names from a text file (one name per line, optional time)"""
        filepath, _ = QFileDialog.getOpenFileName(
            self, "Import Name List", "", "Text Files (*.txt);;All Files (*.*)"
        )
        if not filepath:
            return
        try:
            with open(filepath, 'r') as f:
                for line in f:
                    line = line.strip()
                    if not line or line.startswith('#'):
                        continue
                    parts = line.split(None, 1)
                    name = parts[0]
                    drop_time = parts[1] if len(parts) > 1 else ""
                    if name and drop_time:
                        self._name_queue.append({"name": name, "drop_time": drop_time, "status": "pending"})
            self._save_name_queue()
            self._refresh_queue_table()
        except Exception as e:
            QMessageBox.warning(self, "Error", f"Failed to import: {e}")

    def _queue_start_snipe(self):
        """Start sniping through the queue sequentially"""
        if not self._name_queue:
            QMessageBox.warning(self, "Empty Queue", "Add names to the queue first!")
            return

        # Validate queue entries
        valid_entries = [e for e in self._name_queue if e.get("name") and e.get("drop_time")]
        if not valid_entries:
            QMessageBox.warning(self, "Error", "All queue entries need both a name and drop time!")
            return

        # Extract names and drop times
        names = [e["name"] for e in valid_entries]
        drop_times = [e["drop_time"] for e in valid_entries]

        # Get tokens
        tokens_text = self.tokens_input.toPlainText()
        if self.autoauth_checkbox.isChecked():
            tokens = self._authenticate_accounts_if_needed(tokens_text, log_callback=self._log)
        else:
            tokens = [t.strip() for t in tokens_text.split('\n') if t.strip()]

        if not tokens:
            QMessageBox.warning(self, "Error", "No valid tokens available!")
            return

        # Stop any existing workers
        self.stop_sniper()

        # Update status
        self.status_label.setText("🔥 QUEUE SNIPE ACTIVE!")
        self.status_label.setStyleSheet("padding: 5px; background-color: #3a7bd5; color: white; border-radius: 3px;")

        # Update queue status in table
        for i, entry in enumerate(self._name_queue):
            entry["status"] = "pending"
        self._save_name_queue()
        self._refresh_queue_table()

        self._log(f"🚀 Starting queue sniper: {len(names)} name(s)")
        for i, (n, t) in enumerate(zip(names, drop_times)):
            self._log(f"  {i+1}. {n} → drops {t}")

        # Get settings
        timing_mode = getattr(self.current_sniper_tab, 'timing_mode', 'exact')
        threads_count = self.threads_spin.value()
        priority_mode = "fresh_first" if self.priority_combo.currentText() == "Fresh Tokens First" else "original"
        warmup_enabled = self.warmup_checkbox.isChecked()
        warmup_duration = self.warmup_duration_spin.value()
        adaptive_enabled = self.adaptive_checkbox.isChecked()
        aggressiveness = self.aggressiveness_slider.value()
        fingerprint_rotation = self.fingerprint_checkbox.isChecked()
        token_rotation = self.token_rotation_checkbox.isChecked()
        temporal_jitter = self.temporal_jitter_checkbox.isChecked()
        response_analysis = self.response_analysis_checkbox.isChecked()
        offset_calibration = self.offset_calibration_checkbox.isChecked()
        webhook_url = self.webhook_url_input.text().strip() if hasattr(self, 'webhook_url_input') and self.webhook_enable_checkbox.isChecked() else None
        auto_restart_enabled = getattr(self, 'watchdog_enable_checkbox', None) and self.watchdog_enable_checkbox.isChecked()
        auto_restart_max = self.watchdog_max_restarts_spin.value() if hasattr(self, 'watchdog_max_restarts_spin') else 3

        # Create queue-aware worker
        worker = SniperWorker(
            names, tokens, drop_times, threads_count, timing_mode, priority_mode,
            warmup_enabled, warmup_duration, adaptive_enabled, aggressiveness,
            fingerprint_rotation, token_rotation, temporal_jitter, response_analysis,
            "sequential", 0.1, offset_calibration_enabled=offset_calibration,
            webhook_url=webhook_url,
            auto_restart_enabled=auto_restart_enabled,
            auto_restart_max=auto_restart_max,
            skin_enabled=getattr(self, 'skin_enable_checkbox', None) and self.skin_enable_checkbox.isChecked(),
            skin_file_path=getattr(self, 'skin_file_input', None) and self.skin_file_input.text().strip(),
            skin_model=getattr(self, 'skin_model_combo', None) and self.skin_model_combo.currentText(),
            queue_mode=True,
            queue_callback=lambda idx, status: self._on_queue_name_update(idx, status),
            desktop_notifications_enabled=True,
            fire_and_forget=False,
            dry_run=False,
            pre_validate_tokens=False,
            auto_detect_account_type=False,
            ntp_sync_enabled=True,
        )
        worker.log_signal.connect(self._log)
        worker.progress_signal.connect(self.progress_bar.setValue)
        worker.stats_signal.connect(self._update_stats_display)
        worker.finished.connect(lambda: self.on_sniper_finished())
        worker.start()
        self.workers.append(worker)

    def _on_queue_name_update(self, idx, status):
        """Callback when a queue name completes"""
        if 0 <= idx < len(self._name_queue):
            self._name_queue[idx]["status"] = status
            self._save_name_queue()
            self._refresh_queue_table()

    # ========== Auto Skin Upload Tab ==========

    def _create_skin_tab(self):
        """Auto skin upload configuration panel"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel(
            "🎨 Automatically upload a skin after successfully sniping a name.\n\n"
            "This serves two purposes:\n"
            "  1. Verifies the name change actually took effect\n"
            "  2. Sets your visual identity immediately\n\n"
            "Skin files must be PNG images (max 1MB).\n"
            "Standard Minecraft skin: 64x64 or 64x32 pixels."
        )
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        form = QFormLayout()

        # Enable checkbox
        self.skin_enable_checkbox = QCheckBox("Enable auto skin upload on successful snipe")
        self.skin_enable_checkbox.setChecked(False)
        form.addRow("", self.skin_enable_checkbox)

        # Skin file path
        skin_file_row = QHBoxLayout()
        self.skin_file_input = QLineEdit()
        self.skin_file_input.setPlaceholderText("/path/to/skin.png")
        skin_file_row.addWidget(self.skin_file_input)
        self.skin_file_browse_btn = QPushButton("📁 Browse")
        self.skin_file_browse_btn.clicked.connect(self._browse_skin_file)
        skin_file_row.addWidget(self.skin_file_browse_btn)
        form.addRow("Skin PNG file:", skin_file_row)

        # Skin model (classic vs slim)
        self.skin_model_combo = QComboBox()
        self.skin_model_combo.addItems(["classic", "slim"])
        self.skin_model_combo.setCurrentText("classic")
        self.skin_model_combo.setToolTip(
            "Classic = default Steve/Alex shape (64x64)\n"
            "Slim = thin arms (Alex-style, 64x64)"
        )
        form.addRow("Skin model:", self.skin_model_combo)

        # Skin info display
        self.skin_info_label = QLabel("No skin file selected")
        self.skin_info_label.setStyleSheet("color: #888; padding: 4px; font-size: 11px;")
        form.addRow("File info:", self.skin_info_label)

        # Watch for file changes
        self.skin_file_input.textChanged.connect(self._update_skin_info)

        layout.addLayout(form)
        layout.addStretch()
        return tab

    def _browse_skin_file(self):
        """Open file dialog to select a skin PNG"""
        filepath, _ = QFileDialog.getOpenFileName(
            self,
            "Select Skin PNG",
            str(Path.home()),
            "PNG Images (*.png);;All Files (*.*)"
        )
        if filepath:
            self.skin_file_input.setText(filepath)

    def _update_skin_info(self):
        """Update skin file info display when file path changes"""
        filepath = self.skin_file_input.text().strip()
        if not filepath or not Path(filepath).exists():
            self.skin_info_label.setText("No skin file selected")
            self.skin_info_label.setStyleSheet("color: #888; padding: 4px; font-size: 11px;")
            return

        try:
            from PIL import Image
            img = Image.open(filepath)
            width, height = img.size
            file_size_kb = Path(filepath).stat().st_size / 1024
            self.skin_info_label.setText(
                f"✅ {width}x{height}px, {file_size_kb:.1f}KB"
            )
            self.skin_info_label.setStyleSheet("color: #4CAF50; padding: 4px; font-size: 11px;")
        except ImportError:
            file_size_kb = Path(filepath).stat().st_size / 1024
            self.skin_info_label.setText(f"📄 {file_size_kb:.1f}KB (install Pillow for dimension check)")
            self.skin_info_label.setStyleSheet("color: #FFA500; padding: 4px; font-size: 11px;")
        except Exception as e:
            self.skin_info_label.setText(f"⚠️ Error reading file: {e}")
            self.skin_info_label.setStyleSheet("color: #f44336; padding: 4px; font-size: 11px;")

    # ========== Network Latency Monitor Tab ==========

    def _create_latency_tab(self):
        """Real-time network latency monitoring to MC API endpoints"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("📡 Monitor latency to key Minecraft API endpoints.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        # Latency display
        self.latency_display = QTextEdit()
        self.latency_display.setReadOnly(True)
        self.latency_display.setMaximumHeight(200)
        self.latency_display.setStyleSheet("font-family: monospace; font-size: 12px; background: #1a1a1a; color: #0f0; border: 1px solid #333;")
        layout.addWidget(self.latency_display)

        # Controls
        ctrl_layout = QHBoxLayout()
        self.latency_start_btn = QPushButton("▶️ Start Monitor")
        self.latency_start_btn.clicked.connect(self._toggle_latency_monitor)
        self.latency_start_btn.setStyleSheet("padding: 8px; font-size: 13px;")
        ctrl_layout.addWidget(self.latency_start_btn)

        self.latency_interval_spin = QSpinBox()
        self.latency_interval_spin.setRange(1, 60)
        self.latency_interval_spin.setValue(5)
        self.latency_interval_spin.setSuffix("s")
        ctrl_layout.addWidget(self.latency_interval_spin)
        ctrl_layout.addStretch()
        layout.addLayout(ctrl_layout)

        self.latency_worker = None
        self.latency_running = False
        layout.addStretch()
        return tab

    def _toggle_latency_monitor(self):
        """Start/stop the latency monitor"""
        if self.latency_running:
            self.latency_running = False
            if self.latency_worker:
                self.latency_worker.stop()
                self.latency_worker.wait()
            self.latency_start_btn.setText("▶️ Start Monitor")
            self.latency_start_btn.setStyleSheet("padding: 8px; font-size: 13px;")
            self._log("[📡] Latency monitor stopped")
        else:
            self.latency_running = True
            interval = self.latency_interval_spin.value()
            self.latency_worker = LatencyMonitorWorker(interval)
            self.latency_worker.log_signal.connect(self._append_latency_log)
            self.latency_worker.start()
            self.latency_start_btn.setText("⏹️ Stop Monitor")
            self.latency_start_btn.setStyleSheet("padding: 8px; font-size: 13px; background: #c0392b; color: white;")
            self._log(f"[📡] Latency monitor started (every {interval}s)")

    def _append_latency_log(self, text):
        """Append text to latency display"""
        self.latency_display.append(text)
        # Keep last 50 lines
        doc = self.latency_display.document()
        if doc.blockCount() > 50:
            cursor = self.latency_display.textCursor()
            cursor.movePosition(cursor.Start)
            for _ in range(doc.blockCount() - 50):
                cursor.select(cursor.BlockUnderCursor)
                cursor.insertText("")
            self.latency_display.setTextCursor(cursor)

    # ========== Proxy Tab ==========

    def _create_proxy_tab(self):
        """Proxy configuration for all HTTP requests"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🌐 Route requests through a proxy (HTTP/SOCKS5). Useful for bypassing IP bans or rate limits.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        form = QFormLayout()
        self.proxy_enable_checkbox = QCheckBox("Enable proxy")
        form.addRow("", self.proxy_enable_checkbox)

        self.proxy_url_input = QLineEdit()
        self.proxy_url_input.setPlaceholderText("http://user:pass@proxy_ip:port or socks5://...")
        form.addRow("Proxy URL:", self.proxy_url_input)

        self.proxy_test_btn = QPushButton("🔍 Test Proxy")
        self.proxy_test_btn.clicked.connect(self._test_proxy)
        form.addRow("", self.proxy_test_btn)

        self.proxy_status_label = QLabel("")
        self.proxy_status_label.setStyleSheet("padding: 4px; font-size: 11px;")
        form.addRow("", self.proxy_status_label)

        layout.addLayout(form)
        layout.addStretch()
        return tab

    def _get_proxy_url(self):
        """Get configured proxy URL if enabled"""
        if hasattr(self, 'proxy_enable_checkbox') and self.proxy_enable_checkbox.isChecked():
            return self.proxy_url_input.text().strip()
        return None

    def _test_proxy(self):
        """Test proxy connectivity"""
        proxy_url = self.proxy_url_input.text().strip()
        if not proxy_url:
            self.proxy_status_label.setText("⚠️ Enter a proxy URL first")
            self.proxy_status_label.setStyleSheet("color: #FFA500; padding: 4px; font-size: 11px;")
            return

        self.proxy_status_label.setText("⏳ Testing...")
        self.proxy_status_label.setStyleSheet("color: #2196F3; padding: 4px; font-size: 11px;")
        QApplication.processEvents()

        try:
            proxies = {"http": proxy_url, "https": proxy_url}
            start = time.time()
            resp = httpx.get("https://api.minecraftservices.com/", proxies=proxies, timeout=10)
            elapsed = time.time() - start
            self.proxy_status_label.setText(f"✅ Connected via proxy ({elapsed:.1f}s) — {resp.status_code}")
            self.proxy_status_label.setStyleSheet("color: #4CAF50; padding: 4px; font-size: 11px;")
        except Exception as e:
            self.proxy_status_label.setText(f"❌ Failed: {str(e)[:60]}")
            self.proxy_status_label.setStyleSheet("color: #f44336; padding: 4px; font-size: 11px;")

    # ========== Desktop File Tab ==========

    def _create_desktop_file_tab(self):
        """Generate .desktop launcher file for Linux"""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🖥️ Create a desktop launcher file so you can start the sniper from your desktop environment.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        form = QFormLayout()
        self.desktop_name_input = QLineEdit("Minecraft Username Sniper")
        form.addRow("App Name:", self.desktop_name_input)

        python_path = sys.executable
        script_path = getattr(sys, '_MEIPASS', None)
        if script_path and hasattr(sys, 'frozen'):
            script_path = os.path.join(script_path, 'sniper.py')
        else:
            script_path = os.path.abspath(__file__)
        self.desktop_script_input = QLineEdit(script_path)
        form.addRow("Script Path:", self.desktop_script_input)

        self.desktop_icon_input = QLineEdit("")
        self.desktop_icon_input.setPlaceholderText("Optional: path to icon (.png)")
        form.addRow("Icon Path:", self.desktop_icon_input)

        self.desktop_generate_btn = QPushButton("🖥️ Generate .desktop File")
        self.desktop_generate_btn.clicked.connect(self._generate_desktop_file)
        form.addRow("", self.desktop_generate_btn)

        self.desktop_status_label = QLabel("")
        self.desktop_status_label.setStyleSheet("padding: 4px; font-size: 11px;")
        form.addRow("", self.desktop_status_label)

        layout.addLayout(form)
        layout.addStretch()
        return tab


    def _create_watchdog_tab(self):
        """Auto-restart watchdog configuration tab"""
        tab = QWidget()
        layout = QVBoxLayout(tab)
        info = QLabel("🛡️ Auto-Restart Watchdog")
        info.setStyleSheet("font-size: 16px; font-weight: bold; padding: 10px;")
        layout.addWidget(info)
        desc = QLabel("Automatically restart the sniper if it crashes or fails.")
        desc.setWordWrap(True)
        desc.setStyleSheet("padding: 4px; color: #aaa;")
        layout.addWidget(desc)
        form = QFormLayout()
        self.watchdog_enable = QCheckBox("Enable auto-restart watchdog")
        self.watchdog_enable.setChecked(True)
        form.addRow("Enabled:", self.watchdog_enable)
        self.watchdog_max_restarts = QSpinBox()
        self.watchdog_max_restarts.setRange(1, 10)
        self.watchdog_max_restarts.setValue(3)
        form.addRow("Max restarts:", self.watchdog_max_restarts)
        self.watchdog_cooldown = QSpinBox()
        self.watchdog_cooldown.setRange(5, 300)
        self.watchdog_cooldown.setValue(30)
        self.watchdog_cooldown.setSuffix("s")
        form.addRow("Cooldown:", self.watchdog_cooldown)
        layout.addLayout(form)
        layout.addStretch()
        return tab

    def _create_namemc_search_tab(self):
        """NameMC profile search tab"""
        tab = QWidget()
        layout = QVBoxLayout(tab)
        info = QLabel("🔍 NameMC Profile Search")
        info.setStyleSheet("font-size: 16px; font-weight: bold; padding: 10px;")
        layout.addWidget(info)
        search_layout = QHBoxLayout()
        self.namemc_search_input = QLineEdit()
        self.namemc_search_input.setPlaceholderText("Enter username or UUID")
        search_layout.addWidget(self.namemc_search_input)
        search_btn = QPushButton("Search")
        search_btn.clicked.connect(self._search_namemc)
        search_layout.addWidget(search_btn)
        layout.addLayout(search_layout)
        self.namemc_result = QTextEdit()
        self.namemc_result.setReadOnly(True)
        self.namemc_result.setMaximumHeight(200)
        layout.addWidget(self.namemc_result)
        layout.addStretch()
        return tab

    def _search_namemc(self):
        query = self.namemc_search_input.text().strip()
        if not query:
            return
        try:
            resp = httpx.get(f"https://namemc.com/api/v2/user/{query}", timeout=10.0)
            if resp.status_code == 200:
                data = resp.json()
                self.namemc_result.setText(f"Name: {data.get('username', 'N/A')}\nUUID: {data.get('id', 'N/A')}")
            else:
                self.namemc_result.setText(f"Not found (HTTP {resp.status_code})")
        except Exception as e:
            self.namemc_result.setText(f"Error: {e}")

    def _create_headless_tab(self):
        """Headless CLI launcher tab"""
        tab = QWidget()
        layout = QVBoxLayout(tab)
        info = QLabel("💻 Headless CLI Mode")
        info.setStyleSheet("font-size: 16px; font-weight: bold; padding: 10px;")
        layout.addWidget(info)
        desc = QLabel("Run sniper from command line:\n  python3 sniper.py --headless --name Steve --token TOKEN")
        desc.setWordWrap(True)
        desc.setStyleSheet("padding: 4px; color: #aaa; font-family: monospace;")
        layout.addWidget(desc)
        layout.addStretch()
        return tab

    def _create_time_converter_tab(self):
        """PST to UTC time converter tab"""
        tab = QWidget()
        layout = QVBoxLayout(tab)
        info = QLabel("⏰ Time Zone Converter")
        info.setStyleSheet("font-size: 16px; font-weight: bold; padding: 10px;")
        layout.addWidget(info)
        desc = QLabel("Minecraft name drops happen at midnight UTC.")
        desc.setWordWrap(True)
        desc.setStyleSheet("padding: 4px; color: #aaa;")
        layout.addWidget(desc)
        form = QFormLayout()
        self.time_input = QLineEdit()
        self.time_input.setPlaceholderText("e.g. 2025-06-01 00:00 UTC")
        form.addRow("Drop time (UTC):", self.time_input)
        convert_btn = QPushButton("Convert")
        convert_btn.clicked.connect(self._convert_time)
        form.addRow("", convert_btn)
        self.time_result = QLabel("")
        self.time_result.setStyleSheet("padding: 8px; background: #1e1e1e; border-radius: 4px; color: #4caf50;")
        form.addRow("Local time:", self.time_result)
        layout.addLayout(form)
        layout.addStretch()
        return tab

    def _convert_time(self):
        text = self.time_input.text().strip()
        if not text:
            return
        try:
            dt = datetime.fromisoformat(text.replace("UTC", "+00:00"))
            local = dt.astimezone()
            self.time_result.setText(local.strftime("%Y-%m-%d %H:%M:%S %Z"))
        except Exception as e:
            self.time_result.setText(f"Error: {e}")

    def _generate_desktop_file(self):
        """Generate and install a .desktop file"""
        app_name = self.desktop_name_input.text().strip() or "Minecraft Username Sniper"
        script_path = self.desktop_script_input.text().strip()
        icon_path = self.desktop_icon_input.text().strip()

        if not script_path or not Path(script_path).exists():
            self.desktop_status_label.setText("❌ Script path is invalid")
            self.desktop_status_label.setStyleSheet("color: #f44336; padding: 4px; font-size: 11px;")
            return

        python_path = sys.executable
        desktop_name = app_name.lower().replace(" ", "-") + ".desktop"
        desktop_dir = Path.home() / ".local" / "share" / "applications"
        desktop_dir.mkdir(parents=True, exist_ok=True)
        desktop_file = desktop_dir / desktop_name

        content = f"""[Desktop Entry]
Version=1.0
Type=Application
Name={app_name}
Comment=Minecraft Username Sniper GUI
Exec={python_path} {script_path}
"""
        if icon_path:
            content += f"Icon={icon_path}\n"
        content += "Terminal=true\nCategories=Utility;Game;\nStartupNotify=false\n"

        try:
            desktop_file.write_text(content)
            desktop_file.chmod(0o755)
            self.desktop_status_label.setText(f"✅ Created: {desktop_file}")
            self.desktop_status_label.setStyleSheet("color: #4CAF50; padding: 4px; font-size: 11px;")
            self._log(f"[🖥️] Desktop file created: {desktop_file}")
        except Exception as e:
            self.desktop_status_label.setText(f"❌ Failed: {e}")
            self.desktop_status_label.setStyleSheet("color: #f44336; padding: 4px; font-size: 11px;")

    # =========================================================================
    # NEW FEATURES UI: m1-m8, l1-l6
    # =========================================================================

    def _create_scheduled_snipes_tab(self):
        """m1: Schedule a snipe to auto-start at a future time."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("⏰ Schedule a snipe to automatically start at a specific date/time (UTC).")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        form = QFormLayout()
        self.sched_name_input = QLineEdit()
        self.sched_name_input.setPlaceholderText("Username to snipe")
        form.addRow("Name:", self.sched_name_input)

        self.sched_time_input = QLineEdit()
        self.sched_time_input.setPlaceholderText("2026-06-27 14:30")
        form.addRow("Date/Time (UTC, YYYY-MM-DD HH:MM):", self.sched_time_input)

        self.sched_status = QLabel("")
        self.sched_status.setStyleSheet("color: #888; padding: 4px;")
        form.addRow("Status:", self.sched_status)

        layout.addLayout(form)

        btn_layout = QHBoxLayout()
        self.sched_start_btn = QPushButton("🚀 Schedule Snipe")
        self.sched_start_btn.setStyleSheet("padding: 8px; background: #4CAF50; color: white; font-weight: bold;")
        self.sched_start_btn.clicked.connect(self._start_scheduled_snipe)
        btn_layout.addWidget(self.sched_start_btn)

        self.sched_cancel_btn = QPushButton("❌ Cancel")
        self.sched_cancel_btn.clicked.connect(self._cancel_scheduled_snipe)
        btn_layout.addWidget(self.sched_cancel_btn)

        layout.addLayout(btn_layout)
        layout.addStretch()

        self._sched_worker = None
        return tab

    def _start_scheduled_snipe(self):
        name = self.sched_name_input.text().strip()
        time_str = self.sched_time_input.text().strip()
        if not name or not time_str:
            QMessageBox.warning(self, "Missing Info", "Please enter both a name and a target time.")
            return

        self._sched_worker = ScheduledSnipeWorker(time_str, name)
        self._sched_worker.started.connect(self._on_scheduled_snipe_ready)
        self._sched_worker.finished.connect(lambda msg: self._q_invoke(
            lambda: self.sched_status.setText(f"✅ {msg}")))
        self._sched_worker.start()
        self.sched_status.setText(f"⏳ Waiting until {time_str} UTC...")
        self.sched_status.setStyleSheet("color: #ff9800; padding: 4px;")
        self._log(f"[⏰] Scheduled snipe for '{name}' at {time_str} UTC")

    def _cancel_scheduled_snipe(self):
        if self._sched_worker and self._sched_worker.isRunning():
            self._sched_worker.stop()
            self.sched_status.setText("❌ Cancelled")
            self.sched_status.setStyleSheet("color: #f44336; padding: 4px;")
            self._log("[⏰] Scheduled snipe cancelled")

    def _on_scheduled_snipe_ready(self, name):
        """When scheduled time arrives, auto-fill and start snipe."""
        self._log(f"[⏰] Scheduled time reached for '{name}'! Ready to snipe.")
        send_desktop_notification("Scheduled Snipe Ready", f"Time to snipe: {name}")
        play_voice_alert(f"Ready to snipe {name}")

    def _create_telegram_tab(self):
        """m2: Telegram bot notification settings."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("📱 Send snipe alerts to Telegram via Bot API.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        settings = load_telegram_settings()

        form = QFormLayout()
        self.tg_token_input = QLineEdit(settings.get("bot_token", ""))
        self.tg_token_input.setPlaceholderText("123456:ABC-DEF...")
        form.addRow("Bot Token:", self.tg_token_input)

        self.tg_chat_id_input = QLineEdit(settings.get("chat_id", ""))
        self.tg_chat_id_input.setPlaceholderText("-1001234567890")
        form.addRow("Chat ID:", self.tg_chat_id_input)

        self.tg_enabled_cb = QCheckBox("Enable Telegram notifications")
        self.tg_enabled_cb.setChecked(settings.get("enabled", False))
        form.addRow("", self.tg_enabled_cb)

        layout.addLayout(form)

        btn_layout = QHBoxLayout()
        save_btn = QPushButton("💾 Save")
        save_btn.clicked.connect(self._save_telegram_settings)
        btn_layout.addWidget(save_btn)

        test_btn = QPushButton("📨 Test Message")
        test_btn.clicked.connect(self._test_telegram)
        btn_layout.addWidget(test_btn)

        self.tg_status = QLabel("")
        self.tg_status.setStyleSheet("color: #888; padding: 4px;")
        btn_layout.addWidget(self.tg_status)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return tab

    def _save_telegram_settings(self):
        settings = {
            "bot_token": self.tg_token_input.text().strip(),
            "chat_id": self.tg_chat_id_input.text().strip(),
            "enabled": self.tg_enabled_cb.isChecked(),
        }
        save_telegram_settings(settings)
        self.tg_status.setText("✅ Saved")
        self.tg_status.setStyleSheet("color: #4CAF50; padding: 4px;")
        self._log("[📱] Telegram settings saved")

    def _test_telegram(self):
        settings = {
            "bot_token": self.tg_token_input.text().strip(),
            "chat_id": self.tg_chat_id_input.text().strip(),
        }
        result = send_telegram_message(settings["bot_token"], settings["chat_id"],
            "<b>Test</b>: Sniper GUI Telegram integration is working!")
        if result:
            self.tg_status.setText("✅ Test message sent!")
            self.tg_status.setStyleSheet("color: #4CAF50; padding: 4px;")
        else:
            self.tg_status.setText("❌ Failed to send")
            self.tg_status.setStyleSheet("color: #f44336; padding: 4px;")

    def _create_pushover_tab(self):
        """l6: Pushover notification settings."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🔔 Send snipe alerts via Pushover (pushover.net).")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        settings = load_pushover_settings()

        form = QFormLayout()
        self.push_token_input = QLineEdit(settings.get("token", ""))
        self.push_token_input.setPlaceholderText("App token from Pushover")
        form.addRow("App Token:", self.push_token_input)

        self.push_user_input = QLineEdit(settings.get("user_key", ""))
        self.push_user_input.setPlaceholderText("Your user key")
        form.addRow("User Key:", self.push_user_input)

        self.push_enabled_cb = QCheckBox("Enable Pushover notifications")
        self.push_enabled_cb.setChecked(settings.get("enabled", False))
        form.addRow("", self.push_enabled_cb)

        layout.addLayout(form)

        btn_layout = QHBoxLayout()
        save_btn = QPushButton("💾 Save")
        save_btn.clicked.connect(self._save_pushover_settings)
        btn_layout.addWidget(save_btn)

        test_btn = QPushButton("📨 Test")
        test_btn.clicked.connect(self._test_pushover)
        btn_layout.addWidget(test_btn)

        self.push_status = QLabel("")
        self.push_status.setStyleSheet("color: #888; padding: 4px;")
        btn_layout.addWidget(self.push_status)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return tab

    def _save_pushover_settings(self):
        settings = {
            "token": self.push_token_input.text().strip(),
            "user_key": self.push_user_input.text().strip(),
            "enabled": self.push_enabled_cb.isChecked(),
        }
        save_pushover_settings(settings)
        self.push_status.setText("✅ Saved")
        self.push_status.setStyleSheet("color: #4CAF50; padding: 4px;")

    def _test_pushover(self):
        settings = {
            "token": self.push_token_input.text().strip(),
            "user_key": self.push_user_input.text().strip(),
        }
        result = send_pushover_notification(settings, "Sniper GUI Test", "Pushover integration is working!")
        if result:
            self.push_status.setText("✅ Test sent!")
            self.push_status.setStyleSheet("color: #4CAF50; padding: 4px;")
        else:
            self.push_status.setText("❌ Failed")
            self.push_status.setStyleSheet("color: #f44336; padding: 4px;")

    def _create_token_stats_tab(self):
        """m5: Per-token statistics dashboard."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("📊 Performance statistics per authentication token.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        self.token_stats_table = QTableWidget()
        self.token_stats_table.setColumnCount(7)
        self.token_stats_table.setHorizontalHeaderLabels(["Token", "Requests", "Success", "Failed", "Rate Limited", "Avg Latency", "Snipes Won"])
        self.token_stats_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        layout.addWidget(self.token_stats_table)

        btn_layout = QHBoxLayout()
        refresh_btn = QPushButton("🔄 Refresh")
        refresh_btn.clicked.connect(self._refresh_token_stats)
        btn_layout.addWidget(refresh_btn)

        clear_btn = QPushButton("🗑️ Clear Stats")
        clear_btn.clicked.connect(lambda: (save_token_stats({}), self._refresh_token_stats()))
        btn_layout.addWidget(clear_btn)

        layout.addLayout(btn_layout)
        self._refresh_token_stats()
        return tab

    def _refresh_token_stats(self):
        stats = load_token_stats()
        self.token_stats_table.setRowCount(len(stats))
        for i, (token_key, s) in enumerate(stats.items()):
            avg_lat = s["total_latency_ms"] / s["total_requests"] if s["total_requests"] else 0
            self.token_stats_table.setItem(i, 0, QTableWidgetItem(token_key[:20]))
            self.token_stats_table.setItem(i, 1, QTableWidgetItem(str(s["total_requests"])))
            self.token_stats_table.setItem(i, 2, QTableWidgetItem(str(s["successful"])))
            self.token_stats_table.setItem(i, 3, QTableWidgetItem(str(s["failed"])))
            self.token_stats_table.setItem(i, 4, QTableWidgetItem(str(s["rate_limited"])))
            self.token_stats_table.setItem(i, 5, QTableWidgetItem(f"{avg_lat:.1f}ms"))
            self.token_stats_table.setItem(i, 6, QTableWidgetItem(str(s["snipes_won"])))

    def _create_latency_graph_tab(self):
        """l1: Real-time latency graph visualization."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("📈 Real-time latency graph to api.minecraftservices.com")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        self.latency_graph = LatencyGraphWidget()
        layout.addWidget(self.latency_graph)

        btn_layout = QHBoxLayout()
        self.lat_graph_start_btn = QPushButton("▶ Start Monitoring")
        self.lat_graph_start_btn.clicked.connect(self._toggle_latency_graph)
        btn_layout.addWidget(self.lat_graph_start_btn)

        clear_btn = QPushButton("🗑️ Clear")
        clear_btn.clicked.connect(self.latency_graph.clear)
        btn_layout.addWidget(clear_btn)

        self.lat_graph_status = QLabel("Stopped")
        self.lat_graph_status.setStyleSheet("color: #888; padding: 4px;")
        btn_layout.addWidget(self.lat_graph_status)

        layout.addLayout(btn_layout)

        self.lat_graph_timer = QTimer()
        self.lat_graph_timer.timeout.connect(self._latency_graph_probe)
        self.lat_graph_running = False
        return tab

    def _toggle_latency_graph(self):
        if self.lat_graph_running:
            self.lat_graph_timer.stop()
            self.lat_graph_running = False
            self.lat_graph_start_btn.setText("▶ Start Monitoring")
            self.lat_graph_status.setText("Stopped")
            self.lat_graph_status.setStyleSheet("color: #888; padding: 4px;")
        else:
            self.lat_graph_timer.start(1000)  # probe every second
            self.lat_graph_running = True
            self.lat_graph_start_btn.setText("⏹ Stop")
            self.lat_graph_status.setText("Monitoring...")
            self.lat_graph_status.setStyleSheet("color: #4CAF50; padding: 4px;")

    def _latency_graph_probe(self):
        start = time.perf_counter()
        try:
            httpx.head("https://api.minecraftservices.com", timeout=3)
            latency = (time.perf_counter() - start) * 1000
            self.latency_graph.add_point(latency)
            self.lat_graph_status.setText(f"{latency:.0f}ms")
        except:
            pass

    def _create_session_replay_tab(self):
        """l2: Browse and replay snipe session recordings."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🎬 Browse recorded snipe sessions. Enable recording in snipe options.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        self.replay_list = QTableWidget()
        self.replay_list.setColumnCount(4)
        self.replay_list.setHorizontalHeaderLabels(["File", "Size", "Events", ""])
        self.replay_list.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
        layout.addWidget(self.replay_list)

        self.replay_viewer = QTextEdit()
        self.replay_viewer.setReadOnly(True)
        self.replay_viewer.setMaximumHeight(200)
        self.replay_viewer.setStyleSheet("background: #1a1a1a; color: #0f0; font-family: monospace;")
        layout.addWidget(self.replay_viewer)

        btn_layout = QHBoxLayout()
        refresh_btn = QPushButton("🔄 Refresh")
        refresh_btn.clicked.connect(self._refresh_session_records)
        btn_layout.addWidget(refresh_btn)

        layout.addLayout(btn_layout)
        self._refresh_session_records()
        return tab

    def _refresh_session_records(self):
        records = get_session_records()
        self.replay_list.setRowCount(len(records))
        for i, rec in enumerate(records):
            self.replay_list.setItem(i, 0, QTableWidgetItem(rec["file"]))
            self.replay_list.setItem(i, 1, QTableWidgetItem(f"{rec['size']} bytes"))
            # Count events
            events = load_session_record(rec["path"])
            self.replay_list.setItem(i, 2, QTableWidgetItem(str(len(events))))
            # View button
            view_btn = QPushButton("👁 View")
            view_btn.clicked.connect(lambda checked, p=rec["path"]: self._view_session(p))
            self.replay_list.setCellWidget(i, 3, view_btn)

    def _view_session(self, filepath):
        events = load_session_record(filepath)
        lines = []
        for e in events:
            ts = datetime.fromtimestamp(e.get("timestamp", 0)).strftime("%H:%M:%S.%f")[:-3]
            lines.append(f"[{ts}] {e.get('event', 'unknown')}: {json.dumps({k:v for k,v in e.items() if k != 'timestamp'}, default=str)}")
        self.replay_viewer.setText("\n".join(lines[:200]))  # Limit display

    def _create_plugin_tab(self):
        """l3: Plugin system management."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🧩 Load Python plugins from the 'plugins/' directory.\n"
            "Plugins can hook: on_snipe_start, on_snipe_success, on_snipe_fail, on_gui_init")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        self.plugin_status = QLabel("No plugins loaded")
        self.plugin_status.setStyleSheet("color: #888; padding: 4px;")
        layout.addWidget(self.plugin_status)

        self.plugin_log = QTextEdit()
        self.plugin_log.setReadOnly(True)
        self.plugin_log.setMaximumHeight(150)
        self.plugin_log.setStyleSheet("background: #1a1a1a; color: #0f0; font-family: monospace;")
        layout.addWidget(self.plugin_log)

        btn_layout = QHBoxLayout()
        load_btn = QPushButton("📂 Load Plugins")
        load_btn.clicked.connect(self._load_plugins_ui)
        btn_layout.addWidget(load_btn)

        create_btn = QPushButton("📝 Create Example Plugin")
        create_btn.clicked.connect(self._create_example_plugin)
        btn_layout.addWidget(create_btn)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return tab

    def _load_plugins_ui(self):
        loaded = plugin_manager.load_plugins()
        if loaded:
            self.plugin_status.setText(f"✅ Loaded {len(loaded)} plugin(s): {', '.join(loaded)}")
            self.plugin_status.setStyleSheet("color: #4CAF50; padding: 4px;")
            self.plugin_log.append(f"Loaded: {', '.join(loaded)}")
        else:
            self.plugin_status.setText("No plugins found in plugins/ directory")
            self.plugin_status.setStyleSheet("color: #ff9800; padding: 4px;")
            self.plugin_log.append("No plugins found")

    def _create_example_plugin(self):
        try:
            os.makedirs(PLUGIN_DIR, exist_ok=True)
            example = '''"""Example plugin for Minecraft Sniper GUI"""

def on_snipe_start(name, tokens):
    print(f"[Plugin] Starting snipe for {name} with {len(tokens)} tokens")

def on_snipe_success(name, token):
    print(f"[Plugin] Successfully sniped {name}!")

def on_snipe_fail(name, error):
    print(f"[Plugin] Failed to snipe {name}: {error}")
'''
            with open(os.path.join(PLUGIN_DIR, "example_plugin.py"), 'w') as f:
                f.write(example)
            self.plugin_log.append("Created example_plugin.py in plugins/")
            self.plugin_status.setText("✅ Example plugin created")
            self.plugin_status.setStyleSheet("color: #4CAF50; padding: 4px;")
        except Exception as e:
            self.plugin_log.append(f"Error: {e}")

    def _create_doh_tab(self):
        """l4: DNS over HTTPS resolver."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel("🔒 Resolve Mojang domains via DNS-over-HTTPS to prevent DNS spoofing/poisoning.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        form = QFormLayout()
        self.doh_domain_input = QLineEdit("api.minecraftservices.com")
        form.addRow("Domain:", self.doh_domain_input)

        self.doh_provider = QComboBox()
        self.doh_provider.addItems(["Cloudflare", "Google", "Quad9"])
        form.addRow("Provider:", self.doh_provider)

        layout.addLayout(form)

        self.doh_result = QLabel("Enter a domain and click Resolve")
        self.doh_result.setStyleSheet("color: #0f0; padding: 8px; font-family: monospace; background: #1a1a1a; border-radius: 4px;")
        self.doh_result.setWordWrap(True)
        layout.addWidget(self.doh_result)

        btn_layout = QHBoxLayout()
        resolve_btn = QPushButton("🔍 Resolve")
        resolve_btn.clicked.connect(self._resolve_doh)
        btn_layout.addWidget(resolve_btn)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return tab

    def _resolve_doh(self):
        domain = self.doh_domain_input.text().strip()
        provider = self.doh_provider.currentText()
        if not domain:
            return
        self.doh_result.setText("Resolving...")
        self.doh_result.setStyleSheet("color: #ff0; padding: 8px; font-family: monospace; background: #1a1a1a;")
        ips = resolve_doh(domain, provider)
        if ips:
            self.doh_result.setText(f"{domain} → {', '.join(ips)}\n(via {provider})")
            self.doh_result.setStyleSheet("color: #0f0; padding: 8px; font-family: monospace; background: #1a1a1a;")
        else:
            self.doh_result.setText(f"Failed to resolve {domain}")
            self.doh_result.setStyleSheet("color: #f00; padding: 8px; font-family: monospace; background: #1a1a1a;")

    def _create_update_tab(self):
        """m8: Auto-update checker."""
        tab = QWidget()
        layout = QVBoxLayout(tab)

        info = QLabel(f"🔄 Current version: {CURRENT_VERSION}. Check for updates on GitHub.")
        info.setWordWrap(True)
        info.setStyleSheet("color: #888; padding: 8px;")
        layout.addWidget(info)

        self.update_status = QLabel("")
        self.update_status.setStyleSheet("padding: 8px; font-size: 13px;")
        self.update_status.setWordWrap(True)
        layout.addWidget(self.update_status)

        self.update_changelog = QTextEdit()
        self.update_changelog.setReadOnly(True)
        self.update_changelog.setMaximumHeight(200)
        self.update_changelog.setStyleSheet("background: #1a1a1a; color: #ccc; font-family: monospace;")
        layout.addWidget(self.update_changelog)

        btn_layout = QHBoxLayout()
        check_btn = QPushButton("🔍 Check for Updates")
        check_btn.clicked.connect(self._check_updates_ui)
        btn_layout.addWidget(check_btn)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return tab

    def _check_updates_ui(self):
        self.update_status.setText("Checking...")
        self.update_status.setStyleSheet("color: #ff0; padding: 8px;")
        result = check_for_updates()
        if result["available"]:
            self.update_status.setText(f"✅ Update available: v{result['latest_version']} (you have v{CURRENT_VERSION})")
            self.update_status.setStyleSheet("color: #4CAF50; padding: 8px; font-weight: bold;")
            self.update_changelog.setPlainText(result["changelog"])
        else:
            self.update_status.setText(f"✅ You're up to date (v{CURRENT_VERSION})")
            self.update_status.setStyleSheet("color: #2196F3; padding: 8px;")
            self.update_changelog.setPlainText("No updates available.")

    # =========================================================================
    # M1: Countdown Timer Visual
    # =========================================================================
    def _create_countdown_panel(self, parent_layout):
        """Create a prominent countdown timer display."""
        from PyQt5.QtWidgets import QFrame, QHBoxLayout, QVBoxLayout, QLabel
        from PyQt5.QtCore import Qt, QTimer

        countdown_frame = QFrame()
        countdown_frame.setObjectName("countdownPanel")
        countdown_frame.setStyleSheet("""
            QFrame#countdownPanel {
                background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
                    stop:0 #1a1a2e, stop:1 #16213e);
                border: 1px solid #0f3460;
                border-radius: 8px;
                padding: 8px;
            }
        """)
        countdown_layout = QHBoxLayout(countdown_frame)

        # Status indicator
        self.countdown_status = QLabel("⏸ IDLE")
        self.countdown_status.setStyleSheet("""
            QLabel {
                color: #888;
                font-size: 14px;
                font-weight: bold;
                padding: 4px 12px;
                background: #111;
                border-radius: 4px;
                border: 1px solid #333;
            }
        """)
        self.countdown_status.setFixedSize(120, 30)
        self.countdown_status.setAlignment(Qt.AlignCenter)
        countdown_layout.addWidget(self.countdown_status)

        # Main countdown label
        self.countdown_label = QLabel("--:--:--")
        self.countdown_label.setAlignment(Qt.AlignCenter)
        self.countdown_label.setStyleSheet("""
            QLabel {
                color: #0ff;
                font-family: 'Consolas', 'Courier New', monospace;
                font-size: 36px;
                font-weight: bold;
                padding: 0 20px;
            }
        """)
        countdown_layout.addWidget(self.countdown_label, 1)

        # ETA info
        self.countdown_eta = QLabel("")
        self.countdown_eta.setStyleSheet("""
            QLabel {
                color: #aaa;
                font-size: 12px;
                padding: 4px 12px;
            }
        """)
        self.countdown_eta.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
        countdown_layout.addWidget(self.countdown_eta)

        parent_layout.addWidget(countdown_frame)

        # Start countdown updater
        self.countdown_timer = QTimer(self)
        self.countdown_timer.timeout.connect(self._update_countdown)
        self.countdown_timer.start(500)  # Update every 500ms
        self._countdown_target = None
        self._countdown_name = None

    def _update_countdown(self):
        """Update the countdown display."""
        if not self._countdown_target or not self._is_sniping:
            self.countdown_label.setText("--:--:--")
            self.countdown_label.setStyleSheet(self.countdown_label.styleSheet().replace('#0ff', '#555'))
            return

        from datetime import datetime
        now = datetime.now()
        diff = self._countdown_target - now
        total_seconds = int(diff.total_seconds())

        if total_seconds <= 0:
            self.countdown_label.setText("🔥 NOW")
            self.countdown_label.setStyleSheet("""
                QLabel { color: #ff0; font-family: 'Consolas', monospace;
                    font-size: 36px; font-weight: bold; padding: 0 20px; }
            """)
            return

        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60
        seconds = total_seconds % 60
        display = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
        self.countdown_label.setText(display)

        # Color changes based on urgency
        if total_seconds <= 10:
            color = "#ff0"  # Yellow
            # Pulse effect
            import time
            pulse = abs(int(time.time() * 3) % 2)
            bg = "#222200" if pulse else "#111100"
            self.countdown_label.setStyleSheet(f"""
                QLabel {{ color: #ff0; font-family: 'Consolas', monospace;
                    font-size: 42px; font-weight: bold; padding: 0 20px;
                    background: {bg}; }}
            """)
        elif total_seconds <= 60:
            color = "#f80"  # Orange
            self.countdown_label.setStyleSheet(f"""
                QLabel {{ color: {color}; font-family: 'Consolas', monospace;
                    font-size: 38px; font-weight: bold; padding: 0 20px; }}
            """)
        elif total_seconds <= 300:
            color = "#0f0"  # Green
        else:
            color = "#0ff"  # Cyan

        # Update status
        if self._is_sniping:
            self.countdown_status.setText("🔫 FIRING")
            self.countdown_status.setStyleSheet("""
                QLabel { color: #0f0; font-size: 14px; font-weight: bold;
                    padding: 4px 12px; background: #001a00; border-radius: 4px;
                    border: 1px solid #0f0; }
            """)
        elif total_seconds > 0:
            self.countdown_status.setText("⏳ WAITING")
            self.countdown_status.setStyleSheet("""
                QLabel { color: #ff0; font-size: 14px; font-weight: bold;
                    padding: 4px 12px; background: #1a1a00; border-radius: 4px;
                    border: 1px solid #ff0; }
            """)

        # ETA text
        if self._countdown_name:
            self.countdown_eta.setText(f"Target: {self._countdown_name}")

    def _set_countdown_target(self, target_time, name=None):
        """Set the countdown target time."""
        self._countdown_target = target_time
        self._countdown_name = name

    # =========================================================================
    # L2: Keyboard Shortcuts
    # =========================================================================
    def _setup_shortcuts(self):
        """Register keyboard shortcuts."""
        from PyQt5.QtGui import QKeySequence
        from PyQt5.QtWidgets import QShortcut

        # Space = Start/Stop sniping
        space = QShortcut(QKeySequence("Space"), self)
        space.activated.connect(self._shortcut_toggle_sniper)

        # Ctrl+S = Save settings
        save = QShortcut(QKeySequence("Ctrl+S"), self)
        save.activated.connect(self._save_settings)

        # Ctrl+L = Clear log
        clear = QShortcut(QKeySequence("Ctrl+L"), self)
        clear.activated.connect(lambda: self.log_output.clear())

        # Ctrl+1-9 = Switch tabs
        for i in range(1, 10):
            seq = QShortcut(QKeySequence(f"Ctrl+{i}"), self)
            idx = i - 1
            seq.activated.connect(lambda checked=False, t=idx: self.tabs.setCurrentIndex(t))

        # Escape = Stop sniping
        esc = QShortcut(QKeySequence("Escape"), self)
        esc.activated.connect(self._shortcut_stop_sniper)

        self._log("⌨️  Keyboard shortcuts: Space=Start/Stop, Esc=Stop, Ctrl+1-9=Tabs, Ctrl+S=Save")

    def _shortcut_toggle_sniper(self):
        """Toggle sniping with Space bar."""
        if self._is_sniping:
            self.stop_sniping()
        else:
            self.start_sniping()

    def _shortcut_stop_sniper(self):
        """Stop sniping with Escape."""
        if self._is_sniping:
            self.stop_sniping()

    # =========================================================================
    # M6: Pre-Flight Checklist Tab
    # =========================================================================
    def _create_preflight_tab(self):
        """Create the pre-flight checklist tab."""
        from PyQt5.QtWidgets import (QFrame, QVBoxLayout, QHBoxLayout, QPushButton,
                                      QLabel, QProgressBar, QCheckBox, QGroupBox)
        from PyQt5.QtCore import Qt

        frame = QFrame()
        layout = QVBoxLayout(frame)

        # Header
        header = QLabel("🛡️  Pre-Flight Checklist")
        header.setStyleSheet("font-size:16px;font-weight:bold;color:#0ff;")
        layout.addWidget(header)

        desc = QLabel("Verify everything is ready before sniping. Click 'Run Check' to validate all systems.")
        desc.setStyleSheet("color:#aaa;font-size:12px;")
        desc.setWordWrap(True)
        layout.addWidget(desc)

        # Checklist items
        check_group = QGroupBox("Systems to Verify")
        check_layout = QVBoxLayout(check_group)

        self.preflight_checks = {}
        check_items = [
            ("🌐 Network connectivity", self._check_network),
            ("⏰ System clock sync (NTP)", self._check_clock_sync),
            ("🔑 Token validity", self._check_tokens_valid),
            ("📡 API endpoint reachability", self._check_api_reachability),
            ("💾 Disk space for logs", self._check_disk_space),
            ("🔌 Python dependencies", self._check_dependencies),
            ("🖥️ PyQt5 display", self._check_display),
        ]

        for label, check_fn in check_items:
            checkbox = QCheckBox(label)
            checkbox.setStyleSheet("QCheckBox { color: #ccc; font-size: 13px; padding: 4px; }")
            checkbox.setEnabled(False)
            check_layout.addWidget(checkbox)
            self.preflight_checks[label] = (checkbox, check_fn)

        layout.addWidget(check_group)

        # Progress bar
        self.preflight_progress = QProgressBar()
        self.preflight_progress.setTextVisible(False)
        self.preflight_progress.setStyleSheet("""
            QProgressBar { border: 1px solid #333; border-radius: 4px; background: #111; }
            QProgressBar::chunk { background: #0f0; }
        """)
        self.preflight_progress.setFixedHeight(8)
        layout.addWidget(self.preflight_progress)

        # Results area
        self.preflight_result = QLabel("")
        self.preflight_result.setStyleSheet("color:#fff;font-size:14px;padding:10px;")
        self.preflight_result.setWordWrap(True)
        layout.addWidget(self.preflight_result)

        # Run button
        btn_layout = QHBoxLayout()
        run_btn = QPushButton("🚀 Run Pre-Flight Check")
        run_btn.setObjectName("primaryButton")
        run_btn.setFixedHeight(40)
        run_btn.clicked.connect(self._run_preflight_check)
        btn_layout.addWidget(run_btn)

        skip_btn = QPushButton("Skip & Snipe Anyway")
        skip_btn.setStyleSheet("""QPushButton {
            background: #333; color: #ff0; border: 1px solid #ff0;
            border-radius: 4px; font-size: 13px; padding: 8px 16px;
        }""")
        skip_btn.setFixedHeight(40)
        skip_btn.clicked.connect(lambda: self._log("⚠️  Pre-flight check skipped"))
        btn_layout.addWidget(skip_btn)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return frame

    def _run_preflight_check(self):
        """Run all pre-flight checks in a thread."""
        import threading
        self.preflight_progress.setValue(0)
        self.preflight_result.setText("Running checks...")
        self.preflight_result.setStyleSheet("color:#ff0;font-size:14px;padding:10px;")

        def run_checks():
            results = []
            total = len(self.preflight_checks)
            for i, (label, (cb, fn)) in enumerate(self.preflight_checks.items()):
                try:
                    ok, msg = fn()
                    status = "✅" if ok else "❌"
                    results.append(f"{status} {label}: {msg}")
                    self._q_invoke(lambda cb=cb, s=status: cb.setText(f"{s} {label}"))
                except Exception as e:
                    results.append(f"❌ {label}: Error - {e}")
                    self._q_invoke(lambda cb=cb: cb.setText(f"⚠️  {label}"))
                self._q_invoke(lambda p=i+1, t=total: self.preflight_progress.setValue(int(p/t*100)))

            # Summary
            passed = sum(1 for r in results if r.startswith("✅"))
            failed = total - passed
            summary_color = "#0f0" if failed == 0 else "#f80" if failed <= 2 else "#f00"
            summary_text = (f"<b style='color:{summary_color}'>Pre-Flight Complete: "
                           f"{passed}/{total} passed</b><br><br>" + "<br>".join(results))
            self._q_invoke(lambda t=summary_text: (
                self.preflight_result.setText(t),
                self.preflight_result.setStyleSheet(f"color:{summary_color};font-size:13px;padding:10px;")
            ))
            self._log(f"🛡️  Pre-flight: {passed}/{total} checks passed")

        threading.Thread(target=run_checks, daemon=True).start()

    def _check_network(self):
        """Check network connectivity."""
        import socket
        try:
            socket.create_connection(("1.1.1.1", 53), timeout=3)
            return True, "Connected"
        except Exception as e:
            return False, str(e)

    def _check_clock_sync(self):
        """Check if system clock is reasonably synced."""
        from datetime import datetime
        try:
            import httpx
            client = httpx.Client(timeout=5)
            resp = client.get("https://api.minecraftservices.com/minecraft/profile")
            server_time_str = resp.headers.get("date", "")
            if server_time_str:
                from email.utils import parsedate_to_datetime
                server_time = parsedate_to_datetime(server_time_str)
                now = datetime.utcnow()
                diff = abs((server_time - now).total_seconds())
                if diff < 5:
                    return True, f"Synced ({diff:.1f}s drift)"
                return False, f"Drift: {diff:.1f}s (need <5s)"
            return True, "OK (no date header)"
        except Exception as e:
            return False, f"Cannot check: {e}"

    def _check_tokens_valid(self):
        """Check if tokens are loaded."""
        tokens = self._load_tokens()
        if tokens:
            return True, f"{len(tokens)} tokens loaded"
        return False, "No tokens configured"

    def _check_api_reachability(self):
        """Check if Mojang API is reachable."""
        import httpx
        try:
            client = httpx.Client(timeout=5)
            resp = client.get("https://api.minecraftservices.com/")
            if resp.status_code < 500:
                return True, f"HTTP {resp.status_code}"
            return False, f"HTTP {resp.status_code}"
        except Exception as e:
            return False, str(e)

    def _check_disk_space(self):
        """Check disk space."""
        import shutil
        total, used, free = shutil.disk_usage("/")
        gb = free / (1024**3)
        if gb > 1:
            return True, f"{gb:.1f}GB free"
        return False, f"Only {gb:.1f}GB free"

    def _check_dependencies(self):
        """Check Python dependencies."""
        try:
            import httpx, PyQt5
            return True, "All present"
        except ImportError as e:
            return False, f"Missing: {e}"

    def _check_display(self):
        """Check display environment."""
        return True, "OK (PyQt5 running)"

    def _q_invoke(self, func):
        """Safely invoke a function on the Qt main thread."""
        from PyQt5.QtCore import QMetaObject, QThread
        QMetaObject.invokeMethod(QThread.currentThread(), "update")
        # Simple approach: use QTimer.singleShot(0, func)
        from PyQt5.QtCore import QTimer
        QTimer.singleShot(0, func)

    # =========================================================================
    # M5: Drop Time Predictor
    # =========================================================================
    def _add_drop_time_predictor(self, sniper_tab):
        """Add drop time predictor widget to sniper tab."""
        from PyQt5.QtWidgets import (QGroupBox, QVBoxLayout, QHBoxLayout, QLabel,
                                      QLineEdit, QPushButton, QTextEdit)
        from datetime import datetime, timedelta

        group = QGroupBox("⏰ Drop Time Predictor")
        group.setStyleSheet("QGroupBox { color: #0ff; font-weight: bold; padding-top: 10px; }")
        layout = QVBoxLayout(group)

        info = QLabel("Enter a username to estimate when it will be available for name change.")
        info.setStyleSheet("color:#888;font-size:11px;")
        info.setWordWrap(True)
        layout.addWidget(info)

        input_layout = QHBoxLayout()
        self.drop_predict_input = QLineEdit()
        self.drop_predict_input.setPlaceholderText("Enter username...")
        self.drop_predict_input.setStyleSheet("""QLineEdit {
            background:#111;border:1px solid #333;color:#0ff;
            border-radius:4px;padding:6px 10px;font-size:13px;
        }""")
        input_layout.addWidget(self.drop_predict_input)

        predict_btn = QPushButton("🔮 Predict")
        predict_btn.setObjectName("primaryButton")
        predict_btn.clicked.connect(self._predict_drop_time)
        input_layout.addWidget(predict_btn)
        layout.addLayout(input_layout)

        self.drop_predict_result = QTextEdit()
        self.drop_predict_result.setReadOnly(True)
        self.drop_predict_result.setFixedHeight(80)
        self.drop_predict_result.setStyleSheet("""QTextEdit {
            background:#0a0a1a;border:1px solid #333;color:#ccc;
            border-radius:4px;padding:8px;font-size:12px;
            font-family:'Consolas',monospace;
        }""")
        layout.addWidget(self.drop_predict_result)

        return group

    def _predict_drop_time(self):
        """Predict when a username will be available."""
        username = self.drop_predict_input.text().strip()
        if not username:
            self.drop_predict_result.setText("Enter a username first.")
            return

        result_lines = []
        result_lines.append(f"🔮 Drop Time Prediction for: {username}")
        result_lines.append("-" * 40)

        try:
            import httpx
            client = httpx.Client(timeout=10)

            # Check if name exists
            resp = client.get(f"https://api.mojang.com/users/profiles/minecraft/{username}")
            if resp.status_code != 200:
                self.drop_predict_result.setText(f"❌ Username '{username}' not found on Mojang.\nIt may already be available or never existed.")
                return

            uuid = resp.json().get("id", "")

            # Name change cooldown: 30 days from last name change
            # We can't get exact last change date from API, but we know:
            # - Names can be changed every 30 days
            # - The name will become available for others when owner changes it

            now = datetime.utcnow()
            result_lines.append(f"UUID: {uuid}")
            result_lines.append("")
            result_lines.append("⏱  Name change cooldown: 30 days")
            result_lines.append("")
            result_lines.append("📋 Scenarios:")
            result_lines.append(f"  • Earliest owner can change: {now + timedelta(days=30)}")
            result_lines.append(f"  • Earliest name becomes free: {now + timedelta(days=30)}")
            result_lines.append(f"  • Conservative estimate: {now + timedelta(days=35)}")
            result_lines.append("")
            result_lines.append("💡 Tip: Set a reminder for 30 days from now and start sniping!")

            # Add to countdown if user wants
            target = now + timedelta(days=30)
            self.drop_predict_result.setText("\n".join(result_lines))

        except Exception as e:
            self.drop_predict_result.setText(f"Error: {e}")

    # =========================================================================
    # L3: Preset Profiles
    # =========================================================================
    def _create_presets_tab(self):
        """Create preset profiles tab."""
        from PyQt5.QtWidgets import (QFrame, QVBoxLayout, QHBoxLayout, QPushButton,
                                      QLabel, QGroupBox, QFormLayout, QComboBox, QSpinBox,
                                      QCheckBox, QSlider)
        from PyQt5.QtCore import Qt

        frame = QFrame()
        layout = QVBoxLayout(frame)

        header = QLabel("⚙️  Preset Profiles")
        header.setStyleSheet("font-size:16px;font-weight:bold;color:#0ff;")
        layout.addWidget(header)

        desc = QLabel("Quick configuration profiles for different sniping strategies.")
        desc.setStyleSheet("color:#aaa;font-size:12px;")
        desc.setWordWrap(True)
        layout.addWidget(desc)

        # Preset buttons
        presets_group = QGroupBox("Load Preset")
        presets_layout = QHBoxLayout(presets_group)

        presets = {
            "🔥 Aggressive": {
                "description": "Maximum speed, all tokens, nuclear mode. Use for high-value drops.",
                "min_interval": 120, "max_interval": 180, "nuclear": True,
                "burst_tokens": 10, "warmup": 20,
            },
            "⚖️  Balanced": {
                "description": "Good balance of speed and token conservation.",
                "min_interval": 300, "max_interval": 600, "nuclear": False,
                "burst_tokens": 5, "warmup": 10,
            },
            "🐢 Conservative": {
                "description": "Slow and steady. Preserves tokens for long waits.",
                "min_interval": 800, "max_interval": 1200, "nuclear": False,
                "burst_tokens": 2, "warmup": 5,
            },
            "🤫 Stealth": {
                "description": "Minimal requests to avoid detection. Single token only.",
                "min_interval": 1500, "max_interval": 2000, "nuclear": False,
                "burst_tokens": 1, "warmup": 2,
            },
        }

        self._presets_data = presets

        for name, data in presets.items():
            btn = QPushButton(name)
            btn.setFixedHeight(50)
            btn.setStyleSheet(f"""
                QPushButton {{
                    background: #1a1a2e; border: 2px solid #0f3460;
                    color: #0ff; font-size: 13px; font-weight: bold;
                    border-radius: 6px; padding: 8px;
                }}
                QPushButton:hover {{
                    background: #0f3460; border-color: #0ff;
                }}
            """)
            btn.clicked.connect(lambda checked, n=name: self._apply_preset(n))
            presets_layout.addWidget(btn, 1)

        layout.addWidget(presets_group)

        # Preset description
        self.preset_desc = QLabel("Select a preset above to apply its settings.")
        self.preset_desc.setStyleSheet("color:#aaa;font-size:12px;padding:8px;")
        self.preset_desc.setWordWrap(True)
        layout.addWidget(self.preset_desc)

        # Current settings display
        settings_group = QGroupBox("Current Settings")
        settings_layout = QVBoxLayout(settings_group)

        self.preset_settings_display = QLabel("")
        self.preset_settings_display.setStyleSheet("color:#ccc;font-size:12px;font-family:Consolas,monospace;padding:8px;")
        self.preset_settings_display.setWordWrap(True)
        settings_layout.addWidget(self.preset_settings_display)
        layout.addWidget(settings_group)

        # Custom preset
        custom_group = QGroupBox("Custom Preset")
        custom_layout = QFormLayout(custom_group)

        self.preset_min_input = QSpinBox()
        self.preset_min_input.setRange(100, 5000)
        self.preset_min_input.setValue(300)
        self.preset_min_input.setSuffix(" ms")
        custom_layout.addRow("Min Interval:", self.preset_min_input)

        self.preset_max_input = QSpinBox()
        self.preset_max_input.setRange(100, 5000)
        self.preset_max_input.setValue(600)
        self.preset_max_input.setSuffix(" ms")
        custom_layout.addRow("Max Interval:", self.preset_max_input)

        self.preset_burst_input = QSpinBox()
        self.preset_burst_input.setRange(1, 50)
        self.preset_burst_input.setValue(5)
        custom_layout.addRow("Burst Tokens:", self.preset_burst_input)

        self.preset_warmup_input = QSpinBox()
        self.preset_warmup_input.setRange(0, 100)
        self.preset_warmup_input.setValue(10)
        custom_layout.addRow("Warmup Requests:", self.preset_warmup_input)

        self.preset_nuclear_check = QCheckBox("Enable Nuclear Mode")
        custom_layout.addRow("", self.preset_nuclear_check)

        save_custom = QPushButton("💾 Save & Apply Custom")
        save_custom.setObjectName("primaryButton")
        save_custom.clicked.connect(self._apply_custom_preset)
        custom_layout.addRow("", save_custom)

        layout.addWidget(custom_group)
        layout.addStretch()
        return frame

    def _apply_preset(self, name):
        """Apply a preset profile."""
        data = self._presets_data.get(name, {})
        self.preset_desc.setText(f"Loaded: {name}\n{data.get('description', '')}")
        self.preset_desc.setStyleSheet("color:#0f0;font-size:12px;padding:8px;")

        # Update inputs
        self.preset_min_input.setValue(data.get("min_interval", 300))
        self.preset_max_input.setValue(data.get("max_interval", 600))
        self.preset_burst_input.setValue(data.get("burst_tokens", 5))
        self.preset_warmup_input.setValue(data.get("warmup", 10))
        self.preset_nuclear_check.setChecked(data.get("nuclear", False))

        self._update_preset_display()
        self._log(f"⚙️  Preset '{name}' applied")

    def _apply_custom_preset(self):
        """Apply custom preset values."""
        self._update_preset_display()
        self._log(f"⚙️  Custom preset applied: {self.preset_min_input.value()}-{self.preset_max_input.value()}ms, "
                  f"burst={self.preset_burst_input.value()}, warmup={self.preset_warmup_input.value()}, "
                  f"nuclear={self.preset_nuclear_check.checkState()}")

    def _update_preset_display(self):
        """Update the preset settings display."""
        text = (f"Min Interval: {self.preset_min_input.value()} ms\n"
                f"Max Interval: {self.preset_max_input.value()} ms\n"
                f"Burst Tokens: {self.preset_burst_input.value()}\n"
                f"Warmup: {self.preset_warmup_input.value()} requests\n"
                f"Nuclear: {'ON' if self.preset_nuclear_check.isChecked() else 'OFF'}")
        self.preset_settings_display.setText(text)

    # =========================================================================
    # L1: Sound Customization
    # =========================================================================
    def _add_sound_settings(self, power_tab, power_layout):
        """Add sound customization section to power features tab."""
        from PyQt5.QtWidgets import (QGroupBox, QFormLayout, QComboBox, QSlider,
                                      QPushButton, QLabel)
        from PyQt5.QtCore import Qt

        sound_group = QGroupBox("🔊 Sound Settings")
        sound_group.setStyleSheet("QGroupBox { color: #0ff; font-weight: bold; padding-top: 10px; }")
        sound_layout = QFormLayout(sound_group)

        self.sound_enabled = QCheckBox("Enable sounds")
        self.sound_enabled.setChecked(True)
        sound_layout.addRow("", self.sound_enabled)

        self.sound_type = QComboBox()
        self.sound_type.addItems(["Default beep", "Chime", "Siren", "Custom file..."])
        sound_layout.addRow("Alert sound:", self.sound_type)

        self.sound_volume = QSlider(Qt.Horizontal)
        self.sound_volume.setRange(0, 100)
        self.sound_volume.setValue(80)
        sound_layout.addRow("Volume:", self.sound_volume)

        test_sound_btn = QPushButton("🔊 Test Sound")
        test_sound_btn.clicked.connect(self._test_sound)
        sound_layout.addRow("", test_sound_btn)

        self.sound_custom_file = QLineEdit()
        self.sound_custom_file.setPlaceholderText("Path to .wav or .mp3 file...")
        self.sound_custom_file.setStyleSheet("""QLineEdit {
            background:#111;border:1px solid #333;color:#0ff;
            border-radius:4px;padding:6px 10px;
        }""")
        sound_layout.addRow("Custom file:", self.sound_custom_file)

        return sound_group

    def _test_sound(self):
        """Play a test sound."""
        try:
            from PyQt5.QtMultimedia import QSoundEffect
            self._log("🔊 Playing test sound...")
            # Simple beep fallback
            import os
            if os.name == 'nt':
                import winsound
                winsound.Beep(800, 300)
            else:
                os.system('printf "\a"')
            self._log("✅ Test sound played")
        except Exception as e:
            self._log(f"⚠️  Sound test: {e}")

    # =========================================================================
    # L4: Auto-Export on Snipe
    # =========================================================================
    def _add_auto_export_setting(self, settings_dict):
        """Add auto-export setting to settings."""
        settings_dict['auto_export'] = settings_dict.get('auto_export', True)
        settings_dict['export_format'] = settings_dict.get('export_format', 'txt')
        return settings_dict

    def _export_snipe_proof(self, username, token, timestamp, response_data):
        """Export snipe proof as a file."""
        import os
        from datetime import datetime

        export_dir = os.path.expanduser("~/.sniper_exports")
        os.makedirs(export_dir, exist_ok=True)

        # Sanitize username for filename
        safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', username)
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"snipe_{safe_name}_{ts}.txt"
        filepath = os.path.join(export_dir, filename)

        content = f"""=====================================
  SNIPING PROOF - {username}
=====================================

Timestamp: {timestamp}
Username:  {username}
Token:     {token[:20]}...{'*' * 20}
Date:      {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

Response:
{json.dumps(response_data, indent=2, default=str)}

Hash: {hashlib.sha256(f"{username}{timestamp}".encode()).hexdigest()[:16]}
=====================================
"""
        with open(filepath, 'w') as f:
            f.write(content)

        self._log(f"📁 Export saved: {filepath}")
        return filepath

    # =========================================================================
    # L5: Theme System
    # =========================================================================
    def _apply_theme(self, theme_name="dark"):
        """Apply a color theme to the application."""
        themes = {
            "dark": """
                QMainWindow { background: #0a0a1a; }
                QFrame { background: #0a0a1a; }
                QTabWidget::pane { background: #0a0a1a; border: 1px solid #333; }
                QTabBar::tab { background: #111; color: #888; padding: 8px 16px; border-top: 2px solid transparent; }
                QTabBar::tab:selected { background: #1a1a2e; color: #0ff; border-top: 2px solid #0ff; }
                QPushButton { background: #1a1a2e; color: #ccc; border: 1px solid #333; border-radius: 4px; padding: 6px 12px; }
                QPushButton:hover { background: #0f3460; }
                QLineEdit, QTextEdit, QComboBox { background: #111; color: #0ff; border: 1px solid #333; border-radius: 4px; }
                QTableWidget { background: #111; color: #ccc; gridline-color: #222; }
                QHeaderView::section { background: #1a1a2e; color: #0ff; border: 1px solid #333; padding: 4px; }
                QCheckBox { color: #ccc; }
                QGroupBox { color: #0ff; }
                QProgressBar { border: 1px solid #333; border-radius: 4px; background: #111; }
                QProgressBar::chunk { background: #0f0; }
                QScrollBar:vertical { background: #111; width: 8px; }
                QScrollBar::handle:vertical { background: #333; border-radius: 4px; }
            """,
            "light": """
                QMainWindow { background: #f0f0f0; }
                QFrame { background: #f0f0f0; }
                QTabWidget::pane { background: #f0f0f0; border: 1px solid #ccc; }
                QTabBar::tab { background: #ddd; color: #333; padding: 8px 16px; border-top: 2px solid transparent; }
                QTabBar::tab:selected { background: #fff; color: #0066cc; border-top: 2px solid #0066cc; }
                QPushButton { background: #fff; color: #333; border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; }
                QPushButton:hover { background: #e0e8ff; }
                QLineEdit, QTextEdit, QComboBox { background: #fff; color: #333; border: 1px solid #ccc; border-radius: 4px; }
                QTableWidget { background: #fff; color: #333; gridline-color: #ddd; }
                QHeaderView::section { background: #e0e0e0; color: #0066cc; border: 1px solid #ccc; padding: 4px; }
                QCheckBox { color: #333; }
                QGroupBox { color: #0066cc; }
                QProgressBar { border: 1px solid #ccc; border-radius: 4px; background: #eee; }
                QProgressBar::chunk { background: #0066cc; }
            """,
            "nord": """
                QMainWindow { background: #2e3440; }
                QFrame { background: #2e3440; }
                QTabWidget::pane { background: #2e3440; border: 1px solid #4c566a; }
                QTabBar::tab { background: #3b4252; color: #d8dee9; padding: 8px 16px; }
                QTabBar::tab:selected { background: #434c5e; color: #88c0d0; border-top: 2px solid #88c0d0; }
                QPushButton { background: #434c5e; color: #d8dee9; border: 1px solid #4c566a; border-radius: 4px; padding: 6px 12px; }
                QPushButton:hover { background: #4c566a; }
                QLineEdit, QTextEdit, QComboBox { background: #3b4252; color: #88c0d0; border: 1px solid #4c566a; border-radius: 4px; }
                QTableWidget { background: #3b4252; color: #d8dee9; gridline-color: #4c566a; }
                QHeaderView::section { background: #434c5e; color: #88c0d0; border: 1px solid #4c566a; padding: 4px; }
                QCheckBox { color: #d8dee9; }
                QGroupBox { color: #88c0d0; }
                QProgressBar { border: 1px solid #4c566a; border-radius: 4px; background: #3b4252; }
                QProgressBar::chunk { background: #a3be8c; }
            """,
        }

        theme = themes.get(theme_name, themes["dark"])

        # Preserve existing object-specific stylesheets
        self._current_theme = theme_name
        self.setStyleSheet(theme)
        self._log(f"🎨 Theme changed to: {theme_name}")

    def _add_theme_selector(self, power_tab, power_layout):
        """Add theme selector to power features tab."""
        from PyQt5.QtWidgets import QGroupBox, QHBoxLayout, QComboBox, QLabel
        from PyQt5.QtCore import Qt

        theme_group = QGroupBox("🎨 Theme")
        theme_group.setStyleSheet("QGroupBox { color: #0ff; font-weight: bold; padding-top: 10px; }")
        theme_layout = QHBoxLayout(theme_group)

        theme_label = QLabel("Theme:")
        theme_label.setStyleSheet("color:#ccc;font-size:12px;")
        theme_layout.addWidget(theme_label)

        self.theme_selector = QComboBox()
        self.theme_selector.addItems(["Dark (Default)", "Light", "Nord"])
        self.theme_selector.setStyleSheet("""QComboBox {
            background:#111;border:1px solid #333;color:#0ff;
            border-radius:4px;padding:4px 8px;
        }""")
        self.theme_selector.currentIndexChanged.connect(lambda i: self._apply_theme(["dark", "light", "nord"][i]))
        theme_layout.addWidget(self.theme_selector)

        return theme_group

    # =========================================================================
    # M2 + M3: Live RPS Gauge + Competition Indicator (in stats area)
    # =========================================================================
    def _add_rps_competition_widgets(self, stats_layout):
        """Add RPS gauge and competition indicator to stats panel."""
        from PyQt5.QtWidgets import QLabel, QProgressBar
        from PyQt5.QtCore import Qt

        # RPS gauge
        rps_frame = QLabel("RPS: 0.0")
        rps_frame.setObjectName("rpsGauge")
        rps_frame.setStyleSheet("""
            QLabel#rpsGauge {
                color: #0f0; font-family: 'Consolas', monospace;
                font-size: 14px; font-weight: bold; padding: 4px 12px;
                background: #0a0a1a; border: 1px solid #333; border-radius: 4px;
            }
        """)
        rps_frame.setAlignment(Qt.AlignCenter)
        rps_frame.setFixedSize(100, 28)
        stats_layout.addWidget(rps_frame)
        self.rps_label = rps_frame

        # RPS bar
        self.rps_bar = QProgressBar()
        self.rps_bar.setRange(0, 20)
        self.rps_bar.setValue(0)
        self.rps_bar.setTextVisible(False)
        self.rps_bar.setFixedHeight(6)
        self.rps_bar.setStyleSheet("""
            QProgressBar { border: none; background: #111; border-radius: 3px; }
            QProgressBar::chunk { background: qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #0f0,stop:1 #0ff); border-radius: 3px; }
        """)
        stats_layout.addWidget(self.rps_bar)

        # Competition indicator
        comp_frame = QLabel("🟢 LOW")
        comp_frame.setObjectName("competitionIndicator")
        comp_frame.setStyleSheet("""
            QLabel#competitionIndicator {
                color: #0f0; font-size: 12px; font-weight: bold; padding: 4px 12px;
                background: #0a0a1a; border: 1px solid #0f0; border-radius: 4px;
            }
        """)
        comp_frame.setAlignment(Qt.AlignCenter)
        comp_frame.setFixedSize(100, 28)
        stats_layout.addWidget(comp_frame)
        self.competition_label = comp_frame

        # 409 counter
        self._409_count = 0
        self._404_count = 0
        self._rps_window = []  # Timestamps of recent requests

    def _update_rps(self):
        """Update RPS display from tracked requests."""
        import time
        now = time.time()
        # Keep only last 5 seconds of timestamps
        self._rps_window = [t for t in self._rps_window if now - t < 5]
        rps = len(self._rps_window) / 5.0
        self.rps_label.setText(f"RPS: {rps:.1f}")
        self.rps_bar.setValue(min(int(rps), 20))

        # Color based on RPS
        if rps > 10:
            color = "#0f0"
        elif rps > 5:
            color = "#ff0"
        else:
            color = "#f00"
        self.rps_label.setStyleSheet(f"""
            QLabel#rpsGauge {{
                color: {color}; font-family: 'Consolas', monospace;
                font-size: 14px; font-weight: bold; padding: 4px 12px;
                background: #0a0a1a; border: 1px solid #333; border-radius: 4px;
            }}
        """)

    def _update_competition(self, status_code):
        """Update competition indicator based on response codes."""
        if status_code == 409:
            self._409_count += 1
        elif status_code == 404:
            self._404_count += 1

        total = self._409_count + self._404_count
        if total < 5:
            level, color, border = "🟢 LOW", "#0f0", "#0f0"
        elif self._409_count / max(total, 1) < 0.3:
            level, color, border = "🟡 MED", "#ff0", "#ff0"
        elif self._409_count / max(total, 1) < 0.6:
            level, color, border = "🟠 HIGH", "#f80", "#f80"
        else:
            level, color, border = "🔴 NUCLEAR", "#f00", "#f00"

        self.competition_label.setText(level)
        self.competition_label.setStyleSheet(f"""
            QLabel#competitionIndicator {{
                color: {color}; font-size: 12px; font-weight: bold; padding: 4px 12px;
                background: #0a0a1a; border: 1px solid {border}; border-radius: 4px;
            }}
        """)

    # =========================================================================
    # M4: Sniping History Dashboard
    # =========================================================================
    def _create_history_dashboard_tab(self):
        """Create the sniping history dashboard tab."""
        from PyQt5.QtWidgets import (QFrame, QVBoxLayout, QHBoxLayout, QLabel,
                                      QPushButton, QTableWidget, QTableWidgetItem,
                                      QHeaderView, QGroupBox, QTextEdit)
        from PyQt5.QtCore import Qt

        frame = QFrame()
        layout = QVBoxLayout(frame)

        header = QLabel("📜 Sniping History Dashboard")
        header.setStyleSheet("font-size:16px;font-weight:bold;color:#0ff;")
        layout.addWidget(header)

        # Stats summary
        stats_group = QGroupBox("Summary Stats")
        stats_layout = QHBoxLayout(stats_group)

        self.hist_total = QLabel("0")
        self.hist_success = QLabel("0")
        self.hist_failed = QLabel("0")
        self.hist_rate = QLabel("0%")

        for label, value_label, color in [
            ("Total Attempts:", self.hist_total, "#0ff"),
            ("Successes:", self.hist_success, "#0f0"),
            ("Failed:", self.hist_failed, "#f00"),
            ("Success Rate:", self.hist_rate, "#ff0"),
        ]:
            lbl = QLabel(label)
            lbl.setStyleSheet(f"color:#888;font-size:12px;")
            stats_layout.addWidget(lbl)
            value_label.setStyleSheet(f"color:{color};font-size:14px;font-weight:bold;font-family:Consolas,monospace;")
            stats_layout.addWidget(value_label)

        layout.addWidget(stats_group)

        # History table
        self.history_table = QTableWidget()
        self.history_table.setColumnCount(5)
        self.history_table.setHorizontalHeaderLabels(["Time", "Username", "Status", "Token", "Response"])
        self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.history_table.setStyleSheet("""
            QTableWidget { background: #111; color: #ccc; gridline-color: #222; }
            QHeaderView::section { background: #1a1a2e; color: #0ff; border: 1px solid #333; padding: 4px; font-weight: bold; }
            QTableWidget::item { padding: 4px; }
        """)
        self.history_table.setEditTriggers(QTableWidget.NoEditTriggers)
        layout.addWidget(self.history_table)

        # Buttons
        btn_layout = QHBoxLayout()
        refresh_hist = QPushButton("🔄 Refresh")
        refresh_hist.setObjectName("primaryButton")
        refresh_hist.clicked.connect(self._refresh_history_dashboard)
        btn_layout.addWidget(refresh_hist)

        clear_hist = QPushButton("🗑️ Clear History")
        clear_hist.setStyleSheet("""QPushButton {
            background:#333;color:#f00;border:1px solid #f00;border-radius:4px;padding:6px 12px;
        }""")
        clear_hist.clicked.connect(self._clear_history_dashboard)
        btn_layout.addWidget(clear_hist)

        export_hist = QPushButton("📁 Export CSV")
        export_hist.setStyleSheet("""QPushButton {
            background:#1a1a2e;color:#0ff;border:1px solid #0ff;border-radius:4px;padding:6px 12px;
        }""")
        export_hist.clicked.connect(self._export_history_csv)
        btn_layout.addWidget(export_hist)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return frame

    def _refresh_history_dashboard(self):
        """Refresh the history dashboard from saved data."""
        history = self._load_snipe_history()
        self.history_table.setRowCount(0)
        total = success = failed = 0

        for entry in history[-100:]:  # Last 100 entries
            row = self.history_table.rowCount()
            self.history_table.insertRow(row)

            status = entry.get("status", "unknown")
            if status == "success":
                success += 1
                status_text = "✅ Success"
            else:
                failed += 1
                status_text = "❌ Failed"
            total += 1

            self.history_table.setItem(row, 0, QTableWidgetItem(str(entry.get("time", ""))))
            self.history_table.setItem(row, 1, QTableWidgetItem(str(entry.get("username", ""))))
            self.history_table.setItem(row, 2, QTableWidgetItem(status_text))
            self.history_table.setItem(row, 3, QTableWidgetItem(str(entry.get("token_preview", ""))))
            self.history_table.setItem(row, 4, QTableWidgetItem(str(entry.get("response_code", ""))))

        self.hist_total.setText(str(total))
        self.hist_success.setText(str(success))
        self.hist_failed.setText(str(failed))
        rate = f"{success/max(total,1)*100:.0f}%"
        self.hist_rate.setText(rate)

    def _clear_history_dashboard(self):
        """Clear history."""
        self.history_table.setRowCount(0)
        self.hist_total.setText("0")
        self.hist_success.setText("0")
        self.hist_failed.setText("0")
        self.hist_rate.setText("0%")
        self._log("📜 History cleared")

    # (Duplicate _export_history_csv removed — canonical version at line ~5515 uses SnipeHistoryDB)

    def _load_snipe_history(self):
        """Load snipe history from file."""
        import os
        history_file = os.path.expanduser("~/.sniper_history.json")
        try:
            with open(history_file, 'r') as f:
                return json.load(f)
        except (FileNotFoundError, json.JSONDecodeError):
            return []

    def _save_snipe_history_entry(self, entry):
        """Save a snipe history entry."""
        import os
        history = self._load_snipe_history()
        history.append(entry)
        # Keep last 1000 entries
        history = history[-1000:]
        history_file = os.path.expanduser("~/.sniper_history.json")
        with open(history_file, 'w') as f:
            json.dump(history, f, indent=2, default=str)

    # =========================================================================
    # M7: Account Health Report
    # =========================================================================
    def _create_account_health_tab(self):
        """Create the account health report tab."""
        from PyQt5.QtWidgets import (QFrame, QVBoxLayout, QHBoxLayout, QLabel,
                                      QPushButton, QTableWidget, QTableWidgetItem,
                                      QHeaderView, QProgressBar, QGroupBox)
        from PyQt5.QtCore import Qt

        frame = QFrame()
        layout = QVBoxLayout(frame)

        header = QLabel("💊 Account Health Report")
        header.setStyleSheet("font-size:16px;font-weight:bold;color:#0ff;")
        layout.addWidget(header)

        desc = QLabel("Check the health of your sniping accounts. Shows name change cooldowns, remaining changes, and token status.")
        desc.setStyleSheet("color:#aaa;font-size:12px;")
        desc.setWordWrap(True)
        layout.addWidget(desc)

        # Health table
        self.health_table = QTableWidget()
        self.health_table.setColumnCount(6)
        self.health_table.setHorizontalHeaderLabels([
            "Account", "Status", "Name Changes Left", "Cooldown", "Last Used", "Health"
        ])
        self.health_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.health_table.setStyleSheet("""
            QTableWidget { background: #111; color: #ccc; gridline-color: #222; }
            QHeaderView::section { background: #1a1a2e; color: #0ff; border: 1px solid #333; padding: 4px; font-weight: bold; }
        """)
        self.health_table.setEditTriggers(QTableWidget.NoEditTriggers)
        layout.addWidget(self.health_table)

        # Overall health bar
        health_group = QGroupBox("Overall Fleet Health")
        health_layout = QVBoxLayout(health_group)

        self.fleet_health_bar = QProgressBar()
        self.fleet_health_bar.setValue(100)
        self.fleet_health_bar.setTextVisible(True)
        self.fleet_health_bar.setFormat("%p% Healthy")
        self.fleet_health_bar.setStyleSheet("""
            QProgressBar { border: 1px solid #333; border-radius: 4px; background: #111; font-size: 13px; }
            QProgressBar::chunk { background: qlineargradient(x1:0,y1:0,x2:1,y2:0,stop:0 #0f0,stop:1 #0ff); }
        """)
        health_layout.addWidget(self.fleet_health_bar)

        self.fleet_summary = QLabel("Check accounts to see summary")
        self.fleet_summary.setStyleSheet("color:#aaa;font-size:12px;padding:4px;")
        health_layout.addWidget(self.fleet_summary)

        layout.addWidget(health_group)

        # Buttons
        btn_layout = QHBoxLayout()
        check_btn = QPushButton("🔍 Check All Accounts")
        check_btn.setObjectName("primaryButton")
        check_btn.clicked.connect(self._check_all_accounts)
        btn_layout.addWidget(check_btn)

        refresh_btn = QPushButton("🔄 Refresh")
        refresh_btn.clicked.connect(self._refresh_account_health)
        btn_layout.addWidget(refresh_btn)

        layout.addLayout(btn_layout)
        layout.addStretch()
        return frame

    def _check_all_accounts(self):
        """Check health of all loaded accounts."""
        import threading
        self._log("🔍 Checking all accounts...")

        def check():
            tokens = self._load_tokens()
            if not tokens:
                self._q_invoke(lambda: self._log("⚠️  No tokens loaded"))
                return

            results = []
            for i, token in enumerate(tokens):
                try:
                    import httpx
                    client = httpx.Client(timeout=10)
                    resp = client.get(
                        "https://api.minecraftservices.com/minecraft/profile",
                        headers={"Authorization": f"Bearer {token}"}
                    )
                    if resp.status_code == 200:
                        data = resp.json()
                        username = data.get("name", "Unknown")
                        uuid = data.get("id", "Unknown")
                        results.append({
                            "account": f"{username} ({uuid[:8]})",
                            "status": "✅ Active",
                            "changes_left": "N/A (API limited)",
                            "cooldown": "Unknown",
                            "last_used": "N/A",
                            "health": 100,
                        })
                    elif resp.status_code == 401:
                        results.append({
                            "account": f"Token {token[:12]}...",
                            "status": "❌ Invalid",
                            "changes_left": "0",
                            "cooldown": "N/A",
                            "last_used": "N/A",
                            "health": 0,
                        })
                    else:
                        results.append({
                            "account": f"Token {token[:12]}...",
                            "status": f"⚠️  HTTP {resp.status_code}",
                            "changes_left": "N/A",
                            "cooldown": "N/A",
                            "last_used": "N/A",
                            "health": 50,
                        })
                except Exception as e:
                    results.append({
                        "account": f"Token {token[:12]}...",
                        "status": f"❌ Error: {str(e)[:30]}",
                        "changes_left": "0",
                        "cooldown": "N/A",
                        "last_used": "N/A",
                        "health": 0,
                    })

            # Update UI
            self._q_invoke(lambda: self._populate_account_health_table(results))

        threading.Thread(target=check, daemon=True).start()

    def _populate_account_health_table(self, results):
        """Populate the account health table with results (M7 tab)."""
        self.health_table.setRowCount(0)
        total_health = 0

        for r in results:
            row = self.health_table.rowCount()
            self.health_table.insertRow(row)
            self.health_table.setItem(row, 0, QTableWidgetItem(r["account"]))
            self.health_table.setItem(row, 1, QTableWidgetItem(r["status"]))
            self.health_table.setItem(row, 2, QTableWidgetItem(r["changes_left"]))
            self.health_table.setItem(row, 3, QTableWidgetItem(r["cooldown"]))
            self.health_table.setItem(row, 4, QTableWidgetItem(r["last_used"]))

            health_val = r.get("health", 50)
            total_health += health_val

            # Health bar as item
            health_bar = QTableWidgetItem(f"{health_val}%")
            if health_val >= 80:
                health_bar.setForeground(QtGui.QColor("#0f0"))
            elif health_val >= 50:
                health_bar.setForeground(QtGui.QColor("#ff0"))
            else:
                health_bar.setForeground(QtGui.QColor("#f00"))
            self.health_table.setItem(row, 5, health_bar)

        avg_health = total_health // max(len(results), 1)
        self.fleet_health_bar.setValue(avg_health)
        self.fleet_summary.setText(f"{len(results)} accounts checked | Avg health: {avg_health}%")

    def _refresh_account_health(self):
        """Refresh account health display."""
        self._check_all_accounts()

class LatencyMonitorWorker(QThread):
    """Background thread that pings MC API endpoints and reports latency"""
    log_signal = pyqtSignal(str)

    def __init__(self, interval_seconds=5):
        super().__init__()
        self.interval = interval_seconds
        self._stop = False
        self.endpoints = [
            ("MC API", "https://api.minecraftservices.com/"),
            ("NameMC", "https://namemc.com/"),
            ("LabyName", "https://labynames.com/"),
        ]

    def run(self):
        while not self._stop:
            results = []
            for name, url in self.endpoints:
                try:
                    start = time.time()
                    resp = httpx.head(url, timeout=5, follow_redirects=True)
                    elapsed_ms = (time.time() - start) * 1000
                    status = "🟢" if resp.status_code < 400 else "🔴"
                    results.append(f"  {status} {name}: {elapsed_ms:.0f}ms ({resp.status_code})")
                except Exception as e:
                    results.append(f"  🔴 {name}: FAILED - {str(e)[:30]}")

            timestamp = datetime.now().strftime("%H:%M:%S")
            joined = '\n'.join(results)
            self.log_signal.emit(f"[{timestamp}] {joined}")

            # Sleep in small increments for responsive stopping
            for _ in range(self.interval * 10):
                if self._stop:
                    break
                self.msleep(100)

    def stop(self):
        self._stop = True

# ============================================================================
# Main entry point
# ============================================================================

def capture_snipe_proof(username: str, bearer_token: str = None) -> dict:
    """F7: Capture proof of name ownership change.
    Returns JSON proof with timestamp, UUID, and profile data."""
    import requests
    from datetime import datetime, timezone
    
    proof = {
        "username": username,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "uuid": None,
        "profile": None,
        "owned_by_account": False,
    }
    
    try:
        # Get public profile
        resp = requests.get(f"https://api.minecraftservices.com/minecraft/profile/{username}", timeout=10)
        if resp.status_code == 200:
            proof["profile"] = resp.json()
            proof["uuid"] = resp.json().get("id")
    except:
        pass
    
    # Verify ownership if we have a token
    if bearer_token:
        try:
            resp = requests.get("https://api.minecraftservices.com/minecraft/profile",
                headers={"Authorization": f"Bearer {bearer_token}"}, timeout=10)
            if resp.status_code == 200:
                my_profile = resp.json()
                proof["owned_by_account"] = my_profile.get("name") == username
                proof["my_uuid"] = my_profile.get("id")
        except:
            pass
    
    # Save proof to file
    try:
        from pathlib import Path
        proofs_dir = Path("snipe_proofs")
        proofs_dir.mkdir(exist_ok=True)
        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
        filepath = proofs_dir / f"proof_{username}_{ts}.json"
        with open(filepath, 'w') as f:
            import json
            json.dump(proof, f, indent=2)
        proof["saved_to"] = str(filepath)
    except:
        pass
    
    return proof


# ====================================================================
# FEATURE: SCHEDULED SNIPES (m1)
# Auto-start a snipe at a specified future time
# ====================================================================
class ScheduledSnipeWorker(QThread):
    """Worker that waits until scheduled time then triggers a snipe."""
    started = pyqtSignal(str)  # emits the name when ready
    finished = pyqtSignal(str)
    
    def __init__(self, target_time_str, name, timezone_name="UTC"):
        super().__init__()
        self.target_time_str = target_time_str
        self.name = name
        self.timezone_name = timezone_name
        self._stop = False
    
    def stop(self):
        self._stop = True
    
    def run(self):
        try:
            # Parse target time
            target = datetime.strptime(self.target_time_str, "%Y-%m-%d %H:%M").replace(tzinfo=timezone.utc)
            now = datetime.now(timezone.utc)
            wait_seconds = (target - now).total_seconds()
            
            if wait_seconds <= 0:
                self.finished.emit("Target time already passed")
                return
            
            # Wait in small increments to check for stop
            elapsed = 0
            while elapsed < wait_seconds and not self._stop:
                self.msleep(1000)
                elapsed += 1
            
            if self._stop:
                self.finished.emit("Cancelled")
                return
            
            self.started.emit(self.name)
            self.finished.emit(f"Ready to snipe: {self.name}")
        except Exception as e:
            self.finished.emit(f"Error: {e}")


# ====================================================================
# FEATURE: TELEGRAM NOTIFICATIONS (m2)
# Send alerts via Telegram Bot API
# ====================================================================
TELEGRAM_SETTINGS_FILE = "telegram_settings.json"

def load_telegram_settings() -> dict:
    try:
        with open(TELEGRAM_SETTINGS_FILE, 'r') as f:
            return json.load(f)
    except:
        return {"bot_token": "", "chat_id": "", "enabled": False}

def save_telegram_settings(settings: dict):
    with open(TELEGRAM_SETTINGS_FILE, 'w') as f:
        json.dump(settings, f, indent=2)

def send_telegram_message(bot_token: str, chat_id: str, message: str) -> bool:
    """Send a message via Telegram Bot API."""
    if not bot_token or not chat_id:
        return False
    try:
        url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
        resp = httpx.post(url, json={
            "chat_id": chat_id,
            "text": message,
            "parse_mode": "HTML"
        }, timeout=10)
        return resp.status_code == 200
    except:
        return False

def send_telegram_snipe_alert(settings: dict, username: str, success: bool):
    """Send a snipe alert via Telegram."""
    if not settings.get("enabled") or not settings.get("bot_token"):
        return
    status = "✅ SUCCEEDED" if success else "❌ FAILED"
    msg = f"<b>{status}</b>: Sniped <code>{username}</code>\nTime: {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')}"
    send_telegram_message(settings["bot_token"], settings["chat_id"], msg)


# ====================================================================
# FEATURE: NAME AVAILABILITY CACHE (m4)
# Cache NameMC availability checks to reduce API calls
# ====================================================================
NAME_CACHE_FILE = "name_cache.json"
NAME_CACHE_TTL = 30  # seconds

def load_name_cache() -> dict:
    try:
        with open(NAME_CACHE_FILE, 'r') as f:
            return json.load(f)
    except:
        return {}

def save_name_cache(cache: dict):
    try:
        with open(NAME_CACHE_FILE, 'w') as f:
            json.dump(cache, f, indent=2)
    except:
        pass

def check_name_cache(name: str) -> tuple:
    """Check if name is in cache and still valid. Returns (available, is_fresh)."""
    cache = load_name_cache()
    entry = cache.get(name.lower())
    if not entry:
        return (None, False)
    age = time.time() - entry.get("timestamp", 0)
    is_fresh = age < NAME_CACHE_TTL
    return (entry.get("available"), is_fresh)

def update_name_cache(name: str, available: bool):
    cache = load_name_cache()
    cache[name.lower()] = {"available": available, "timestamp": time.time()}
    save_name_cache(cache)


# ====================================================================
# FEATURE: PER-TOKEN STATS DASHBOARD (m5)
# Track statistics per authentication token
# ====================================================================
TOKEN_STATS_FILE = "token_stats.json"

def load_token_stats() -> dict:
    try:
        with open(TOKEN_STATS_FILE, 'r') as f:
            return json.load(f)
    except:
        return {}

def save_token_stats(stats: dict):
    with open(TOKEN_STATS_FILE, 'w') as f:
        json.dump(stats, f, indent=2)

def record_token_request(token: str, endpoint: str, status_code: int, latency_ms: float, success: bool):
    """Record a request stat for a token."""
    stats = load_token_stats()
    token_key = token[:16] + "..." if len(token) > 16 else token
    if token_key not in stats:
        stats[token_key] = {
            "full_token_hash": hashlib.sha256(token.encode()).hexdigest()[:16],
            "total_requests": 0,
            "successful": 0,
            "failed": 0,
            "rate_limited": 0,
            "total_latency_ms": 0.0,
            "snipes_won": 0,
            "last_used": None,
        }
    s = stats[token_key]
    s["total_requests"] += 1
    s["total_latency_ms"] += latency_ms
    s["last_used"] = datetime.now().isoformat()
    if success:
        s["successful"] += 1
    else:
        s["failed"] += 1
    if status_code == 429:
        s["rate_limited"] += 1
    save_token_stats(stats)

def record_token_snipe_win(token: str):
    stats = load_token_stats()
    token_key = token[:16] + "..." if len(token) > 16 else token
    if token_key in stats:
        stats[token_key]["snipes_won"] += 1
        save_token_stats(stats)


# ====================================================================
# FEATURE: MULTI-REGION ROUTING (m6)
# Route requests through nearest Mojang edge server
# ====================================================================
MOJANG_EDGE_ENDPOINTS = [
    {"region": "US-East", "url": "https://api.minecraftservices.com", "weight": 1.0},
    {"region": "EU-West", "url": "https://api.minecraftservices.com", "weight": 1.0},
]

def measure_region_latency(url: str, timeout: float = 3.0) -> float:
    """Measure latency to a Mojang endpoint."""
    start = time.perf_counter()
    try:
        resp = httpx.head(url, timeout=timeout, follow_redirects=False)
        return (time.perf_counter() - start) * 1000
    except:
        return float('inf')

def get_best_region() -> dict:
    """Find the fastest Mojang edge endpoint."""
    best = None
    best_latency = float('inf')
    for ep in MOJANG_EDGE_ENDPOINTS:
        lat = measure_region_latency(ep["url"])
        if lat < best_latency:
            best_latency = lat
            best = ep
    return best or MOJANG_EDGE_ENDPOINTS[0]


# ====================================================================
# FEATURE: AUTO-UPDATE CHECKER (m8)
# Check GitHub releases for new versions
# ====================================================================
CURRENT_VERSION = "11.0"

def check_for_updates() -> dict:
    """Check GitHub for latest release."""
    try:
        resp = httpx.get(
            "https://api.github.com/repos/SNJELR/minecraft-sniper/releases/latest",
            timeout=10,
            headers={"User-Agent": "Minecraft-Sniper-GUI"}
        )
        if resp.status_code == 200:
            data = resp.json()
            latest = data.get("tag_name", "").lstrip("v")
            return {
                "available": latest != CURRENT_VERSION,
                "latest_version": latest,
                "download_url": data.get("html_url", ""),
                "changelog": data.get("body", "")[:500],
            }
    except:
        pass
    return {"available": False, "latest_version": CURRENT_VERSION, "download_url": "", "changelog": ""}


# ====================================================================
# FEATURE: REAL-TIME LATENCY GRAPH (l1)
# Simple line graph using PyQt5 QPainter
# ====================================================================
class LatencyGraphWidget(QWidget):
    """Real-time latency graph widget using QPainter."""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.latency_points = []  # list of (timestamp, latency_ms)
        self.max_points = 60
        self.min_latency = 0
        self.max_latency = 200
        self.setMinimumHeight(150)
        self.setStyleSheet("background-color: #1a1a2e; border: 1px solid #333;")
    
    def add_point(self, latency_ms: float):
        self.latency_points.append(latency_ms)
        if len(self.latency_points) > self.max_points:
            self.latency_points.pop(0)
        # Auto-scale
        if self.latency_points:
            self.min_latency = min(self.latency_points) * 0.9
            self.max_latency = max(self.latency_points) * 1.1
            if self.max_latency == self.min_latency:
                self.max_latency = self.min_latency + 50
        self.update()
    
    def clear(self):
        self.latency_points = []
        self.update()
    
    def paintEvent(self, event):
        if not self.latency_points:
            return
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        
        w, h = self.width(), self.height()
        padding = 30
        
        # Grid
        painter.setPen(QColor("#333"))
        for i in range(5):
            y = padding + (h - 2 * padding) * i // 4
            painter.drawLine(padding, y, w - padding, y)
        
        # Labels
        painter.setPen(QColor("#888"))
        painter.setFont(QFont("monospace", 8))
        for i in range(5):
            val = self.max_latency - (self.max_latency - self.min_latency) * i // 4
            y = padding + (h - 2 * padding) * i // 4
            painter.drawText(2, y + 4, f"{val:.0f}ms")
        
        # Line
        painter.setPen(QColor("#00ff88"))
        painter.setStrokeStyle(Qt.DotLine)
        
        points = self.latency_points
        n = len(points)
        for i in range(n - 1):
            x1 = padding + (w - 2 * padding) * i // (self.max_points - 1)
            x2 = padding + (w - 2 * padding) * (i + 1) // (self.max_points - 1)
            y1 = padding + (h - 2 * padding) * (1 - (points[i] - self.min_latency) / (self.max_latency - self.min_latency))
            y2 = padding + (h - 2 * padding) * (1 - (points[i+1] - self.min_latency) / (self.max_latency - self.min_latency))
            y1 = max(padding, min(h - padding, y1))
            y2 = max(padding, min(h - padding, y2))
            painter.drawLine(x1, y1, x2, y2)
        
        # Fill area
        painter.setPen(Qt.NoPen)
        painter.setBrush(QColor("0,255,136,30"))
        path = QPainterPath()
        first_x = padding
        first_y = padding + (h - 2 * padding) * (1 - (points[0] - self.min_latency) / (self.max_latency - self.min_latency))
        path.moveTo(first_x, max(padding, min(h - padding, first_y)))
        for i in range(1, n):
            x = padding + (w - 2 * padding) * i // (self.max_points - 1)
            y = padding + (h - 2 * padding) * (1 - (points[i] - self.min_latency) / (self.max_latency - self.min_latency))
            path.lineTo(x, max(padding, min(h - padding, y)))
        path.lineTo(padding + (w - 2 * padding) * (n - 1) // (self.max_points - 1), h - padding)
        path.lineTo(padding, h - padding)
        painter.drawPath(path)
        
        painter.end()


# ====================================================================
# FEATURE: SNIPE SESSION REPLAY (l2)
# Record and replay snipe sessions
# ====================================================================
SESSION_RECORD_DIR = "session_records"

def start_session_recording(session_id: str) -> str:
    """Start recording a snipe session. Returns file path."""
    os.makedirs(SESSION_RECORD_DIR, exist_ok=True)
    filepath = os.path.join(SESSION_RECORD_DIR, f"{session_id}_{int(time.time())}.jsonl")
    with open(filepath, 'w') as f:
        f.write(json.dumps({"event": "session_start", "timestamp": time.time(), "session_id": session_id}) + "\n")
    return filepath

def record_session_event(filepath: str, event: str, data: dict):
    """Append an event to a session recording."""
    try:
        with open(filepath, 'a') as f:
            f.write(json.dumps({"event": event, "timestamp": time.time(), **data}) + "\n")
    except:
        pass

def get_session_records() -> list:
    """List all session recordings."""
    records = []
    if not os.path.exists(SESSION_RECORD_DIR):
        return records
    for f in sorted(os.listdir(SESSION_RECORD_DIR)):
        if f.endswith('.jsonl'):
            filepath = os.path.join(SESSION_RECORD_DIR, f)
            size = os.path.getsize(filepath)
            records.append({"file": f, "path": filepath, "size": size})
    return records

def load_session_record(filepath: str) -> list:
    """Load a session recording."""
    events = []
    try:
        with open(filepath, 'r') as f:
            for line in f:
                line = line.strip()
                if line:
                    events.append(json.loads(line))
    except:
        pass
    return events


# ====================================================================
# FEATURE: PLUGIN SYSTEM (l3)
# Load and execute Python plugins
# ====================================================================
PLUGIN_DIR = "plugins"

class PluginManager:
    """Simple plugin system for extending sniper functionality."""
    
    def __init__(self):
        self.plugins = []
        self.hooks = {}  # hook_name -> [plugin_funcs]
    
    def load_plugins(self):
        """Scan and load all .py plugins from the plugins directory."""
        if not os.path.exists(PLUGIN_DIR):
            os.makedirs(PLUGIN_DIR, exist_ok=True)
            return []
        
        loaded = []
        for filename in os.listdir(PLUGIN_DIR):
            if filename.endswith('.py') and not filename.startswith('_'):
                filepath = os.path.join(PLUGIN_DIR, filename)
                try:
                    spec = __import__('importlib.util').util.spec_from_file_location(filename.replace('.py', ''), filepath)
                    module = __import__('importlib').util.module_from_spec(spec)
                    spec.loader.exec_module(module)
                    
                    # Register hooks
                    if hasattr(module, 'on_snipe_start'):
                        self._register_hook('on_snipe_start', module.on_snipe_start)
                    if hasattr(module, 'on_snipe_success'):
                        self._register_hook('on_snipe_success', module.on_snipe_success)
                    if hasattr(module, 'on_snipe_fail'):
                        self._register_hook('on_snipe_fail', module.on_snipe_fail)
                    if hasattr(module, 'on_gui_init'):
                        module.on_gui_init  # Will be called manually
                    
                    self.plugins.append(module)
                    loaded.append(filename)
                except Exception as e:
                    pass  # Silent fail for bad plugins
        
        return loaded
    
    def _register_hook(self, hook_name, func):
        if hook_name not in self.hooks:
            self.hooks[hook_name] = []
        self.hooks[hook_name].append(func)
    
    def fire_hook(self, hook_name, **kwargs):
        """Fire a hook, calling all registered plugins."""
        results = []
        for func in self.hooks.get(hook_name, []):
            try:
                result = func(**kwargs)
                results.append(result)
            except Exception as e:
                pass
        return results


# Global plugin manager
plugin_manager = PluginManager()


# ====================================================================
# FEATURE: DNS OVER HTTPS (l4)
# Resolve Mojang domains via DoH for anti-DNS-spoofing
# ====================================================================
DOH_PROVIDERS = {
    "Cloudflare": "https://cloudflare-dns.com/dns-query",
    "Google": "https://dns.google/resolve",
    "Quad9": "https://dns.quad9.net/dns-query",
}

def resolve_doh(domain: str, provider: str = "Cloudflare") -> list:
    """Resolve a domain via DNS-over-HTTPS."""
    url = DOH_PROVIDERS.get(provider, DOH_PROVIDERS["Cloudflare"])
    try:
        resp = httpx.get(url, params={"name": domain, "type": "A"},
            headers={"Accept": "application/dns-json"}, timeout=5)
        if resp.status_code == 200:
            data = resp.json()
            return [r["data"] for r in data.get("Answer", []) if r.get("type") == 1]
    except:
        pass
    return []


# ====================================================================
# FEATURE: VOICE ALERTS (l5)
# Text-to-speech alerts on snipe success
# ====================================================================
def play_voice_alert(message: str):
    """Speak a message using system TTS."""
    try:
        if platform.system() == "Darwin":  # macOS
            subprocess.run(["say", message], timeout=10)
        elif platform.system() == "Windows":
            subprocess.run(["powershell", "-Command",
                f"$s=[Speech.Synthesis.SpeechSynthesizer]'New';$s.Speak('{message}')"],
                timeout=10, shell=True)
        else:  # Linux
            subprocess.run(["spd-say", message], timeout=10)
    except:
        pass


# ====================================================================
# FEATURE: PUSHOVER NOTIFICATIONS (l6)
# Send alerts via Pushover API
# ====================================================================
PUSHOVER_SETTINGS_FILE = "pushover_settings.json"

def load_pushover_settings() -> dict:
    try:
        with open(PUSHOVER_SETTINGS_FILE, 'r') as f:
            return json.load(f)
    except:
        return {"token": "", "user_key": "", "enabled": False}

def save_pushover_settings(settings: dict):
    with open(PUSHOVER_SETTINGS_FILE, 'w') as f:
        json.dump(settings, f, indent=2)

def send_pushover_notification(settings: dict, title: str, message: str):
    """Send a notification via Pushover API."""
    if not settings.get("enabled") or not settings.get("token") or not settings.get("user_key"):
        return False
    try:
        resp = httpx.post("https://api.pushover.net/1/messages.json", data={
            "token": settings["token"],
            "user": settings["user_key"],
            "title": title,
            "message": message,
            "priority": 1,
        }, timeout=10)
        return resp.status_code == 200
    except:
        return False


def main():
    """Main entry point for the application."""
    # Increase file descriptor limit on Linux for concurrent connections
    try:
        import resource
        resource.setrlimit(resource.RLIMIT_NOFILE, (4096, 4096))
    except (ImportError, ValueError, OSError):
        pass  # Not available on Windows or limited

    app = QApplication.instance() or QApplication(sys.argv)
    app.setStyle("Fusion")
    window = SniperGUI()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
