"""
cjavi — Aviator WebSocket Bot for betpawa.ug

Strategy (Multi-Level High-Multiplier):
  12 levels, each with escalating stakes and a cashout multiplier target.
  On loss  → advance to next stake. Level exhausted → next level.
  On win   → reset to Level 1 / Stake 1, switch to other account.
  Win verified by balance comparison (current > cycle start balance).
  Two accounts rotate on every win. Auto-recovers from WS disconnects.
  Writes status to bot_status.json for dashboard monitoring.

  Level 1:  cashout >=69x   | stakes 1.5 → 10.0
  Level 2:  cashout >=38x   | stakes 10.5 → 20.0
  Level 3:  cashout >=44x   | stakes 20.5 → 30.0
  Level 4:  cashout >=52x   | stakes 30.5 → 40.0
  Level 5:  cashout >=62x   | stakes 40.5 → 50.0
  Level 6:  cashout >=72x   | stakes 50.5 → 60.0
  Level 7:  cashout >=82x   | stakes 60.5 → 70.0
  Level 8:  cashout >=92x   | stakes 70.5 → 80.0
  Level 9:  cashout >=101x  | stakes 80.5 → 90.0
  Level 10: cashout >=111x  | stakes 90.5 → 100.0
  Level 11: cashout >=121x  | stakes 100.5 → 110.0
  Level 12: cashout >=131x  | stakes 110.5 → 120.0

Usage:
    pip install playwright
    playwright install chromium
    python cjavi.py
"""

import json
import os
import re
import struct
import sys
import time
import random
import threading
import urllib.request
import urllib.error
from datetime import datetime
from pathlib import Path
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

# Status file (local backup) + remote dashboard URL
STATUS_FILE = Path(__file__).parent / "bot_status.json"
DASHBOARD_URL = "https://www.linkiefy.com/cjavi.php"
DASHBOARD_KEY = "cjavi_secret_2026"                     # <-- Shared auth key


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  CONFIGURATION                                                      ║
# ╚══════════════════════════════════════════════════════════════════════╝

# Accounts — bot switches between them on every win
ACCOUNTS = [
    {"mobile": "750918940",  "password": "Favour@2016"},
    {"mobile": "788866776",  "password": "Favour@2016"},
]

LOGIN_URL    = "https://www.betpawa.ug/login"
AVIATOR_URL  = "https://www.betpawa.ug/casino?gameId=aviator&filter=all"

# Each level has a cashout multiplier target and a list of escalating stakes.
# On loss → next stake. Level exhausted → next level. On win → reset to L1/S1.
LEVELS = [
    {"cashout": 69.0,
     "stakes": [1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0,
                5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0]},
    {"cashout": 38.0,
     "stakes": [10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5, 14.0,
                14.5, 15.0, 15.5, 16.0, 16.5, 17.0, 17.5, 18.0, 18.5, 19.0, 19.5, 20.0]},
    {"cashout": 44.0,
     "stakes": [20.5, 21.0, 21.5, 22.0, 22.5, 23.0, 23.5, 24.0,
                24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 27.5, 28.0, 28.5, 29.0, 29.5, 30.0]},
    {"cashout": 52.0,
     "stakes": [30.5, 31.0, 31.5, 32.0, 32.5, 33.0, 33.5, 34.0,
                34.5, 35.0, 35.5, 36.0, 36.5, 37.0, 37.5, 38.0, 38.5, 39.0, 39.5, 40.0]},
    {"cashout": 62.0,
     "stakes": [40.5, 41.0, 41.5, 42.0, 42.5, 43.0, 43.5, 44.0,
                44.5, 45.0, 45.5, 46.0, 46.5, 47.0, 47.5, 48.0, 48.5, 49.0, 49.5, 50.0]},
    {"cashout": 72.0,
     "stakes": [50.5, 51.0, 51.5, 52.0, 52.5, 53.0, 53.5, 54.0,
                54.5, 55.0, 55.5, 56.0, 56.5, 57.0, 57.5, 58.0, 58.5, 59.0, 59.5, 60.0]},
    {"cashout": 82.0,
     "stakes": [60.5, 61.0, 61.5, 62.0, 62.5, 63.0, 63.5, 64.0,
                64.5, 65.0, 65.5, 66.0, 66.5, 67.0, 67.5, 68.0, 68.5, 69.0, 69.5, 70.0]},
    {"cashout": 92.0,
     "stakes": [70.5, 71.0, 71.5, 72.0, 72.5, 73.0, 73.5, 74.0,
                74.5, 75.0, 75.5, 76.0, 76.5, 77.0, 77.5, 78.0, 78.5, 79.0, 79.5, 80.0]},
    {"cashout": 101.0,
     "stakes": [80.5, 81.0, 81.5, 82.0, 82.5, 83.0, 83.5, 84.0,
                84.5, 85.0, 85.5, 86.0, 86.5, 87.0, 87.5, 88.0, 88.5, 89.0, 89.5, 90.0]},
    {"cashout": 111.0,
     "stakes": [90.5, 91.0, 91.5, 92.0, 92.5, 93.0, 93.5, 94.0,
                94.5, 95.0, 95.5, 96.0, 96.5, 97.0, 97.5, 98.0, 98.5, 99.0, 99.5, 100.0]},
    {"cashout": 121.0,
     "stakes": [100.5, 101.0, 101.5, 102.0, 102.5, 103.0, 103.5, 104.0,
                104.5, 105.0, 105.5, 106.0, 106.5, 107.0, 107.5, 108.0, 108.5, 109.0, 109.5, 110.0]},
    {"cashout": 131.0,
     "stakes": [110.5, 111.0, 111.5, 112.0, 112.5, 113.0, 113.5, 114.0,
                114.5, 115.0, 115.5, 116.0, 116.5, 117.0, 117.5, 118.0, 118.5, 119.0, 119.5, 120.0]},
]

STOP_LOSS          = 10000     # Max session loss before pausing (UGX)


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  LOGGING                                                            ║
# ╚══════════════════════════════════════════════════════════════════════╝

def log(msg, level="INFO"):
    ts = datetime.now().strftime("%H:%M:%S.%f")[:-3]
    print(f"[{ts}] [{level}] {msg}")

def log_section(title):
    print()
    border = "=" * 60
    print(f"  {border}")
    print(f"  ||  {title:<54}  ||")
    print(f"  {border}")
    print()

def log_divider():
    print()
    log("─" * 56)


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  DASHBOARD STATUS WRITER                                            ║
# ╚══════════════════════════════════════════════════════════════════════╝

# Round history — last 100 rounds stored in memory
round_history = []
MAX_HISTORY = 200

# Per-account balance tracking: { "***8940": {"start": 1000, "current": 1200}, ... }
account_balances = {}

