"""Lê o saldo da plataforma (7K.bet.br / Betano / etc) via CDP da página pai.

Estratégia:
- Encontra o target CDP que NÃO é o iframe Spribe (a chrome da plataforma).
- Eval JS que extrai padrões de saldo do innerText (regex robusto).
- Cache em memória, atualiza a cada 8s. Server expõe via /api/cenario/balance.
"""
import json
import re
import threading
import time
import urllib.request

try:
    import websocket  # pip install websocket-client
    _WS_OK = True
    _WS_ERR = None
except ImportError as _e:
    websocket = None
    _WS_OK = False
    _WS_ERR = str(_e)

CDP_URL = 'http://localhost:9222'

_state = {
    'balance': None,
    'currency': None,
    'last_at': 0.0,
    'source': 'idle',     # idle / cdp / error
    'error': None,
    'target_url': None,
}
_lock = threading.Lock()


def _list_targets():
    try:
        r = urllib.request.urlopen(f'{CDP_URL}/json', timeout=3)
        return json.loads(r.read())
    except Exception:
        return []


def _find_candidate_targets():
    """Retorna lista ordenada de targets onde pode estar o saldo do user.
    O Spribe iframe é PRIORIDADE — eles renderizam o saldo do user dentro
    do próprio iframe, independente da plataforma host (7K, Blaze, etc).
    Depois tenta as pages das plataformas como fallback."""
    targets = _list_targets()
    spribe = [t for t in targets if 'spribegaming' in (t.get('url') or '') or 'aviator-next' in (t.get('url') or '')]
    pages = [t for t in targets if t.get('type') == 'page']
    EXCLUDE = ('localhost', 'irondome-cloud.vercel.app', 'twitch.tv', 'youtube.com', 'about:', 'chrome://')
    plat_pages = [t for t in pages if not any(x in (t.get('url') or '') for x in EXCLUDE)
                                     and 'spribegaming' not in (t.get('url') or '')
                                     and 'aviator-next' not in (t.get('url') or '')]
    PRIORITY = ('7k.bet.br', 'betano.com', 'blaze.bet.br', 'blaze.com', 'casino', 'bet.br')
    ordered_plat = []
    for dom in PRIORITY:
        for t in plat_pages:
            if dom in (t.get('url') or '') and t not in ordered_plat:
                ordered_plat.append(t)
    for t in plat_pages:
        if t not in ordered_plat:
            ordered_plat.append(t)
    return spribe + ordered_plat


# JS robusto: seletores específicos por plataforma, depois fallback genérico
_PROBE_JS = r'''
(function() {
    function parseBR(s) {
        if (s.includes(',')) s = s.replace(/\./g, '').replace(',', '.');
        const v = parseFloat(s);
        return (isFinite(v) && v >= 0) ? v : null;
    }
    // ----- Estratégia 0: seletores conhecidos por plataforma -----
    const PLATFORM_SELECTORS = [
        // Spribe (dentro do iframe — saldo real do user em qualquer plataforma que embede Spribe)
        'app-header .balance .balance-amount',
        '.balance .balance-amount',
        '.balance-amount',
        // Blaze
        '.wallet .amount .currency',
        '.wallet .currency',
        '.wallet .amount',
        // outros candidatos genéricos
        '.balance-value', '.user-balance',
        '[data-balance]',
    ];
    for (const sel of PLATFORM_SELECTORS) {
        try {
            const el = document.querySelector(sel);
            if (!el) continue;
            const txt = (el.textContent || '').trim();
            const m = /(\d{1,3}(?:[.,]\d{3})*[.,]\d{2})/.exec(txt);
            if (m) {
                const v = parseBR(m[1]);
                if (v !== null) return {value: v, raw: m[1], strategy: 'platform-selector', sel, ctx: txt.substr(0, 80)};
            }
        } catch (e) {}
    }
    function fromRoot(root) {
        // Estratégia 1: elementos com class/id contendo balance/saldo/wallet
        const sels = [
            '[class*="balance" i]', '[class*="saldo" i]', '[class*="wallet" i]',
            '[id*="balance" i]', '[id*="saldo" i]', '[id*="wallet" i]',
            '[data-testid*="balance" i]', '[data-testid*="saldo" i]',
            '[data-balance]', '[data-saldo]', '[data-wallet]',
        ];
        for (const sel of sels) {
            const els = root.querySelectorAll(sel);
            for (const el of els) {
                const txt = (el.textContent || '').trim();
                if (txt.length > 200) continue;  // filtra texto muito longo (provavelmente erro)
                const m = /(\d{1,3}(?:[.,]\d{3})*[.,]\d{2})/.exec(txt);
                if (m) {
                    const v = parseBR(m[1]);
                    if (v !== null && v < 1e7) return {value: v, raw: m[1], strategy: 'selector', sel: sel.substr(0, 30), ctx: txt.substr(0, 80)};
                }
            }
        }
        return null;
    }
    function collectText(rootDoc) {
        let txt = '';
        try { txt += rootDoc.body.innerText || ''; } catch (e) {}
        const all = rootDoc.querySelectorAll('*');
        for (const el of all) {
            if (el.shadowRoot) {
                try { txt += '\n' + el.shadowRoot.textContent; } catch (e) {}
            }
        }
        return txt;
    }
    // Estratégia 1: seletores no doc principal + shadow roots
    let result = fromRoot(document);
    if (!result) {
        const all = document.querySelectorAll('*');
        for (const el of all) {
            if (el.shadowRoot) {
                result = fromRoot(el.shadowRoot);
                if (result) break;
            }
        }
    }
    if (result) return result;
    // Estratégia 2: regex próximo de palavras-chave (innerText + outerHTML)
    const text = collectText(document) + '\n' + (document.body ? document.body.outerHTML.substr(0, 200000) : '');
    // normaliza nbsp e múltiplos espaços
    const normText = text.replace(/\u00A0/g, ' ').replace(/[\s\n]+/g, ' ');
    const reCtx = /(saldo|balance|carteira|wallet|dispon[íi]vel)[^\d]{0,30}(\d{1,3}(?:[.,]\d{3})*[.,]\d{2})/i;
    const mc = reCtx.exec(normText);
    if (mc) {
        const v = parseBR(mc[2]);
        if (v !== null && v < 1e7) return {value: v, raw: mc[2], strategy: 'keyword', ctx: normText.substr(Math.max(0, mc.index - 30), 100)};
    }
    // Estratégia 3: regex BRL com tolerância a nbsp + tags entre número e BRL
    const candidates = [];
    let m;
    const re1 = /(\d{1,3}(?:[.,]\d{3})*[.,]\d{2})[\s\u00A0]*(?:<[^>]+>[\s\u00A0]*)*(?:BRL|R\$)/g;
    while ((m = re1.exec(normText)) !== null) {
        const v = parseBR(m[1]);
        if (v !== null && v < 1e6) candidates.push({raw: m[1], v: v, ctx: normText.substr(Math.max(0, m.index - 30), 100)});
    }
    // pega "BRL"/"R$" antes do valor também
    const re2 = /(?:BRL|R\$)[\s\u00A0]*(?:<[^>]+>[\s\u00A0]*)*(\d{1,3}(?:[.,]\d{3})*[.,]\d{2})/g;
    while ((m = re2.exec(normText)) !== null) {
        const v = parseBR(m[1]);
        if (v !== null && v < 1e6) candidates.push({raw: m[1], v: v, ctx: normText.substr(Math.max(0, m.index - 30), 100)});
    }
    if (candidates.length) {
        // ordena por menor valor (saldos do user normalmente menores que jackpots)
        candidates.sort((a, b) => a.v - b.v);
        const c = candidates[0];
        return {value: c.v, raw: c.raw, strategy: 'fallback-min', ctx: c.ctx, total: candidates.length};
    }
    return {value: null, debug: 'no match', htmlLen: normText.length, sample: normText.substr(0, 300)};
})()
'''


