#!/usr/bin/env python3
"""
Arena 持久 DOM Watcher - 陣列式多席位系統（v3 + ca.js 精準版）
每個席位一個獨立長駐 browser，每 30 秒一個 cycle
- 見 Stop 掣（生成緊）→ 唔郁
- 見 Keep working → 撳 → 等 5 秒 → reload → 等頁面生成 → 等 5 秒 → 確認消失 → send
- 閒置 → 直接 send
- Send 後：等 10 秒 → reload → 等生成 → 等 5 秒 → 有 Stop 掣/Keep working = 成功
- Browser crash → 自動重開
- 登入檢查：跟 ca.js（電郵 + Log In 掣），未登入自動行 login flow（見密碼即停手）
- 不登出、不清 cookies/cache/profile

用法: watcher.py <席位ID> [--once]
  --once: 只跑一次（測試用）
"""
import sys
import os
import time
import json
from pathlib import Path

# 配置
SYSTEM_DIR = Path.home() / "workspace" / "arena-system"
SEATS_CONF = SYSTEM_DIR / "seats.conf"
VPN_CONF = Path.home() / "workspace" / "vpn-tunnel" / "vpn-proxies.conf"
PROFILES_DIR = Path.home() / "workspace" / "arena-system" / "local-profiles"  # 直接用 workspace，唔用 RAM（owner 2026-10-05）

# v0.92: CA 兩套 checkbox 完整跟足（owner 2026-10-05 15:38 下令）
# 第1套（橙色）：檢測 (527,409) #f6821f → 點擊 (270,420)
# 第2套（藍色 Cloudflare）：檢測 (910,420) 200x60 #1A73E8 → 點擊 (276,425)
# v0.92 第三套（紫色）
PURPLE_CHECK_X = 715
PURPLE_CHECK_Y = 415
PURPLE_CHECK_COLOR = "#f3e9f7"
PURPLE_CLICK_X = 580
PURPLE_CLICK_Y = 605

ORANGE_CHECK_X = 527
ORANGE_CHECK_Y = 409
ORANGE_CHECK_COLOR = '#f6821f'
ORANGE_CLICK_X = 270
ORANGE_CLICK_Y = 420
BLUE_CHECK_X = 910
BLUE_CHECK_Y = 420
BLUE_CHECK_W = 200
BLUE_CHECK_H = 60
BLUE_CHECK_COLOR = '#1A73E8'
BLUE_CLICK_X = 276
BLUE_CLICK_Y = 425
MASTER_PROFILES_DIR = SYSTEM_DIR / "profiles"  # 只讀 master
LOGS_DIR = SYSTEM_DIR / "logs"
RECALL_MSG = "Continue SESSION 2147483647"
CHECK_INTERVAL = 30  # 秒

def load_seats():
    seats = {}
    with open(SEATS_CONF) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = line.split()
            if len(parts) >= 6:
                sid, profile, vpn, url, account = parts[0], parts[1], parts[2], parts[3], parts[4]
                status = parts[5] if len(parts) > 5 else "active"
                seats[sid] = {
                    "profile": profile, "vpn": vpn, "url": url,
                    "account": account, "status": status
                }
    return seats

def load_vpn_ports():
    ports = {}
    with open(VPN_CONF) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            parts = line.split()
            if len(parts) >= 2:
                ports[parts[0]] = int(parts[1])
    return ports

def log(sid, msg):
    ts = time.strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] [{sid}] {msg}"
    print(line, flush=True)
    logfile = LOGS_DIR / f"watcher-{sid}.log"
    with open(logfile, "a") as f:
        f.write(line + "\n")
    # trim to 5000 lines（errors=ignore 防亂碼 crash，2026-10-05 修）
    with open(logfile, errors="ignore") as f:
        lines = f.readlines()
    if len(lines) > 5000:
        with open(logfile, "w") as f:
            f.writelines(lines[-5000:])

def is_generating(page):
    """檢查係咪生成緊（跟 ca.js：唔係搵 ■ 字，係搵 Stop 掣）
    selector: button[aria-label="Stop generating"], button[aria-label="Stop generation"]
    用 .last()，check is_visible()。有 = 生成緊。
    """
    try:
        stop_btn = page.locator('button[aria-label="Stop generating"], button[aria-label="Stop generation"]').last
        if stop_btn.count() > 0:
            try:
                if stop_btn.is_visible(timeout=300):
                    return True
            except Exception:
                pass
        return False
    except Exception:
        return False

def get_arena_logged_in_email(page):
    """跟 ca.js getArenaLoggedInEmail：用 JS evaluate 喺 sidebar DOM 搵電郵。
    搵：ul[data-sidebar="menu"] button → ul[data-sidebar="menu"] → button → 成個 body
    用 email regex 抽。搵到 = 已登入（ca.js：唯一登入證據＝真實電郵）。
    """
    try:
        email = page.evaluate("""() => {
            const emailRe = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i;
            const roots = Array.from(document.querySelectorAll('ul[data-sidebar="menu"] button, ul[data-sidebar="menu"], button'));
            for (const el of roots) {
                const text = (el.innerText || el.textContent || '').trim();
                const m = text.match(emailRe);
                if (m) return m[0];
            }
            const bodyText = (document.body && document.body.innerText || '').trim();
            const m = bodyText.match(emailRe);
            return m ? m[0] : null;
        }""")
        return email
    except Exception:
        return None

def find_exact_text_login_button(page):
    """跟 ca.js findExactTextLoginButton（已修正：唔分大小寫，因頁面有時係 "Log in"）：
    先 get_by_role('button', name='Log In', exact=True)，唔得就逐個 button 睇 innerText（case-insensitive）
    """
    import re
    try:
        rb = page.get_by_role("button", name="Log In", exact=True).first
        if rb.count() > 0:
            try:
                if rb.is_visible(timeout=700):
                    return rb
            except Exception:
                pass
    except Exception:
        pass
    try:
        # 唔分大小寫：Log In / Log in 都得
        rb2 = page.get_by_role("button", name=re.compile(r"^log in$", re.IGNORECASE)).first
        if rb2.count() > 0:
            try:
                if rb2.is_visible(timeout=700):
                    return rb2
            except Exception:
                pass
    except Exception:
        pass
    try:
        buttons = page.locator('button[type="button"], button')
        count = buttons.count()
        for i in range(count):
            try:
                b = buttons.nth(i)
                if not b.is_visible(timeout=300):
                    continue
                t = b.inner_text(timeout=1000).replace("\n", " ").strip()
                t = re.sub(r"\s+", " ", t)
                if t.lower() == "log in":
                    return b
            except Exception:
                continue
    except Exception:
        pass
    return None

def wait_for_sidebar_button(page, sid, timeout_ms=120000):
    """120s 內一直搵左面 slide menu 個制，搵到 = loaded。
    用 expand_sidebar 嗰兩組 selector（group/toggle-btn + ca.js aria-label），淨係搵唔撳。
    """
    import time as _time
    deadline = _time.time() * 1000 + timeout_ms
    sels = [
        'button[class*="group/toggle-btn"]',
        'button[aria-label*="sidebar" i], button[aria-label*="menu" i], button[aria-label*="open" i]',
    ]
    while _time.time() * 1000 < deadline:
        for sel in sels:
            try:
                btn = page.locator(sel).first
                if btn.count() > 0 and btn.is_visible(timeout=1000):
                    log(sid, f"搵到 slide menu 制（{sel}）= loaded")
                    return True
            except Exception:
                pass
        try:
            page.wait_for_timeout(1000)
        except Exception:
            break
    log(sid, "120s 都搵唔到 slide menu 制")
    return False