def write_status(strat, balance=None, cycle_balance=None,
                 session_start_balance=None, current_round=0,
                 last_result=None, last_mult=None, ws_connected=False,
                 bot_state="running", extra=None):
    """Write bot status to local JSON + POST to remote PHP dashboard."""
    try:
        data = {
            "timestamp": datetime.now().isoformat(),
            "bot_state": bot_state,  # running, paused, stopped, error
            "level": strat.level_idx + 1,
            "total_levels": len(LEVELS),
            "stake_index": strat.stake_idx + 1,
            "stakes_in_level": strat.stakes_in_level,
            "current_stake": strat.current_stake,
            "current_cashout": strat.current_cashout,
            "position": strat.position_str(),
            "total_bets": strat.total_bets,
            "total_wins": strat.total_wins,
            "total_losses": strat.total_losses,
            "session_profit": round(strat.session_profit, 2),
            "total_wagered": round(strat.total_wagered, 2),
            "highest_level": strat.highest_level + 1,
            "highest_stake": strat.highest_stake,
            "biggest_win_profit": round(strat.biggest_win_profit, 2),
            "balance": round(balance, 2) if balance else None,
            "cycle_balance": round(cycle_balance, 2) if cycle_balance else None,
            "session_start_balance": round(session_start_balance, 2) if session_start_balance else None,
            "current_round": current_round,
            "last_result": last_result,
            "last_mult": round(last_mult, 2) if last_mult else None,
            "ws_connected": ws_connected,
            "paused": strat.paused,
            "pause_reason": strat.pause_reason if strat.paused else None,
            "stop_loss": STOP_LOSS,
            "current_account": f"***{get_current_account()['mobile'][-4:]}",
            "account_balances": account_balances,
            "round_history": round_history[-50:],  # last 50 for the dashboard
        }
        if extra:
            data.update(extra)

        # 1. Local backup
        try:
            tmp = STATUS_FILE.with_suffix(".tmp")
            with open(tmp, "w") as f:
                json.dump(data, f, indent=2)
            tmp.replace(STATUS_FILE)
        except Exception:
            pass

        # 2. POST to remote PHP dashboard (non-blocking via thread)
        def _post():
            try:
                payload = json.dumps({"key": DASHBOARD_KEY, "data": data}).encode("utf-8")
                req = urllib.request.Request(
                    DASHBOARD_URL,
                    data=payload,
                    headers={"Content-Type": "application/json"},
                    method="POST"
                )
                urllib.request.urlopen(req, timeout=5)
            except Exception:
                pass  # Silent — don't let remote failures affect the bot
        threading.Thread(target=_post, daemon=True).start()

    except Exception:
        pass  # Don't let dashboard writes crash the bot


def add_round_to_history(round_num, result, stake, cashout_target,
                          actual_mult, profit, balance, level, stake_idx,
                          cashout_info=None):
    """Add a round to the history buffer."""
    entry = {
        "round": round_num,
        "time": datetime.now().strftime("%H:%M:%S"),
        "result": result,  # "win", "loss", "phantom", "error"
        "stake": stake,
        "cashout_target": cashout_target,
        "actual_mult": round(actual_mult, 2) if actual_mult else 0,
        "profit": round(profit, 2),
        "balance": round(balance, 2) if balance else None,
        "level": level + 1,
        "stake_index": stake_idx + 1,
        "cashout_button": cashout_info.get("button_clicked", False) if cashout_info else False,
        "cashout_refresh": cashout_info.get("refresh_done", False) if cashout_info else False,
    }
    round_history.append(entry)
    if len(round_history) > MAX_HISTORY:
        round_history.pop(0)


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  GAME STATE (thread-safe — WS callbacks fire during Playwright ops) ║
# ╚══════════════════════════════════════════════════════════════════════╝

class GameState:
    def __init__(self):
        self._lock = threading.Lock()
        self.multiplier = 0.0
        self.round_active = False
        self.bet_placed = False
        self.crashed = False
        self.betting_open = False
        self.round_start_time = None

        # WS health
        self.ws_connected = False
        self.ws_msg_count = 0
        self.ws_multiplier_working = False
        self.last_ws_time = time.time()

        # Crash tracking
        self.crash_count = 0
        self.last_crash_value = 0.0

    def reset_round(self):
        with self._lock:
            self.multiplier = 0.0
            self.round_active = False
            self.bet_placed = False
            self.crashed = False
            self.betting_open = False
            self.round_start_time = None

    def set_multiplier(self, val):
        with self._lock:
            self.multiplier = val

    def get_multiplier(self):
        with self._lock:
            return self.multiplier


state = GameState()


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  STRATEGY TRACKER                                                   ║
# ║  Tracks level/stake position, stats, highest values, pauses         ║
# ╚══════════════════════════════════════════════════════════════════════╝

STATE_SAVE_FILE = Path(__file__).parent / "cjavi_state.json"

