"""
Aviator bot (av1): Playwright + OpenCV + OCR.
Strategy: Wait for 2 consecutive crash multipliers < 1.5, then bet. Cash out when OCR sees multiplier ≥ 2.0 = win → refresh, first stake (10). Crash (FLEW AWAY) = loss → no refresh, next stake.
Writes status to bot_status.json for PHP dashboard.
"""
import json
import os
import re
import sqlite3
import sys
import time
import random
import warnings
from datetime import datetime, timezone

# Hide PyTorch pin_memory warning when no GPU (e.g. EasyOCR on CPU)
warnings.filterwarnings("ignore", message=".*pin_memory.*", category=UserWarning)

import numpy as np
import cv2
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout


def _human_delay(min_s=0.15, max_s=0.5):
    """Short random pause to mimic human reaction time."""
    time.sleep(random.uniform(min_s, max_s))


def _human_type(locator, text, delay_min_ms=50, delay_max_ms=140):
    """Type text character by character with random delay between keys (human-like)."""
    if not text:
        return
    locator.press_sequentially(text, delay=random.randint(delay_min_ms, delay_max_ms))


def _human_click(locator, timeout=10000):
    """Click like a human: scroll into view, short pause, click with press delay."""
    locator.scroll_into_view_if_needed(timeout=timeout)
    _human_delay(0.1, 0.35)
    locator.click(delay=random.randint(80, 220), timeout=timeout)


def _click_cash_out(frame, timeout=3000):
    """Click Cash Out immediately (delay=0) so we don't miss the window."""
    # Try primary selector, then fallbacks (site markup may vary)
    for selector in ["button.btn-warning.cashout", "button.cashout", "[class*='cashout']", "button:has-text('Cash Out')", "button:has-text('Cash out')"]:
        try:
            loc = frame.locator(selector).first
            if loc.count() > 0 and loc.is_visible(timeout=500):
                loc.click(delay=0, timeout=timeout)
                return
        except Exception as e:
            continue
    raise RuntimeError("Cash Out button not found or not clickable")


def _refresh_aviator_page(page):
    """Refresh the Aviator page and return the game frame. Use when Cash Out fails so we can continue to next stake."""
    page.reload(wait_until="domcontentloaded", timeout=30000)
    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=15000)
    return page.frame_locator("iframe[src*='spribegaming.com/aviator']").first


def _is_cash_out_unavailable_error(e: Exception) -> bool:
    """True if the error means Cash Out button was not found or not clickable (e.g. round already ended)."""
    msg = str(e).lower()
    return "not found" in msg and "clickable" in msg


def get_balance(frame) -> float | None:
    """Read current balance from game iframe (e.g. 507.32). Returns None if unreadable."""
    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 try_read_multiplier_from_dom(frame) -> float | None:
    """Try to read last crash multiplier from game iframe DOM (history/result elements). Returns None if not found."""
    # Common selectors used by Aviator/Spribe UIs for last result or history
    selectors = [
        "[class*='crash'][class*='value']",
        "[class*='multiplier'][class*='result']",
        "[data-crash]",
        ".last-multiplier",
        ".round-result",
        ".crash-value",
        "[class*='history'] [class*='multiplier']",
        ".game-history .multiplier",
        "span[class*='coefficient']",
    ]
    for sel in selectors:
        try:
            loc = frame.locator(sel).first
            if loc.count() > 0:
                text = loc.text_content(timeout=500)
                if text:
                    match = re.search(r"(\d+\.?\d*)\s*[xX]?", text.strip())
                    if match:
                        val = float(match.group(1))
                        if 1.0 <= val <= 1000.0:
                            return val
        except Exception:
            continue
    return None

# --- Config (use env vars in production) ---
MOBILE = os.environ.get("AVIATOR_MOBILE", "750918940")
PASSWORD = os.environ.get("AVIATOR_PASSWORD", "Favour@2016")
LOGIN_URL = "https://www.betpawa.ug/login"
AVIATOR_URL = "https://www.betpawa.ug/casino?gameId=aviator&filter=all"

# Strategy: wait for 2 consecutive crash multipliers < 1.5, then bet. Stakes (UGX): 10, 20, 40, 80, 160, 320, 640, 1280.
STAKES = [10, 20, 40, 80, 160, 320, 640, 1280]
CONSECUTIVE_LOW_NEEDED = 2   # place bet after this many consecutive crashes below threshold
MULT_THRESHOLD = 1.5         # "low" = crash multiplier < this
CASHOUT_AT_MULTIPLIER = 2.0  # cash out when OCR sees multiplier >= this (no timer)