def expand_sidebar(page, sid):
    """撳 sidebar toggle，等 sidebar 內容 load 出嚟。
    跟 ca.js + 用戶俾嘅正確 DOM：
    1. agent page：button[class*="group/toggle-btn"]（無 aria-label，用戶俾嘅）
    2. Landing/homepage：button[aria-label*="sidebar" i], button[aria-label*="menu" i], button[aria-label*="open" i]（ca.js）
    撳之前先 check 開咗未（見到 Log In 掣或電郵 = 已開），開咗就唔撳，避免開閂打架。
    撳一下，delay 3s。
    """
    # 先 check：sidebar 已開（見到 Log In 掣或電郵）就唔使撳
    try:
        if find_exact_text_login_button(page) is not None:
            return True
    except Exception:
        pass
    try:
        if get_arena_logged_in_email(page):
            return True
    except Exception:
        pass
    # 方法 1：agent page 版（用戶俾嘅正確 DOM）
    try:
        btn = page.locator('button[class*="group/toggle-btn"]').first
        if btn.count() > 0 and btn.is_visible(timeout=3000):
            btn.click(timeout=8000)
            page.wait_for_timeout(5000)
            log(sid, "已撳 sidebar toggle（group/toggle-btn）")
            return True
    except Exception as e:
        log(sid, f"撳 group/toggle-btn 出錯: {e}")
    # 方法 2：ca.js 版（三個 aria-label pattern）
    try:
        tg = page.locator('button[aria-label*="sidebar" i], button[aria-label*="menu" i], button[aria-label*="open" i]').first
        if tg.count() > 0 and tg.is_visible(timeout=3000):
            tg.click(timeout=8000)
            page.wait_for_timeout(5000)
            log(sid, "已撳 sidebar toggle（ca.js aria-label 版）")
            return True
    except Exception as e:
        log(sid, f"撳 ca.js sidebar toggle 出錯/冇掣: {e}")
    return False

def ca_page_logged_in(page, sid):
    """跟 ca.js caPageLoggedIn：5 次輪詢搵電郵，奇數次開 sidebar。搵到電郵 = 已登入。"""
    for i in range(5):
        try:
            email = get_arena_logged_in_email(page)
            if email:
                log(sid, f"搵到電郵：{email}（第 {i+1} 次）→ 已登入")
                return True
            if i % 2 == 1:
                expand_sidebar(page, sid)
                email2 = get_arena_logged_in_email(page)
                if email2:
                    log(sid, f"開 sidebar 後搵到電郵：{email2} → 已登入")
                    return True
        except Exception as e:
            log(sid, f"登入輪詢第 {i+1} 次出錯: {e}")
        page.wait_for_timeout(900)
    return False

def find_keep_working(page, sid):
    """用 CSS :has-text() selector，逐個試，is_visible 先回傳。
    順序跟 ca.js fallback，包埋「有进展/有進展/Some progress」變體。
    """
    candidates = [
        'button:has-text("Keep working")',
        'button:has-text("有进展")',
        'button:has-text("有進展")',
        'button:has-text("Some progress")',
        'span:has-text("Keep working")',
        'span:has-text("有进展")',
        'span:has-text("有進展")',
        'button:has-text("继续工作")',
        'button:has-text("繼續工作")',
        'span:has-text("继续工作")',
        'span:has-text("繼續工作")',
    ]
    for sel in candidates:
        try:
            loc = page.locator(sel).first
            if loc.count() > 0 and loc.is_visible(timeout=600):
                log(sid, f"找到 Keep working：{sel}")
                return loc
        except Exception:
            continue
    return None

def wait_for_login_indicators(page, sid, timeout_ms=120000):
    """等登入指標出現：電郵（已登入）或 Log In 掣（未登入），唔用死時間。
    用 ca_page_logged_in 同 find_exact_text_login_button 輪詢。
    返 True = 見到其中一個，False = 超時。
    """
    import time as _time
    deadline = _time.time() * 1000 + timeout_ms
    while _time.time() * 1000 < deadline:
        try:
            if ca_page_logged_in(page, sid):
                log(sid, "等到登入指標：已登入（見到電郵）")
                return True
        except Exception:
            pass
        try:
            if find_exact_text_login_button(page) is not None:
                log(sid, "等到登入指標：未登入（見到 Log In 掣）")
                return True
        except Exception:
            pass
        try:
            page.wait_for_timeout(1000)
        except Exception:
            break
    log(sid, "等登入指標超時（未見電郵亦未見 Log In 掣）")
    return False

def wait_for_dom_stable(page, sid, timeout_ms=30000, stable_rounds=3, poll_ms=1000):
    """等 DOM 穩定（唔用死時間）：每 poll_ms 計一次 body hash，
    連續 stable_rounds 次無變化即當穩定。超時照返。
    """
    last_h = None
    stable = 0
    waited = 0
    while waited < timeout_ms:
        try:
            body = page.content()
            h = hash(body) & 0xFFFFFFFF
        except Exception:
            h = None
        if h is not None and h == last_h:
            stable += 1
            if stable >= stable_rounds:
                log(sid, f"DOM 已穩定（hash={h:08x}，{waited}ms）")
                return True
        else:
            stable = 0
        last_h = h
        page.wait_for_timeout(poll_ms)
        waited += poll_ms
    log(sid, f"等 DOM 穩定超時（{timeout_ms}ms），繼續")
    return False

def wait_for_page_generated(page, sid, timeout_ms=15000):
    """等頁面生成（有實際內容）後先返。唔係齋等 reload 完。
    用 body innerText 長度 > 100 做指標，代表頁面內容 render 咗。
    """
    try:
        page.wait_for_function(
            "document.body && document.body.innerText && document.body.innerText.length > 100",
            timeout=timeout_ms
        )
        return True
    except Exception as e:
        log(sid, f"等頁面生成超時: {e}")
        return False

def find_input_box(page, sid):
    """
    搵輸入框，三個方法逐個試（跟 8080：textarea 用 .last，contenteditable 用 .first，textbox 用 .last）
    避開 password field（Chrome autofill 會填密碼，唔可以掂）。
    返 locator 或 None
    """
    def is_password_field(loc):
        """檢查係咪 password field"""
        try:
            tag = loc.evaluate("el => el.tagName.toLowerCase()").lower()
            typ = loc.evaluate("el => (el.getAttribute('type') || '').toLowerCase()")
            if tag == "input" and typ == "password":
                return True
            # 檢查附近有冇密碼相關（name/id 含 password/passwd/pwd）
            name_id = loc.evaluate("el => (el.getAttribute('name') || '') + ' ' + (el.getAttribute('id') || '') + ' ' + (el.getAttribute('autocomplete') || '')").lower()
            if any(k in name_id for k in ["password", "passwd", "current-password", "new-password"]):
                return True
        except Exception:
            pass
        return False

    cands = [
        ("textarea-last", lambda: page.locator("textarea").last),
        ("contenteditable-first", lambda: page.locator('[contenteditable="true"]').first),
        ("role-textbox-last", lambda: page.get_by_role("textbox").last),
    ]
    for name, fn in cands:
        try:
            loc = fn()
            if loc.count() > 0 and loc.is_visible(timeout=3000):
                if is_password_field(loc):
                    log(sid, f"⚠️ 跳過 password field（{name}），唔掂密碼")
                    continue
                log(sid, f"找到輸入框: {name}")
                return loc
        except Exception:
            pass
    return None