class Strategy:
    def __init__(self):
        self.level_idx = 0
        self.stake_idx = 0
        self.total_bets = 0
        self.total_wins = 0
        self.total_losses = 0
        self.session_profit = 0.0
        self.total_wagered = 0.0
        self.highest_level = 0
        self.highest_stake = 0.0
        self.biggest_win_profit = 0.0
        self.paused = False
        self.pause_reason = ""
        self.cycle_balance = 0.0  # stored here for persistence

    def save_state(self):
        """Save strategy state to disk so bot can resume after restart."""
        try:
            data = {
                "level_idx": self.level_idx,
                "stake_idx": self.stake_idx,
                "total_bets": self.total_bets,
                "total_wins": self.total_wins,
                "total_losses": self.total_losses,
                "session_profit": self.session_profit,
                "total_wagered": self.total_wagered,
                "highest_level": self.highest_level,
                "highest_stake": self.highest_stake,
                "biggest_win_profit": self.biggest_win_profit,
                "cycle_balance": self.cycle_balance,
                "round_num": getattr(self, '_round_num', 0),
                "account_idx": current_account_idx,
                "account_balances": account_balances,
                "round_history": round_history[-50:],
                "saved_at": datetime.now().isoformat(),
            }
            tmp = STATE_SAVE_FILE.with_suffix(".tmp")
            with open(tmp, "w") as f:
                json.dump(data, f, indent=2)
            tmp.replace(STATE_SAVE_FILE)
            log(f"  State saved to {STATE_SAVE_FILE.name}")
        except Exception as e:
            log(f"  Failed to save state: {e}", "ERROR")

    def load_state(self):
        """Load strategy state from disk. Returns True if loaded."""
        try:
            if not STATE_SAVE_FILE.exists():
                return False
            with open(STATE_SAVE_FILE, "r") as f:
                data = json.load(f)
            self.level_idx = data.get("level_idx", 0)
            self.stake_idx = data.get("stake_idx", 0)
            self.total_bets = data.get("total_bets", 0)
            self.total_wins = data.get("total_wins", 0)
            self.total_losses = data.get("total_losses", 0)
            self.session_profit = data.get("session_profit", 0.0)
            self.total_wagered = data.get("total_wagered", 0.0)
            self.highest_level = data.get("highest_level", 0)
            self.highest_stake = data.get("highest_stake", 0.0)
            self.biggest_win_profit = data.get("biggest_win_profit", 0.0)
            self.cycle_balance = data.get("cycle_balance", 0.0)
            # Restore round history
            saved_history = data.get("round_history", [])
            if saved_history:
                round_history.clear()
                round_history.extend(saved_history)
            # Validate indices
            if self.level_idx >= len(LEVELS):
                self.level_idx = 0
                self.stake_idx = 0
            elif self.stake_idx >= len(LEVELS[self.level_idx]["stakes"]):
                self.stake_idx = 0
            self._round_num = data.get("round_num", 0)
            # Restore account index and balances
            global current_account_idx, account_balances
            current_account_idx = data.get("account_idx", 0) % len(ACCOUNTS)
            saved_bals = data.get("account_balances", {})
            if saved_bals:
                account_balances.update(saved_bals)
            self.paused = False
            self.pause_reason = ""
            acct = get_current_account()
            log(f"  Resumed from saved state: {self.position_str()}")
            log(f"  Account: ***{acct['mobile'][-4:]}")
            log(f"  Session: {self.total_wins}W / {self.total_losses}L  |  "
                f"P/L: {self.session_profit:+.1f} UGX")
            return True
        except Exception as e:
            log(f"  Failed to load state: {e}", "WARN")
            return False

    def clear_saved_state(self):
        """Delete saved state file (e.g. after clean session end)."""
        try:
            if STATE_SAVE_FILE.exists():
                STATE_SAVE_FILE.unlink()
        except Exception:
            pass

    @property
    def current_level(self):
        return LEVELS[self.level_idx]

    @property
    def current_stake(self):
        return self.current_level["stakes"][self.stake_idx]

    @property
    def current_cashout(self):
        return self.current_level["cashout"]

    @property
    def stakes_in_level(self):
        return len(self.current_level["stakes"])

    def record_win(self, stake, cashout_mult):
        """Record a verified win. Returns profit."""
        profit = stake * (cashout_mult - 1)
        self.session_profit += profit
        self.total_wagered += stake
        self.total_bets += 1
        self.total_wins += 1
        if stake > self.highest_stake:
            self.highest_stake = stake
        if profit > self.biggest_win_profit:
            self.biggest_win_profit = profit
        return profit

    def record_loss(self, stake):
        """Record a loss."""
        self.session_profit -= stake
        self.total_wagered += stake
        self.total_bets += 1
        self.total_losses += 1
        if stake > self.highest_stake:
            self.highest_stake = stake
        if self.level_idx > self.highest_level:
            self.highest_level = self.level_idx

    def advance_after_loss(self):
        """
        Move to next stake. If level exhausted, move to next level.
        Returns: "next_stake", "next_level", or "exhausted"
        """
        self.stake_idx += 1
        if self.stake_idx >= len(self.current_level["stakes"]):
            # Level exhausted — try next level
            if self.level_idx + 1 < len(LEVELS):
                self.level_idx += 1
                self.stake_idx = 0
                if self.level_idx > self.highest_level:
                    self.highest_level = self.level_idx
                return "next_level"
            else:
                return "exhausted"
        return "next_stake"

    def reset_after_win(self):
        """Reset to Level 1, Stake 1 after a verified win."""
        self.level_idx = 0
        self.stake_idx = 0

    def pause(self, reason):
        self.paused = True
        self.pause_reason = reason

    def position_str(self):
        """Short position string: L1/S3 etc."""
        return f"L{self.level_idx + 1}/S{self.stake_idx + 1}"

    def detail_lines(self):
        """Detailed status lines for logging."""
        level = self.current_level
        return [
            f"  Position     : Level {self.level_idx + 1}/{len(LEVELS)}  |  "
            f"Stake {self.stake_idx + 1}/{len(level['stakes'])}",
            f"  Current Bet  : {self.current_stake} UGX  |  "
            f"Cashout target: >= {self.current_cashout:.0f}x",
            f"  Session      : {self.total_wins}W / {self.total_losses}L  "
            f"({self.total_bets} bets)",
            f"  Profit       : {self.session_profit:+.1f} UGX  |  "
            f"Wagered: {self.total_wagered:.1f} UGX",
            f"  Highest      : Level {self.highest_level + 1}  |  "
            f"Stake {self.highest_stake:.1f} UGX  |  "
            f"Best win: +{self.biggest_win_profit:.1f} UGX",
        ]


strat = Strategy()


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  BINARY WEBSOCKET PROTOCOL DECODER                                  ║
# ╚══════════════════════════════════════════════════════════════════════╝

_PAT_X          = b'\x00\x01\x78\x07'
_PAT_CRASHX     = b'\x00\x06crashX\x07'
_PAT_STATE_ID   = b'\x00\x0anewStateId\x04'
_PAT_BALANCE    = b'\x00\x0anewBalance\x07'
_PAT_BET        = b'\x00\x03bet\x07'
_PAT_CODE       = b'\x00\x04code\x04'


def decode_game_binary(data):
    result = {}
    if not data or len(data) < 10:
        return result
    if data[0] != 0x80:
        return result

    idx = data.find(_PAT_X)
    if idx >= 0 and idx + 4 + 8 <= len(data):
        mult = struct.unpack('>d', data[idx + 4: idx + 12])[0]
        if 0.5 <= mult <= 100000.0:
            result['x'] = round(mult, 2)

    idx = data.find(_PAT_CRASHX)
    if idx >= 0 and idx + 9 + 8 <= len(data):
        crash = struct.unpack('>d', data[idx + 9: idx + 17])[0]
        if 1.0 <= crash <= 100000.0:
            result['crashX'] = round(crash, 2)

    idx = data.find(_PAT_STATE_ID)
    if idx >= 0 and idx + 12 + 4 <= len(data):
        state_id = struct.unpack('>I', data[idx + 12: idx + 16])[0]
        if state_id in (1, 2, 3):
            result['newStateId'] = state_id

    idx = data.find(_PAT_BALANCE)
    if idx >= 0 and idx + 12 + 8 <= len(data):
        bal = struct.unpack('>d', data[idx + 12: idx + 20])[0]
        result['newBalance'] = round(bal, 2)

    idx = data.find(_PAT_BET)
    if idx >= 0 and idx + 5 + 8 <= len(data):
        bet_amt = struct.unpack('>d', data[idx + 5: idx + 13])[0]
        result['bet'] = round(bet_amt, 2)

    return result


def process_decoded_binary(decoded):
    if not decoded:
        return

    state.last_ws_time = time.time()

    if 'x' in decoded:
        state.set_multiplier(decoded['x'])
        state.round_active = True
        state.ws_multiplier_working = True

    if 'crashX' in decoded:
        crash_val = decoded['crashX']
        state.crashed = True
        state.round_active = False
        state.betting_open = False
        state.crash_count += 1
        state.last_crash_value = crash_val
        log(f"  [WS] CRASH at {crash_val:.2f}x  (observed: {state.crash_count})")

    if 'newStateId' in decoded:
        sid = decoded['newStateId']
        if sid == 1:
            state.betting_open = True
            state.round_active = False
            state.set_multiplier(0.0)
        elif sid == 2:
            state.round_active = True
            state.betting_open = False
            state.round_start_time = time.time()


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  SOCKJS / STOMP PARSER                                              ║
# ╚══════════════════════════════════════════════════════════════════════╝

def unwrap_sockjs(raw):
    if not raw:
        return []
    if raw == "o" or raw == "h":
        return []
    if raw.startswith("a["):
        try:
            return [m for m in json.loads(raw[1:]) if isinstance(m, str)]
        except json.JSONDecodeError:
            return []
    return [] if raw.startswith("c[") else [raw]


def parse_stomp_frame(raw):
    if not raw or "\n" not in raw:
        return None
    parts = raw.split("\n\n", 1)
    if len(parts) < 2:
        return None
    lines = parts[0].split("\n")
    command = lines[0].strip()
    headers = {}
    for line in lines[1:]:
        if ":" in line:
            k, v = line.split(":", 1)
            headers[k.strip()] = v.strip()
    return command, headers, parts[1].rstrip("\x00")


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  WEBSOCKET INTERCEPTION                                             ║
# ╚══════════════════════════════════════════════════════════════════════╝