def _eval_in_target(ws_url: str):
    ws = websocket.create_connection(ws_url, timeout=5)
    try:
        ws.send(json.dumps({'id': 1, 'method': 'Runtime.evaluate',
                             'params': {'expression': _PROBE_JS, 'returnByValue': True, 'awaitPromise': False}}))
        deadline = time.time() + 5
        while time.time() < deadline:
            try:
                ws.settimeout(2)
                msg = json.loads(ws.recv())
            except Exception:
                continue
            if msg.get('id') == 1:
                res = msg.get('result', {}).get('result', {})
                return res.get('value')
        return None
    finally:
        try: ws.close()
        except Exception: pass


def _loop():
    if not _WS_OK:
        with _lock:
            _state['source'] = 'error'
            _state['error'] = f'lib websocket-client não instalada'
        return
    while True:
        try:
            cands = _find_candidate_targets()
            if not cands:
                with _lock:
                    _state['source'] = 'error'
                    _state['error'] = 'Brave precisa estar aberto na página da plataforma de apostas'
                time.sleep(8); continue
            # tenta cada página até achar saldo válido (não-zero idealmente, ou via selector)
            best = None
            best_url = None
            for t in cands:
                try:
                    val = _eval_in_target(t['webSocketDebuggerUrl'])
                    if val and val.get('value') is not None:
                        # selector preciso (platform-selector) > regex genérico
                        if val.get('strategy') == 'platform-selector':
                            best = val; best_url = t.get('url'); break
                        if best is None:
                            best = val; best_url = t.get('url')
                except Exception:
                    continue
            with _lock:
                if best and best.get('value') is not None:
                    _state['balance'] = float(best['value'])
                    _state['currency'] = 'BRL'
                    _state['last_at'] = time.time()
                    _state['source'] = 'cdp'
                    _state['error'] = None
                    _state['target_url'] = best_url
                    _state['strategy'] = best.get('strategy')
                else:
                    _state['source'] = 'error'
                    _state['error'] = 'saldo não encontrado em nenhuma plataforma aberta'
                    _state['target_url'] = ', '.join(c.get('url', '')[:40] for c in cands[:3])
        except Exception as e:
            with _lock:
                _state['source'] = 'error'
                _state['error'] = str(e)
        # 30s de intervalo — não compete com o daemon de crashes (que faz Runtime.evaluate a cada ~150ms)
        time.sleep(30)


def get_balance() -> dict:
    with _lock:
        s = dict(_state)
    s['age_seconds'] = (time.time() - s['last_at']) if s['last_at'] else None
    return s


def start():
    threading.Thread(target=_loop, daemon=True, name='balance-reader').start()