def get_page_heartbeat(page, sid):
    """
    心跳：記錄 page 狀態，防假成功。
    記錄：hash、Stop 掣、Keep working、輸入框、body 字數
    """
    try:
        body = page.inner_text("body")
        h = hash(body) & 0xFFFFFFFF
        has_gen = is_generating(page)
        has_kw = find_keep_working(page, sid) is not None
        inp = find_input_box(page, sid)
        inp_state = "無"
        if inp:
            try:
                inp_state = "有(可見)" if inp.is_visible(timeout=1000) else "有(隱藏)"
            except:
                inp_state = "有(未知)"
        hb = f"hash={h:08x} stop={'有' if has_gen else '無'} kw={'有' if has_kw else '無'} 輸入框={inp_state} body={len(body)}字"
        return hb
    except Exception as e:
        return f"心跳出錯: {e}"

def verify_email_via_sidebar(page, sid):
    """明確用 slide menu 開閂去確認有無 email：
    開 sidebar → 睇有無 email → 有＝已登入，冇＝要 login。
    返 email 字串或 None。
    """
    try:
        # 確保 sidebar 開咗
        expand_sidebar(page, sid)
        page.wait_for_timeout(2000)
        email = get_arena_logged_in_email(page)
        if email:
            log(sid, f"sidebar 確認有 email：{email}")
            return email
        else:
            log(sid, "sidebar 開咗但搵唔到 email → 要 login")
            return None
    except Exception as e:
        log(sid, f"verify_email_via_sidebar 出錯: {e}")
        return None

def check_login_state(page, sid, account_email, browser_ctx=None):
    """
    登入狀態檢查（跟 ca.js）：
    1. ca_page_logged_in：5 次輪詢搵電郵 → 搵到＝已登入
    2. 搵唔到 → 睇有冇 Log In 掣 → 有＝未登入 → 自動行 login flow
    3. 兩個都冇＝異常
    返：'logged_in' / 'logged_out' / 'unknown' / 'login_failed' / 'session_cleared_retry'
    """
    try:
        # 1. 用 ca.js 方法搵電郵
        if ca_page_logged_in(page, sid):
            return 'logged_in'

        # 2. 冇電郵 → 睇下係咪有 Log In 掣
        login_btn = find_exact_text_login_button(page)
        if login_btn is not None:
            log(sid, "見到 Log In 掣 → 嘗試自動登入")
            try:
                login_btn.click(timeout=8000)
                page.wait_for_timeout(3000)
            except Exception as e:
                log(sid, f"撳 Log In 出錯: {e}")
            # 行自動登入流程
            login_result = handle_arena_login_flow(page, sid, browser_ctx)
            if login_result == "SESSION_CLEARED":
                log(sid, "Arena session missing，已清 arena，要重跑 watcher")
                return 'session_cleared_retry'
            if login_result:
                log(sid, "自動登入成功，再驗證一次")
                page.wait_for_timeout(3000)
                if ca_page_logged_in(page, sid):
                    return 'logged_in'
                else:
                    log(sid, "自動登入後都搵唔到電郵")
                    return 'login_failed'
            else:
                log(sid, "自動登入失敗（可能要密碼或超時）")
                return 'login_failed'

        # 3. 兩個都冇＝異常
        log(sid, "⚠️ 電郵同 Log In 都搵唔到＝頁面異常，截圖留證")
        try:
            page.screenshot(path=f"/tmp/{sid.lower()}-login-unknown-{int(time.time())}.png")
        except:
            pass
        return 'unknown'

    except Exception as e:
        log(sid, f"登入檢查出錯: {e}")
        return 'unknown'

def wait_for_login_signal(page, predicate, timeout_ms=2000, poll_ms=200):
    """跟 ca.js waitForLoginSignal：poll predicate，成功即返，唔使等足 timeout"""
    import time as _time
    deadline = _time.time() * 1000 + timeout_ms
    while _time.time() * 1000 < deadline:
        try:
            if predicate():
                return True
        except Exception:
            pass
        page.wait_for_timeout(poll_ms)
    return False

def is_login_flow_stuck_on_session_missing(page, sid):
    """檢測 login flow 卡住（Google missing Arena session）：
    在 Arena 按 Log In 後、Google login 期間，DOM 出現 "session missing" 文字。
    返 True = 卡住，要清 Arena 後重行 first-run login flow。
    """
    try:
        body_text = page.locator("body").inner_text(timeout=3000).lower()
        if "session missing" in body_text:
            log(sid, f"⚠️ 檢測到 login flow 卡住（DOM 有 'session missing'，URL: {page.url[:80]}）")
            return True
    except Exception:
        pass
    return False

def is_conversation_too_long(page, sid):
    """檢測對話超長：頁面出現紅字 "The conversation is too long for the model. Please start a new chat."
    呢個代表 AI 已完結，URL 無效，要交 Muse AI（worker Muses）handle。
    返 True = 對話超長。
    """
    try:
        body_text = page.locator("body").inner_text(timeout=3000)
        if "The conversation is too long for the model" in body_text:
            log(sid, "⚠️ 檢測到對話超長（The conversation is too long）→ AI 已完結，URL 無效")
            return True
    except Exception:
        pass
    return False

def is_arena_session_missing(page, sid):
    """檢查 Arena（經 Google login）有冇出現 session missing。
    Google 本身 login 冇問題，係 Arena 個 session 唔見咗。
    成日出現但難捉 DOM，用多種 pattern 鬆匹配。
    """
    try:
        body_text = page.locator("body").inner_text(timeout=3000).lower()
        # session missing 相關字眼（中英文）
        has_session = "session" in body_text or "工作階段" in body_text or "會話" in body_text
        has_missing = "missing" in body_text or "expired" in body_text or "遺失" in body_text or "過期" in body_text or "不存在" in body_text
        if has_session and has_missing:
            log(sid, f"偵測到 Arena session missing（URL: {page.url[:80]}）")
            return True
        return False
    except Exception:
        return False

def clear_arena_site_data(browser_ctx, page, sid):
    """清 arena.ai site data（經 CDP），保留 Google cookies。
    用 Storage.clearDataForOrigin 只清 arena.ai 個 origin。
    """
    try:
        cdp = browser_ctx.new_cdp_session(page)
        cdp.send("Storage.clearDataForOrigin", {
            "origin": "https://arena.ai",
            "storageTypes": "all"
        })
        log(sid, "已清 arena.ai site data（保留 Google cookies）")
        return True
    except Exception as e:
        log(sid, f"清 arena site data 出錯: {e}")
        return False

