#!/usr/bin/env python3
# ============================================================================
# RAMA A (BORRADOR — NO desplegado) · cross-check main-thread en el watchdog
# ============================================================================
# OBJETIVO: dejar de reiniciar el JUEGO por FALSOS POSITIVOS del watchdog.
#
# HOY: _watchdog_action declara "congelado" MIRANDO SOLO el heartbeat (last_hb).
#   heartbeat mudo >30s  ->  force-stop + relaunch de Evony (pierde login, ~1-1,5min).
# El monitor del 2026-08-24 (60min) mostró 2 de estos, ambos con el MAIN THREAD sano
# (mt.freezes=0, gap 500ms) durante la ráfaga AUTO-JOIN+GEN_STATES -> probable falso positivo.
#
# QUÉ HACE ESTE PATCH (backend-only, sin recompilar el agente):
#   1) Sella AGENT["mt_live_ts"] cada vez que mt.ticks (frames del MAIN THREAD, se incrementa
#      DENTRO del hook WMM.Update) avanza -> "cuándo vi por última vez a Unity renderizar".
#   2) En el watchdog: un freeze SOLO por heartbeat viejo, pero con mt.ticks FRESCO (<MT_LIVE_MAX)
#      y escena cargada (ready), NO reinicia el juego -> hace RE-ATTACH (barato; y como el proceso
#      NO está wedged —mt fresco lo prueba— el re-attach NO cae en los 6 timeouts que motivaron
#      saltárselo en un freeze real). El freeze REAL (mt también viejo) y el zombie (never_ready)
#      siguen la escalada actual hot->cold intacta.
#
# ⚠️ EFECTIVIDAD CONDICIONADA A LA VENTANA DE CONFIRMACIÓN:
#   scan_metrics (quien reporta mt.ticks) es un setInterval(10s) en el MISMO hilo de frida que el
#   heartbeat. Si en un lapso mueren los DOS timers a la vez, mt.ticks se congela y esta rama NO
#   dispara (haría falta la RAMA B: sonda activa ping->pong drenada en WMM.Update, que sí requiere
#   recompilar el agente). La ventana de 90min (watch_freeze_cause.py, señal mt.ticks) dirá si
#   durante los lapsos mt.ticks SIGUE avanzando (=> Rama A vale) o se congela (=> hace falta Rama B).
#   NO desplegar hasta ver ese dato.
#
# TUNING: MT_LIVE_MAX=25s (~2,5× la cadencia de scan_metrics). Bajar acerca a "solo cuenta un
#   reporte muy reciente"; subir es más permisivo (menos reinicios, más riesgo de no reiniciar un
#   freeze real). Backward-compatible: el nuevo arg mt_live_age=1e9 por defecto => comportamiento
#   idéntico al actual si no se pasa (no rompe tests de _watchdog_action).
#
# RIESGO RESIDUAL: si el re-attach no revive el heartbeat pero mt sigue fresco, podría re-attachar
#   varias veces; lo acota que script=None bloquea re-entrar hasta attachar, y el camino
#   "re-attach imposible (wedged)" ya escala a reinicio del juego a los 90s. Revisar en Rama B.
#
# CÓMO APLICAR (tras confirmar en la ventana + OK del usuario):
#   /usr/bin/python3 apply_rama_a_watchdog.py           # crea bot_web.py.bak_ramaA, valida AST
#   <reiniciar la V2>                                    # el cambio NO está activo hasta reiniciar
# ROLLBACK:  cp bot_web.py.bak_ramaA bot_web.py  && reiniciar
# ============================================================================
import io, os, ast, sys

P = os.path.join(os.path.dirname(os.path.abspath(__file__)), "bot_web.py")
s = io.open(P, encoding="utf-8").read()
orig = s


def rep(old, new, tag):
    global s
    n = s.count(old)
    assert n == 1, "ANCLA MOVIDA [%s]: encontrada %d veces (esperaba 1) -> revisar bot_web.py a mano" % (tag, n)
    s = s.replace(old, new)


# 1) AGENT: añadir mt_live_ts / mt_ticks
rep('"stopped_kick": False}',
    '"stopped_kick": False, "mt_live_ts": 0, "mt_ticks": 0}',
    "AGENT init")