def _payload_to_string(payload):
    if isinstance(payload, str):
        return payload
    if isinstance(payload, bytes):
        try:
            return payload.decode("utf-8")
        except UnicodeDecodeError:
            return None
    if isinstance(payload, dict):
        inner = payload.get("payload", payload.get("data", ""))
        if isinstance(inner, str):
            return inner
        if isinstance(inner, bytes):
            try:
                return inner.decode("utf-8")
            except UnicodeDecodeError:
                return None
    return None


def setup_ws_interception(page):
    def on_ws(ws):
        url = ws.url
        is_game = "bluebox" in url.lower()
        tag = "GAME" if is_game else "OTHER"
        log(f"  WebSocket [{tag}] connected: {url[:100]}...")
        if is_game:
            state.ws_connected = True

        def on_received(payload):
            state.ws_msg_count += 1
            state.last_ws_time = time.time()

            raw_bytes = None
            if isinstance(payload, bytes):
                raw_bytes = payload
            elif isinstance(payload, dict):
                inner = payload.get("payload", payload.get("data", b""))
                if isinstance(inner, bytes):
                    raw_bytes = inner

            if raw_bytes is not None:
                if is_game:
                    decoded = decode_game_binary(raw_bytes)
                    if decoded:
                        process_decoded_binary(decoded)
                return

            raw = _payload_to_string(payload)
            if not raw or raw in ("o", "h"):
                return
            for msg in unwrap_sockjs(raw):
                parse_stomp_frame(msg)

        def on_close():
            log(f"  WebSocket [{tag}] DISCONNECTED", "WARN")
            if is_game:
                state.ws_connected = False

        ws.on("framereceived", on_received)
        ws.on("close", on_close)

    page.on("websocket", on_ws)
    log("WebSocket interception enabled")


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  HUMAN-LIKE HELPERS                                                 ║
# ╚══════════════════════════════════════════════════════════════════════╝

def human_delay(lo=0.15, hi=0.5):
    time.sleep(random.uniform(lo, hi))

def human_type(locator, text, lo_ms=50, hi_ms=140):
    locator.press_sequentially(text, delay=random.randint(lo_ms, hi_ms))


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  LOGIN                                                              ║
# ╚══════════════════════════════════════════════════════════════════════╝

# ── Account rotation tracker ──
current_account_idx = 0

def get_current_account():
    """Return the current account credentials."""
    return ACCOUNTS[current_account_idx % len(ACCOUNTS)]

def switch_account():
    """Switch to the next account in the rotation."""
    global current_account_idx
    current_account_idx = (current_account_idx + 1) % len(ACCOUNTS)
    acct = ACCOUNTS[current_account_idx]
    log(f"  >> Switched to account: ***{acct['mobile'][-4:]}")
    return acct


def logout(page):
    """Logout from betpawa by clearing cookies — 100% reliable."""
    log("  Logging out (clearing session)...")
    try:
        page.context.clear_cookies()
        page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=30000)
        human_delay(2, 3)
        log("  Logged out — session cleared.")
        return True
    except Exception as e:
        log(f"  Logout error: {e}", "WARN")
        try:
            page.context.clear_cookies()
        except Exception:
            pass
        return True  # cookies cleared = logged out regardless


def login(page, mobile=None, password=None):
    """Login to betpawa with given or current account credentials."""
    if mobile is None or password is None:
        acct = get_current_account()
        mobile = acct["mobile"]
        password = acct["password"]

    log(f"Logging in as ***{mobile[-4:]}...")
    page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
    try:
        page.wait_for_load_state("load", timeout=20000)
    except Exception:
        pass
    human_delay(2, 4)

    mobile_selectors = [
        "input[type='tel']",
        "input[type='text'][inputmode='tel']",
        "input[name='phone']", "input[name='mobile']",
        "input[placeholder*='phone' i]", "input[placeholder*='mobile' i]",
        "input[placeholder*='number' i]", "form input[type='text']",
    ]
    mobile_input = None
    for attempt in range(3):
        for sel in mobile_selectors:
            try:
                loc = page.locator(sel).first
                if loc.count() > 0 and loc.is_visible(timeout=3000):
                    mobile_input = loc
                    log(f"  Found mobile input: {sel}")
                    break
            except Exception:
                continue
        if mobile_input:
            break
        log(f"  Mobile input not found (attempt {attempt + 1}/3), retrying...", "WARN")
        human_delay(2, 4)
    if not mobile_input:
        raise RuntimeError("Could not find mobile/phone input on login page.")

    mobile_input.click(timeout=10000)
    human_delay(0.2, 0.5)
    mobile_input.fill("")
    human_type(mobile_input, mobile, 60, 150)
    human_delay(0.3, 0.7)

    pw_input = page.locator("input[type='password']").first
    pw_input.click(timeout=10000)
    human_delay(0.15, 0.4)
    pw_input.fill("")
    human_type(pw_input, password, 50, 130)
    human_delay(0.4, 0.9)

    pw_input.press("Enter")
    time.sleep(random.uniform(2.5, 4))

    if "/login" in page.url.lower():
        for sel in ["input[type='submit'][value='Log In']", "input[value='Log In']",
                    "button:has-text('Log In')", "button:has-text('Login')", "[type='submit']"]:
            try:
                btn = page.locator(sel).first
                if btn.count() > 0 and btn.is_visible(timeout=2000):
                    btn.click(timeout=5000)
                    break
            except Exception:
                continue
        time.sleep(random.uniform(2.5, 4))

    try:
        page.wait_for_selector("text=DEPOSIT", timeout=20000)
    except PlaywrightTimeout:
        try:
            page.wait_for_selector("text=Deposit", timeout=5000)
        except PlaywrightTimeout:
            page.wait_for_url(lambda u: "/login" not in u.lower(), timeout=15000)
    log(f"Login successful! (***{mobile[-4:]})")


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  NAVIGATE TO AVIATOR                                                ║
# ╚══════════════════════════════════════════════════════════════════════╝

IFRAME_SELECTORS = [
    "iframe[src*='spribegaming.com/aviator']",
    "iframe[src*='spribegaming.com']",
    "iframe[src*='aviator']",
    "iframe.game-iframe",
    "iframe[class*='game']",
    "iframe",
]


def switch_account_and_relogin(page):
    """
    Full account switch: logout → switch account → login → navigate to Aviator.
    Returns (success, new_frame).
    """
    log_section("SWITCHING ACCOUNT")

    # Reset WS state for fresh connection
    state.ws_connected = False
    state.ws_msg_count = 0
    state.crashed = False
    state.round_active = False
    state.betting_open = False

    # Logout
    logout(page)
    human_delay(1, 2)

    # Switch to next account
    new_acct = switch_account()

    # Login with new account
    try:
        login(page, mobile=new_acct["mobile"], password=new_acct["password"])
        human_delay(1, 2)
    except Exception as e:
        log(f"  Login to new account failed: {e}", "ERROR")
        return False, None

    # Navigate to Aviator
    try:
        frame = navigate_to_aviator(page)
    except Exception as e:
        log(f"  Navigation to Aviator failed: {e}", "ERROR")
        return False, None

    # Wait for WS to reconnect
    log("  Waiting for WebSocket on new account...")
    state.last_ws_time = time.time()
    t0 = time.time()
    while time.time() - t0 < 30:
        if state.ws_connected and state.ws_msg_count > 0:
            break
        page.wait_for_timeout(500)

    if not state.ws_connected:
        log("  WS not connected on new account — will retry", "WARN")

    # Wait for game to stabilize
    t0 = time.time()
    while time.time() - t0 < 30:
        if state.crashed or state.betting_open:
            break
        if wait_for_bet_button(frame, timeout=2):
            break
        page.wait_for_timeout(500)

    acct = get_current_account()
    log(f"  Account switch complete! Now on ***{acct['mobile'][-4:]}")
    return True, frame