def handle_arena_login_flow(page, sid, browser_ctx=None):
    """照搬 ca.js _handleArenaLoginFlow，但改咗：見到要入密碼就即刻停手（唔填密碼）。
    步驟：Terms of Use → Continue with Google → 帳號選擇 →（密碼→停手）→ 2FA/challenge → 返 arena.ai
    返 True = 登入成功（返到 arena.ai），False = 失敗/要密碼/超時，
    "SESSION_CLEARED" = Arena session missing，已清 arena data，要重跑 watcher。
    """
    import time as _time
    MAX_LOGIN_WAIT = 60000
    login_start = _time.time() * 1000
    log(sid, "開始自動登入流程（見到密碼框即停手）…")

    def need_password_stop():
        """檢查有冇密碼框（pwd challenge 頁除外，由 3c 自動打）"""
        try:
            url = page.url
            if "/challenge/pwd" in url or "/signin/v3/challenge/pwd" in url:
                return False  # 3c 會自動打，唔停手
            pwd = page.locator('input[type="password"]').first
            if pwd.count() > 0 and pwd.is_visible(timeout=500):
                log(sid, "見到密碼框（非 pwd 頁）→ 停手")
                return True
        except Exception:
            pass
        return False

    # 1. 處理 Terms of Use dialog（15 秒）
    terms_deadline = _time.time() * 1000 + 15000
    while _time.time() * 1000 < terms_deadline:
        try:
            dlg = page.locator('[role="dialog"]').filter(has=page.locator('h2:has-text("Terms of Use")')).first
            if dlg.count() > 0 and dlg.is_visible(timeout=1000):
                agree = page.locator('[role="dialog"] button[type="submit"]:has-text("Agree")').first
                if agree.count() > 0 and agree.is_visible(timeout=3000):
                    agree.click(timeout=5000)
                else:
                    page.keyboard.press("Enter")
                log(sid, "已處理 Terms of Use")
                wait_for_login_signal(page,
                    lambda: not (dlg.count() > 0 and dlg.is_visible(timeout=250)),
                    2000)
                break
        except Exception:
            pass
        if _time.time() * 1000 - login_start > MAX_LOGIN_WAIT:
            break
        page.wait_for_timeout(200)

    # 2. 撳 Continue with Google（20 秒）
    google_deadline = _time.time() * 1000 + 20000
    while _time.time() * 1000 < google_deadline:
        try:
            gbtn = page.locator('button:has-text("Continue with Google")').first
            if gbtn.count() > 0 and gbtn.is_visible(timeout=1000):
                url_before = page.url
                gbtn.click(timeout=5000)
                log(sid, "已撳 Continue with Google")
                wait_for_login_signal(page,
                    lambda: page.url != url_before or "accounts.google.com" in page.url or
                            page.locator('input[type="password"]').first.is_visible(timeout=200),
                    3000)
                break
        except Exception:
            pass
        if _time.time() * 1000 - login_start > MAX_LOGIN_WAIT * 2:
            break
        page.wait_for_timeout(200)

    # 3. Google 帳號頁處理（25 秒）
    account_handled = False
    account_deadline = _time.time() * 1000 + 25000
    while _time.time() * 1000 < account_deadline:
        try:
            url = page.url
            # 3a0. Arena session missing（經 Google login）→ 即刻清 arena 再重跑
            # 時機限定：Arena 按 Log In 後、Google login 期間先算
            if is_login_flow_stuck_on_session_missing(page, sid) or is_arena_session_missing(page, sid):
                log(sid, "Arena session missing → 清 arena.ai 重跑 watcher")
                if browser_ctx is not None:
                    clear_arena_site_data(browser_ctx, page, sid)
                return "SESSION_CLEARED"
            # 3a. 先檢查係咪要密碼
            if need_password_stop():
                return False
            # 3b. Challenge（2FA）頁
            if "accounts.google.com" in url and "/challenge/" in url:
                if "/challenge/pwd" not in url and "/challenge/selection" not in url:
                    log(sid, f"遇到二次驗證，等 10 秒睇下會唔會自動過…")
                    if wait_for_login_signal(page, lambda: "arena.ai" in page.url, 10000):
                        log(sid, "二次驗證後返到 arena.ai，視為通過")
                        account_handled = True
                        break
                    continue
                if "/challenge/selection" in url:
                    pwd_opt = page.locator('div[data-challengeentrytype="1"]').first
                    if pwd_opt.count() > 0 and pwd_opt.is_visible(timeout=1000):
                        url_before = page.url
                        pwd_opt.click(timeout=5000)
                        wait_for_login_signal(page,
                            lambda: page.url != url_before or
                                    page.locator('input[type="password"]').first.is_visible(timeout=200),
                            2000)
                        continue
                    else:
                        log(sid, "挑戰選擇頁搵唔到密碼選項，終止登入")
                        return False
            # 3c. 密碼頁 → 自動打密碼（跟 CA arena-headed-login.py，owner 2026-10-05 下令）
            if "/challenge/pwd" in url or "/signin/v3/challenge/pwd" in url:
                try:
                    pwd_input = page.locator('input[type="password"]').first
                    pwd_input.wait_for(state="visible", timeout=15000)
                    pwd_input.fill("Kimkim11@@")
                    log(sid, "已自動填密碼")
                    # 撳 Next/繼續（timeout 加長到 15000，owner 2026-10-05 14:34 下令）
                    for btn_sel in ['button:has-text("Next")', 'button:has-text("繼續")', '#passwordNext']:
                        btn = page.locator(btn_sel).first
                        if btn.count() > 0 and btn.is_visible(timeout=5000):
                            btn.click(timeout=15000)
                            log(sid, "已撳密碼 Next")
                            break
                    continue
                except Exception as e:
                    log(sid, f"自動填密碼失敗: {e}，停手")
                    return False
            # 3d. 帳號選擇
            for sel in ['[data-identifier]', 'div[role="link"][data-authuser]', '.VV3oRb[role="link"]']:
                acc = page.locator(sel).first
                if acc.count() > 0 and acc.is_visible(timeout=3000):
                    url_before = page.url
                    acc.click(timeout=5000)
                    log(sid, f"已揀帳號（{sel}）")
                    wait_for_login_signal(page,
                        lambda: page.url != url_before or
                                page.locator('input[type="password"]').first.is_visible(timeout=200),
                        2000)
                    break
            # 3e. 返 arena.ai 確認
            return_deadline = _time.time() * 1000 + 20000
            while _time.time() * 1000 < return_deadline:
                url = page.url
                if "arena.ai" in url and "accounts.google.com" not in url:
                    page.wait_for_load_state("domcontentloaded", timeout=10000)
                    page.wait_for_timeout(500)
                    account_handled = True
                    break
                if "accounts.google.com" in url:
                    if need_password_stop():
                        return False
                for sel in ['[jsname="uRHG6"] button', 'button:has-text("繼續")', 'button:has-text("Continue")', 'button:has-text("Allow")', '.tyoyWc button']:
                    cb = page.locator(sel).first
                    if cb.count() > 0 and cb.is_visible(timeout=1000):
                        url_before = page.url
                        cb.click(timeout=5000)
                        log(sid, f"已撳繼續/Allow（{sel}）")
                        wait_for_login_signal(page,
                            lambda: page.url != url_before or
                                    ("arena.ai" in page.url and "accounts.google.com" not in page.url),
                            3000)
                        break
                page.wait_for_timeout(200)
            if account_handled:
                break
        except Exception as e:
            log(sid, f"登入流程出錯: {e}")
        if _time.time() * 1000 - login_start > MAX_LOGIN_WAIT * 3:
            break
        page.wait_for_timeout(200)

    if account_handled:
        log(sid, "自動登入成功，已返 arena.ai")
    else:
        log(sid, "自動登入失敗（超時或中斷）")
    return account_handled

