#!/usr/bin/env python3.12
"""
Auto-click daemon pro Aviator (SEPARADO do sistema principal).
- Só age se config.enabled == True
- Sempre começa em dryRun=True por padrão
- Respeita stop-loss, stop-win, max bets, loss streak
- Jitter de timing contra antifraude
- Helper JS injetado no iframe faz a detecção de botões e multiplier
"""
import json
import os
import random
import ssl
import sys
import threading
import time
import traceback
import urllib.request

import websocket

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


# Helper JS injetado dentro do iframe Spribe.
# Descoberta via heurística — pode precisar ajuste fino conforme UI Spribe.
HELPER_JS = r"""
(function () {
  window.__aviatorAutoHelper = true;

  function visibleText(el) {
    try { return (el.innerText || el.textContent || '').trim(); } catch { return ''; }
  }
  function isVisible(el) {
    if (!el) return false;
    const r = el.getBoundingClientRect();
    return r.width > 0 && r.height > 0;
  }

  // Spribe tem 2 painéis (panel 0 = esquerdo, panel 1 = direito).
  // Cada painel tem:
  //   .btn.bet (APOSTAR / CANCELAR — próxima rodada)
  //   .btn.btn-warning.cashout (CASHOUT — durante o flight ativo daquele painel)
  //   input numérico (valor da aposta)
  // Usamos querySelectorAll e indexamos pelo parâmetro panel (0 ou 1).

  // Cada painel é um <app-bet-control>. Ordenamos pela posição X (esquerda=0, direita=1).
  function findPanelContainers() {
    const panels = [...document.querySelectorAll('app-bet-control, APP-BET-CONTROL')]
      .filter(el => {
        const r = el.getBoundingClientRect();
        return r.width > 20 && r.height > 20;
      })
      .sort((a, b) => a.getBoundingClientRect().left - b.getBoundingClientRect().left);
    return panels;
  }

  function findBetButton(panel) {
    const c = findPanelContainers()[panel];
    if (!c) return null;
    const b = c.querySelector('button.btn.bet');
    return b && isVisible(b) ? b : null;
  }
  function findCashoutButton(panel) {
    const c = findPanelContainers()[panel];
    if (!c) return null;
    const b = c.querySelector('button.btn.btn-warning.cashout, button.cashout.btn-warning');
    if (!b || !isVisible(b) || b.classList.contains('disabled')) return null;
    return b;
  }
  function findBetInput(panel) {
    const c = findPanelContainers()[panel];
    if (!c) return null;
    // 1.17.0: Spribe usa input type=text + inputmode=decimal dentro de .bet-block.
    // Há também um spinner de auto-cashout (disabled/invisível) — precisa filtrar.
    // Tenta seletor específico primeiro
    let i = c.querySelector('.bet-block input[inputmode="decimal"]') ||
            c.querySelector('.bet-block input');
    if (i && isVisible(i)) return i;
    // Fallback: itera todos os inputs visíveis e pega o que NÃO é cashout-spinner
    const all = c.querySelectorAll('input');
    for (const inp of all) {
      if (!isVisible(inp)) continue;
      // pula inputs dentro de cashout-spinner
      let p = inp.parentElement, blocked = false;
      for (let n = 0; n < 5 && p; n++) {
        if (p.className && p.className.toString().includes('cashout')) { blocked = true; break; }
        p = p.parentElement;
      }
      if (blocked) continue;
      return inp;
    }
    return null;
  }

  function findLiveCashoutValue(panel) {
    const btn = findCashoutButton(panel);
    if (!btn) return null;
    const txt = visibleText(btn);
    const m = txt.match(/(\d{1,3}(?:[.,]\d{3})*(?:[.,]\d+)|\d+\.\d+|\d+)/);
    if (!m) return null;
    let raw = m[1];
    if (raw.includes('.') && raw.includes(',')) {
      raw = raw.replace(/\./g, '').replace(',', '.');
    } else if (raw.includes(',') && !raw.includes('.')) {
      raw = raw.replace(',', '.');
    }
    const v = parseFloat(raw);
    return isNaN(v) ? null : v;
  }

  function getPhase(panel) {
    const cashBtn = findCashoutButton(panel);
    if (cashBtn) {
      return { phase: 'flight', action: 'cashout', text: visibleText(cashBtn).slice(0, 40), cls: cashBtn.className.slice(0, 60) };
    }
    const betBtn = findBetButton(panel);
    if (!betBtn) return { phase: 'unknown', action: null };
    const cls = betBtn.className || '';
    const text = visibleText(betBtn);
    if (cls.includes('btn-success')) return { phase: 'betting', action: 'bet', text, cls };
    if (cls.includes('btn-danger'))  return { phase: 'waiting_flight', action: 'cancel', text, cls };
    return { phase: 'unknown', action: null, text, cls };
  }

  function setBetAmount(panel, amount) {
    const input = findBetInput(panel);
    if (!input) return { ok: false, reason: 'input nao encontrado panel=' + panel };
    const proto = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
    proto.call(input, String(amount));
    input.dispatchEvent(new Event('input', { bubbles: true }));
    input.dispatchEvent(new Event('change', { bubbles: true }));
    input.dispatchEvent(new Event('blur', { bubbles: true }));
    return { ok: true, value: input.value };
  }

  function detectState(panel) {
    const p = getPhase(panel);
    const cashoutValue = findLiveCashoutValue(panel);
    return { phase: p.phase, action: p.action, btnText: p.text, btnCls: p.cls, cashoutValue };
  }

  function detectAll() {
    return { panel0: detectState(0), panel1: detectState(1) };
  }

  window.__aviator = {
    detectState, detectAll, setBetAmount,
    clickBet: (panel) => {
      const b = findBetButton(panel);
      if (!b) return { ok: false, reason: 'bet btn nao encontrado panel=' + panel };
      b.click();
      return { ok: true, text: visibleText(b).slice(0, 40) };
    },
    clickCashout: (panel) => {
      const b = findCashoutButton(panel);
      if (!b) return { ok: false, reason: 'cashout btn nao encontrado panel=' + panel };
      b.click();
      return { ok: true, text: visibleText(b).slice(0, 40) };
    },
    // "Arma" o cashout direto no browser: a cada 20ms lê o valor do botão
    // e clica quando (valor / betAmount) ≥ target. Muito mais confiável que
    // polling via CDP (roundtrip de ~100ms+).
    armCashout: (panel, target, betAmount) => {
      const key = '__armCashoutTimer_' + panel;
      const rkey = '__armCashoutResult_' + panel;
      if (window[key]) { clearInterval(window[key]); }
      window[rkey] = null;
      const t0 = Date.now();
      window[key] = setInterval(() => {
        if (Date.now() - t0 > 60000) {
          clearInterval(window[key]); window[key] = null;
          window[rkey] = { timeout: true };
          return;
        }
        try {
          const b = findCashoutButton(panel);
          if (!b) return;
          const txt = visibleText(b);
          const m = txt.match(/(\d+(?:[.,]\d+)?)/);
          if (!m) return;
          const val = parseFloat(m[1].replace(',', '.'));
          if (!isFinite(val) || val <= 0 || !isFinite(betAmount) || betAmount <= 0) return;
          const mult = val / betAmount;
          if (mult >= target) {
            b.click();
            clearInterval(window[key]); window[key] = null;
            window[rkey] = { clicked: true, mult, val, ts: Date.now() };
          }
        } catch (e) {}
      }, 20);
      return { armed: true, panel, target, betAmount };
    },
    checkArmedResult: (panel) => {
      const rkey = '__armCashoutResult_' + panel;
      const r = window[rkey] || null;
      if (r) window[rkey] = null;
      return r;
    },
    cancelArmed: (panel) => {
      const key = '__armCashoutTimer_' + panel;
      if (window[key]) { clearInterval(window[key]); window[key] = null; }
      window['__armCashoutResult_' + panel] = null;
      return { cancelled: true };
    },
  };
  return 'installed';
})();
"""