def navigate_to_aviator(page):
    log("Navigating to Aviator game...")
    for attempt in range(3):
        if attempt > 0:
            log(f"  Retry {attempt + 1}/3 — reloading page...")
            page.reload(wait_until="domcontentloaded", timeout=30000)
        else:
            page.goto(AVIATOR_URL, wait_until="domcontentloaded", timeout=30000)
        try:
            page.wait_for_load_state("load", timeout=20000)
        except Exception:
            pass
        human_delay(4, 6)

        for sel in IFRAME_SELECTORS:
            try:
                iloc = page.locator(sel).first
                if iloc.count() > 0:
                    log(f"  Found iframe: {sel}")
                    iloc.wait_for(state="visible", timeout=20000)
                    frame = page.frame_locator(sel).first
                    try:
                        frame.locator("button.btn-success.bet").wait_for(
                            state="visible", timeout=15000)
                        log("Aviator game loaded and ready!")
                        return frame
                    except Exception:
                        log("  Iframe found but game not loaded — trying next...", "WARN")
            except Exception:
                continue

        log(f"  Game iframe not found (attempt {attempt + 1}/3)", "WARN")
        for btn_sel in ["button:has-text('Play')", "a:has-text('Play')",
                        "[class*='play']", "button:has-text('Launch')"]:
            try:
                btn = page.locator(btn_sel).first
                if btn.count() > 0 and btn.is_visible(timeout=2000):
                    log(f"  Clicking launch button: {btn_sel}")
                    btn.click(timeout=5000)
                    human_delay(3, 5)
                    break
            except Exception:
                continue

    raise RuntimeError("Could not load Aviator game after 3 attempts.")


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  BALANCE                                                            ║
# ╚══════════════════════════════════════════════════════════════════════╝

def get_balance(frame):
    try:
        text = frame.locator("span.balance-amount").first.text_content(timeout=3000)
        if text:
            cleaned = text.strip().replace(",", "").replace(" ", "")
            return float(re.sub(r"[^\d.]", "", cleaned) or "0")
    except Exception:
        pass
    return None


def get_balance_reliable(frame, page, retries=3, delay_ms=1500):
    for attempt in range(retries):
        if attempt > 0:
            page.wait_for_timeout(delay_ms)
        bal = get_balance(frame)
        if bal is not None and bal > 0:
            return bal
    return get_balance(frame)


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  PLACE BET                                                          ║
# ╚══════════════════════════════════════════════════════════════════════╝

def place_bet(frame, amount):
    try:
        bet_input = frame.locator("input[inputmode='decimal'][type='text']").first
        bet_input.wait_for(state="visible", timeout=8000)

        deadline = time.time() + 10
        while time.time() < deadline:
            try:
                if bet_input.is_enabled():
                    break
            except Exception:
                pass
            time.sleep(0.1)

        bet_input.click(timeout=5000)
        human_delay(0.05, 0.15)
        bet_input.fill("")

        # Handle decimal stakes (e.g., 1.5) and integer stakes (e.g., 10)
        if amount == int(amount):
            bet_input.fill(str(int(amount)))
        else:
            bet_input.fill(f"{amount:.1f}")

        human_delay(0.1, 0.2)

        bet_btn = frame.locator("button.btn-success.bet").first
        bet_btn.click(timeout=6000)

        state.bet_placed = True
        return True

    except Exception as e:
        log(f"  Bet placement failed: {e}", "ERROR")
        return False


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  CASHOUT (dual: click button + refresh)                             ║
# ╚══════════════════════════════════════════════════════════════════════╝

CASHOUT_SELECTORS = [
    "button.btn-warning.cashout",
    "button.cashout",
    "[class*='cashout'] button",
    "button[class*='cashout']",
]


def click_cashout_button(frame):
    for sel in CASHOUT_SELECTORS:
        try:
            btn = frame.locator(sel).first
            if btn.count() > 0 and btn.is_visible(timeout=300):
                btn.click(timeout=1000)
                return True
        except Exception:
            continue
    return False


def cashout(page, frame, multiplier_at_cashout):
    """Dual cashout: try button click + page refresh. Returns (new_frame, cashout_info)."""
    cashout_info = {"button_clicked": False, "refresh_done": False}

    clicked = click_cashout_button(frame)
    if clicked:
        log(f"  >>> CASHOUT BUTTON clicked at {multiplier_at_cashout:.2f}x")
        cashout_info["button_clicked"] = True
    else:
        log(f"  >>> Cashout button not found — using refresh only")

    log(f"  >>> REFRESHING PAGE at {multiplier_at_cashout:.2f}x — backup cashout")
    page.reload(wait_until="domcontentloaded", timeout=30000)
    cashout_info["refresh_done"] = True
    try:
        page.wait_for_load_state("load", timeout=15000)
    except Exception:
        pass
    human_delay(2, 4)

    page.locator("iframe[src*='spribegaming.com/aviator']").first.wait_for(
        state="visible", timeout=25000)
    new_frame = page.frame_locator("iframe[src*='spribegaming.com/aviator']").first
    new_frame.locator("button.btn-success.bet").wait_for(state="visible", timeout=20000)
    return new_frame, cashout_info


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  WAIT FOR BET BUTTON                                                ║
# ╚══════════════════════════════════════════════════════════════════════╝

def wait_for_bet_button(frame, timeout=120):
    start = time.time()
    while time.time() - start < timeout:
        try:
            btn = frame.locator("button.btn-success.bet").first
            if btn.count() > 0 and btn.is_visible(timeout=500):
                text = (btn.text_content(timeout=500) or "").strip().lower()
                if "bet" in text:
                    try:
                        if not btn.is_disabled(timeout=300):
                            return True
                    except Exception:
                        pass
        except Exception:
            pass
        time.sleep(0.3)
    return False


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  WAIT FOR CRASH                                                     ║
# ╚══════════════════════════════════════════════════════════════════════╝

def wait_for_crash(page, count=1):
    initial = state.crash_count
    target = initial + count
    seen = 0
    start = time.time()

    log(f"  Waiting for {count} crash before betting...")

    while state.crash_count < target:
        if not state.ws_connected:
            log("  WebSocket disconnected!", "WARN")
            return False
        if time.time() - state.last_ws_time > 60:
            log("  No WS data for 60s!", "WARN")
            return False
        if time.time() - start > 300:
            log("  Timeout waiting for crash (5 min)", "WARN")
            return False

        new_seen = state.crash_count - initial
        if new_seen > seen:
            seen = new_seen
            log(f"  Crash observed ({state.last_crash_value:.2f}x) — fresh round starting!")

        page.wait_for_timeout(200)

    return True


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  CHECK WEBSOCKET HEALTH + AUTO-RECOVERY                             ║
# ╚══════════════════════════════════════════════════════════════════════╝