def _do_send_once(page, sid):
    """搵輸入框 → click → 打字 → Enter。成功返 True"""
    inp = find_input_box(page, sid)
    if not inp:
        log(sid, "ERROR: 搵唔到輸入框 → 截圖＋dump DOM")
        try:
            page.screenshot(path=f"/tmp/{sid.lower()}-no-input.png")
        except:
            pass
        try:
            dom = page.content()
            with open(f"/tmp/{sid.lower()}-no-input-dom.html", "w") as f:
                f.write(dom)
        except:
            pass
        return False
    try:
        inp.click(timeout=8000)
        page.wait_for_timeout(800)
        inp.fill("")
        page.keyboard.type(RECALL_MSG, delay=25)
        page.wait_for_timeout(800)
        page.keyboard.press("Enter")
        log(sid, "已按 Enter 發送")
        # v0.92: 發完訊息後過 checkbox 關（跟 CA POST-SEND 模式）
        # 過唔到就唔繼續，直接返
        page.wait_for_timeout(2000)
        if not ca_checkbox_gate(page, sid, "POST-SEND"):
            log(sid, "POST-SEND checkbox 未過，停")
            return False
        # owner 2026-10-05 15:23 下令：send 完 3 秒後自動 cap 圖（捉藍 checkbox）
        page.wait_for_timeout(3000)
        try:
            page.screenshot(path=f"/tmp/{sid}-postsend-3s.png")
            log(sid, f"已cap圖 /tmp/{sid}-postsend-3s.png（send後3秒）")
        except Exception as e:
            log(sid, f"cap圖失敗: {e}")
        return True
    except Exception as e:
        log(sid, f"打字/發送出錯: {e}")
        return False

def do_send_cycle(page, sid):
    """v3 運作邏輯（用戶逐點修正）：
    1. 見 Stop 掣（生成緊）→ 乜都唔做，SKIP_GENERATING
    2. 見 Keep working → 撳 → 等 5 秒 → reload → 等頁面生成 → 等 5 秒 → 確認消失 → send
       仲有 → 再撳一次 → 等 5 秒 → reload → 等生成 → 等 5 秒 → 仲有 → HOLD_ERROR
    3. 冇 Stop 掣、冇 Keep working → 直接 send
    4. Send 後：等 10 秒 → reload → 等頁面生成 → 等 5 秒
       有 Stop 掣 → SUCCESS；有 Keep working → SUCCESS_NEXT_CYCLE（即刻下一個循環）
       兩個都冇 → 再 send 一次
    5. 第二次：等 10 秒 → reload → 等生成 → 等 5 秒
       有 Stop 掣 → SUCCESS；有 Keep working → SUCCESS_NEXT_CYCLE；兩個都冇 → ERROR
    """
    # 1. 見 Stop 掣（生成緊）→ 乜都唔做
    if is_generating(page):
        log(sid, "見到 Stop 掣（生成緊），乜都唔做，等下個 cycle")
        return "SKIP_GENERATING"
    # 2. 見 Keep working → 撳
    kw = find_keep_working(page, sid)
    if kw:
        try:
            kw.click(timeout=8000)
            log(sid, "撳咗 Keep working，等 5 秒")
            page.wait_for_timeout(3000)
            page.reload(wait_until="domcontentloaded", timeout=60000)
            wait_for_page_generated(page, sid)
            page.wait_for_timeout(5000)
            log(sid, "Reload 完（等頁面生成後 5 秒），確認有冇 Keep working")
            if find_keep_working(page, sid) is not None:
                log(sid, "仲有 Keep working，再試一次")
                kw2 = find_keep_working(page, sid)
                if kw2:
                    kw2.click(timeout=8000)
                    page.wait_for_timeout(5000)
                    page.reload(wait_until="domcontentloaded", timeout=60000)
                    wait_for_page_generated(page, sid)
                    page.wait_for_timeout(5000)
                if find_keep_working(page, sid) is not None:
                    log(sid, "HOLD: 兩次都仲有 Keep working → 報告錯誤，停手")
                    return "HOLD_ERROR"
            log(sid, "確認冇 Keep working，可以 send")
        except Exception as e:
            log(sid, f"撳 Keep working 出錯: {e}")
            return "KW_CLICK_ERROR"
    else:
        log(sid, "無 Keep working 掣，直接 send")
    # 3. Send
    if not _do_send_once(page, sid):
        return "NO_INPUT"
    # 4. 驗證（第一次）：等 10 秒 → 10秒內有冇出現 → 無就 skip gate 直接 reload
    # owner 2026-10-05 16:30 下令：10s 內冇出現就唔使過 checkbox 關
    log(sid, "等 10 秒，監測有冇 checkbox 出現…")
    import time
    wid = _get_browser_window_id()
    appeared = False
    for i in range(10):
        time.sleep(1)
        if not wid:
            break
        # 快 check 三套色（唔點，淨睇有冇）
        c_orange = _read_window_pixel(wid, ORANGE_CHECK_X, ORANGE_CHECK_Y)
        c_purple = _read_window_pixel(wid, PURPLE_CHECK_X, PURPLE_CHECK_Y)
        c_blue = _check_window_area_color(wid, BLUE_CHECK_X, BLUE_CHECK_Y, BLUE_CHECK_W, BLUE_CHECK_H, BLUE_CHECK_COLOR)
        if (c_orange and c_orange.lower() == ORANGE_CHECK_COLOR.lower()) or \
           (c_purple and c_purple.lower() == PURPLE_CHECK_COLOR.lower()) or c_blue:
            log(sid, f"第 {i+1} 秒見到 checkbox，行完整 gate")
            appeared = True
            break
    
    if appeared:
        # 有出現先過完整 gate
        if not ca_checkbox_gate(page, sid, "VERIFY-BEFORE-RELOAD"):
            log(sid, "VERIFY checkbox 未過，唔 reload")
            return "CHECKBOX_BLOCKED"
    else:
        log(sid, "10 秒內冇 checkbox，直接 reload")
    
    page.reload(wait_until="domcontentloaded", timeout=60000)
    wait_for_page_generated(page, sid)
    page.wait_for_timeout(5000)
    has_gen = is_generating(page)
    has_kw = find_keep_working(page, sid) is not None
    if has_gen or has_kw:
        if has_kw:
            log(sid, "Reload 後有 Keep working → 成功，即刻開始下一個循環")
            return "SUCCESS_NEXT_CYCLE"
        log(sid, "Reload 後有 Stop 掣（生成緊）→ 成功")
        return "SUCCESS"
    # 5. 兩個都冇 → 再送一次
    log(sid, "Reload 後無 Stop 掣又無 Keep working → 再送一次")
    if not _do_send_once(page, sid):
        return "NO_INPUT_2"
    log(sid, "第二次送出，等 10 秒…")
    page.wait_for_timeout(10000)
    page.reload(wait_until="domcontentloaded", timeout=60000)
    wait_for_page_generated(page, sid)
    page.wait_for_timeout(5000)
    has_gen2 = is_generating(page)
    has_kw2 = find_keep_working(page, sid) is not None
    if has_gen2 or has_kw2:
        if has_kw2:
            log(sid, "第二次 reload 有 Keep working → 成功，即刻開始下一個循環")
            return "SUCCESS_NEXT_CYCLE"
        log(sid, "第二次 reload 有 Stop 掣（生成緊）→ 成功")
        return "SUCCESS"
    else:
        log(sid, "ERROR: 第二次都無 Stop 掣又無 Keep working → 報告錯誤")
        return "ERROR"