def post_event(kind, msg, panel=None, **extra):
    payload = {'kind': kind, 'msg': msg, **extra}
    if panel is not None:
        payload['panel'] = panel
    try:
        req = urllib.request.Request(
            BACKEND + '/api/autoclick/event',
            data=json.dumps(payload).encode(),
            headers={'Content-Type': 'application/json'},
            method='POST',
        )
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2).read()
    except Exception:
        pass
    print(f'[autoclick] {kind.upper()}: {msg}')


def get_config():
    try:
        r = urllib.request.urlopen(BACKEND + '/api/autoclick/config', context=ssl_ctx, timeout=2)
        return json.loads(r.read())
    except Exception:
        return {}


def get_state():
    try:
        r = urllib.request.urlopen(BACKEND + '/api/state', context=ssl_ctx, timeout=2)
        return json.loads(r.read())
    except Exception:
        return {}


def trigger_tech_pause(minutes, reason, extend=False):
    """Dispara parada técnica por N minutos (techPauseMinutes da config)."""
    try:
        data = json.dumps({
            'minutes': float(minutes),
            'reason': reason,
            'extend': bool(extend),
        }).encode()
        req = urllib.request.Request(BACKEND + '/api/autoclick/pause', data=data, method='POST')
        req.add_header('Content-Type', 'application/json')
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2)
        return True
    except Exception:
        return False


def cancel_tech_pause():
    try:
        data = json.dumps({'minutes': 0}).encode()
        req = urllib.request.Request(BACKEND + '/api/autoclick/pause', data=data, method='POST')
        req.add_header('Content-Type', 'application/json')
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2)
        return True
    except Exception:
        return False


def post_panel_state(panel, remaining, kind):
    try:
        data = json.dumps({'panel': panel, 'remaining': int(remaining), 'kind': kind}).encode()
        req = urllib.request.Request(BACKEND + '/api/autoclick/panel_state', data=data, method='POST')
        req.add_header('Content-Type', 'application/json')
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2)
    except Exception:
        pass


def post_calibration_state(remaining, total):
    """Reporta estado da calibração pra UI mostrar."""
    try:
        data = json.dumps({'remaining': int(remaining), 'total': int(total)}).encode()
        req = urllib.request.Request(BACKEND + '/api/autoclick/calibration', data=data, method='POST')
        req.add_header('Content-Type', 'application/json')
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2)
    except Exception:
        pass


def post_shadow_result(panel, hit, target=None, pattern=None, probability=None, crash=None):
    """Reporta resultado shadow. Server atualiza shadow_stats + shadow_results
    + janela de 8. `crash` é o multiplicador da rodada simulada — usado pela
    UI pra mostrar a sequência de sinais com fundo verde/vermelho."""
    try:
        data = json.dumps({
            'panel': panel, 'hit': bool(hit),
            'target': target, 'pattern': pattern, 'probability': probability,
            'crash': crash,
        }).encode()
        req = urllib.request.Request(BACKEND + '/api/autoclick/shadow', data=data, method='POST')
        req.add_header('Content-Type', 'application/json')
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2)
    except Exception:
        pass


def get_intelligence():
    try:
        r = urllib.request.urlopen(BACKEND + '/api/intelligence/state', context=ssl_ctx, timeout=2)
        return json.loads(r.read())
    except Exception:
        return {'config': {}, 'state': {}}


def ia_gate(target, panel=None, margin=None):
    """Consulta /api/intelligence/gate pra um target específico (e painel).
    Retorna (approve: bool, probability: float, pattern_key: str | None)."""
    try:
        url = BACKEND + f'/api/intelligence/gate?target={target}'
        if panel is not None:
            url += f'&panel={panel}'
        if margin is not None:
            url += f'&margin={margin}'
        r = urllib.request.urlopen(url, context=ssl_ctx, timeout=2)
        d = json.loads(r.read())
        pat = d.get('pattern')
        pat_key = pat.get('pattern') if isinstance(pat, dict) else None
        return d.get('decision') == 'approve', d.get('probability', 0), pat_key
    except Exception:
        return True, None, None


# Mapeamento modo de operação → margem (fração)
OP_MODE_MARGINS = {
    'conservador': 0.15,
    'equilibrado': 0.10,
    'agressivo': 0.05,
}


def margin_for_panel(p_cfg):
    mode = (p_cfg.get('operationMode') or 'equilibrado').lower()
    return OP_MODE_MARGINS.get(mode, 0.10)


def find_iframe_target():
    try:
        r = urllib.request.urlopen(CDP_HOST + '/json/list', timeout=3)
        for t in json.loads(r.read()):
            if 'spribegaming.com' in t.get('url', '') or 'spribe.games' in t.get('url', ''):
                return t
    except Exception:
        pass
    return None


class CDP:
    def __init__(self, ws_url):
        self.ws_url = ws_url
        self.ws = websocket.create_connection(ws_url, timeout=10)
        # após conectar, remove o timeout pra recv() não estourar em idle
        self.ws.settimeout(None)
        self.next_id = 1
        self.pending = {}
        self.lock = threading.Lock()
        self.alive = True
        self.reader_error = None
        threading.Thread(target=self._reader, daemon=True).start()

    def _reader(self):
        while self.alive:
            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)
            except Exception as e:
                self.reader_error = str(e)
                self.alive = False
                break

    def call(self, method, params=None, timeout=5):
        if not self.alive:
            raise ConnectionError(f'CDP dead: {self.reader_error}')
        with self.lock:
            cid = self.next_id; self.next_id += 1
            self.pending[cid] = []
        try:
            self.ws.send(json.dumps({'id': cid, 'method': method, 'params': params or {}}))
        except Exception as e:
            self.alive = False
            raise ConnectionError(f'send failed: {e}')
        start = time.time()
        while time.time() - start < timeout:
            if not self.alive:
                raise ConnectionError(f'CDP died mid-call: {self.reader_error}')
            with self.lock:
                if self.pending.get(cid):
                    m = self.pending[cid].pop(0)
                    del self.pending[cid]
                    return m
            time.sleep(0.02)
        raise TimeoutError(method)

    def eval(self, expr, await_promise=False):
        res = self.call('Runtime.evaluate', {
            'expression': expr, 'returnByValue': True, 'awaitPromise': await_promise,
        })
        return res.get('result', {}).get('result', {}).get('value')

    def close(self):
        self.alive = False
        try: self.ws.close()
        except: pass