MAX_WS_RECOVERY_ATTEMPTS = 3

def check_ws_health():
    """Returns True if WS is healthy. Does NOT pause on failure."""
    if not state.ws_connected:
        return False
    stale = time.time() - state.last_ws_time
    if stale > 45:
        return False
    return True


def recover_ws(page):
    """
    Attempt to recover a broken WebSocket by reloading the page
    and re-navigating to the Aviator game.
    Returns (success: bool, new_frame).
    """
    for attempt in range(1, MAX_WS_RECOVERY_ATTEMPTS + 1):
        log(f"  >> WS Recovery attempt {attempt}/{MAX_WS_RECOVERY_ATTEMPTS}...", "WARN")

        try:
            # Reset WS state
            state.ws_connected = False
            state.ws_msg_count = 0
            state.last_ws_time = time.time()
            state.crashed = False
            state.round_active = False
            state.betting_open = False

            # Reload page
            log(f"     Reloading page...")
            page.reload(wait_until="domcontentloaded", timeout=30000)
            page.wait_for_timeout(3000)

            # Re-find Aviator iframe
            log(f"     Looking for Aviator iframe...")
            frame = None
            IFRAME_SELECTORS = [
                "iframe[src*='spribegaming.com/aviator']",
                "iframe[src*='spribegaming.com']",
                "iframe[src*='aviator']",
                "iframe.game-iframe",
                "iframe",
            ]
            for sel in IFRAME_SELECTORS:
                try:
                    loc = page.locator(sel).first
                    if loc.is_visible(timeout=5000):
                        frame = page.frame_locator(sel).first
                        log(f"     Found iframe: {sel}")
                        break
                except Exception:
                    continue

            if not frame:
                log(f"     Iframe not found, retrying...", "WARN")
                page.wait_for_timeout(5000)
                continue

            # Wait for WS to reconnect
            log(f"     Waiting for WebSocket to reconnect...")
            t0 = time.time()
            while time.time() - t0 < 30:
                if state.ws_connected and state.ws_msg_count > 0:
                    break
                page.wait_for_timeout(500)

            if not state.ws_connected:
                log(f"     WS still disconnected after 30s", "WARN")
                continue

            # Wait for game to stabilize
            log(f"     WS reconnected! Waiting for game to stabilize...")
            t0 = time.time()
            while time.time() - t0 < 30:
                if state.crashed or state.betting_open:
                    break
                if wait_for_bet_button(frame, timeout=2):
                    break
                page.wait_for_timeout(500)

            log(f"  >> WS Recovery SUCCESSFUL! Resuming bot.", "INFO")
            return True, frame

        except Exception as e:
            log(f"     Recovery attempt {attempt} failed: {e}", "ERROR")
            page.wait_for_timeout(5000)

    log(f"  >> WS Recovery FAILED after {MAX_WS_RECOVERY_ATTEMPTS} attempts.", "ERROR")
    return False, None


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  MONITOR ROUND                                                      ║
# ║  Watches WS multiplier. Cashout target is dynamic per level.        ║
# ║  Returns: ("win", multiplier, new_frame)                            ║
# ║           ("loss", multiplier, frame)                                ║
# ║           ("error", 0.0, frame)                                     ║
# ╚══════════════════════════════════════════════════════════════════════╝

def monitor_round(page, frame, cashout_target):
    """Monitor round. Cashout at >= cashout_target."""

    # Phase 1: Wait for takeoff
    log("  Waiting for plane to take off...")
    t0 = time.time()
    started = False

    while time.time() - t0 < 60:
        if state.round_active:
            mult = state.get_multiplier()
            if mult >= 1.0:
                started = True
                log(f"  Plane flying! {mult:.2f}x  (target: {cashout_target:.0f}x)")
                break
        if state.crashed:
            log("  Plane crashed before takeoff!", "WARN")
            return ("loss", state.get_multiplier(), frame, None)
        page.wait_for_timeout(50)

    if not started:
        log("  Timeout waiting for takeoff (60s)", "WARN")
        return ("error", 0.0, frame, None)

    # Phase 2: Monitor multiplier
    log(f"  Monitoring... cashout target: >= {cashout_target:.0f}x")
    t0 = time.time()
    last_log = 0.0

    while True:
        current = state.get_multiplier()

        # CASHOUT
        if current >= cashout_target:
            new_frame, cashout_info = cashout(page, frame, current)
            return ("win", current, new_frame, cashout_info)

        # Log milestones — every 1x for high targets, more frequent near target
        step = 5.0 if cashout_target > 20 else 1.0
        if current >= 1.0 and current >= last_log + step:
            pct = (current / cashout_target) * 100
            log(f"    {current:.2f}x  ({pct:.0f}% of target)")
            last_log = current

        # Crash
        if state.crashed:
            return ("loss", current, frame, None)

        # Timeout
        if time.time() - t0 > 300:
            log("  Round timeout (5 min)", "WARN")
            return ("error", 0.0, frame, None)

        page.wait_for_timeout(20)


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  MAIN BOT LOOP                                                      ║
# ╚══════════════════════════════════════════════════════════════════════╝

