#!/usr/bin/env python3.12
"""Daemon CDP v2: intercepta WebSocket frames diretamente via Network domain.
Bypassa completamente JS isolation — captura TUDO que passa pela WS do iframe."""
import base64
import json
import os
import ssl
import threading
import time
import urllib.request
import zlib
import re

import websocket

BACKEND = 'https://localhost:8766'
ssl_ctx = ssl._create_unverified_context()

PATTERNS = [
    re.compile(r'["\']?multiplier["\']?\s*[:=]\s*(\d+\.\d+)', re.IGNORECASE),
    re.compile(r'["\']?crashPoint["\']?\s*[:=]\s*(\d+\.\d+)', re.IGNORECASE),
    re.compile(r'["\']?coefficient["\']?\s*[:=]\s*(\d+\.\d+)', re.IGNORECASE),
    re.compile(r'["\']?rate["\']?\s*[:=]\s*(\d+\.\d+)', re.IGNORECASE),
    re.compile(r'["\']?result["\']?\s*[:=]\s*(\d+\.\d+)', re.IGNORECASE),
    re.compile(r'\b(\d+\.\d{2,3})\b'),
]
last_crash = {'value': None, 'at': 0}


def post_backend(payload):
    try:
        req = urllib.request.Request(
            BACKEND + '/api/events',
            data=json.dumps(payload).encode(),
            headers={'Content-Type': 'application/json'},
            method='POST',
        )
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2).read()
        return True
    except Exception as e:
        print(f'[backend] erro: {e}')
        return False


recently_registered = []  # [(value, timestamp)]

def register_crash(v):
    now = time.time()
    if v <= 1.0 or v >= 1000: return
    # Dedup forte: se valor idêntico nos últimos 60s, ignora
    for val, ts in recently_registered:
        if abs(val - v) < 0.005 and (now - ts) < 60:
            return
    recently_registered.append((v, now))
    if len(recently_registered) > 100:
        recently_registered[:] = recently_registered[-50:]
    print(f'[cdp] CRASH: {v:.2f}x')
    post_backend({'kind': 'crash', 'value': v})


def extract_crash(text):
    for pat in PATTERNS:
        m = pat.search(text)
        if m:
            try:
                v = float(m.group(1))
                if 1.0 < v < 1000 and v != int(v):
                    return v
            except: pass
    return None


def bytes_to_ascii(bs):
    return ''.join(chr(b) if 32 <= b < 127 else ' ' for b in bs)


import struct


def decompress_any(data):
    """Tenta zlib/raw deflate/gzip com vários offsets de prefixo SFS2X."""
    for prefix in (0, 1, 2, 3, 4):
        for wb in (15, -15, 31):
            try:
                return zlib.decompress(data[prefix:], wbits=wb)
            except zlib.error:
                continue
    return None


def extract_sfs_multipliers(blob):
    """Procura pela chave 'multiplier' em payload SFS2X binário e extrai doubles big-endian."""
    vals = []
    pos = 0
    key = b'multiplier'
    while True:
        i = blob.find(key, pos)
        if i == -1: break
        pos = i + 1
        after = blob[i + len(key): i + len(key) + 9]
        if len(after) < 9: continue
        marker = after[0]
        if marker == 0x07:  # DOUBLE
            try:
                v = struct.unpack('>d', after[1:9])[0]
                if 1.0 < v < 1000 and not (v != v):  # v != v checks NaN
                    vals.append(v)
            except struct.error:
                pass
        elif marker == 0x06:  # FLOAT
            try:
                v = struct.unpack('>f', after[1:5])[0]
                if 1.0 < v < 1000:
                    vals.append(v)
            except struct.error:
                pass
    # Também procura por 'crashPoint' no caso de formato diferente
    for key2 in (b'crashPoint', b'coefficient', b'result', b'rate'):
        pos = 0
        while True:
            i = blob.find(key2, pos)
            if i == -1: break
            pos = i + 1
            after = blob[i + len(key2): i + len(key2) + 9]
            if len(after) < 9: continue
            marker = after[0]
            if marker == 0x07:
                try:
                    v = struct.unpack('>d', after[1:9])[0]
                    if 1.0 < v < 1000:
                        vals.append(v)
                except: pass
    return vals


# Estado da rodada: acompanha state transitions do Spribe (1=betting, 2=flight)
# e o maior multiplier visto durante o flight — esse é o crash.
round_state = {
    'state': None,
    'peak_flight': None,
}


def extract_new_state_id(blob):
    i = blob.find(b'newStateId')
    if i < 0: return None
    after = blob[i + len(b'newStateId'): i + len(b'newStateId') + 5]
    if len(after) >= 5 and after[0] == 0x04:
        try: return struct.unpack('>i', after[1:5])[0]
        except: pass
    return None