def _get_browser_window_id():
    """攞 browser window ID（最大嗰個 Chrome window）"""
    import subprocess
    try:
        out = subprocess.run(['xdotool', 'search', '--name', 'Chrome'],
                           capture_output=True, text=True, timeout=5,
                           env={**__import__('os').environ, 'DISPLAY': ':99'})
        wids = [w for w in out.stdout.strip().split('\n') if w]
        # 搵最大嗰個（browser 主視窗）
        best, best_area = None, 0
        for wid in wids:
            try:
                g = subprocess.run(['xdotool', 'getwindowgeometry', wid],
                                 capture_output=True, text=True, timeout=3,
                                 env={**__import__('os').environ, 'DISPLAY': ':99'})
                for line in g.stdout.split('\n'):
                    if 'Geometry:' in line:
                        parts = line.split()[1].split('x')
                        area = int(parts[0]) * int(parts[1])
                        if area > best_area:
                            best, best_area = wid, area
            except:
                pass
        return best
    except:
        return None

def _read_window_pixel(window_id, x, y):
    """跟 CA：用 import -window 讀 window 相對座標 pixel"""
    import subprocess
    try:
        out = subprocess.run(['import', '-window', str(window_id),
                            '-crop', f'1x1+{x}+{y}', '-depth', '8', 'txt:-'],
                           capture_output=True, text=True, timeout=5,
                           env={**__import__('os').environ, 'DISPLAY': ':99'})
        import re
        m = re.search(r'#([0-9a-fA-F]{6})', out.stdout)
        return '#' + m.group(1).lower() if m else None
    except:
        return None