def check_stop_conditions(cfg, stats):
    """Retorna (stop, motivo) se algum limite compartilhado foi atingido."""
    if stats['pnl_today'] <= -abs(cfg.get('stopLoss', 50)):
        return True, f"stop-loss atingido (R${abs(stats['pnl_today']):.2f} perdidos)"
    if stats['pnl_today'] >= abs(cfg.get('stopWin', 100)):
        return True, f"stop-win atingido (R${stats['pnl_today']:.2f} ganhos)"
    if stats['bets_today'] >= cfg.get('maxBets', 50):
        return True, f"máximo de apostas atingido ({stats['bets_today']})"
    if stats['loss_streak'] >= cfg.get('maxLossStreak', 10):
        return True, f"Tivemos {stats['loss_streak']} perdas consecutivas"
    return False, None


def in_schedule_window(panel_cfg):
    """True se agora está dentro da janela configurada; True também quando
    não há horário definido. Janela cruzando meia-noite (ex: 22:00–06:00)
    é suportada."""
    st = (panel_cfg.get('startTime') or '').strip()
    et = (panel_cfg.get('endTime') or '').strip()
    if not st or not et:
        return True
    try:
        sh, sm = [int(x) for x in st.split(':')]
        eh, em = [int(x) for x in et.split(':')]
    except Exception:
        return True
    now = time.localtime()
    cur_min = now.tm_hour * 60 + now.tm_min
    start_min = sh * 60 + sm
    end_min = eh * 60 + em
    if start_min == end_min:
        return True
    if start_min < end_min:
        return start_min <= cur_min < end_min
    # cruza meia-noite
    return cur_min >= start_min or cur_min < end_min


def should_enter(panel_cfg, api_state):
    """Decide se o painel deve abrir aposta nessa rodada."""
    trigger = panel_cfg.get('entryTrigger', 'bet_only')
    if trigger == 'always':
        return True, 'always'
    signals = api_state.get('signals', [])
    last = signals[-1] if signals else None
    if trigger == 'bet_only':
        return (last and last.get('type') == 'BET', f"last sig: {last.get('type') if last else None}")
    if trigger == 'bet_atencao':
        return (last and last.get('type') in ('BET', 'ATENCAO'), f"last sig: {last.get('type') if last else None}")
    if trigger == 'gap_threshold':
        gap = api_state.get('stats', {}).get('intervalo_atual', 0)
        thr = panel_cfg.get('gapThreshold', 18)
        return (gap >= thr, f"gap={gap} thr={thr}")
    return False, 'trigger desconhecido'


def jitter(cfg):
    mn = cfg.get('jitterMin', 400); mx = cfg.get('jitterMax', 1200)
    return random.randint(mn, mx) / 1000.0


def get_strategy_state():
    """Pega estado atual da estratégia (None se desligada)."""
    try:
        r = urllib.request.urlopen(BACKEND + '/api/strategy/state', context=ssl_ctx, timeout=2)
        d = json.loads(r.read())
        return d if d.get('active_code') else None
    except Exception:
        return None


def get_operation_mode():
    """1.22.2 — retorna o modo atual ('manual'/'hedge'/'presetup'/'pos10x')."""
    try:
        r = urllib.request.urlopen(BACKEND + '/api/operation_mode', context=ssl_ctx, timeout=2)
        d = json.loads(r.read())
        return d.get('mode', 'manual')
    except Exception:
        return 'manual'


def get_tail_signal():
    """1.22.0 — Pega sinal pendente do detector de cauda (None se nenhum/auto_bet off).
    Quando retorna dict, daemon: 1) sobrescreve P0/P1 com stake/target da config,
    2) ignora todos os filtros de segurança (override total).
    1.25.0 — bet_p0 / bet_p1 controlam quais painéis entram (independentes)."""
    try:
        r = urllib.request.urlopen(BACKEND + '/api/tail/state', context=ssl_ctx, timeout=2)
        d = json.loads(r.read())
        cfg = d.get('config', {}) or {}
        if not cfg.get('auto_bet'): return None
        if not d.get('pending_signal'): return None
        bet_p0 = bool(cfg.get('bet_p0', True))
        bet_p1 = bool(cfg.get('bet_p1', True))
        if not bet_p0 and not bet_p1: return None  # ambos off = sem aposta
        return {
            'pattern': d['pending_signal'].get('pattern', '?'),
            'stake_p0': float(cfg.get('stake_p0', 1.0)),
            'stake_p1': float(cfg.get('stake_p1', 2.0)),
            'target_p0': float(cfg.get('target_p0', 4.0)),
            'target_p1': float(cfg.get('target_p1', 10.0)),
            'bet_p0': bet_p0,
            'bet_p1': bet_p1,
        }
    except Exception:
        return None


def report_strategy_outcome(any_panel_won: bool):
    try:
        data = json.dumps({'any_panel_won': bool(any_panel_won)}).encode()
        req = urllib.request.Request(BACKEND + '/api/strategy/outcome',
                                     data=data, method='POST')
        req.add_header('Content-Type', 'application/json')
        urllib.request.urlopen(req, context=ssl_ctx, timeout=2)
    except Exception:
        pass