def process_payload(data, is_binary):
    if not is_binary:
        return

    # Frames de TRANSIÇÃO DE ESTADO são pequenos e UNCOMPRESSED
    # Frames de progressão de flight são GRANDES e zlib-compressed
    decomp = decompress_any(data)
    is_compressed = decomp is not None
    source = decomp if is_compressed else data

    if not is_compressed and len(data) < 300:
        # Frame provável de state transition
        new_state = extract_new_state_id(data)
        if new_state is not None:
            prev = round_state['state']
            print(f'[cdp] STATE: {prev} -> {new_state} peak={round_state["peak_flight"]}')
            if prev == 2 and new_state == 1 and round_state['peak_flight'] is not None:
                register_crash(round_state['peak_flight'])
            round_state['state'] = new_state
            if new_state == 2:
                round_state['peak_flight'] = None
        return

    # Frame grande (compressed) = dados do jogo / progressão de flight
    if round_state['state'] == 2:
        vals = extract_sfs_multipliers(source)
        if vals:
            candidate = max(vals)
            if round_state['peak_flight'] is None or candidate > round_state['peak_flight']:
                round_state['peak_flight'] = candidate
                if candidate > 1.5:
                    print(f'[cdp] flight peak {candidate:.2f}x')


class CDPClient:
    def __init__(self, ws_url):
        self.ws = websocket.create_connection(ws_url, timeout=10)
        self.next_id = 1
        self.pending = {}
        self.events = []
        self.lock = threading.Lock()
        self.running = True
        self.reader = threading.Thread(target=self._read_loop, daemon=True)
        self.reader.start()

    def _read_loop(self):
        while self.running:
            try:
                raw = self.ws.recv()
                msg = json.loads(raw)
                if 'id' in msg:
                    with self.lock:
                        if msg['id'] in self.pending:
                            self.pending[msg['id']].append(msg)
                elif 'method' in msg:
                    with self.lock:
                        self.events.append(msg)
            except websocket.WebSocketException:
                self.running = False
                break
            except Exception:
                pass

    def call(self, method, params=None, session_id=None, timeout=5):
        with self.lock:
            call_id = self.next_id
            self.next_id += 1
            self.pending[call_id] = []
        payload = {'id': call_id, 'method': method, 'params': params or {}}
        if session_id: payload['sessionId'] = session_id
        self.ws.send(json.dumps(payload))
        start = time.time()
        while time.time() - start < timeout:
            with self.lock:
                if self.pending.get(call_id):
                    msg = self.pending[call_id].pop(0)
                    del self.pending[call_id]
                    return msg
            time.sleep(0.02)
        raise TimeoutError(f'{method} timeout')

    def drain_events(self):
        with self.lock:
            evts = self.events
            self.events = []
        return evts


def main():
    r = urllib.request.urlopen('http://localhost:9222/json/version', timeout=3)
    browser_ws_url = json.loads(r.read())['webSocketDebuggerUrl']
    print(f'[cdp] conectando: {browser_ws_url}')
    cdp = CDPClient(browser_ws_url)

    # Auto-attach em todos os targets (inclusive worker, iframe, page)
    cdp.call('Target.setAutoAttach', {
        'autoAttach': True,
        'waitForDebuggerOnStart': False,
        'flatten': True,
    })
    print('[cdp] auto-attach ativo')

    # Discover existing targets — attach em page/iframe/worker
    existing = cdp.call('Target.getTargets').get('result', {}).get('targetInfos', [])
    for ti in existing:
        if ti.get('type') in ('page', 'iframe', 'worker', 'service_worker'):
            try:
                cdp.call('Target.attachToTarget', {'targetId': ti['targetId'], 'flatten': True})
                print(f'[cdp] attach {ti["type"]} {ti.get("url", "")[:80]}')
            except Exception as e:
                print(f'[cdp] erro: {e}')

    # Network.enable em cada session pra receber webSocketFrameReceived
    sessions_netenabled = set()
    ws_msgs_count = 0
    session_urls = {}
    heartbeat_last = 0

    print('[cdp] loop principal...')
    try:
        while True:
            events = cdp.drain_events()
            for evt in events:
                method = evt.get('method')
                params = evt.get('params', {})
                sid = evt.get('sessionId')

                if method == 'Target.attachedToTarget':
                    new_sid = params.get('sessionId')
                    ti = params.get('targetInfo', {})
                    session_urls[new_sid] = ti.get('url', '')[:90]
                    try:
                        cdp.call('Network.enable', {}, session_id=new_sid)
                        sessions_netenabled.add(new_sid)
                        print(f'[cdp] Network.enable [{ti.get("type")}] {ti.get("url","")[:80]}')
                    except Exception as e:
                        print(f'[cdp] Network.enable erro: {e}')

                elif method == 'Network.webSocketCreated':
                    print(f'[cdp] WS criado: {params.get("url", "")[:80]}')

                elif method == 'Network.webSocketFrameReceived':
                    ws_msgs_count += 1
                    resp = params.get('response', {})
                    payload = resp.get('payloadData', '')
                    is_binary = (resp.get('opcode', 1) == 2)
                    try:
                        if is_binary:
                            data = base64.b64decode(payload) if payload else b''
                        else:
                            data = payload
                        process_payload(data, is_binary)
                    except Exception as e:
                        pass

                elif method == 'Network.webSocketFrameSent':
                    pass  # não interessa por ora

            # Heartbeat
            now = time.time()
            if now - heartbeat_last > 2:
                post_backend({'kind': 'heartbeat', 'connected': ws_msgs_count > 0, 'msgsReceived': ws_msgs_count})
                heartbeat_last = now

            time.sleep(0.1)
    except KeyboardInterrupt:
        print('\n[cdp] ctrl+c')


if __name__ == '__main__':
    main()