def _check_window_area_color(window_id, cx, cy, w, h, hex_color):
    """跟 CA checkAreaColor：檢查範圍內有冇目標色"""
    import subprocess
    try:
        # 抽樣檢查範圍內幾點
        for dx, dy in [(0,0), (w//2,h//2), (w-1,0), (0,h-1), (w-1,h-1)]:
            c = _read_window_pixel(window_id, cx+dx, cy+dy)
            if c and c.lower() == hex_color.lower():
                return True
        return False
    except:
        return False

def _xdotool_click(window_id, x, y):
    """跟 CA：xdotool 三步點擊"""
    import subprocess, time
    env = {**__import__('os').environ, 'DISPLAY': ':99'}
    try:
        subprocess.run(['xdotool', 'windowraise', str(window_id)], env=env, timeout=3, capture_output=True)
        time.sleep(0.2)
        subprocess.run(['xdotool', 'windowactivate', '--sync', str(window_id)], env=env, timeout=3, capture_output=True)
        time.sleep(0.3)
        subprocess.run(['xdotool', 'mousemove', '--window', str(window_id), str(x), str(y), 'click', '1'],
                      env=env, timeout=5, capture_output=True)
        return True
    except:
        return False

def ca_checkbox_gate(page, sid, label):
    """v0.92: CA 兩套 checkbox 關卡（跟足 CA）
    第1套（橙色）：import -window 讀 (527,409) #f6821f → xdotool (270,420)
    第2套（藍色）：import -window 讀 (910,420,200x60) #1A73E8 → xdotool (276,425)
    """
    try:
        wid = _get_browser_window_id()
        if not wid:
            return True
        
        # 第1套：紫色（優先） — check (715,415) #f3e9f7 → click (580,605)
        c3 = _read_window_pixel(wid, PURPLE_CHECK_X, PURPLE_CHECK_Y)
        if c3 and c3.lower() == PURPLE_CHECK_COLOR.lower():
            _xdotool_click(wid, PURPLE_CLICK_X, PURPLE_CLICK_Y)
            log(sid, f"[CHECKBOX-GATE] {label} 紫色 ({c3}) 已點 ({PURPLE_CLICK_X},{PURPLE_CLICK_Y})")
            
            # 點完等 8 秒，再 5 秒內每 1 秒 check
            log(sid, f"[CHECKBOX-GATE] {label} 等 8 秒後開始 5 秒監測...")
            time.sleep(8)
            reappeared = False
            for i in range(5):
                time.sleep(1)
                cc = _read_window_pixel(wid, PURPLE_CHECK_X, PURPLE_CHECK_Y)
                if cc and cc.lower() == PURPLE_CHECK_COLOR.lower():
                    log(sid, f"[CHECKBOX-GATE] {label} 點後第 {i+1} 秒紫色再出現")
                    reappeared = True
                    break
            
            if not reappeared:
                log(sid, f"[CHECKBOX-GATE] {label} 5 秒內冇再出現，pass")
                return True
            
            # 再出現：第 2 次 click
            log(sid, f"[CHECKBOX-GATE] {label} 第 2 次點擊 ({PURPLE_CLICK_X},{PURPLE_CLICK_Y})")
            _xdotool_click(wid, PURPLE_CLICK_X, PURPLE_CLICK_Y)
            time.sleep(2)
            c4 = _read_window_pixel(wid, PURPLE_CHECK_X, PURPLE_CHECK_Y)
            if c4 and c4.lower() == PURPLE_CHECK_COLOR.lower():
                log(sid, f"[CHECKBOX-GATE] {label} 第 2 次點完仲喺度，HOLD 報 Muse")
                return False  # hold住，等 Muse 處理
            
            log(sid, f"[CHECKBOX-GATE] {label} 第 2 次點完消失，pass")
        

        # 第2套：藍色（200x60 範圍）— 跟 CA check 兩次
        # 第一次：點之前 check
        if _check_window_area_color(wid, BLUE_CHECK_X, BLUE_CHECK_Y, BLUE_CHECK_W, BLUE_CHECK_H, BLUE_CHECK_COLOR):
            _xdotool_click(wid, BLUE_CLICK_X, BLUE_CLICK_Y)
            log(sid, f"[CHECKBOX-GATE] {label} 藍色已點 ({BLUE_CLICK_X},{BLUE_CLICK_Y})")
            return False
        
        # 點 checkbox
        _xdotool_click(wid, BLUE_CLICK_X, BLUE_CLICK_Y)
        log(sid, f"[CHECKBOX-GATE] {label} 已點 checkbox ({BLUE_CLICK_X},{BLUE_CLICK_Y})")
        
        # 第二次：點之後 5 秒內每 0.5 秒 check（跟 CA）
        for i in range(10):
            time.sleep(0.5)
            if _check_window_area_color(wid, BLUE_CHECK_X, BLUE_CHECK_Y, BLUE_CHECK_W, BLUE_CHECK_H, BLUE_CHECK_COLOR):
                log(sid, f"[CHECKBOX-GATE] {label} 點後 {(i+1)*0.5} 秒出現藍色（image challenge）")
                return False
        
        # 藍色搞掂，繼續第3套

        # 第3套：橙色 — 跟足 CA 完整流程
        unreadable = 0
        for attempt in range(3):  # CA: 連續讀唔到先放行
            c1 = _read_window_pixel(wid, ORANGE_CHECK_X, ORANGE_CHECK_Y)
            if c1 is None:
                unreadable += 1
                if unreadable >= 3:
                    log(sid, f"[CHECKBOX-GATE] {label} 連續 3 次讀唔到像素，放行")
                    break
                time.sleep(0.7)
                continue
            unreadable = 0
            
            if c1.lower() != ORANGE_CHECK_COLOR.lower():
                break  # 唔係橙色，放行
            
            # 係橙色：跟 CA 流程
            log(sid, f"[CHECKBOX-GATE] {label} 橙色出現 ({c1})")
            time.sleep(5)  # CA: 等 5 秒先點
            _xdotool_click(wid, ORANGE_CLICK_X, ORANGE_CLICK_Y)
            log(sid, f"[CHECKBOX-GATE] {label} 已點 ({ORANGE_CLICK_X},{ORANGE_CLICK_Y})，等 5 秒再驗")
            time.sleep(5)
            
            # 點完再驗
            c2 = _read_window_pixel(wid, ORANGE_CHECK_X, ORANGE_CHECK_Y)
            if c2 and c2.lower() != ORANGE_CHECK_COLOR.lower():
                log(sid, f"[CHECKBOX-GATE] {label} 橙色已消失，放行")
                break
            
            # 仲係橙色：CA 會 reload（呢度簡化，返 False 等上層處理）
            log(sid, f"[CHECKBOX-GATE] {label} 橙色仲喺度，要處理")
            return False
        
        # 如果上面 loop 係因為橙色而 return False，已經返咗
        # 嚟到呢度即係冇橙色，繼續藍色檢查
        
        return True
    except Exception as e:
        log(sid, f"[CHECKBOX-GATE] {label} 異常: {e}")
        return True

def ca_checkbox_click(window_id, x, y):
    """CA 式：用 xdotool 做 OS 層級滑鼠點擊（唔係 DOM click）"""
    import subprocess
    env = {'DISPLAY': ':99'}
    try:
        subprocess.run(['xdotool', 'windowraise', str(window_id)], env={**os.environ, **env}, timeout=3, capture_output=True)
        subprocess.run(['xdotool', 'windowactivate', '--sync', str(window_id)], env={**os.environ, **env}, timeout=3, capture_output=True)
        subprocess.run(['xdotool', 'mousemove', '--window', str(window_id), str(x), str(y), 'click', '1'],
                      env={**os.environ, **env}, timeout=5, capture_output=True)
        return True
    except Exception as e:
        return False

def is_page_hung(page, timeout=5000):
    """
    檢測頁面 JS 是否 hang 咗（用戶 2026-10-02 22:25 提出嘅關鍵問題）
    Arena 係動態 JS 頁，有時 renderer 死咗但 DOM 停喺舊狀態呃人
    用 F12 Inspector 級別嘅檢測：心跳＋event loop
    """
    try:
        # 心跳 1: Date.now() 應該要郁
        t1 = page.evaluate("Date.now()")
        time.sleep(0.5)
        t2 = page.evaluate("Date.now()")
        if t2 <= t1:
            return True  # 時間冇郁，JS 已死

        # 心跳 2: requestAnimationFrame 測試 event loop
        raf_ok = page.evaluate("""new Promise((resolve) => {
            let done = false;
            requestAnimationFrame(() => { done = true; resolve(true); });
            setTimeout(() => { if (!done) resolve(false); }, 2000);
        })""")
        if not raf_ok:
            return True  # event loop 唔郁

        return False
    except Exception:
        return True  # evaluate 都 timeout，肯定 hang 咗

def run_watcher(sid, once=False):
    try:
        from playwright.sync_api import sync_playwright
    except ImportError:
        log(sid, "ERROR: playwright 未安裝")
        return 1

    seats = load_seats()
    vpn_ports = load_vpn_ports()

    if sid not in seats:
        log(sid, f"ERROR: 席位 {sid} 不在 seats.conf")
        return 1

    cfg = seats[sid]
    if cfg["status"] != "active":
        log(sid, f"席位狀態為 {cfg['status']}，跳過")
        return 0

    if cfg["vpn"] not in vpn_ports:
        log(sid, f"ERROR: VPN {cfg['vpn']} 不在 vpn-proxies.conf")
        return 1

    port = vpn_ports[cfg["vpn"]]

    # 直接用 workspace storage（唔用 RAM，owner 2026-10-05 下令）
    profile_dir = PROFILES_DIR / cfg["profile"]
    profile_dir.mkdir(parents=True, exist_ok=True)
    log(sid, f"Profile 直接用 workspace: {profile_dir}")

    log(sid, f"啟動 watcher: profile={cfg['profile']}, vpn={cfg['vpn']}:{port}, url={cfg['url']}")

    with sync_playwright() as p:
        browser = None
        try:
            browser = p.chromium.launch_persistent_context(
                str(profile_dir),
                headless=False,
                proxy={"server": f"socks5://127.0.0.1:{port}"},
                args=[
                                "--test-type",
                                "--no-sandbox",
                                "--disable-setuid-sandbox",
                                "--disable-gpu",
                                "--disable-dev-shm-usage",
                                "--disable-blink-features=AutomationControlled",
                                "--disable-infobars",
                                "--no-first-run",
                                "--no-default-browser-check",
                                "--disable-crash-reporter",
                                "--disable-breakpad",
                                "--disable-component-update",
                                "--disable-extensions",
                                "--hide-crash-restore-bubble",
                                "--disable-session-crashed-bubble",
                                "--disable-features=TranslateUI,PrivacySandboxSettings4,OptimizationGuideModelExecution,ChromeWhatsNewUI,AutomationControlled,RestoreOnStartup",
                                "--use-gl=swiftshader",
                                "--disable-popup-blocking",
                                "--disable-notifications",
                                "--window-size=1408,900",  # 外框1408x900 → content 1400x900
                            ],
                            ignore_default_args=["--enable-automation"],
                            viewport={'width': 1400, 'height': 900},
            )
            page = browser.new_page() if not browser.pages else browser.pages[0]
            first_load = True
            cycle_count = 0  # 計 cycle 數，每 120 個 cycle（約 1 小時）做一次登入檢查

            while True:
                try:
                    # 檢查 browser 仲生唔生
                    if browser is None:
                        log(sid, "Browser 已死，重開中...")
                        browser = p.chromium.launch_persistent_context(
                            str(profile_dir), headless=False,
                            proxy={"server": f"socks5://127.0.0.1:{port}"},
                            args=[
                                "--test-type",
                                "--no-sandbox",
                                "--disable-setuid-sandbox",
                                "--disable-gpu",
                                "--disable-dev-shm-usage",
                                "--disable-blink-features=AutomationControlled",
                                "--disable-infobars",
                                "--no-first-run",
                                "--no-default-browser-check",
                                "--disable-crash-reporter",
                                "--disable-breakpad",
                                "--disable-component-update",
                                "--disable-extensions",
                                "--hide-crash-restore-bubble",
                                "--disable-session-crashed-bubble",
                                "--disable-features=TranslateUI,PrivacySandboxSettings4,OptimizationGuideModelExecution,ChromeWhatsNewUI,AutomationControlled,RestoreOnStartup",
                                "--use-gl=swiftshader",
                                "--disable-popup-blocking",
                                "--disable-notifications",
                                "--window-size=1408,900",  # 外框1408x900 → content 1400x900
                            ],
                            ignore_default_args=["--enable-automation"],
                            viewport={'width': 1400, 'height': 900},
                        )
                        page = browser.new_page() if not browser.pages else browser.pages[0]
                        # 強制 viewport 1400x900（確保座標校準啱）
                        try:
                            page.set_viewport_size({'width': 1400, 'height': 900})
                        except:
                            pass
                        first_load = True

                    # 首次載入或需要時先 goto，之後直接 check DOM（唔好每 30 秒 reload）
                    if first_load:
                        # 先去 https://arena.ai/agent 登入，登入咗先再去指定 URL
                        # 搵到email=登入成功；搵唔到email但搵到login=要登入；兩個都搵唔到=有問題（page未load/阻塞）
                        page.goto("https://arena.ai/agent", wait_until="domcontentloaded", timeout=60000)
                        # v0.92: URL 變動後過 checkbox 關（跟 CA URL-CHANGE 模式）
                        ca_checkbox_gate(page, sid, "URL-CHANGE")
                        log(sid, "先去 arena.ai/agent 登入頁，120s 內搵 slide menu 制（搵到=loaded）...")
                        wait_for_sidebar_button(page, sid)
                        # startup 登入檢查：等登入指標（電郵/Log In掣）出現先行
                        wait_for_login_indicators(page, sid)
                        login_state = check_login_state(page, sid, cfg["account"], browser)
                        log(sid, f"Startup 登入檢查（agent 頁）：{login_state}")
                        if login_state == 'session_cleared_retry':
                            log(sid, "Arena session missing 已清 arena，重開 browser 再試")
                            try:
                                browser.close()
                            except:
                                pass
                            browser = None
                            continue
                        # 淨係搵到 email（登入成功）先至去指定 URL；其他狀態唔去，留返 first_load=True 重試
                        # 雙重確認：用 sidebar 明確 verify 一次 email，冇就唔去 URL
                        if login_state == 'logged_in':
                            verified_email = verify_email_via_sidebar(page, sid)
                            if not verified_email:
                                log(sid, "⚠️ sidebar 二次確認搵唔到 email → 唔去指定 URL，轉去 login")
                                login_state = 'logged_out'
                            else:
                                log(sid, f"已登入（sidebar 確認有email），去指定 URL：{cfg['url']}")
                            page.goto(cfg["url"], wait_until="domcontentloaded", timeout=60000)
                            # v0.92: URL 變動後過 checkbox 關（跟 CA URL-CHANGE 模式）
                            ca_checkbox_gate(page, sid, "URL-CHANGE")
                            log(sid, "指定 URL 載入中，120s 內搵 slide menu 制...")
                            wait_for_sidebar_button(page, sid)
                            first_load = False
                        elif login_state in ('logged_out', 'login_failed'):
                            log(sid, "⚠️ 搵唔到email但搵到login→要登入，登入失敗，等人手處理（唔去指定URL，重試）")
                        else:  # unknown
                            log(sid, "⚠️ 搵唔到email+搵唔到login=有問題（page未load/阻塞），唔去指定URL，重試")
                    else:
                        # 非首次：唔 goto，直接檢查當前 DOM
                        pass

                    # 先檢查頁面係咪 hang 咗（唔好俾假 DOM 呃）
                    # 注意：首次載入後等夠耐先 check，唔好誤判
                    # owner 2026-10-05 14:50-14:51 下令：is_page_hung 只准用喺 Arena URL
                    # （指定 URL＋agent 頁＋Arena 主頁），其他 URL（Google 登入等）唔好用，
                    # 會誤判閂 browser
                    _cur_url = page.url if hasattr(page, 'url') else ''
                    if "arena.ai" in _cur_url and is_page_hung(page):
                        log(sid, "⚠️ 頁面 JS hang 咗（DOM 可能係假狀態），reload 緊...")
                        page.reload(wait_until="domcontentloaded", timeout=60000)
                        time.sleep(3)
                        # reload 後再 check 一次，如果仲 hang 就當 browser 死重開
                        if is_page_hung(page):
                            log(sid, "Reload 後仲 hang，懷疑 renderer 死咗，重開 browser")
                            try:
                                browser.close()
                            except:
                                pass
                            browser = None
                            continue
                        else:
                            log(sid, "Reload 後恢復正常")

                    # 檢查 DOM 狀態
                    content = page.content()

                    # 心跳記錄（防假成功）：每次 cycle 記錄 page 狀態
                    heartbeat = get_page_heartbeat(page, sid)
                    log(sid, f"心跳: {heartbeat}")

                    # v3：一個 cycle 做晒（do_send_cycle 處理：Stop掣→skip；Keep working→撳+5s+reload；send；10s+reload驗證）
                    result = do_send_cycle(page, sid)
                    log(sid, f"Cycle 結果: {result}")

                    # 檢測對話超長 → AI 已完結，URL 無效，交 Muse AI handle
                    if is_conversation_too_long(page, sid):
                        log(sid, "🚨 對話超長確認：呢個 AI 已完結，URL 無效 → 交 Muse AI handle")
                        # 寫 bus 訊息俾 host（經 SSH 去 server /root/muse-mesh/inbox/host/，host 會掃到）
                        try:
                            import json as _json, time as _t, subprocess as _sp
                            msg = {
                                "from": f"watcher-{sid.lower()}",
                                "to": "host",
                                "timestamp": int(_t.time()),
                                "priority": "urgent",
                                "type": "conversation_too_long",
                                "body": f"{sid} 對話超長，AI 已完結，URL 無效：{cfg['url']}"
                            }
                            ts = int(_t.time())
                            # 經 SSH 寫去 server bus（host inbox）
                            _sp.run([
                                "/home/hatch/workspace/ssh-newserver.sh",
                                f"cat > /root/muse-mesh/inbox/host/{ts}-watcher-{sid.lower()}.json && chmod 600 /root/muse-mesh/inbox/host/{ts}-watcher-{sid.lower()}.json"
                            ], input=_json.dumps(msg).encode(), timeout=30, capture_output=True)
                            log(sid, "已寫 bus 訊息去 server：conversation_too_long（host 會掃到）")
                        except Exception as e:
                            log(sid, f"寫 bus 訊息出錯: {e}")
                        # 停呢個 watcher，等 Muse AI 開新 URL
                        log(sid, "停 watcher，等 Muse AI 開新對話")
                        break

                    if result in ("HOLD_ERROR", "ERROR", "NO_INPUT", "NO_INPUT_2", "KW_CLICK_ERROR"):
                        log(sid, f"⚠️ {result} → 需要人手處理，{CHECK_INTERVAL}秒後重試")
                        # （is_login_flow_stuck_on_session_missing 只准喺 Google login 期間用，唔喺主 loop 用）
                    elif result == "LOGIN_FAILED":
                        log(sid, "⚠️ 登入失敗，需要人手登入！")

                    # 每 120 個 cycle（約 1 小時）做一次登入檢查
                    cycle_count += 1
                    if cycle_count % 120 == 0:
                        login_state = check_login_state(page, sid, cfg["account"], browser)
                        log(sid, f"定時登入檢查：{login_state}")
                        if login_state == 'session_cleared_retry':
                            log(sid, "Arena session missing 已清 arena，重開 browser 再試")
                            try:
                                browser.close()
                            except:
                                pass
                            browser = None
                            continue
                        if login_state in ('logged_out', 'login_failed'):
                            log(sid, "⚠️ 檢測到登出/登入失敗，需要人手登入！")
                        elif login_state == 'unknown':
                            log(sid, "⚠️ 登入狀態不明，已截圖留證")

                    if once:
                        break

                    # SUCCESS_NEXT_CYCLE：即刻下一個循環，唔等 30 秒
                    if result == "SUCCESS_NEXT_CYCLE":
                        log(sid, "即刻開始下一個循環（唔等 30 秒）")
                        continue

                    time.sleep(CHECK_INTERVAL)

                except Exception as e:
                    log(sid, f"檢查出錯: {e}，{CHECK_INTERVAL}秒後重試")
                    # 嘗試關閉壞掉的 browser，下輪重開
                    try:
                        if browser:
                            browser.close()
                    except:
                        pass
                    browser = None
                    if once:
                        break
                    time.sleep(CHECK_INTERVAL)

        finally:
            if browser:
                try:
                    browser.close()
                except:
                    pass

    return 0

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <席位ID> [--once]")
        sys.exit(1)
    sid = sys.argv[1].upper()
    once = "--once" in sys.argv
    sys.exit(run_watcher(sid, once))