# 2) scan_metrics: sellar liveness cuando mt.ticks avanza
rep('''            elif k == "scan_metrics":
                scanner_store.ingest_metrics(p)''',
    '''            elif k == "scan_metrics":
                scanner_store.ingest_metrics(p)
                _tk = (p.get("mt") or {}).get("ticks")   # RAMA A: mt.ticks = frames del MAIN THREAD (se incrementa dentro de WMM.Update); si cambia, Unity está VIVO
                if isinstance(_tk, (int, float)) and _tk != (AGENT.get("mt_ticks") or 0):
                    AGENT["mt_ticks"] = _tk; AGENT["mt_live_ts"] = time.time()''',
    "scan_metrics liveness stamp")

# 3) MT_LIVE_MAX + firma de _watchdog_action con mt_live_age
rep('def _watchdog_action(now, script, ready, hb, att, last_hot, cold_age):',
    'MT_LIVE_MAX = 25   # RAMA A: si el MAIN THREAD reportó frames (mt.ticks) hace < esto, Unity está VIVO -> un heartbeat viejo NO justifica reiniciar el juego (~2,5x la cadencia de scan_metrics, 10s)\n'
    'def _watchdog_action(now, script, ready, hb, att, last_hot, cold_age, mt_live_age=1e9):',
    "MT_LIVE_MAX + firma")

# 4) gate del freeze: atajo a re-attach si el main thread está vivo
rep('''    if not ((hb and stale > 30) or never_ready): return None
    hot_ago = now - last_hot''',
    '''    hb_freeze = bool(hb and stale > 30)
    if not (hb_freeze or never_ready): return None
    # RAMA A (cross-check main-thread): freeze SOLO por heartbeat viejo, pero con el MAIN THREAD aún
    # avanzando frames (mt.ticks fresco) y escena cargada (ready) = el bucle de frida se retrasó pero
    # Unity vive y la sesión es buena -> reiniciar el JUEGO sería falso positivo. RE-ATTACH: barato, y
    # como el proceso NO está wedged (mt fresco lo prueba) el re-attach NO cae en los 6 timeouts.
    if hb_freeze and not never_ready and ready and mt_live_age < MT_LIVE_MAX:
        return "reattach"
    hot_ago = now - last_hot''',
    "freeze gate + atajo re-attach")

# 5) call-site: calcular y pasar mt_live_age
rep('''        act = _watchdog_action(now, AGENT.get("script"), AGENT.get("ready"), AGENT.get("last_hb") or 0,
                               AGENT.get("attached_at") or 0, wd["last_game_restart"], _cold_boot_age())''',
    '''        _mt_age = (now - AGENT["mt_live_ts"]) if AGENT.get("mt_live_ts") else 1e9
        act = _watchdog_action(now, AGENT.get("script"), AGENT.get("ready"), AGENT.get("last_hb") or 0,
                               AGENT.get("attached_at") or 0, wd["last_game_restart"], _cold_boot_age(), _mt_age)''',
    "call-site mt_live_age")

# 6) rama del caller "reattach": log claro + gracia para que el agente nuevo lata
rep('''        elif act == "reattach":
            logmsg("WATCHDOG: sin heartbeat / escena no cargada -> re-attach"); AGENT["script"] = None; AGENT["ready"] = False''',
    '''        elif act == "reattach":
            logmsg("WATCHDOG: heartbeat viejo pero MAIN-THREAD VIVO (mt.ticks fresco) -> re-attach (NO reinicio el juego: la sesion sigue buena)")
            AGENT["script"] = None; AGENT["ready"] = False; AGENT["last_hb"] = now; AGENT["attached_at"] = now   # gracia: el agente nuevo tiene ~30s para su 1er latido''',
    "caller reattach branch")

# validar sintaxis ANTES de escribir
try:
    ast.parse(s)
except SyntaxError as e:
    print("ABORTADO: el resultado no compila (%s). bot_web.py NO tocado." % e); sys.exit(1)

if s == orig:
    print("Sin cambios (¿ya aplicado?)."); sys.exit(0)

with open(P + ".bak_ramaA", "w", encoding="utf-8") as f:
    f.write(orig)
io.open(P, "w", encoding="utf-8").write(s)
print("RAMA A aplicada a bot_web.py (backup en bot_web.py.bak_ramaA). Reinicia la V2 para activarla.")