# OCR poll interval; SETTLE_SEC = ignore FLEW AWAY for this long after bet (avoid false from previous round)
OCR_CHECK_INTERVAL = 0.1
SETTLE_SEC = 4.0
MAX_ROUND_WAIT_SEC = 90      # safety: exit round loop after this long

# Rounds database: store every round multiplier for future strategy analysis (source: network > dom > ocr)
ROUNDS_DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rounds.db")

# MySQL (phpMyAdmin): set env vars to save rounds to MySQL. Create database in phpMyAdmin first (e.g. "aviator2026").
MYSQL_HOST = os.environ.get("MYSQL_HOST", "localhost")
MYSQL_PORT = int(os.environ.get("MYSQL_PORT", "3306"))
MYSQL_USER = os.environ.get("MYSQL_USER", "root")
MYSQL_PASSWORD = os.environ.get("MYSQL_PASSWORD", "")  # XAMPP default: root with no password
MYSQL_DATABASE = os.environ.get("MYSQL_DATABASE", "aviator2026")
MYSQL_TABLE = os.environ.get("MYSQL_TABLE", "rounds")

def _mysql_configured():
    """True if MySQL is configured (user and database set)."""
    return bool(MYSQL_USER and MYSQL_DATABASE)

def init_rounds_db():
    """Create rounds table if not exists. Safe to call on every run."""
    conn = sqlite3.connect(ROUNDS_DB_PATH)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS rounds (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            multiplier REAL NOT NULL,
            source TEXT NOT NULL,
            created_at TEXT NOT NULL,
            crash_seconds REAL
        )
    """)
    conn.execute("CREATE INDEX IF NOT EXISTS idx_rounds_created_at ON rounds(created_at)")
    conn.execute("CREATE INDEX IF NOT EXISTS idx_rounds_source ON rounds(source)")
    try:
        conn.execute("ALTER TABLE rounds ADD COLUMN crash_seconds REAL")
    except sqlite3.OperationalError:
        pass  # column already exists (new DBs have it in CREATE TABLE)
    conn.commit()
    conn.close()
    print(f"Rounds DB (SQLite): {ROUNDS_DB_PATH}")

def init_mysql_rounds_db():
    """Create MySQL rounds table if not exists. Call when MySQL is configured."""
    if not _mysql_configured():
        return
    try:
        import pymysql
        conn = pymysql.connect(
            host=MYSQL_HOST,
            port=MYSQL_PORT,
            user=MYSQL_USER,
            password=MYSQL_PASSWORD,
            database=MYSQL_DATABASE,
        )
        with conn.cursor() as cur:
            cur.execute(f"""
                CREATE TABLE IF NOT EXISTS `{MYSQL_TABLE}` (
                    id INT AUTO_INCREMENT PRIMARY KEY,
                    multiplier DECIMAL(10,4) NOT NULL,
                    source VARCHAR(20) NOT NULL,
                    created_at VARCHAR(50) NOT NULL,
                    crash_seconds DECIMAL(10,2) NULL,
                    INDEX idx_created_at (created_at),
                    INDEX idx_source (source)
                )
            """)
        conn.commit()
        conn.close()
        print(f"Rounds DB (MySQL): {MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}.{MYSQL_TABLE}")
    except Exception as e:
        print(f"MySQL init error (rounds will still save to SQLite): {e}")

def save_round_to_mysql(multiplier: float, source: str, created_at: str, crash_seconds: float | None):
    """Insert one round into MySQL. Called from save_round_to_db when MySQL is configured."""
    if not _mysql_configured():
        return
    try:
        import pymysql
        conn = pymysql.connect(
            host=MYSQL_HOST,
            port=MYSQL_PORT,
            user=MYSQL_USER,
            password=MYSQL_PASSWORD,
            database=MYSQL_DATABASE,
        )
        with conn.cursor() as cur:
            cur.execute(
                f"INSERT INTO `{MYSQL_TABLE}` (multiplier, source, created_at, crash_seconds) VALUES (%s, %s, %s, %s)",
                (round(multiplier, 4), source, created_at, round(crash_seconds, 2) if crash_seconds is not None else None),
            )
        conn.commit()
        conn.close()
    except Exception as e:
        print(f"MySQL save round error: {e}")

def save_round_to_db(multiplier: float, source: str = "ocr", crash_seconds: float | None = None):
    """Save one round multiplier with date/time and optional time-to-crash (seconds). Saves to SQLite and to MySQL when configured."""
    if not (1.0 <= multiplier <= 1000.0):
        return
    created_at = datetime.now(timezone.utc).isoformat()
    mult_val = round(multiplier, 4)
    crash_val = round(crash_seconds, 2) if crash_seconds is not None else None
    # SQLite
    try:
        conn = sqlite3.connect(ROUNDS_DB_PATH)
        conn.execute(
            "INSERT INTO rounds (multiplier, source, created_at, crash_seconds) VALUES (?, ?, ?, ?)",
            (mult_val, source, created_at, crash_val),
        )
        conn.commit()
        conn.close()
    except Exception as e:
        print(f"SQLite save round error: {e}")
    # MySQL (phpMyAdmin)
    save_round_to_mysql(mult_val, source, created_at, crash_val)

# Dashboard: bot writes status here for dashboard.php
DASHBOARD_STATUS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bot_status.json")
_dashboard = {
    "last_updated": None,
    "round_num": 0,
    "phase": "analyzing",       # "analyzing" | "betting"
    "consecutive_low": 0,        # count of consecutive crash mults < 1.5
    "stake_index": 0,            # 0..7 for STAKES
    "current_stake": STAKES[0],
    "elapsed_sec": None,
    "last_multiplier": None,
    "total_wins": 0,
    "start_balance": None,
    "current_balance": None,
    "recovery_target_balance": None,  # balance before first-stake bet; reset to 10 only when current >= this
    "pause_betting": False,            # True when balance not recovered after last stake; pause until current >= recovery target
    "status": "Idle",
    "events": [],
}


def update_dashboard(message=None, **kwargs):
    """Update dashboard state and write to JSON for PHP dashboard."""
    global _dashboard
    allowed = ("round_num", "phase", "consecutive_low", "stake_index", "current_stake", "elapsed_sec", "last_multiplier", "total_wins", "start_balance", "current_balance", "recovery_target_balance", "pause_betting", "status")
    _dashboard.update({k: v for k, v in kwargs.items() if k in _dashboard or k in allowed})
    if message:
        _dashboard.setdefault("events", []).append({"t": datetime.now(timezone.utc).strftime("%H:%M:%S"), "msg": message})
        _dashboard["events"] = _dashboard["events"][-50:]
    _dashboard["last_updated"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    try:
        with open(DASHBOARD_STATUS_FILE, "w", encoding="utf-8") as f:
            json.dump(_dashboard, f, indent=2)
    except Exception:
        pass

# OCR target
FLEW_AWAY_TEXT = "FLEW AWAY!"

# OCR backend: "tesseract", "easyocr", or None (not set yet)
_ocr_backend = None
_easyocr_reader = None

# Tesseract path on Windows (install from https://github.com/UB-Mannheim/tesseract/wiki)
_tesseract_path = r"C:\Program Files\Tesseract-OCR\tesseract.exe" if os.name == "nt" else None


def _ocr_image_to_string(cv2_image_bgr) -> str:
    """Run OCR on an OpenCV BGR image; use Tesseract if available, else EasyOCR. Returns uppercase text."""
    global _ocr_backend, _easyocr_reader
    # Try Tesseract first (if we haven't already decided it's unavailable)
    if _ocr_backend != "easyocr":
        try:
            import pytesseract
            if _tesseract_path and os.path.isfile(_tesseract_path):
                pytesseract.pytesseract.tesseract_cmd = _tesseract_path
            gray = cv2.cvtColor(cv2_image_bgr, cv2.COLOR_BGR2GRAY)
            _, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
            text = pytesseract.image_to_string(thresh)
            _ocr_backend = "tesseract"
            return (text or "").upper()
        except Exception:
            _ocr_backend = "easyocr"
    # Fallback: EasyOCR (no system install needed)
    if _ocr_backend == "easyocr":
        try:
            import easyocr
            if _easyocr_reader is None:
                _easyocr_reader = easyocr.Reader(["en"], gpu=False, verbose=False)
            result = _easyocr_reader.readtext(cv2_image_bgr)
            text = " ".join([r[1] for r in result]).upper()
            return text
        except Exception as e:
            raise RuntimeError(
                "OCR failed. Install Tesseract (https://github.com/UB-Mannheim/tesseract/wiki) "
                "or run: pip install easyocr"
            ) from e
    return ""


def _check_ocr_available():
    """Verify OCR works at startup; exit with instructions if not."""
    global _ocr_backend
    try:
        import pytesseract
        if _tesseract_path and os.path.isfile(_tesseract_path):
            pytesseract.pytesseract.tesseract_cmd = _tesseract_path
        pytesseract.image_to_string(np.zeros((50, 50), dtype=np.uint8))
        _ocr_backend = "tesseract"
        print("OCR: using Tesseract")
        return
    except Exception:
        pass
    try:
        import easyocr
        _ = easyocr.Reader(["en"], gpu=False, verbose=False)
        _ocr_backend = "easyocr"
        print("OCR: using EasyOCR (no Tesseract install needed)")
        return
    except Exception:
        pass
    print(
        "ERROR: No OCR engine available. Either:\n"
        "  1. Install Tesseract: https://github.com/UB-Mannheim/tesseract/wiki\n"
        "  2. Or run: pip install easyocr\n"
        "Then run this script again.",
        file=sys.stderr,
    )
    sys.exit(1)


def _roi_from_screenshot(screenshot_bytes: bytes):
    """Decode screenshot and return center ROI (same as FLEW AWAY / multiplier area). Returns (roi BGR, None if fail)."""
    nparr = np.frombuffer(screenshot_bytes, np.uint8)
    img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
    if img is None:
        return None
    h, w = img.shape[:2]
    x1, x2 = int(w * 0.25), int(w * 0.75)
    y1, y2 = int(h * 0.28), int(h * 0.55)
    roi = img[y1:y2, x1:x2]
    max_side = 360
    rw, rh = roi.shape[1], roi.shape[0]
    if max(rw, rh) > max_side:
        scale = max_side / max(rw, rh)
        roi = cv2.resize(roi, (int(rw * scale), int(rh * scale)), interpolation=cv2.INTER_AREA)
    return roi


def detect_flew_away_cv(screenshot_bytes: bytes) -> bool:
    """Use OpenCV + OCR to detect 'FLEW AWAY!' in the game area screenshot."""
    roi = _roi_from_screenshot(screenshot_bytes)
    if roi is None:
        return False
    try:
        text = _ocr_image_to_string(roi)
    except RuntimeError:
        raise
    return FLEW_AWAY_TEXT.upper() in text or "FLEW AWAY" in text


def get_game_state(screenshot_bytes: bytes) -> tuple[bool, float | None]:
    """OCR game area: return (flew_away, multiplier). When flew_away, multiplier is crash value if parseable else None."""
    roi = _roi_from_screenshot(screenshot_bytes)
    if roi is None:
        return False, None
    try:
        text = _ocr_image_to_string(roi)
    except RuntimeError:
        return False, None
    # Parse multiplier e.g. "1.30x" or "1.3x" (crash value often shown with FLEW AWAY)
    match = re.search(r"(\d+\.?\d*)\s*[xX]", text)
    parsed = None
    if match:
        try:
            val = float(match.group(1))
            if 1.0 <= val <= 1000.0:
                parsed = val
        except ValueError:
            pass
    if FLEW_AWAY_TEXT.upper() in text or "FLEW AWAY" in text:
        return True, parsed  # crash: return parsed multiplier if available
    return False, parsed


def _extract_multiplier_from_response(response) -> float | None:
    """Try to extract crash/multiplier from API response body. Returns None if not found or not JSON."""
    try:
        url = response.url.lower()
        if "spribe" not in url and "aviator" not in url and "game" not in url and "api" not in url:
            return None
        ct = (response.headers.get("content-type") or "").lower()
        if "json" not in ct and "text" not in ct:
            return None
        body = response.body()
        if not body or len(body) > 100_000:
            return None
        data = json.loads(body.decode("utf-8", errors="ignore"))
        if not isinstance(data, dict):
            return None
        # Common keys used by game APIs for crash point / multiplier
        for key in ("crashPoint", "crash_point", "multiplier", "crash", "result", "coefficient", "point"):
            val = data.get(key)
            if val is not None:
                try:
                    n = float(val)
                    if 1.0 <= n <= 1000.0:
                        return n
                except (TypeError, ValueError):
                    pass
        # Nested: e.g. data.round.crashPoint
        for nest in ("round", "game", "result", "data"):
            sub = data.get(nest)
            if isinstance(sub, dict):
                for key in ("crashPoint", "crash_point", "multiplier", "crash"):
                    val = sub.get(key)
                    if val is not None:
                        try:
                            n = float(val)
                            if 1.0 <= n <= 1000.0:
                                return n
                        except (TypeError, ValueError):
                            pass
    except Exception:
        pass
    return None


def run_bot():
    _check_ocr_available()
    init_rounds_db()
    init_mysql_rounds_db()
    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()

        def on_response(response):
            mult = _extract_multiplier_from_response(response)
            if mult is not None:
                save_round_to_db(mult, "network")

        page.on("response", on_response)

        try:
            # --- Login ---
            page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
            try:
                page.wait_for_load_state("load", timeout=20000)
            except Exception:
                pass

            # Give the page time to render (JS/login form may load after domcontentloaded)
            _human_delay(1.5, 2.5)
            # Wait for login form: try multiple selectors for phone/mobile input (site markup may vary)
            mobile_selectors = [
                "input[type='tel']",
                "input[type='text'][inputmode='tel']",
                "input[name='phone']",
                "input[name='mobile']",
                "input[id*='phone']",
                "input[id*='mobile']",
                "input[placeholder*='phone']",
                "input[placeholder*='mobile']",
                "input[placeholder*='number']",
                "form input[type='text']",  # first text input in form
            ]
            mobile_input = None
            for sel in mobile_selectors:
                try:
                    loc = page.locator(sel).first
                    if loc.count() > 0 and loc.is_visible(timeout=2000):
                        mobile_input = loc
                        break
                except Exception:
                    continue
            if not mobile_input:
                raise RuntimeError("Could not find mobile/phone input. Check login page or selectors.")
            # Human-like: click field first, pause, then type with delays
            mobile_input.click(timeout=10000)
            _human_delay(0.2, 0.5)
            mobile_input.fill("")  # clear any placeholder
            _human_type(mobile_input, MOBILE, delay_min_ms=60, delay_max_ms=150)
            _human_delay(0.3, 0.7)

            # Password: click field, pause, clear then type with delays
            password_input = page.locator("input[type='password']").first
            password_input.click(timeout=10000)
            _human_delay(0.15, 0.4)
            password_input.fill("")
            _human_type(password_input, PASSWORD, delay_min_ms=50, delay_max_ms=130)
            _human_delay(0.4, 0.9)

            # Submit: press Enter (natural after password) or click Log In
            password_input.press("Enter")
            time.sleep(random.uniform(2.5, 4))

            # If still on login page, click Log In button (human-like)
            if "/login" in page.url.lower():
                login_clicked = False
                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):
                            _human_click(btn, timeout=5000)
                            login_clicked = True
                            break
                    except Exception:
                        continue
                if not login_clicked:
                    raise RuntimeError("Could not find or click Log In button.")
                time.sleep(random.uniform(2.5, 4))

            # Wait for login success: DEPOSIT text, or URL left /login, or balance visible
            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)
            print("Login successful.")
            update_dashboard(status="Logged in", message="Login successful.")
            _human_delay(1.2, 2.2)

            # --- Go directly to Aviator game (no need to find tab) ---
            page.goto(AVIATOR_URL, wait_until="domcontentloaded", timeout=30000)
            try:
                page.wait_for_load_state("load", timeout=15000)
            except Exception:
                pass
            _human_delay(4, 6)

            # --- Switch to game iframe ---
            iframe = page.locator("iframe[src*='spribegaming.com/aviator']").first
            iframe.wait_for(state="visible", timeout=15000)
            frame = page.frame_locator("iframe[src*='spribegaming.com/aviator']").first
            # Wait for Bet button to be present
            frame.locator("button.btn-success.bet").wait_for(state="visible", timeout=15000)
            print("Aviator game loaded.")

            # --- Strategy: wait for 5 consecutive crash multipliers < 1.5, then bet. Stakes: 10,20,40,...,1280. Win → analyze again; loss → next stake (or reset to 10). ---
            start_balance = get_balance(frame)
            if start_balance is not None:
                print(f"Start balance: {start_balance:.2f} UGX")
            stake_index = 0
            consecutive_low = 0
            round_num = 0
            ocr_error_count = 0
            POLL_MS = 0.15
            round_start_time = time.time()
            last_was_flew_away = False
            balance_before_first_stake_bet = None  # note balance before bet at first stake; only reset to 10 when current >= this
            pause_betting = False  # pause when balance not recovered after last stake; resume when current >= recovery target
            update_dashboard(start_balance=start_balance, phase="analyzing", consecutive_low=0, stake_index=0, current_stake=STAKES[0], pause_betting=False, status=f"Analyzing — waiting for {CONSECUTIVE_LOW_NEEDED} consecutive crashes < 1.5×", message=f"Start balance: {start_balance:.2f} UGX" if start_balance else "Game loaded")

            while True:
                # --- Phase: Analyzing — wait for 2 consecutive crash multipliers < 1.5 ---
                while consecutive_low < CONSECUTIVE_LOW_NEEDED:
                    try:
                        box = page.locator("iframe[src*='spribegaming.com/aviator']").first.bounding_box()
                        if box:
                            flew_away, crash_mult = get_game_state(page.screenshot(clip=box))
                            if flew_away:
                                # Time from round start to crash (seconds)
                                crash_sec = (time.time() - round_start_time) if not last_was_flew_away else None
                                if not last_was_flew_away:
                                    last_was_flew_away = True
                                # Prefer DOM over OCR (more accurate); save one row per round to DB with date/time and crash_seconds
                                mult_to_use = crash_mult
                                dom_mult = try_read_multiplier_from_dom(frame)
                                if dom_mult is not None:
                                    mult_to_use = dom_mult
                                    save_round_to_db(dom_mult, "dom", crash_seconds=crash_sec)
                                elif crash_mult is not None:
                                    save_round_to_db(crash_mult, "ocr", crash_seconds=crash_sec)
                                if mult_to_use is not None:
                                    if mult_to_use < MULT_THRESHOLD:
                                        consecutive_low += 1
                                        print(f"Crash {mult_to_use:.2f}× (< {MULT_THRESHOLD}) — consecutive low: {consecutive_low}/{CONSECUTIVE_LOW_NEEDED}")
                                        update_dashboard(phase="analyzing", consecutive_low=consecutive_low, last_multiplier=mult_to_use, status=f"Analyzing — {consecutive_low}/{CONSECUTIVE_LOW_NEEDED} consecutive < {MULT_THRESHOLD}×", message=f"Crash {mult_to_use:.2f}× — {consecutive_low}/{CONSECUTIVE_LOW_NEEDED} low")
                                    else:
                                        consecutive_low = 0
                                        print(f"Crash {mult_to_use:.2f}× (≥ {MULT_THRESHOLD}) — reset count to 0")
                                        update_dashboard(phase="analyzing", consecutive_low=0, last_multiplier=mult_to_use, status=f"Analyzing — reset (crash {mult_to_use:.2f}× ≥ {MULT_THRESHOLD}×)", message=f"Crash {mult_to_use:.2f}× — reset")
                                else:
                                    # FLEW AWAY but couldn't read multiplier — don't change count (avoid false resets)
                                    pass
                                time.sleep(2.5)  # wait for next round to start so we don't re-read same crash
                            else:
                                # Not FLEW AWAY — new round in progress; next crash duration will be from now
                                if last_was_flew_away:
                                    round_start_time = time.time()
                                    last_was_flew_away = False
                    except RuntimeError:
                        raise
                    except Exception as e:
                        ocr_error_count += 1
                        if ocr_error_count <= 1 or ocr_error_count % 30 == 0:
                            print(f"OCR error: {e}")
                    time.sleep(POLL_MS)

                # If paused (balance not recovered after last stake), wait until current balance >= recovery target
                just_unpaused = False
                while pause_betting and balance_before_first_stake_bet is not None:
                    b = get_balance(frame)
                    update_dashboard(phase="analyzing", current_balance=b, status=f"Paused — balance not recovered. Current {(b or 0):.2f} < {balance_before_first_stake_bet:.2f} UGX. Resume when balance ≥ {balance_before_first_stake_bet:.2f}", message=f"Paused — current {(b or 0):.2f}, need ≥ {balance_before_first_stake_bet:.2f}")
                    if b is not None and b >= balance_before_first_stake_bet:
                        pause_betting = False
                        stake_index = 0
                        consecutive_low = 0
                        just_unpaused = True
                        update_dashboard(pause_betting=False, stake_index=0, current_stake=STAKES[0], consecutive_low=0, status="Resumed — balance recovered, first stake 10 UGX", message="Balance recovered — resuming at 10 UGX")
                        print("Balance recovered — resuming at first stake 10 UGX")
                        break
                    time.sleep(2)

                # After unpause go back to analyzing (need 2 consecutive low again); if still paused skip placing bet
                if just_unpaused or pause_betting:
                    continue
                round_num += 1
                stake = STAKES[stake_index]
                print(f"\n--- Bet #{round_num}: stake {stake} UGX (level {stake_index + 1}/8), cash out at ≥{CASHOUT_AT_MULTIPLIER}× (OCR) ---")
                update_dashboard(round_num=round_num, phase="betting", consecutive_low=consecutive_low, stake_index=stake_index, current_stake=stake, status=f"Placing bet {stake} UGX", message=f"{CONSECUTIVE_LOW_NEEDED} consecutive < {MULT_THRESHOLD}× — bet {stake}, cash out at ≥{CASHOUT_AT_MULTIPLIER}×")

                # Wait for FLEW AWAY so we're between rounds, then place bet
                while True:
                    try:
                        box = page.locator("iframe[src*='spribegaming.com/aviator']").first.bounding_box()
                        if box and detect_flew_away_cv(page.screenshot(clip=box)):
                            break
                    except Exception:
                        pass
                    time.sleep(POLL_MS)

                bet_input = frame.locator("input[inputmode='decimal'][type='text']").first
                bet_input.wait_for(state="visible", timeout=8000)
                deadline = time.time() + 12
                while time.time() < deadline:
                    try:
                        if bet_input.is_enabled():
                            break
                    except Exception:
                        pass
                    time.sleep(0.1)
                else:
                    print("Bet input not ready. Retrying round.")
                    consecutive_low = 0
                    update_dashboard(phase="analyzing", consecutive_low=0, status="Analyzing — bet input not ready, retry")
                    continue

                # Note balance before bet at first stake (10); only reset to 10 when current balance >= this
                if stake_index == 0:
                    b = get_balance(frame)
                    if b is not None:
                        balance_before_first_stake_bet = b
                        update_dashboard(recovery_target_balance=b)
                        print(f"Balance before first-stake bet: {b:.2f} UGX (reset to 10 only when current >= this)")

                try:
                    bet_input.click(timeout=8000)
                    time.sleep(random.uniform(0.05, 0.15))
                    bet_input.fill("")
                    bet_input.fill(str(int(stake)) if stake == int(stake) else str(stake))
                    time.sleep(random.uniform(0.1, 0.25))
                    frame.locator("button.btn-success.bet").first.click(timeout=6000)
                    print(f"Bet {stake} UGX placed. OCR: cash out when ≥{CASHOUT_AT_MULTIPLIER}×...")
                    update_dashboard(status="Bet placed", message=f"Bet {stake} UGX placed, cash out at ≥{CASHOUT_AT_MULTIPLIER}× (OCR)")
                except PlaywrightTimeout as e:
                    print(f"Bet failed: {e}. Retry.")
                    consecutive_low = 0
                    update_dashboard(phase="analyzing", consecutive_low=0, status="Analyzing — bet failed, retry")
                    continue

                # In-round: OCR only — cash out when multiplier >= 2.0 (win → refresh, first stake); crash (FLEW AWAY) → no refresh, next stake
                round_start = time.time()
                update_dashboard(status="Waiting for multiplier ≥ 2.0 (OCR)", message="OCR: waiting for ≥ 2.0×")
                cashed_out = False
                last_logged_mult = -1.0
                while time.time() - round_start < MAX_ROUND_WAIT_SEC:
                    try:
                        box = page.locator("iframe[src*='spribegaming.com/aviator']").first.bounding_box()
                        if not box:
                            time.sleep(OCR_CHECK_INTERVAL)
                            continue
                        screenshot_bytes = page.screenshot(clip=box)
                        flew_away, mult = get_game_state(screenshot_bytes)
                        if flew_away:
                            print("Crashed (FLEW AWAY) — no refresh, next stake.")
                            update_dashboard(message="Crashed — no refresh, next stake", last_multiplier=None)
                            break
                        if mult is not None:
                            if mult >= CASHOUT_AT_MULTIPLIER:
                                try:
                                    _click_cash_out(frame)
                                    print(f"Multiplier {mult:.2f}× ≥ {CASHOUT_AT_MULTIPLIER} — cash out, win.")
                                    update_dashboard(status="Cashed out", message=f"Win at {mult:.2f}× — refreshing, first stake 10 UGX")
                                    frame = _refresh_aviator_page(page)
                                    cashed_out = True
                                except Exception as e:
                                    if _is_cash_out_unavailable_error(e):
                                        print("Cash out failed (round ended) — no refresh, next stake.")
                                        update_dashboard(message="Cash out failed — no refresh, next stake")
                                    else:
                                        print(f"Cash out failed: {e}")
                                break
                            update_dashboard(last_multiplier=mult)
                            if mult >= last_logged_mult + 0.5:
                                print(f"  {mult:.2f}×")
                                update_dashboard(message=f"{mult:.2f}×")
                                last_logged_mult = mult
                    except RuntimeError:
                        raise
                    except Exception as e:
                        pass
                    time.sleep(OCR_CHECK_INTERVAL)
                else:
                    print("Round timeout — no refresh, next stake.")
                    update_dashboard(message="Round timeout — next stake")

                # Win: reset to first stake only when balance recovered; else pause. Loss: always next stake; if last stake lost and not recovered, pause
                consecutive_low = 0
                current_balance = get_balance(frame)

                def _can_reset_to_first_stake():
                    if balance_before_first_stake_bet is None:
                        return True
                    if current_balance is None:
                        return True
                    return current_balance >= balance_before_first_stake_bet

                if cashed_out:
                    total_wins = _dashboard.get("total_wins", 0) + 1
                    if _can_reset_to_first_stake():
                        stake_index = 0
                        update_dashboard(phase="analyzing", consecutive_low=0, stake_index=0, current_stake=STAKES[0], current_balance=current_balance, total_wins=total_wins, pause_betting=False, status="Win — refreshed, first stake 10 UGX", message=f"Win — next stake 10 UGX (total wins: {total_wins})")
                        print("Win — refreshed, first stake 10 UGX")
                    else:
                        pause_betting = True
                        update_dashboard(phase="analyzing", consecutive_low=0, current_balance=current_balance, total_wins=total_wins, pause_betting=True, status=f"Paused — balance not recovered after win. Current {(current_balance or 0):.2f} < {balance_before_first_stake_bet:.2f} UGX. Resume when balance ≥ {balance_before_first_stake_bet:.2f}", message=f"Paused — current {(current_balance or 0):.2f} < {balance_before_first_stake_bet:.2f}")
                        print(f"Paused — balance {(current_balance or 0):.2f} < {balance_before_first_stake_bet:.2f}. Resume when balance ≥ {balance_before_first_stake_bet:.2f}")
                else:
                    # Loss: always go to next stake
                    next_index = stake_index + 1
                    if next_index >= len(STAKES):
                        # Last stake (1280) lost — would reset to 10; only reset if balance recovered, else pause
                        if _can_reset_to_first_stake():
                            next_index = 0
                            stake_index = next_index
                            next_stake = STAKES[stake_index]
                            update_dashboard(phase="analyzing", consecutive_low=0, stake_index=stake_index, current_stake=next_stake, current_balance=current_balance, pause_betting=False, status=f"Loss at max stake — reset to 10 UGX (balance {(current_balance or 0):.2f} ≥ {(balance_before_first_stake_bet or 0):.2f})", message=f"Loss — next stake {next_stake} UGX")
                            print(f"Loss at max stake — reset to 10 UGX. Next stake {next_stake} UGX")
                        else:
                            pause_betting = True
                            update_dashboard(phase="analyzing", consecutive_low=0, current_balance=current_balance, pause_betting=True, status=f"Paused — balance not recovered after last stake (1280) loss. Current {(current_balance or 0):.2f} < {balance_before_first_stake_bet:.2f} UGX. Resume when balance ≥ {balance_before_first_stake_bet:.2f}", message=f"Paused — last stake lost, current {(current_balance or 0):.2f} < {balance_before_first_stake_bet:.2f}")
                            print(f"Paused — last stake (1280) lost. Balance {(current_balance or 0):.2f} < {balance_before_first_stake_bet:.2f}. Resume when balance ≥ {balance_before_first_stake_bet:.2f}")
                    else:
                        stake_index = next_index
                        next_stake = STAKES[stake_index]
                        update_dashboard(phase="analyzing", consecutive_low=0, stake_index=stake_index, current_stake=next_stake, current_balance=current_balance, status=f"Loss — no refresh, next stake {next_stake} UGX", message=f"Loss — next stake {next_stake} UGX")
                        print(f"Loss — no refresh, next stake {next_stake} UGX")
                time.sleep(random.uniform(0.5, 1.0))

        except KeyboardInterrupt:
            print("\nStopped by user.")
        finally:
            browser.close()


if __name__ == "__main__":
    run_bot()