def run():
    # ── Startup Banner ──
    total_stakes = sum(s for lvl in LEVELS for s in lvl["stakes"])
    print()
    print("  " + "=" * 60)
    print("  ||                                                        ||")
    print("  ||   cjavi — Aviator Multi-Level Bot                      ||")
    print("  ||                                                        ||")
    for i, lvl in enumerate(LEVELS):
        n = len(lvl["stakes"])
        lo, hi = lvl["stakes"][0], lvl["stakes"][-1]
        print(f"  ||   Level {i+1}: cashout >= {lvl['cashout']:.0f}x  |  "
              f"{n} stakes ({lo}–{hi} UGX){' ' * (10 - len(str(hi)))}||")
    print("  ||                                                        ||")
    print(f"  ||   On WIN  : reset to L1/S1, switch account             ||")
    print(f"  ||   On LOSS : next stake (level if exhausted)            ||")
    print(f"  ||   Stop-loss: {STOP_LOSS} UGX                                  ||")
    print(f"  ||   Total risk (all levels): {total_stakes:.1f} UGX                ||")
    print("  ||                                                        ||")
    print("  " + "=" * 60)
    print()

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        context = browser.new_context(
            viewport={"width": 1280, "height": 800}, locale="en-US")
        page = context.new_page()
        setup_ws_interception(page)

        try:
            # Login
            login(page)
            human_delay(1, 2)

            # Navigate
            frame = navigate_to_aviator(page)
            session_start_balance = get_balance(frame)
            if session_start_balance:
                log(f"Starting balance: {session_start_balance:.2f} UGX")

            # Record initial balance for this account
            acct_key = f"***{get_current_account()['mobile'][-4:]}"
            if acct_key not in account_balances:
                account_balances[acct_key] = {
                    "start": round(session_start_balance, 2) if session_start_balance else 0,
                    "current": round(session_start_balance, 2) if session_start_balance else 0,
                }
            else:
                # Update current balance on reconnect
                if session_start_balance:
                    account_balances[acct_key]["current"] = round(session_start_balance, 2)

            # Try to resume from saved state
            resumed = strat.load_state()
            if resumed:
                cycle_balance = strat.cycle_balance if strat.cycle_balance > 0 else (session_start_balance or 0)
                log(f"  Cycle balance (restored): {cycle_balance:.0f} UGX")
            else:
                # Fresh start — cycle_balance = current balance
                cycle_balance = session_start_balance or 0

            # Wait for WS
            log("Waiting for WebSocket connection...")
            page.wait_for_timeout(5000)
            if state.ws_connected:
                log(f"WebSocket connected! ({state.ws_msg_count} messages)")
            else:
                log("WebSocket not detected yet", "WARN")

            # Wait for stable state
            log("Waiting for game to stabilize...")
            t0 = time.time()
            while time.time() - t0 < 60:
                if state.crashed or state.betting_open:
                    break
                if wait_for_bet_button(frame, timeout=2):
                    break
                page.wait_for_timeout(500)

            log_section("GAME SESSION STARTED")

            # ═══════════════════════════════════════════
            #  MAIN LOOP
            # ═══════════════════════════════════════════
            round_num = getattr(strat, '_round_num', 0)

            while True:

                # ── Paused? (only for unrecoverable issues like stop-loss / exhausted) ──
                if strat.paused:
                    log_section("BOT PAUSED")
                    log(f"  REASON: {strat.pause_reason}", "PAUSE")
                    print()
                    for line in strat.detail_lines():
                        log(line)
                    bal = get_balance(frame)
                    if bal:
                        log(f"  Balance    : {bal:.0f} UGX")
                    print()
                    strat._round_num = round_num
                    strat.save_state()
                    log("Bot stopped. Will auto-restart via service.")
                    break

                # ── WS health — auto-recover if disconnected ──
                if not check_ws_health():
                    reason = "DISCONNECTED" if not state.ws_connected else "STALE"
                    log(f"  WebSocket {reason} — attempting auto-recovery...", "WARN")

                    # Save state before recovery attempt
                    strat.cycle_balance = cycle_balance
                    strat._round_num = round_num
                    strat.save_state()

                    # Update dashboard
                    write_status(
                        strat, balance=get_balance(frame),
                        cycle_balance=cycle_balance,
                        session_start_balance=session_start_balance,
                        current_round=round_num, ws_connected=False,
                        bot_state="running",
                        extra={"recovering": True}
                    )

                    success, new_frame = recover_ws(page)
                    if success and new_frame:
                        frame = new_frame
                        log(f"  Recovery complete — continuing from {strat.position_str()}")
                        continue
                    else:
                        # Recovery failed — exit and let systemd restart
                        log(f"  Recovery failed — saving state and exiting for restart.", "ERROR")
                        strat.cycle_balance = cycle_balance
                        strat._round_num = round_num
                        strat.save_state()
                        write_status(
                            strat, bot_state="error",
                            ws_connected=False,
                            extra={"error": "WS recovery failed, restarting..."}
                        )
                        sys.exit(1)  # systemd will restart us

                # ── Stop-loss ──
                if strat.session_profit <= -STOP_LOSS:
                    strat.pause(
                        f"STOP-LOSS triggered.\n"
                        f"         Session loss: {strat.session_profit:+.0f} UGX "
                        f"(limit: -{STOP_LOSS} UGX).")
                    continue

                round_num += 1
                current_stake = strat.current_stake
                current_cashout = strat.current_cashout

                # ─── Round Header ───
                log_divider()
                log(f"ROUND #{round_num}  [{strat.position_str()}]")
                log(f"")
                for line in strat.detail_lines():
                    log(line)
                print()

                # ── Step 1: Wait for betting phase (bet every round) ──
                state.reset_round()
                log(f"  Waiting for betting phase...")

                bet_ready = False
                t0 = time.time()
                while time.time() - t0 < 120:
                    if state.betting_open:
                        if wait_for_bet_button(frame, timeout=5):
                            bet_ready = True
                            break
                    if wait_for_bet_button(frame, timeout=2):
                        if not state.round_active or state.get_multiplier() < 1.0:
                            bet_ready = True
                            break
                        else:
                            page.wait_for_timeout(1000)
                            continue
                    page.wait_for_timeout(300)

                if not bet_ready:
                    log("  Timeout — refreshing page", "WARN")
                    page.reload(wait_until="domcontentloaded", timeout=30000)
                    human_delay(3, 5)
                    try:
                        page.locator("iframe[src*='spribegaming.com/aviator']").first.wait_for(
                            state="visible", timeout=20000)
                        frame = page.frame_locator(
                            "iframe[src*='spribegaming.com/aviator']").first
                        frame.locator("button.btn-success.bet").wait_for(
                            state="visible", timeout=15000)
                    except Exception:
                        log("  Reload failed", "ERROR")
                        page.wait_for_timeout(5000)
                    continue

                # ── Step 3: Place bet ──
                log(f"  Placing bet: {current_stake} UGX  "
                    f"[{strat.position_str()}]  "
                    f"(cashout target: {current_cashout:.0f}x)")
                human_delay(0.2, 0.5)

                if not place_bet(frame, current_stake):
                    log("  Bet failed — retrying", "ERROR")
                    page.wait_for_timeout(5000)
                    continue

                log(f"  BET PLACED: {current_stake} UGX")

                # Reset for monitoring
                state.crashed = False
                state.round_active = False
                state.set_multiplier(0.0)

                # ── Step 5: Monitor round ──
                result, mult, frame, cashout_info = monitor_round(page, frame, current_cashout)

                # ═══════════════════════════════════════
                #  PROCESS RESULT
                # ═══════════════════════════════════════
                print()

                if result == "win":
                    # Verify with balance — compare against CYCLE starting balance (L1/S1)
                    balance_after = get_balance_reliable(frame, page, retries=3, delay_ms=1500)
                    phantom = False

                    if cycle_balance > 0 and balance_after is not None:
                        log(f"  Balance check: cycle start {cycle_balance:.0f} → "
                            f"now {balance_after:.0f} UGX")
                        if balance_after <= cycle_balance:
                            phantom = True

                    if phantom:
                        log(f"  !!! PHANTOM CASHOUT !!!", "WARN")
                        log(f"  Refreshed at {mult:.2f}x but balance is at or "
                            f"below cycle start balance.")
                        log(f"  Cycle start: {cycle_balance:.0f} → "
                            f"Now: {balance_after:.0f} UGX")
                        log(f"  Recording as LOSS (-{current_stake} UGX)")

                        strat.record_loss(current_stake)
                        action = strat.advance_after_loss()

                        if action == "next_stake":
                            log(f"  >> Next stake: {strat.current_stake} UGX "
                                f"[{strat.position_str()}]")
                        elif action == "next_level":
                            print()
                            log(f"  >> Level exhausted! Moving to Level {strat.level_idx + 1} "
                                f"(cashout {strat.current_cashout:.0f}x, "
                                f"starting stake {strat.current_stake} UGX)")
                        elif action == "exhausted":
                            strat.pause(
                                f"All {len(LEVELS)} levels EXHAUSTED after phantom cashout.\n"
                                f"         Session profit: {strat.session_profit:+.0f} UGX.")

                    else:
                        # ── GENUINE WIN ──
                        profit = strat.record_win(current_stake, mult)
                        payout = current_stake * mult

                        log(f"  +++ WIN (VERIFIED) +++")
                        log(f"  Cashed out at {mult:.2f}x")
                        log(f"  Bet: {current_stake} UGX  -->  "
                            f"Payout: {payout:.1f} UGX  (+{profit:.1f} UGX)")
                        if balance_after:
                            log(f"  Balance: {balance_after:.0f} UGX "
                                f"(cycle started at {cycle_balance:.0f})")

                        # Reset to Level 1 / Stake 1
                        strat.reset_after_win()
                        print()
                        log(f"  >> WIN! Resetting to Level 1 / Stake 1 ({LEVELS[0]['stakes'][0]} UGX)")

                        # Save state before account switch
                        strat.save_state()

                        # Record final balance of current account before switching
                        acct_before = get_current_account()
                        acct_before_key = f"***{acct_before['mobile'][-4:]}"
                        if balance_after and acct_before_key in account_balances:
                            account_balances[acct_before_key]["current"] = round(balance_after, 2)

                        log(f"  >> Switching from {acct_before_key} to next account...")

                        write_status(
                            strat, balance=balance_after, cycle_balance=cycle_balance,
                            session_start_balance=session_start_balance,
                            current_round=round_num, last_result="win",
                            last_mult=mult, ws_connected=state.ws_connected,
                            bot_state="running",
                            extra={"switching_account": True}
                        )

                        success, new_frame = switch_account_and_relogin(page)
                        if success and new_frame:
                            frame = new_frame
                            # Read new account balance as new cycle balance
                            new_bal = get_balance(frame)
                            cycle_balance = new_bal or 0
                            strat.cycle_balance = cycle_balance
                            session_start_balance = new_bal or session_start_balance

                            # Record new account's balance
                            acct_now = get_current_account()
                            acct_now_key = f"***{acct_now['mobile'][-4:]}"
                            if acct_now_key not in account_balances:
                                account_balances[acct_now_key] = {
                                    "start": round(new_bal, 2) if new_bal else 0,
                                    "current": round(new_bal, 2) if new_bal else 0,
                                }
                            else:
                                if new_bal:
                                    account_balances[acct_now_key]["current"] = round(new_bal, 2)

                            log(f"  >> Now on {acct_now_key}  "
                                f"|  Balance: {cycle_balance:.0f} UGX")
                            strat.save_state()
                        else:
                            log(f"  >> Account switch failed — continuing on same account", "WARN")
                            # Re-read cycle balance on current account
                            cycle_balance = balance_after if balance_after else get_balance(frame) or 0
                            strat.cycle_balance = cycle_balance

                elif result == "loss":
                    strat.record_loss(current_stake)

                    log(f"  --- LOSS ---")
                    log(f"  Crashed at {mult:.2f}x  (needed {current_cashout:.0f}x)")
                    log(f"  Lost: {current_stake} UGX")

                    action = strat.advance_after_loss()

                    if action == "next_stake":
                        log(f"  >> Next stake: {strat.current_stake} UGX "
                            f"[{strat.position_str()}]")
                    elif action == "next_level":
                        print()
                        log(f"  >> Level {strat.level_idx} exhausted! "
                            f"Moving to Level {strat.level_idx + 1}")
                        log(f"     Cashout: {strat.current_cashout:.0f}x  |  "
                            f"Starting stake: {strat.current_stake} UGX")
                    elif action == "exhausted":
                        strat.pause(
                            f"All {len(LEVELS)} levels EXHAUSTED.\n"
                            f"         Lost at every stake across all levels.\n"
                            f"         Total wagered: {strat.total_wagered:.1f} UGX\n"
                            f"         Session profit: {strat.session_profit:+.0f} UGX.")

                elif result == "error":
                    strat.record_loss(current_stake)
                    log(f"  --- ERROR (counted as loss) ---")
                    log(f"  Lost: {current_stake} UGX")

                    action = strat.advance_after_loss()
                    if action == "next_stake":
                        log(f"  >> Next: {strat.current_stake} UGX [{strat.position_str()}]")
                    elif action == "next_level":
                        log(f"  >> Level exhausted → Level {strat.level_idx + 1}")
                    elif action == "exhausted":
                        strat.pause(
                            f"All levels exhausted.\n"
                            f"         Session: {strat.session_profit:+.0f} UGX")

                # ── Post-round summary ──
                print()
                bal = get_balance(frame)

                # Update current account balance tracker
                acct_key = f"***{get_current_account()['mobile'][-4:]}"
                if bal and acct_key in account_balances:
                    account_balances[acct_key]["current"] = round(bal, 2)
                log(f"  ┌─────────────────────────────────────────────────")
                log(f"  │ Balance      : {f'{bal:.0f}' if bal else '???'} UGX")
                log(f"  │ Session P/L  : {strat.session_profit:+.1f} UGX")
                log(f"  │ Record       : {strat.total_wins}W / {strat.total_losses}L "
                    f"({strat.total_bets} bets)")
                log(f"  │ Next bet     : {strat.current_stake} UGX "
                    f"[{strat.position_str()}] @ {strat.current_cashout:.0f}x")
                log(f"  │ Highest      : Level {strat.highest_level + 1}  |  "
                    f"Stake {strat.highest_stake:.1f} UGX  |  "
                    f"Best win: +{strat.biggest_win_profit:.1f}")
                log(f"  └─────────────────────────────────────────────────")

                # ── Dashboard update ──
                rnd_profit = 0
                if result == "win":
                    rnd_profit = current_stake * (mult - 1)
                elif result == "loss" or result == "error":
                    rnd_profit = -current_stake

                add_round_to_history(
                    round_num, result, current_stake, current_cashout,
                    mult, rnd_profit, bal,
                    strat.level_idx, strat.stake_idx,
                    cashout_info=cashout_info
                )
                write_status(
                    strat, balance=bal, cycle_balance=cycle_balance,
                    session_start_balance=session_start_balance,
                    current_round=round_num,
                    last_result=result, last_mult=mult,
                    ws_connected=state.ws_connected,
                    bot_state="paused" if strat.paused else "running"
                )

                human_delay(1, 2)

        except KeyboardInterrupt:
            log_section("STOPPED BY USER (Ctrl+C)")
            for line in strat.detail_lines():
                log(line)
            bal = get_balance(frame) if 'frame' in dir() else None
            if bal:
                log(f"  Final balance: {bal:.0f} UGX")
            strat.cycle_balance = cycle_balance if 'cycle_balance' in dir() else 0
            strat._round_num = round_num if 'round_num' in dir() else 0
            strat.save_state()
            write_status(strat, balance=bal, bot_state="stopped",
                         current_round=round_num if 'round_num' in dir() else 0,
                         ws_connected=False)
        except Exception as e:
            log_section("FATAL ERROR")
            log(f"  {e}", "ERROR")
            import traceback
            traceback.print_exc()
            print()
            for line in strat.detail_lines():
                log(line)
            strat.cycle_balance = cycle_balance if 'cycle_balance' in dir() else 0
            strat._round_num = round_num if 'round_num' in dir() else 0
            strat.save_state()
            write_status(strat, bot_state="error",
                         extra={"error": str(e)},
                         ws_connected=False)
            sys.exit(1)  # Let systemd restart us
        finally:
            try:
                browser.close()
            except Exception:
                pass
            print()
            log("Browser closed. Goodbye!")


if __name__ == "__main__":
    run()