def get_panels_cfg(cfg):
    """Extrai config de cada painel do config global. Backward compat com formato antigo.
    Prioridade: 1.22.0 tail-signal > hedge > strategy > panels do cfg.
    1.19.0: hedge_enabled sobrescreve P0=seguro e P1=alvo.
    1.20.0: strategy ativa sobrescreve P0/P1 com stake+target da rodada atual.
    1.22.0: detector de cauda em modo Pós-10x sobrescreve P0/P1 com stakes config."""
    # Tail signal: prioridade absoluta. Vale só pra próxima rodada (server-side resolve).
    tail = get_tail_signal()
    if tail:
        base = list(cfg.get('panels') or [{}, {}])
        while len(base) < 2: base.append({})
        # 1.25.0 — respeita bet_p0/bet_p1: painel desligado fica enabled=False, sem override
        p0 = dict(base[0])
        if tail.get('bet_p0', True):
            p0.update({'enabled': True, 'betAmount': tail['stake_p0'], 'cashoutTarget': tail['target_p0'], '_tail_override': True, '_tail_pattern': tail['pattern']})
        else:
            p0['enabled'] = False
        p1 = dict(base[1])
        if tail.get('bet_p1', True):
            p1.update({'enabled': True, 'betAmount': tail['stake_p1'], 'cashoutTarget': tail['target_p1'], '_tail_override': True, '_tail_pattern': tail['pattern']})
        else:
            p1['enabled'] = False
        return [p0, p1]
    if cfg.get('hedge_enabled'):
        try:
            total = float(cfg.get('hedge_total_stake') or 0)
            profit_goal = float(cfg.get('hedge_profit_target') or 0)
            t_a = float(cfg.get('hedge_t_a') or 2.0)
            if total > 0 and profit_goal > 0 and t_a > 1.0:
                s_a = round(total / t_a, 2)
                s_b = round(total - s_a, 2)
                if s_b > 0.01:
                    t_b = round(1.0 + (profit_goal + s_a) / s_b, 2)
                    base = list(cfg.get('panels') or [{}, {}])
                    while len(base) < 2: base.append({})
                    p0 = dict(base[0]); p0.update({'enabled': True, 'betAmount': s_a, 'cashoutTarget': t_a})
                    p1 = dict(base[1]); p1.update({'enabled': True, 'betAmount': s_b, 'cashoutTarget': t_b})
                    return [p0, p1]
        except Exception:
            pass
    # 1.20.0: estratégia ativa sobrescreve P0/P1 com stake+target da rodada atual
    st = get_strategy_state()
    if st and st.get('current_row') and not st.get('exhausted'):
        row = st['current_row']
        unit = float(st.get('stake_unit') or 1.0)
        b1_stake = round(float(row.get('b1_stake', 1)) * unit, 2)
        b1_target = float(row.get('b1_target', 4.0))
        b2_stake = round(float(row.get('b2_stake', 1)) * unit, 2)
        b2_target = float(row.get('b2_target', 10.0))
        base = list(cfg.get('panels') or [{}, {}])
        while len(base) < 2: base.append({})
        p0 = dict(base[0]); p0.update({'enabled': True, 'betAmount': b1_stake, 'cashoutTarget': b1_target})
        p1 = dict(base[1]); p1.update({'enabled': True, 'betAmount': b2_stake, 'cashoutTarget': b2_target})
        return [p0, p1]
    if 'panels' in cfg:
        return cfg['panels']
    # Config antigo (single panel) — trata como panels[0]
    p0 = {
        'enabled': cfg.get('enabled', False),
        'betAmount': cfg.get('betAmount', 1),
        'entryTrigger': cfg.get('entryTrigger', 'always'),
        'gapThreshold': cfg.get('gapThreshold', 18),
        'cooldown': cfg.get('cooldown', 0),
        'cooldownAfterLoss': cfg.get('cooldownAfterLoss', 0),
        'cashoutTarget': cfg.get('cashoutTarget', 1.5),
        'cashoutPartial': cfg.get('cashoutPartial'),
        'partialFraction': cfg.get('partialFraction', 0.5),
    }
    return [p0, {**p0, 'enabled': False}]


