"""
avimon — Aviator Multiplier Monitor Bot

Logs into betpawa.ug, opens Aviator, and records every round's
crash multiplier to a remote MySQL database via API.

This bot does NOT place any bets — it only observes and records data.

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

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


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

MOBILE      = "0785559778"
PASSWORD    = "Favour@2016"
LOGIN_URL   = "https://www.betpawa.ug/login"
AVIATOR_URL = "https://www.betpawa.ug/casino?gameId=aviator&filter=all"

API_URL     = "https://www.linkiefy.com/api.php"
API_KEY     = "avimon_2026"

# How many rounds to buffer before sending a batch to the API
BATCH_SIZE  = 5
# Max time (seconds) between API flushes even if batch not full
FLUSH_INTERVAL = 60


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

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


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  GAME STATE                                                         ║
# ╚══════════════════════════════════════════════════════════════════════╝

class MonitorState:
    def __init__(self):
        self._lock = threading.Lock()
        self.multiplier = 0.0
        self.round_active = False
        self.crashed = False
        self.betting_open = False
        self.ws_connected = False
        self.ws_msg_count = 0
        self.last_ws_time = time.time()
        self.crash_count = 0
        self.last_crash_value = 0.0
        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 = MonitorState()


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  ROUND BUFFER & API SENDER                                         ║
# ╚══════════════════════════════════════════════════════════════════════╝

round_buffer = []
buffer_lock = threading.Lock()
total_sent = 0
total_errors = 0
last_flush_time = time.time()


def add_round(multiplier, duration_ms=None):
    """Add a completed round to the buffer."""
    global last_flush_time
    entry = {
        "round_id": f"r_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}_{state.crash_count}",
        "multiplier": round(multiplier, 2),
        "duration_ms": duration_ms,
        "timestamp": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"),
    }
    with buffer_lock:
        round_buffer.append(entry)
        buf_size = len(round_buffer)

    # Flush if batch full or time exceeded
    now = time.time()
    if buf_size >= BATCH_SIZE or (now - last_flush_time) > FLUSH_INTERVAL:
        flush_buffer()
        last_flush_time = now


def flush_buffer():
    """Send buffered rounds to the API."""
    global total_sent, total_errors
    with buffer_lock:
        if not round_buffer:
            return
        batch = list(round_buffer)
        round_buffer.clear()

    def _send():
        global total_sent, total_errors
        try:
            payload = json.dumps({
                "key": API_KEY,
                "rounds": batch
            }).encode("utf-8")
            req = urllib.request.Request(
                API_URL,
                data=payload,
                headers={"Content-Type": "application/json"},
                method="POST"
            )
            resp = urllib.request.urlopen(req, timeout=10)
            result = json.loads(resp.read().decode())
            inserted = result.get("inserted", 0)
            total_db = result.get("total_rounds", "?")
            total_sent += inserted
            log(f"  [API] Sent {len(batch)} rounds → {inserted} inserted  "
                f"(DB total: {total_db}  |  Session sent: {total_sent})")
        except Exception as e:
            total_errors += 1
            log(f"  [API] Send failed: {e}", "ERROR")
            # Put back in buffer for retry
            with buffer_lock:
                round_buffer.extend(batch)

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


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

_PAT_X        = b'\x00\x01\x78\x07'
_PAT_CRASHX   = b'\x00\x06crashX\x07'
_PAT_STATE_ID = b'\x00\x0anewStateId\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

    return result


def process_decoded(decoded):
    """Process decoded WS data — track state and record crashes."""
    if not decoded:
        return

    state.last_ws_time = time.time()

    # Detect round start: first multiplier after a crash/betting phase
    if 'x' in decoded:
        was_active = state.round_active
        state.set_multiplier(decoded['x'])
        state.round_active = True
        state.crashed = False
        # If round just started (wasn't active before), mark the start time
        if not was_active and state.round_start_time is None:
            state.round_start_time = time.time()
            log(f"  Round started (plane flying)")

    if 'crashX' in decoded:
        crash_val = decoded['crashX']
        duration = None
        if state.round_start_time:
            duration = int((time.time() - state.round_start_time) * 1000)

        state.crashed = True
        state.round_active = False
        state.betting_open = False
        state.crash_count += 1
        state.last_crash_value = crash_val
        state.round_start_time = None  # Reset for next round

        log(f"  CRASH #{state.crash_count}: {crash_val:.2f}x"
            f"  ({duration}ms)" if duration else
            f"  CRASH #{state.crash_count}: {crash_val:.2f}x")

        # Record this round
        add_round(crash_val, duration)

    if 'newStateId' in decoded:
        sid = decoded['newStateId']
        if sid == 1:
            # Betting phase — reset for next round
            state.betting_open = True
            state.round_active = False
            state.set_multiplier(0.0)
            state.round_start_time = None  # Clear so next x triggers it
        elif sid == 2:
            # Round starting (plane taking off)
            state.round_active = True
            state.betting_open = False
            if state.round_start_time is None:
                state.round_start_time = time.time()


# ╔══════════════════════════════════════════════════════════════════════╗
# ║  SOCKJS UNWRAPPER                                                   ║
# ╚══════════════════════════════════════════════════════════════════════╝

def _payload_to_string(payload):
    if isinstance(payload, str):
        return payload
    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", errors="replace")
            except Exception:
                return None
    if isinstance(payload, bytes):
        try:
            return payload.decode("utf-8", errors="replace")
        except Exception:
            return None
    return None


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

def setup_ws(page):
    def on_ws(ws):
        url = ws.url
        is_game = "bluebox" in url.lower()
        tag = "GAME" if is_game else "OTHER"
        log(f"  WS [{tag}] connected: {url[:80]}...")
        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(decoded)
                return

        def on_close():
            log(f"  WS [{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")


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

def login(page):
    log(f"Logging in as {MOBILE}...")
    page.goto(LOGIN_URL, wait_until="domcontentloaded", timeout=60000)
    try:
        page.wait_for_load_state("load", timeout=20000)
    except Exception:
        pass
    time.sleep(random.uniform(2, 4))

    mobile_selectors = [
        "input[type='tel']",
        "input[type='text'][inputmode='tel']",
        "input[name='phone']", "input[name='mobile']",
        "input[placeholder*='phone' 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
                    break
            except Exception:
                continue
        if mobile_input:
            break
        time.sleep(random.uniform(2, 3))

    if not mobile_input:
        raise RuntimeError("Could not find mobile input.")

    mobile_input.click(timeout=10000)
    mobile_input.fill("")
    mobile_input.press_sequentially(MOBILE, delay=random.randint(50, 120))
    time.sleep(random.uniform(0.3, 0.7))

    pw_input = page.locator("input[type='password']").first
    pw_input.click(timeout=10000)
    pw_input.fill("")
    pw_input.press_sequentially(PASSWORD, delay=random.randint(50, 120))
    time.sleep(random.uniform(0.4, 0.8))

    pw_input.press("Enter")
    time.sleep(random.uniform(3, 5))

    if "/login" in page.url.lower():
        for sel 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(3, 5))

    try:
        page.wait_for_selector("text=DEPOSIT", timeout=20000)
    except PlaywrightTimeout:
        try:
            page.wait_for_url(lambda u: "/login" not in u.lower(), timeout=15000)
        except Exception:
            pass
    log("Login successful!")


def navigate_to_aviator(page):
    log("Navigating to Aviator...")
    page.goto(AVIATOR_URL, wait_until="domcontentloaded", timeout=60000)
    time.sleep(random.uniform(3, 5))

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

    for attempt in range(3):
        for sel in IFRAME_SELECTORS:
            try:
                loc = page.locator(sel).first
                if loc.is_visible(timeout=10000):
                    log(f"  Found game iframe: {sel}")
                    frame = page.frame_locator(sel).first
                    # Verify game loaded
                    frame.locator("button.btn-success.bet").wait_for(
                        state="visible", timeout=20000)
                    log("  Game loaded!")
                    return frame
            except Exception:
                continue

        log(f"  Iframe not found (attempt {attempt + 1}/3), reloading...", "WARN")
        page.reload(wait_until="domcontentloaded", timeout=30000)
        time.sleep(random.uniform(3, 5))

    raise RuntimeError("Could not find Aviator game iframe.")


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

def run():
    print()
    print("  " + "=" * 52)
    print("  ||                                              ||")
    print("  ||   avimon — Aviator Multiplier Monitor        ||")
    print("  ||                                              ||")
    print("  ||   Records every round crash multiplier       ||")
    print("  ||   to MySQL via API. No bets placed.          ||")
    print("  ||                                              ||")
    print(f"  ||   API: {API_URL:<39}  ||")
    print(f"  ||   Batch size: {BATCH_SIZE}  |  Flush: {FLUSH_INTERVAL}s{' ' * 17}||")
    print("  ||                                              ||")
    print("  " + "=" * 52)
    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(page)

        try:
            login(page)
            time.sleep(random.uniform(1, 2))
            navigate_to_aviator(page)

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

            log("")
            log("=" * 50)
            log("  MONITORING ACTIVE — recording all crashes")
            log("=" * 50)
            log("")

            # Keep-alive loop — just keep Playwright event loop running
            last_status = time.time()
            last_crash_count = 0

            while True:
                page.wait_for_timeout(1000)

                # Periodic status log every 60 seconds
                now = time.time()
                if now - last_status > 60:
                    new_crashes = state.crash_count - last_crash_count
                    stale = now - state.last_ws_time

                    log(f"  [STATUS] Crashes: {state.crash_count} total  "
                        f"(+{new_crashes} last min)  |  "
                        f"Sent: {total_sent}  |  Errors: {total_errors}  |  "
                        f"WS: {'OK' if state.ws_connected else 'DOWN'}  "
                        f"({stale:.0f}s ago)")

                    last_crash_count = state.crash_count
                    last_status = now

                    # Flush any remaining buffer
                    flush_buffer()

                # WS health check — auto-recover
                if not state.ws_connected or (now - state.last_ws_time > 60):
                    log("  WS disconnected or stale — reloading page...", "WARN")
                    flush_buffer()  # Save what we have

                    try:
                        page.reload(wait_until="domcontentloaded", timeout=30000)
                        page.wait_for_timeout(5000)

                        # Re-find iframe
                        for sel in ["iframe[src*='spribegaming.com/aviator']",
                                    "iframe[src*='spribegaming.com']", "iframe"]:
                            try:
                                loc = page.locator(sel).first
                                if loc.is_visible(timeout=5000):
                                    break
                            except Exception:
                                continue

                        # Wait for WS reconnect
                        t0 = time.time()
                        while time.time() - t0 < 30:
                            if state.ws_connected:
                                log("  WS reconnected!")
                                break
                            page.wait_for_timeout(500)

                        if not state.ws_connected:
                            log("  WS still down after reload — will retry", "WARN")

                    except Exception as e:
                        log(f"  Recovery error: {e}", "ERROR")
                        page.wait_for_timeout(10000)

        except KeyboardInterrupt:
            log("")
            log("=" * 50)
            log("  STOPPED BY USER")
            log(f"  Total crashes recorded: {state.crash_count}")
            log(f"  Sent to API: {total_sent}  |  Errors: {total_errors}")
            log("=" * 50)
            flush_buffer()
        except Exception as e:
            log(f"FATAL: {e}", "ERROR")
            import traceback
            traceback.print_exc()
            flush_buffer()
            sys.exit(1)
        finally:
            try:
                browser.close()
            except Exception:
                pass
            log("Browser closed.")


if __name__ == "__main__":
    run()