def main():
    print('[autoclick] aguardando iframe spribe...')
    target = None
    for _ in range(120):
        target = find_iframe_target()
        if target: break
        time.sleep(1)
    if not target:
        print('[autoclick] timeout — iframe não encontrado. Saindo.')
        return

    print(f'[autoclick] target: {target["url"][:80]}')
    cdp = CDP(target['webSocketDebuggerUrl'])
    cdp.call('Runtime.enable')
    cdp.call('Page.enable')
    cdp.eval(HELPER_JS)
    post_event('info', 'helper JS injetado no iframe')

    # Estado local POR PAINEL (0 e 1)
    active_bets = {0: None, 1: None}
    last_action_at = {0: 0, 1: 0}
    cooldown_until_crash = {0: 0, 1: 0}
    loss_wait_until = {0: 0, 1: 0}
    win_wait_until = {0: 0, 1: 0}
    # Shadow bets: simulação durante parada técnica.
    # 1.16.3: driven por crash. Cada incremento de api_total durante
    # parada técnica = 1 shadow por painel enabled.
    shadow_bets = {0: None, 1: None}      # legacy (não mais usado)
    shadow_ready = {0: True, 1: True}     # legacy (não mais usado)
    last_shadow_api_total = {0: -1, 1: -1}  # último api_total processado por painel
    pending_hits = {0: None, 1: None}    # hit aguardando crash da rodada
    last_tech_paused = False
    tech_pause_start_at = 0.0  # 1.17.0: filtra crashes pré-parada do shadow
    last_posted_cd = {0: None, 1: None}  # (remaining, kind) do último POST
    # 1.17.0 — calibração: ao ligar o bot, observa N rodadas antes de apostar
    CALIBRATION_ROUNDS = 10
    bot_was_enabled = False
    calibration_remaining = 0
    calibration_start_total = 0
    SHADOW_MIN_SAMPLES = 5
    SHADOW_MIN_RATE = 0.50
    SHADOW_EXTEND_MIN = 10
    last_phase = {0: None, 1: None}
    flight_peak = {0: 0, 1: 0}
    last_ia_decision = {0: None, 1: None}   # 'approve' ou 'reject' — só loga quando MUDA
    last_skip_reason = {0: None, 1: None}

    # Re-inject helper periodicamente (caso iframe recarregue)
    last_helper_inject = time.time()

    def reconnect():
        nonlocal target, cdp, last_helper_inject
        err = cdp.reader_error if cdp else 'no cdp'
        post_event('info', f'reconectando (motivo: {err})')
        new_target = find_iframe_target()
        if not new_target:
            time.sleep(3)
            return False
        target = new_target
        try: cdp.close()
        except: pass
        cdp = CDP(target['webSocketDebuggerUrl'])
        cdp.call('Runtime.enable')
        cdp.eval(HELPER_JS)
        last_helper_inject = time.time()
        post_event('info', f'reconectado em {target["id"][:10]}')
        return True

    while True:
        try:
            # Se CDP morreu, reconecta
            if not cdp.alive:
                if not reconnect():
                    continue

            # Re-detecta/re-injeta a cada 30s
            if time.time() - last_helper_inject > 30:
                try:
                    new_target = find_iframe_target()
                    target_id = target.get('id') if isinstance(target, dict) else None
                    new_id = new_target.get('id') if isinstance(new_target, dict) else None
                    if new_id and target_id and new_id != target_id:
                        reconnect()
                    else:
                        try: cdp.eval(HELPER_JS)
                        except Exception: pass
                except Exception as re_err:
                    post_event('err', f'erro ao re-detectar target: {re_err}')
                last_helper_inject = time.time()

            cfg = get_config()
            api_state = get_state()
            api_total = api_state.get('stats', {}).get('total', 0)
            intel = get_intelligence()
            istate = intel.get('state', {}) or {}
            icfg = intel.get('config', {}) or {}

            # cfg.enabled é o toggle MESTRE (liga/desliga todo o sistema).
            # Cada painel tem seu próprio 'enabled' dentro de panels[].
            cur_enabled = bool(cfg.get('enabled'))
            if not cur_enabled:
                bot_was_enabled = False
                calibration_remaining = 0
                time.sleep(1.5)
                continue
            # 1.17.0: detecta transição OFF→ON, dispara janela de calibração
            # 1.22.2: em modo Pós-10x, IA é bypassada — calibração é inútil. Pula.
            if not bot_was_enabled and cur_enabled:
                if get_operation_mode() == 'pos10x':
                    calibration_remaining = 0
                    post_event('info', '🎯 modo Pós-10x — sem calibração (IA bypassada). Aguardando sinal do detector.')
                else:
                    calibration_remaining = CALIBRATION_ROUNDS
                    calibration_start_total = api_total
                    post_event('info', f'🎯 CALIBRAÇÃO: observando {CALIBRATION_ROUNDS} rodadas antes de apostar')
            bot_was_enabled = cur_enabled
            # Avança/finaliza calibração conforme api_total cresce
            if calibration_remaining > 0:
                done = api_total - calibration_start_total
                remaining = max(0, CALIBRATION_ROUNDS - done)
                if remaining != calibration_remaining:
                    calibration_remaining = remaining
                    post_calibration_state(remaining, CALIBRATION_ROUNDS)
                    if remaining == 0:
                        post_event('info', f'✓ CALIBRAÇÃO concluída ({CALIBRATION_ROUNDS} rodadas observadas) — bot começa a apostar')
                    else:
                        post_event('info', f'🎯 calibrando: {done}/{CALIBRATION_ROUNDS} rodadas observadas')

            stats = get_state_for_autoclick()
            # 1.22.0 — tail signal ativo? Override total ignora todos os filtros de risco.
            panels_cfg = get_panels_cfg(cfg)
            tail_override_active = any(p.get('_tail_override') for p in panels_cfg)
            # 1.22.2 — modo atual (cacheia por iteração). Em pos10x, calibração é bypassada.
            op_mode_now = get_operation_mode()
            if op_mode_now == 'pos10x' and calibration_remaining > 0:
                calibration_remaining = 0
                post_calibration_state(0, CALIBRATION_ROUNDS)
                post_event('info', '🎯 modo Pós-10x ativado — calibração cancelada')
            # Parada técnica: daemon continua lendo painéis mas não coloca bets.
            paused_until_val = stats.get('paused_until', 0) or 0
            tech_paused = (paused_until_val > time.time()) and not tail_override_active
            # 1.16.0: avaliação da janela de 8 shadow bets é feita no SERVER
            # (em /api/autoclick/shadow). Daemon só detecta transição
            # True→False — pode ser por cancelamento manual ou por window
            # de 8 atingindo ≥4 acertos.
            if last_tech_paused and not tech_paused:
                post_event('info', '✓ Parada técnica encerrada — retomando operação real')
                shadow_bets = {0: None, 1: None}
                tech_pause_start_at = 0.0
            # marca timestamp da entrada na parada técnica (filtro de crashes pré-pausa)
            if not last_tech_paused and tech_paused:
                tech_pause_start_at = time.time()
            last_tech_paused = tech_paused
            # 1.22.0 — bypass stop-loss se tail signal ativo (override total)
            stop, reason = check_stop_conditions(cfg, stats) if (not tech_paused and not tail_override_active) else (False, None)
            if stop:
                pause_min = float(cfg.get('techPauseMinutes', 30) or 30)
                post_event('err', f'PARADA TÉCNICA — {reason}')
                if trigger_tech_pause(pause_min, reason):
                    post_event('info', f'⏸ bot em parada técnica por {pause_min:.0f} min (só monitorando)')
                else:
                    post_event('err', 'Falha ao iniciar parada técnica via API')
                time.sleep(3)
                continue

            # Busca estado dos DOIS painéis em uma chamada só
            try:
                all_state_json = cdp.eval('JSON.stringify(window.__aviator.detectAll())')
                all_state = json.loads(all_state_json) if all_state_json else {}
            except Exception:
                all_state = {}
            # panels_cfg e tail_override_active já calculados acima (linha ~715)

            # Processa cada painel independentemente
            for p_idx in (0, 1):
                p_cfg = panels_cfg[p_idx] if p_idx < len(panels_cfg) else {'enabled': False}
                if not p_cfg.get('enabled'):
                    # Painel desligado → garante cooldown=0 na UI
                    if last_posted_cd[p_idx] != (0, None):
                        post_panel_state(p_idx, 0, None)
                        last_posted_cd[p_idx] = (0, None)
                    continue

                # Calcula maior cooldown remaining e reporta ao server (só quando muda)
                cd_gen = max(0, cooldown_until_crash[p_idx] - api_total)
                cd_loss = max(0, loss_wait_until[p_idx] - api_total)
                cd_win = max(0, win_wait_until[p_idx] - api_total)
                if cd_win >= cd_loss and cd_win >= cd_gen:
                    cd_rem, cd_kind = cd_win, ('win' if cd_win > 0 else None)
                elif cd_loss >= cd_gen:
                    cd_rem, cd_kind = cd_loss, ('loss' if cd_loss > 0 else None)
                else:
                    cd_rem, cd_kind = cd_gen, ('general' if cd_gen > 0 else None)
                if active_bets[p_idx] is not None:
                    cd_rem, cd_kind = 0, None  # aposta aberta, não é cooldown
                cur_state = (cd_rem, cd_kind)
                if cur_state != last_posted_cd[p_idx]:
                    post_panel_state(p_idx, cd_rem, cd_kind)
                    last_posted_cd[p_idx] = cur_state

                g = all_state.get(f'panel{p_idx}', {}) or {}
                phase = g.get('phase')
                cashout_value = g.get('cashoutValue')
                active_bet = active_bets[p_idx]

                mult = None
                if cashout_value is not None and active_bet is not None:
                    bet_amt = active_bet['amount']
                    if bet_amt > 0:
                        mult = cashout_value / bet_amt

                # Log mudança de fase
                if phase != last_phase[p_idx]:
                    post_event('info', f'[P{p_idx+1}] FASE: {last_phase[p_idx]} → {phase} (btn={g.get("btnText","")[:30]} val={cashout_value})')
                    last_phase[p_idx] = phase
                    if phase != 'flight':
                        flight_peak[p_idx] = 0

                # Log progressão do flight (a cada 0.5x+)
                if phase == 'flight' and mult is not None and mult >= flight_peak[p_idx] + 0.5:
                    flight_peak[p_idx] = mult
                    post_event('info', f'[P{p_idx+1}] flight: {mult:.2f}x (target={active_bet["target"] if active_bet else "—"})')

                # === RESOLVE PENDING HIT: espera crash real da rodada ===
                if pending_hits[p_idx] is not None:
                    ph = pending_hits[p_idx]
                    crash_val = None
                    k = api_total - ph['snap']  # quantos crashes registraram desde a aposta
                    if k >= 1:
                        crashes_list = api_state.get('crashes') or []
                        # O crash DA rodada da aposta é o k-ésimo do fim (posição -k).
                        # Se o daemon atrasou e k cresceu, ainda achamos o certo.
                        if len(crashes_list) >= k:
                            crash_val = crashes_list[-k].get('value')
                    timeout = (time.time() - ph['placed_at']) > 60
                    if crash_val is not None or timeout:
                        # Fallback: se não pegou o crash, usa o cashout_mult
                        send_crash = crash_val if crash_val is not None else ph['cashout_mult']
                        post_event(
                            'info', f'[P{p_idx+1}] hit: +R${ph["delta"]:.2f}',
                            panel=p_idx, hit=True, delta_pnl=ph['delta'],
                            target=ph['target'], crash=send_crash,
                            snap=ph['snap'], placed_at=ph['placed_at'],
                        )
                        caw = int(p_cfg.get('cooldownAfterWin', 2) or 0)
                        if caw > 0:
                            # 1.16.1: ancora no snap da aposta, não no api_total
                            # atual (que já avançou ao resolver o pending hit).
                            win_wait_until[p_idx] = ph['snap'] + caw + 1
                            post_event('info', f'[P{p_idx+1}] aguardando {caw} rodada(s) após ganho pra reanálise', panel=p_idx)
                        pending_hits[p_idx] = None

                # === SHADOW EVALUATION (durante parada técnica) ===
                # 1.16.3: driven por crash. Cada incremento de api_total =
                # nova rodada terminou. Usamos o crash dessa rodada como
                # resultado simulado pro target configurado do painel.
                # Não depende mais de CDP phase detection (que pode ficar
                # inconsistente em pausas longas).
                # 1.17.0: shadow simulation removida.
                # Parada técnica agora apenas aguarda N rodadas e libera (server-side watchdog).
                # Daemon não opera durante pause (já bloqueado mais abaixo).

                # === BETTING PHASE ===
                if phase == 'betting' and active_bet is None:
                    # Se inteligência pediu pausa, bloqueia entrada
                    intel_pause = bool(icfg.get('enabled') and istate.get('should_pause'))
                    intel_cd_extra = int(istate.get('cooldown_add', 0)) if icfg.get('enabled') else 0
                    intel_mult = float(istate.get('bet_multiplier', 1.0)) if icfg.get('enabled') else 1.0

                    # 1.22.0 — tail_override bypassa todos os filtros abaixo
                    p_tail_override = bool(p_cfg.get('_tail_override'))
                    # 1.22.2 — em modo pos10x, calibração nunca bloqueia
                    if calibration_remaining > 0 and not p_tail_override and op_mode_now != 'pos10x':
                        pass  # 1.17.0 — em calibração inicial, observando rodadas
                    elif intel_pause and not p_tail_override:
                        pass  # bot em pausa pela inteligência
                    elif tech_paused and not p_tail_override:
                        pass  # parada técnica global: só observa
                    elif api_total < cooldown_until_crash[p_idx] and not p_tail_override:
                        pass
                    elif api_total < loss_wait_until[p_idx] and not p_tail_override:
                        pass
                    elif api_total < win_wait_until[p_idx] and not p_tail_override:
                        pass
                    else:
                        if p_tail_override:
                            # 1.22.0 — sinal Pós-10x força entrada, ignora should_enter
                            enter, why = True, 'tail_override'
                        else:
                            enter, why = should_enter(p_cfg, api_state)

                        # Janela de horário configurada no painel
                        if enter and not in_schedule_window(p_cfg) and not p_tail_override:
                            st = p_cfg.get('startTime', ''); et = p_cfg.get('endTime', '')
                            if last_ia_decision[p_idx] != 'out_of_schedule':
                                last_ia_decision[p_idx] = 'out_of_schedule'
                                post_event('skip', f'[P{p_idx+1}] fora da janela {st}–{et}')
                            enter = False

                        # Gate de IA: se painel tem useIA=true, consulta IA antes
                        ia_used = False
                        ia_approved = False
                        pattern_key = None
                        prob = None  # 1.23.2 — inicializa pra evitar UnboundLocalError quando IA é pulada
                        if enter and p_cfg.get('useIA') and not p_tail_override:
                            ia_used = True
                            target_check = p_cfg.get('cashoutTarget', 1.5)
                            approve, prob, pattern_key = ia_gate(target_check, panel=p_idx, margin=margin_for_panel(p_cfg))
                            ia_approved = approve
                            decision = 'approve' if approve else 'reject'
                            # Só loga quando a decisão MUDA (evita spam)
                            if decision != last_ia_decision[p_idx]:
                                last_ia_decision[p_idx] = decision
                                if approve:
                                    post_event('info', f'[P{p_idx+1}] IA liberou: prob {(prob or 0)*100:.1f}%')
                                else:
                                    post_event('skip', f'[P{p_idx+1}] IA aguarda: prob {(prob or 0)*100:.1f}% < teórico')
                            if not approve:
                                enter = False

                        if enter and time.time() - last_action_at[p_idx] > 2:
                            base_amount = p_cfg.get('betAmount', 1)
                            amount = round(base_amount * intel_mult, 2)
                            # Sizing por confiança da IA: só faz sentido se useIA=true
                            # e o gate retornou probabilidade.
                            conf_tier = None
                            if p_cfg.get('confidenceSizing') and ia_used and isinstance(prob, (int, float)):
                                # UI grava thresholds em % (ex: 85), prob vem do gate em fração (0.85).
                                # Normaliza: se valor > 1.5, assume que está em % e divide por 100.
                                def _norm_thr(v, default):
                                    v = float(v if v is not None else default)
                                    return v / 100 if v > 1.5 else v
                                high_t = _norm_thr(p_cfg.get('confHighThreshold'), 85)
                                med_t = _norm_thr(p_cfg.get('confMedThreshold'), 70)
                                high_m = float(p_cfg.get('confHighMultiplier', 5) or 1)
                                med_m = float(p_cfg.get('confMedMultiplier', 2) or 1)
                                if prob >= high_t:
                                    amount = round(amount * high_m, 2); conf_tier = f'ALTA ({prob*100:.0f}% ≥ {high_t*100:.0f}%) ×{high_m:g}'
                                elif prob >= med_t:
                                    amount = round(amount * med_m, 2); conf_tier = f'MÉDIA ({prob*100:.0f}% ≥ {med_t*100:.0f}%) ×{med_m:g}'
                                else:
                                    conf_tier = f'BASE ({prob*100:.0f}% < {med_t*100:.0f}%) ×1'
                            if amount < 0.50:
                                amount = 0.50
                            target = p_cfg.get('cashoutTarget', 1.5)
                            if conf_tier:
                                post_event('info', f'[P{p_idx+1}] confiança {conf_tier} → aposta R${amount:.2f}', panel=p_idx)
                            delay = jitter(cfg)
                            post_event('info', f'[P{p_idx+1}] entrada decidida ({why}) — jitter {delay:.2f}s')
                            time.sleep(delay)
                            # 1.17.0: re-checa config logo antes de clicar — evita race
                            # com watchdog do cenário desabilitando painel no meio.
                            # 1.23.1: pula em tail_override (panels raw ficam False em pos10x;
                            # o tail-override é quem liga dinamicamente).
                            if not p_tail_override:
                                try:
                                    fresh_cfg = get_config()
                                    fresh_panels = (fresh_cfg or {}).get('panels')
                                    if isinstance(fresh_panels, list) and len(fresh_panels) > p_idx:
                                        fresh_p = fresh_panels[p_idx] or {}
                                        if 'enabled' in fresh_p and not fresh_p['enabled']:
                                            post_event('info', f'[P{p_idx+1}] aborta — painel foi desativado pelo cenário', panel=p_idx)
                                            continue
                                except Exception:
                                    pass  # erro silencioso = mantém decisão
                            if cfg.get('dryRun'):
                                post_event('dry', f'[P{p_idx+1}] [DRY-RUN] apostaria R${amount:.2f} visando {target}x', panel=p_idx, ia_approved=ia_approved, amount=amount, target=target, pattern=pattern_key, probability=prob)
                                active_bets[p_idx] = {
                                    'amount': amount, 'target': target,
                                    'placed_at': time.time(), 'api_total_snap': api_total,
                                    'panel': p_idx, 'dry': True,
                                }
                            else:
                                cdp.eval(f'JSON.stringify(window.__aviator.setBetAmount({p_idx}, {amount}))')
                                clicked = json.loads(cdp.eval(f'JSON.stringify(window.__aviator.clickBet({p_idx}))'))
                                if clicked.get('ok'):
                                    fresh_total = get_state().get('stats', {}).get('total', api_total)
                                    post_event('bet', f'[P{p_idx+1}] APOSTA R${amount:.2f} visando {target}x (snap {fresh_total})', panel=p_idx, ia_approved=ia_approved, amount=amount, target=target, pattern=pattern_key, probability=prob)
                                    active_bets[p_idx] = {
                                        'amount': amount, 'target': target,
                                        'placed_at': time.time(), 'api_total_snap': fresh_total,
                                        'panel': p_idx,
                                    }
                                    # Arma cashout no lado do navegador (20ms loop
                                    # no iframe). Clica assim que atinge o target.
                                    try:
                                        cdp.eval(f'JSON.stringify(window.__aviator.armCashout({p_idx}, {target}, {amount}))')
                                    except Exception:
                                        pass
                                else:
                                    post_event('err', f'[P{p_idx+1}] bet falhou: {clicked.get("reason","?")}', panel=p_idx)
                            last_action_at[p_idx] = time.time()
                            cooldown_until_crash[p_idx] = api_total + p_cfg.get('cooldown', 0) + 1 + intel_cd_extra

                # === FLIGHT PHASE: cashout (só em modo real) ===
                if phase == 'flight' and active_bet is not None and not active_bet.get('dry'):
                    target_mult = active_bet['target']
                    amount = active_bet['amount']
                    # Primeiro verifica se o arm do browser já clicou. Isso é o
                    # caminho feliz — cashout 20ms após a DOM atualizar.
                    armed_fired = False
                    try:
                        armed_json = cdp.eval(f'JSON.stringify(window.__aviator.checkArmedResult({p_idx}))')
                        armed = json.loads(armed_json) if armed_json and armed_json != 'null' else None
                    except Exception:
                        armed = None
                    if armed and armed.get('clicked'):
                        armed_fired = True
                        arm_mult = float(armed.get('mult', target_mult))
                        arm_val = float(armed.get('val', amount * target_mult))
                        post_event('out', f'[P{p_idx+1}] CASHOUT em {arm_mult:.2f}x (valor R${arm_val:.2f})', panel=p_idx)
                        delta = arm_val - amount
                        pending_hits[p_idx] = {
                            'amount': amount, 'target': target_mult,
                            'cashout_mult': arm_mult, 'delta': delta,
                            'snap': api_total, 'placed_at': time.time(),
                        }
                        active_bets[p_idx] = None
                    # Fallback: se arm não disparou e o poll Python capturou
                    # mult >= target (raro, o arm deveria ter feito primeiro).
                    elif mult is not None and mult >= target_mult:
                        clicked_json = cdp.eval(f'JSON.stringify(window.__aviator.clickCashout({p_idx}))')
                        clicked = json.loads(clicked_json) if clicked_json else {'ok': False}
                        if clicked.get('ok'):
                            post_event('out', f'[P{p_idx+1}] CASHOUT em {mult:.2f}x (valor R${cashout_value:.2f}) [fallback]', panel=p_idx)
                            delta = cashout_value - amount
                            pending_hits[p_idx] = {
                                'amount': amount, 'target': target_mult,
                                'cashout_mult': mult, 'delta': delta,
                                'snap': api_total, 'placed_at': time.time(),
                            }
                            active_bets[p_idx] = None
                            try: cdp.eval(f'window.__aviator.cancelArmed({p_idx})')
                            except Exception: pass
                        else:
                            post_event('err', f'[P{p_idx+1}] cashout FALHOU em {mult:.2f}x — {clicked.get("reason","?")}', panel=p_idx)

                # Marca saw_flight pra detecção de miss (apostas reais)
                if active_bets[p_idx] is not None and not active_bets[p_idx].get('dry') and phase == 'flight':
                    active_bets[p_idx]['saw_flight'] = True

                def _register_miss(reason_tag):
                    ab = active_bets[p_idx]
                    if ab is None: return
                    amount = ab['amount']
                    caf = int(p_cfg.get('cooldownAfterLoss', 0) or 0)
                    # Cancela arm no browser pra não clicar depois
                    try: cdp.eval(f'window.__aviator.cancelArmed({p_idx})')
                    except Exception: pass
                    # Pega o crash da rodada APOSTADA (indexado pela distância
                    # entre api_total atual e o snap da aposta).
                    snap = ab.get('api_total_snap', api_total)
                    k = max(1, api_total - snap)
                    crashes_list = api_state.get('crashes') or []
                    crash_val = None
                    if len(crashes_list) >= k:
                        crash_val = crashes_list[-k].get('value')
                    post_event('info', f'[P{p_idx+1}] miss: −R${amount:.2f} ({reason_tag})', panel=p_idx, hit=False, delta_pnl=-amount, target=ab.get('target'), crash=crash_val, snap=ab.get('api_total_snap'), placed_at=ab.get('placed_at'))
                    active_bets[p_idx] = None
                    if caf > 0:
                        # 1.16.1: ancora no snap da aposta. Se a detecção do
                        # miss veio tarde (api_total já passou snap+2+),
                        # cooldown não fica mais longo que o configurado.
                        loss_wait_until[p_idx] = snap + caf + 1
                        post_event('info', f'[P{p_idx+1}] aguardando {caf} rodada(s) após perda', panel=p_idx)

                # Resolução de aposta simulada (DRY-RUN): acha o PRIMEIRO crash
                # cujo timestamp é POSTERIOR ao momento da aposta. Imune à race
                # em que api_total advança por snippet atrasado (crash da rodada
                # ANTERIOR) — sem contagem, só tempo real.
                if active_bets[p_idx] is not None and active_bets[p_idx].get('dry'):
                    ab = active_bets[p_idx]
                    snap = ab.get('api_total_snap', api_total)
                    amount = ab['amount']
                    target_mult = ab['target']
                    placed_at = ab.get('placed_at', time.time())
                    age = time.time() - placed_at
                    # Procura 1º crash ESTRITAMENTE após a colocação da aposta.
                    # Sem margem — evita que crash do instante anterior ao bet
                    # seja usado por engano (bug da "derrota imediata ao ligar").
                    crashes_list = api_state.get('crashes') or []
                    final_mult = None
                    for c in crashes_list:
                        if (c.get('at') or 0) > placed_at:
                            final_mult = c.get('value'); break
                    if final_mult is not None:
                        if final_mult >= target_mult:
                            delta = round(amount * (target_mult - 1), 2)
                            post_event('dry', f'[P{p_idx+1}] [DRY-RUN] CASHOUT em {target_mult:.2f}x (crash da rodada: {final_mult:.2f}x)', panel=p_idx)
                            post_event('info', f'[P{p_idx+1}] hit (DRY): +R${delta:.2f}', panel=p_idx, hit=True, delta_pnl=delta, target=target_mult, crash=final_mult, snap=snap, placed_at=ab.get('placed_at'))
                            caw = int(p_cfg.get('cooldownAfterWin', 2) or 0)
                            if caw > 0:
                                # 1.16.1: ancora no snap, não no api_total.
                                win_wait_until[p_idx] = snap + caw + 1
                                post_event('info', f'[P{p_idx+1}] aguardando {caw} rodada(s) após ganho pra reanálise', panel=p_idx)
                        else:
                            shown = f'{final_mult:.2f}x' if final_mult is not None else '?'
                            post_event('info', f'[P{p_idx+1}] miss (DRY): −R${amount:.2f} (crash {shown} < target {target_mult:.2f}x)', panel=p_idx, hit=False, delta_pnl=-amount, target=target_mult, crash=final_mult, snap=snap, placed_at=ab.get('placed_at'))
                            caf = int(p_cfg.get('cooldownAfterLoss', 0) or 0)
                            if caf > 0:
                                # 1.16.1: ancora no snap, não no api_total.
                                loss_wait_until[p_idx] = snap + caf + 1
                                post_event('info', f'[P{p_idx+1}] aguardando {caf} rodada(s) após perda', panel=p_idx)
                        active_bets[p_idx] = None
                    elif age > 90:
                        post_event('info', f'[P{p_idx+1}] miss (DRY): timeout {age:.0f}s', panel=p_idx, hit=False, delta_pnl=-amount)
                        active_bets[p_idx] = None

                # Miss de aposta REAL (3 caminhos) — mas ANTES checa se o
                # JS arm já disparou cashout que o daemon ainda não consumiu.
                # Race típica: plane reached target, JS clicou, daemon não
                # conseguiu ler armed result a tempo, phase já virou betting.
                if active_bets[p_idx] is not None and not active_bets[p_idx].get('dry'):
                    ab = active_bets[p_idx]
                    snap = ab.get('api_total_snap', api_total)
                    age = time.time() - ab.get('placed_at', time.time())
                    # phase voltou a 'betting' vinda de algum estado
                    # de jogo em andamento = rodada terminou.
                    round_ended_via_phase = (
                        phase == 'betting' and age > 3
                        and last_phase[p_idx] in ('waiting_flight', 'flight', 'unknown')
                    )
                    potential_miss = (
                        (ab.get('saw_flight') and phase != 'flight')
                        or (api_total >= snap + 2)
                        or round_ended_via_phase
                        or (age > 90)
                    )
                    if potential_miss:
                        # Última chance: checa armed result. Se o JS clicou
                        # cashout com sucesso, converte em hit.
                        armed_late = None
                        try:
                            aj = cdp.eval(f'JSON.stringify(window.__aviator.checkArmedResult({p_idx}))')
                            armed_late = json.loads(aj) if aj and aj != 'null' else None
                        except Exception:
                            armed_late = None
                        if armed_late and armed_late.get('clicked'):
                            arm_mult = float(armed_late.get('mult', ab['target']))
                            arm_val = float(armed_late.get('val', ab['amount'] * ab['target']))
                            delta_late = arm_val - ab['amount']
                            post_event('out', f'[P{p_idx+1}] CASHOUT em {arm_mult:.2f}x (valor R${arm_val:.2f}) [arm tardio]', panel=p_idx)
                            pending_hits[p_idx] = {
                                'amount': ab['amount'], 'target': ab['target'],
                                'cashout_mult': arm_mult, 'delta': delta_late,
                                'snap': snap, 'placed_at': time.time(),
                            }
                            active_bets[p_idx] = None
                        else:
                            # De fato miss
                            if ab.get('saw_flight') and phase != 'flight':
                                _register_miss('fim do flight sem cashout')
                            elif api_total >= snap + 2:
                                _register_miss(f'rodada completou invisível (api_total {snap}→{api_total})')
                            elif round_ended_via_phase:
                                _register_miss('rodada encerrada sem flight visível')
                            elif age > 90:
                                _register_miss(f'timeout {age:.0f}s sem resolução')

            # Polling agressivo (50ms) quando QUALQUER painel está com aposta
            # aberta em flight — evita perder a janela de cashout em rounds curtos.
            any_live_flight = any(
                ab is not None and not ab.get('dry')
                for ab in active_bets.values()
            ) and any(
                (all_state.get(f'panel{i}', {}) or {}).get('phase') == 'flight'
                for i in (0, 1)
            )
            time.sleep(0.05 if any_live_flight else 0.15)
        except KeyboardInterrupt:
            print('\n[autoclick] ctrl+c')
            break
        except (ConnectionError, TimeoutError) as e:
            post_event('err', f'conexão CDP caiu: {e}')
            if cdp: cdp.alive = False
            time.sleep(2)
        except Exception as e:
            tb = traceback.format_exc()
            print(tb)
            post_event('err', f'exception: {type(e).__name__}: {e} | {tb.splitlines()[-3] if len(tb.splitlines())>=3 else tb}')
            time.sleep(2)

    cdp.close()


def get_state_for_autoclick():
    """Pega stats acumuladas + estado de pausa via /api/autoclick/status."""
    try:
        r = urllib.request.urlopen(BACKEND + '/api/autoclick/status', context=ssl_ctx, timeout=2)
        d = json.loads(r.read())
        stats = d.get('stats', {'pnl_today': 0, 'bets_today': 0, 'loss_streak': 0})
        stats['paused_until'] = d.get('paused_until', 0) or 0
        return stats
    except Exception:
        return {'pnl_today': 0, 'bets_today': 0, 'loss_streak': 0, 'paused_until': 0}


if __name__ == '__main__':
    main()
