/**
 * Nova Core v3.9 - Monitor Core (Dual Target + Partial Mode + Adaptive Delay + Resilient Reporting)
 * Author: Nova
 * Target: Node.js 22 Native Environment (WebSocket + fetch globally available)
 */

"use strict";

const crypto = require("crypto");
const http = require("http");
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");

// ============================================================================
// Error Classification & Retry Policy System
// ============================================================================

class ErrorClassifier {
    static TYPES = {
        TRANSIENT: "TRANSIENT",
        RECOVERABLE: "RECOVERABLE",
        FATAL: "FATAL",
        DEGRADED: "DEGRADED",
    };

    static PATTERNS = {
        TRANSIENT: [
            /timeout/i, /ETIMEDOUT/i, /ECONNRESET/i, /ECONNREFUSED/i,
            /WebSocket/i, /not open/i, /closed/i, /network/i,
            /ENOTFOUND/i, /EHOSTUNREACH/i, /EPIPE/i, /socket hang up/i,
        ],
        RECOVERABLE: [
            /not found/i, /no container/i, /navigation/i,
            /DOM/i, /element/i, /selector/i, /target.*not found/i,
        ],
        FATAL: [
            /Invalid key/i, /ENOSPC/i,
            /permission denied/i, /ENOMEM/i, /EACCES/i,
        ],
        DEGRADED: [
            /partial/i, /API Error/i, /HTTP [45]\d{2}/i,
            /unavailable/i, /core_latest_ledger/i,
        ],
    };

    /**
     * Classify an error into one of 4 types based on message pattern matching.
     * @param {Error|string} error
     * @returns {string} One of ErrorClassifier.TYPES
     */
    static classify(error) {
        const msg = error instanceof Error ? error.message : String(error);
        for (const [type, patterns] of Object.entries(ErrorClassifier.PATTERNS)) {
            for (const pattern of patterns) {
                if (pattern.test(msg)) return type;
            }
        }
        return ErrorClassifier.TYPES.TRANSIENT;
    }

    static isRetryable(errorType) {
        return errorType !== ErrorClassifier.TYPES.FATAL;
    }

    static isCritical(errorType) {
        return errorType === ErrorClassifier.TYPES.FATAL || errorType === ErrorClassifier.TYPES.TRANSIENT;
    }
}

class RetryPolicy {
    static PRESETS = {
        TRANSIENT:    { maxRetries: 3, baseDelay: 1000,  maxDelay: 30000, backoffMultiplier: 2 },
        RECOVERABLE:  { maxRetries: 2, baseDelay: 5000,  maxDelay: 60000, backoffMultiplier: 2 },
        FATAL:        { maxRetries: 0, baseDelay: 0,     maxDelay: 0,     backoffMultiplier: 1 },
        DEGRADED:     { maxRetries: 1, baseDelay: 2000,  maxDelay: 5000,  backoffMultiplier: 1.5 },
    };

    static getPolicy(errorType) {
        return RetryPolicy.PRESETS[errorType] || RetryPolicy.PRESETS.TRANSIENT;
    }

    static getDelay(errorType, attempt) {
        const policy = RetryPolicy.getPolicy(errorType);
        const delay = Math.min(
            policy.baseDelay * Math.pow(policy.backoffMultiplier, attempt),
            policy.maxDelay
        );
        const jitter = delay * 0.2 * (Math.random() * 2 - 1);
        return Math.max(0, Math.round(delay + jitter));
    }

    static shouldRetry(errorType, attempt) {
        const policy = RetryPolicy.getPolicy(errorType);
        return attempt < policy.maxRetries;
    }

    /**
     * Execute an async function with automatic retry based on error classification.
     * @param {Function} fn - Async function to execute
     * @param {string} context - Log context label (e.g., "Step 1")
     * @param {Object} [options]
     * @param {Function} [options.onRetry] - Callback(attempt, errorType, delay) before each retry
     * @param {Function} [options.onFail] - Callback(error, errorType) on final failure
     * @returns {Promise<{success: boolean, result?: any, error?: Error, errorType?: string, attempts: number}>}
     */
    static async execute(fn, context, options = {}) {
        let attempts = 0;
        while (true) {
            try {
                const result = await fn();
                return { success: true, result, attempts: attempts + 1 };
            } catch (e) {
                const errorType = ErrorClassifier.classify(e);
                attempts++;
                if (!RetryPolicy.shouldRetry(errorType, attempts - 1)) {
                    console.error(`[${context}] ${errorType} error (no more retries): ${e.message}`);
                    if (options.onFail) options.onFail(e, errorType);
                    return { success: false, error: e, errorType, attempts };
                }
                const delay = RetryPolicy.getDelay(errorType, attempts - 1);
                console.warn(`[${context}] ${errorType} error, retry ${attempts}/${RetryPolicy.getPolicy(errorType).maxRetries} in ${delay}ms: ${e.message}`);
                if (options.onRetry) options.onRetry(attempts, errorType, delay);
                await new Promise(r => setTimeout(r, delay));
            }
        }
    }
}

class CircuitBreaker {
    static STATES = { CLOSED: "CLOSED", OPEN: "OPEN", HALF_OPEN: "HALF_OPEN" };

    /**
     * @param {Object} options
     * @param {number} options.failureThreshold - Failures before opening (default: 3)
     * @param {number} options.baseCooldown - Base cooldown in ms when circuit opens (default: 600000 = 10min)
     * @param {number} options.maxCooldown - Maximum cooldown in ms (default: 3600000 = 60min)
     * @param {number} options.cooldownMultiplier - Cooldown multiplier on repeated trips (default: 2)
     * @param {string} options.name - Circuit name for logging
     */
    constructor(options = {}) {
        this.failureThreshold = options.failureThreshold || 3;
        this.baseCooldown = options.baseCooldown || 10 * 60 * 1000;
        this.maxCooldown = options.maxCooldown || 60 * 60 * 1000;
        this.cooldownMultiplier = options.cooldownMultiplier || 2;
        this.name = options.name || "CircuitBreaker";

        this.state = CircuitBreaker.STATES.CLOSED;
        this.failureCount = 0;
        this.consecutiveTrips = 0;
        this.openUntil = 0;
        this.lastFailureTime = 0;
    }

    /** Current cooldown based on consecutive trips. */
    get currentCooldown() {
        const cd = Math.min(
            this.baseCooldown * Math.pow(this.cooldownMultiplier, this.consecutiveTrips),
            this.maxCooldown
        );
        return cd;
    }

    /**
     * Check if the circuit allows an operation. Returns an action hint:
     * - "ALLOW" → proceed normally (CLOSED state)
     * - "PROBE" → allow one probe request (HALF_OPEN state)
     * - "REJECT" → skip operation (OPEN state, still in cooldown)
     */
    check() {
        if (this.state === CircuitBreaker.STATES.CLOSED) return "ALLOW";
        if (this.state === CircuitBreaker.STATES.HALF_OPEN) return "PROBE";
        // OPEN state — check if cooldown elapsed
        if (Date.now() >= this.openUntil) {
            this.state = CircuitBreaker.STATES.HALF_OPEN;
            console.log(`[${this.name}] Cooldown elapsed, transitioning to HALF_OPEN (probe allowed)`);
            return "PROBE";
        }
        return "REJECT";
    }

    /** Record a successful operation. Resets circuit to CLOSED. */
    recordSuccess() {
        const wasOpen = this.state !== CircuitBreaker.STATES.CLOSED;
        this.state = CircuitBreaker.STATES.CLOSED;
        this.failureCount = 0;
        if (wasOpen) {
            this.consecutiveTrips = 0;
            console.log(`[${this.name}] Circuit CLOSED (recovered). Backoff reset.`);
        }
    }

    /** Record a failed operation. May trip the circuit. */
    recordFailure() {
        this.failureCount++;
        this.lastFailureTime = Date.now();

        if (this.state === CircuitBreaker.STATES.HALF_OPEN) {
            // Probe failed — re-open with doubled cooldown
            this.consecutiveTrips++;
            const cooldown = this.currentCooldown;
            this.openUntil = Date.now() + cooldown;
            this.state = CircuitBreaker.STATES.OPEN;
            this.failureCount = 0;
            console.log(`[${this.name}] Probe FAILED. Circuit re-OPEN for ${Math.round(cooldown / 60000)}min (trip #${this.consecutiveTrips})`);
            return true;
        }

        if (this.failureCount >= this.failureThreshold) {
            const cooldown = this.currentCooldown;
            this.openUntil = Date.now() + cooldown;
            this.state = CircuitBreaker.STATES.OPEN;
            this.failureCount = 0;
            this.consecutiveTrips++;
            console.log(`[${this.name}] Circuit OPEN for ${Math.round(cooldown / 60000)}min (trip #${this.consecutiveTrips})`);
            return true;
        }
        return false;
    }

    /** Get debug info for reporting. */
    getStatus() {
        return {
            state: this.state,
            failureCount: this.failureCount,
            consecutiveTrips: this.consecutiveTrips,
            cooldownRemaining: this.state === CircuitBreaker.STATES.OPEN ? Math.max(0, this.openUntil - Date.now()) : 0,
        };
    }
}


class SecurityLayer {
    constructor(keyHex) {
        if (keyHex.length !== 64) throw new Error("Invalid key length.");
        this.key = Buffer.from(keyHex, "hex");
        console.log("[Nova-Security] Security Layer Active");
    }
    encrypt(text) {
        const iv = crypto.randomBytes(12);
        const cipher = crypto.createCipheriv("aes-256-gcm", this.key, iv);
        const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
        const tag = cipher.getAuthTag();
        return Buffer.concat([iv, encrypted, tag]).toString("base64");
    }
    decrypt(base64) {
        const data = Buffer.from(base64, "base64");
        const iv = data.subarray(0, 12);
        const tag = data.subarray(data.length - 16);
        const encrypted = data.subarray(12, data.length - 16);
        const decipher = crypto.createDecipheriv("aes-256-gcm", this.key, iv);
        decipher.setAuthTag(tag);
        return decipher.update(encrypted, undefined, "utf8") + decipher.final("utf8");
    }
}

class Reporter {
    constructor(securityLayer, endpoint) {
        this.securityLayer = securityLayer;
        this.endpoint = new URL(endpoint);
        this.logFile = path.join(process.env.TEMP || "C:\\Temp", "pi_monitor_debug.log");
    }
    logToFile(msg) {
        try { fs.appendFileSync(this.logFile, `[${new Date().toISOString()}] ${msg}\n`); } catch (_e) {}
    }
    async sendReport(type, payload = null) {
        const report = { type: String(type), payload: payload !== null && payload !== undefined ? payload : null, timestamp: Date.now() };
        this.logToFile(`REPORT: ${JSON.stringify(report)}`);
        return new Promise((resolve) => {
            try {
                const plaintext = JSON.stringify(report);
                const encrypted = this.securityLayer.encrypt(plaintext);
                const postData = JSON.stringify({ data: encrypted });
                const options = {
                    hostname: this.endpoint.hostname,
                    port: this.endpoint.port,
                    path: this.endpoint.pathname,
                    method: "POST",
                    headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData) },
                    timeout: 10000,
                };
                const req = http.request(options, (res) => {
                    res.resume();
                    if (res.statusCode >= 200 && res.statusCode < 300) {
                        this.logToFile("HTTP SUCCESS");
                        console.log(`[Nova-Reporter] 上报成功: ${type}`);
                        resolve(true);
                    } else {
                        this.logToFile(`HTTP ERROR: ${res.statusCode}`);
                        resolve(false);
                    }
                });
                req.on("error", (e) => { this.logToFile(`HTTP FAIL: ${e.message}`); resolve(false); });
                req.on("timeout", () => { req.destroy(); resolve(false); });
                req.write(postData);
                req.end();
            } catch (error) { resolve(false); }
        });
    }
}

class CDPClient {
    constructor(wsUrl) {
        this.wsUrl = wsUrl;
        this.ws = null;
        this.id = 0;
        this.handlers = new Map();
        this.eventHandlers = new Map();
        this.connected = false;
    }
    async connect() {
        return new Promise((resolve, reject) => {
            console.log(`[Nova-CDP] Connecting to: ${this.wsUrl}`);
            try { this.ws = new WebSocket(this.wsUrl); } catch (e) { return reject(e); }
            const connectTimeout = setTimeout(() => { if (!this.connected) { try { this.ws.close(); } catch (_e) {} reject(new Error("WebSocket timeout")); } }, 30000);
            this.ws.onopen = () => { clearTimeout(connectTimeout); this.connected = true; console.log("[Nova-CDP] Connected."); resolve(); };
            this.ws.onmessage = (event) => {
                try {
                    const data = JSON.parse(event.data);
                    if (data.id) { const cb = this.handlers.get(data.id); if (cb) cb(data); }
                    else if (data.method) { const cb = this.eventHandlers.get(data.method); if (cb) cb(data.params); }
                } catch (e) {}
            };
            this.ws.onerror = (err) => { clearTimeout(connectTimeout); if (!this.connected) reject(err); };
            this.ws.onclose = () => { clearTimeout(connectTimeout); this.connected = false; console.log("[Nova-CDP] Closed."); };
        });
    }
    isConnected() { return this.connected && this.ws && this.ws.readyState === WebSocket.OPEN; }
    /** Track consecutive timeouts to detect stale connections */
    get consecutiveTimeouts() { return this._consecutiveTimeouts || 0; }
    set consecutiveTimeouts(v) { this._consecutiveTimeouts = v; }
    async call(method, params = {}) {
        return new Promise((resolve, reject) => {
            if (!this.isConnected()) return reject(new Error("WebSocket not open"));
            const id = ++this.id;
            const message = JSON.stringify({ id, method, params });
            const timeout = setTimeout(() => {
                this.handlers.delete(id);
                this.consecutiveTimeouts++;
                // After 2+ consecutive timeouts, mark connection as stale and force-close
                if (this.consecutiveTimeouts >= 2) {
                    console.warn(`[Nova-CDP] ${this.consecutiveTimeouts} consecutive timeouts — closing stale connection`);
                    this.close();
                }
                reject(new Error(`Timeout: ${method}`));
            }, 30000);
            this.handlers.set(id, (response) => {
                clearTimeout(timeout);
                this.handlers.delete(id);
                this.consecutiveTimeouts = 0; // Reset on any successful response
                if (response.error) reject(new Error(response.error.message));
                else resolve(response.result);
            });
            try { this.ws.send(message); } catch (e) { clearTimeout(timeout); this.handlers.delete(id); reject(e); }
        });
    }
    on(method, callback) { this.eventHandlers.set(method, callback); }
    close() { if (this.ws) { try { this.ws.close(); } catch (_e) {} } this.connected = false; }
}

class ContextManager {
    constructor(client) {
        this.client = client;
        this.contexts = new Map();
        this.frameToContext = new Map();
        this.client.on("Runtime.executionContextCreated", (params) => {
            try {
                const ctx = params.context;
                const frameId = ctx.auxData?.frameId;
                const isDefault = ctx.auxData?.isDefault;
                this.contexts.set(ctx.id, { id: ctx.id, origin: ctx.origin, name: ctx.name, frameId: frameId, isDefault: isDefault });
                if (isDefault && frameId) this.frameToContext.set(frameId, ctx.id);
            } catch (_e) {}
        });
        this.client.on("Runtime.executionContextDestroyed", (params) => {
            const ctx = this.contexts.get(params.executionContextId);
            if (ctx?.frameId && this.frameToContext.get(ctx.frameId) === params.executionContextId) this.frameToContext.delete(ctx.frameId);
            this.contexts.delete(params.executionContextId);
        });
        this.client.on("Runtime.executionContextsCleared", () => { this.contexts.clear(); this.frameToContext.clear(); });
    }
    getContextForFrame(frameId) { return this.frameToContext.get(frameId) || null; }
}
class StateMachine {
    static STATES = { INIT: "INIT", LOGIN: "LOGIN", MAIN: "MAIN" };
    constructor(clientLocal, clientRemote, reporter, contextManager, hunter, config = {}) {
        this.clientLocal = clientLocal;
        this.clientRemote = clientRemote;
        this.client = clientRemote; 
        this.reporter = reporter;
        this.cm = contextManager;
        this.hunter = hunter;
        this.state = StateMachine.STATES.INIT;
        this.interval = config.interval || 3600000;
        this.delay = config.delay || 3000;
        this.lastFetch = 0;
        this.running = true;
        this.lastSignInCode = null;
        this.lastLoginPendingSnapshotCode = null;
        this.forceRefreshCode = false;
        this.lastKnownUrl = config.initialRemoteUrl || null;
        this.lastUrl = null;
        this.lightningClicks = [];
        this.sessionData = {
            localBlock: "0",
            latestBlock: "0",
            isSynced: false,
            apiEnabled: false,
            apiMiningTime: "0",
            apiData: {},
            dockerState: "unknown",
            dockerRestartCount: 0,
            piPageLatencyMs: null,
            geoIp: "",
            geoCountry: "",
            geoCity: "",
        };
        this.dockerCircuit = new CircuitBreaker({ failureThreshold: 3, baseCooldown: 10 * 60 * 1000, maxCooldown: 60 * 60 * 1000, cooldownMultiplier: 2, name: "Docker" });
        this.cdpCircuit = new CircuitBreaker({ failureThreshold: 5, baseCooldown: 60 * 1000, maxCooldown: 5 * 60 * 1000, cooldownMultiplier: 2, name: "CDP-Reconnect" });
        this.cdpReconnectAttempt = 0;
        this.lastSuccessfulCollection = 0;
        this.healthCheckInterval = 5 * 60 * 1000; // Check health every 5min
        this.lastHealthCheck = 0;
        this.pingInterval = 2 * 60 * 1000; // Send heartbeat PING every 2min
        this.lastPingTime = Date.now(); // Don't ping immediately on startup (PING sent in main())
        this.popupCheckInterval = 30 * 1000; // Throttle popup DOM scanning
        this.lastPopupCheck = 0;
        this.idleLoopDelay = Math.max(this.delay, 15 * 1000); // Reduce busy looping between long collection intervals
        // stale 阈值必须晚于用户配置的采集间隔，避免“配置20分钟却在15分钟被提前触发”
        const staleGraceMs = Math.max(this.healthCheckInterval, 2 * 60 * 1000);
        this.staleDataThreshold = Math.max(this.interval + staleGraceMs, 15 * 60 * 1000);
        this._cachedContainerName = null;
        const localAppData = (process.env.LOCALAPPDATA || "").replace(/\\/g, "/");
        this.localBaseUrl = `file:///${localAppData}/Programs/pi-network-desktop/resources/app.asar/dist/renderer/index.html`;
        this.commandEndpoint = `http://127.0.0.1:${config.apiPort || 13140}/command`;
        console.log(`[Nova-SM] Local Base URL: ${this.localBaseUrl}`);
    }

    async pollCommandOnce() {
        try {
            const res = await fetchWithTimeout(this.commandEndpoint, 1500);
            if (!res.ok) return;
            const data = await res.json();
            const cmd = (data && data.command) ? String(data.command).trim() : "";
            if (!cmd) return;

            if (cmd === "UPDATE_TOKEN") {
                this.forceRefreshCode = true;
                this.lastSignInCode = null;
                console.log("[Nova-SM] Received command: UPDATE_TOKEN");
            }
        } catch (_e) {}
    }

    async tryClickByKeywords(keywords = []) {
        if (!this.clientRemote || !this.clientRemote.isConnected()) return { clicked: false, reason: "remote_disconnected" };
        const escaped = keywords.map((k) => k.replace(/\\/g, "\\\\").replace(/'/g, "\\'")).join("|");
        const script = `(function() {
            try {
                var re = new RegExp('${escaped}', 'i');
                var nodes = document.querySelectorAll('button, [role="button"], a, div, span');
                for (var i = 0; i < nodes.length; i++) {
                    var el = nodes[i];
                    var text = (el.textContent || '').trim();
                    if (!text || !re.test(text)) continue;
                    var target = el.closest('button, [role="button"], a') || el;
                    var rect = target.getBoundingClientRect();
                    if (rect.width <= 0 || rect.height <= 0) continue;
                    return JSON.stringify({ found: true, text: text.slice(0, 120), x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
                }
                return JSON.stringify({ found: false });
            } catch (e) {
                return JSON.stringify({ found: false, error: e.message });
            }
        })()`;

        const res = JSON.parse(await this.evalRemote(script) || "{}");
        if (!res.found) return { clicked: false, reason: "not_found" };

        await this.clientRemote.call("Input.dispatchMouseEvent", { type: "mousePressed", x: res.x, y: res.y, button: "left", clickCount: 1 });
        await sleep(50);
        await this.clientRemote.call("Input.dispatchMouseEvent", { type: "mouseReleased", x: res.x, y: res.y, button: "left", clickCount: 1 });
        return { clicked: true, text: res.text, x: res.x, y: res.y };
    }

    async ensureRemoteSignInPageForTokenRefresh() {
        if (!this.clientRemote || !this.clientRemote.isConnected()) return false;
        const currentUrl = String(this.lastUrl || "").toLowerCase();
        if (currentUrl.includes("node-signin")) return true;

        await this.navigateRemote("https://app-cdn.minepi.com/mobile-app-ui/node-signin");
        this.lastUrl = "https://app-cdn.minepi.com/mobile-app-ui/node-signin";
        this.transition(StateMachine.STATES.LOGIN);
        await sleep(2000);
        return true;
    }

    async processRefreshTokenCommand() {
        if (!this.forceRefreshCode) return;
        if (!this.clientRemote || !this.clientRemote.isConnected()) return;
        await this.ensureRemoteSignInPageForTokenRefresh();

        // Priority 1: click "Obtain another code" style buttons
        const refreshTry = await this.tryClickByKeywords([
            "obtain\\s+another\\s+code",
            "another\\s+code",
            "obtain\\s+a\\s+new\\s+code",
            "obtain\\s+new\\s+code",
            "new\\s+code",
            "refresh\\s+code",
            "get\\s+new\\s+code",
        ]);

        if (refreshTry.clicked) {
            console.log(`[Nova-SM] UPDATE_TOKEN executed: clicked refresh button '${refreshTry.text}' at ${refreshTry.x},${refreshTry.y}`);
            this.forceRefreshCode = false;
            this.lastSignInCode = null;
            await this.reporter.sendReport("STATUS", { value: "UPDATE_TOKEN_DONE" });
            await sleep(1200);
            return;
        }

        // Priority 2: if still at pre-login view, click login to enter code page
        const loginTry = await this.tryClickByKeywords([
            "log\\s*in",
            "login",
            "sign\\s*in",
        ]);

        if (loginTry.clicked) {
            console.log(`[Nova-SM] UPDATE_TOKEN fallback: clicked login button '${loginTry.text}' at ${loginTry.x},${loginTry.y}`);
            await sleep(1500);
            return;
        }

        console.log("[Nova-SM] UPDATE_TOKEN pending: refresh/login button not found on current page");
    }

    async reconnectRemote() {
        if (this.clientRemote && this.clientRemote.isConnected()) {
            this.cdpReconnectAttempt = 0;
            return true;
        }
        if (!this.hunter) return false;

        const cdpAction = this.cdpCircuit.check();
        if (cdpAction === "REJECT") {
            const status = this.cdpCircuit.getStatus();
            console.log(`[Nova-SM] CDP reconnect circuit OPEN, ${Math.round(status.cooldownRemaining / 1000)}s remaining`);
            return false;
        }

        // Exponential backoff: 2s → 4s → 8s → 16s → max 60s
        const backoffDelay = Math.min(2000 * Math.pow(2, this.cdpReconnectAttempt), 60000);
        if (this.cdpReconnectAttempt > 0) {
            console.log(`[Nova-SM] CDP reconnect backoff: ${Math.round(backoffDelay / 1000)}s (attempt ${this.cdpReconnectAttempt + 1})`);
            await sleep(backoffDelay);
        }

        try {
            console.log("[Nova-SM] Remote disconnected, attempting reconnect...");
            const targets = await this.hunter.findTargets();
            if (!targets || !targets.remote) {
                console.log("[Nova-SM] Remote target not found");
                this.cdpReconnectAttempt++;
                this.cdpCircuit.recordFailure();
                return false;
            }
            const newClient = new CDPClient(targets.remote.webSocketDebuggerUrl);
            await newClient.connect();
            this.cm = new ContextManager(newClient);
            this.attachRemoteNavigationTracking(newClient);
            await newClient.call("Page.enable");
            await newClient.call("Runtime.enable");
            await newClient.call("DOM.enable");
            this.clientRemote = newClient;
            this.client = newClient;
            this.cdpReconnectAttempt = 0;
            this.cdpCircuit.recordSuccess();
            console.log("[Nova-SM] Remote reconnected ✅");
            return true;
        } catch (e) {
            this.cdpReconnectAttempt++;
            this.cdpCircuit.recordFailure();
            console.error(`[Nova-SM] Remote reconnect failed (attempt ${this.cdpReconnectAttempt}): ${e.message}`);
            return false;
        }
    }

    /**
     * Periodic heartbeat PING — independent of data collection.
     * Ensures the backend knows the agent is alive even when data collection fails.
     */
    async sendPeriodicPing() {
        const now = Date.now();
        if (now - this.lastPingTime < this.pingInterval) return;
        this.lastPingTime = now;
        try {
            await this.reporter.sendReport("PING");
            console.log("[Heartbeat] PING sent");
        } catch (e) {
            console.warn(`[Heartbeat] PING failed: ${e.message}`);
        }
    }

    attachRemoteNavigationTracking(client) {
        if (!client) return;
        client.on("Page.frameNavigated", (params) => {
            const url = params && params.frame && params.frame.url;
            if (url) this.handleNavigation(url);
        });
    }

    async refreshRemoteNavigationState() {
        if (!this.clientRemote || !this.clientRemote.isConnected()) return;
        const { frameTree } = await this.clientRemote.call("Page.getFrameTree");
        const url = frameTree && frameTree.frame && frameTree.frame.url;
        if (url) this.handleNavigation(url);
    }

    handleNavigation(url) {
        if (url === this.lastUrl) return;
        this.lastUrl = url;
        this.lastKnownUrl = url;
        console.log(`[Nova-SM] Navigation: ${url}`);
        if (url.includes("node-signin")) this.transition(StateMachine.STATES.LOGIN);
        else if (url.includes("minepi.com") && !url.includes("node-signin")) {
            console.log("[Nova-SM] Login confirmed!");
            this.transition(StateMachine.STATES.MAIN);
        }
    }


    async sendLoginPendingSnapshot() {
        const remoteUrl = String(this.lastUrl || "").trim().toLowerCase();
        const isSigninPage = remoteUrl.includes("node-signin");
        if (!isSigninPage) return false;

        const code = this.lastSignInCode || "";
        if (!code || code === this.lastLoginPendingSnapshotCode) return false;

        await this.stepEnsureDockerContainer();

        const payload = {
            username: "-",
            balance: "0",
            mining_rate: "0 π/hr",
            pi_status: "starting",
            login_status: false,
            external_network_status: true,
            local_block_number: this.sessionData.localBlock || "0",
            is_synced: false,
            node_name: "Pi-Node",
            ip_address: await resolvePublicIp(this.sessionData.geoIp || ""),
            hashrate: "0",
            rewards: "0",
            mining_time: "0",
            block_height: this.sessionData.latestBlock || "0",
            docker_state: this.sessionData.dockerState || null,
            docker_restart_count: this.sessionData.dockerRestartCount || null,
            pi_page_latency_ms: Number.isFinite(Number(this.sessionData.piPageLatencyMs)) ? Number(this.sessionData.piPageLatencyMs) : null,
            ip_country: this.sessionData.geoCountry ? this.sessionData.geoCountry : null,
            ip_city: this.sessionData.geoCity ? this.sessionData.geoCity : null,
            timestamp: new Date().toISOString(),
            token: code
        };

        console.log("[Nova-SM] Emitting login-pending snapshot");
        const ok = await this.reporter.sendReport("MINING_DATA", payload);
        if (ok) {
            this.lastLoginPendingSnapshotCode = code;
        }
        return ok;
    }

    /**
     * Periodic health check: CDP alive, Docker container status, stale data detection.
     * Runs at most once per healthCheckInterval. Returns health status object.
     */
    async runHealthCheck() {
        const now = Date.now();
        if (now - this.lastHealthCheck < this.healthCheckInterval) return null;
        this.lastHealthCheck = now;

        const health = {
            timestamp: now,
            cdpLocal: !!(this.clientLocal && this.clientLocal.isConnected()),
            cdpRemote: !!(this.clientRemote && this.clientRemote.isConnected()),
            dockerCircuit: this.dockerCircuit.getStatus(),
            cdpCircuit: this.cdpCircuit.getStatus(),
            lastSuccessfulCollection: this.lastSuccessfulCollection,
            staleData: this.lastSuccessfulCollection > 0 && (now - this.lastSuccessfulCollection) > this.staleDataThreshold,
            memoryMB: Math.round(process.memoryUsage().rss / 1024 / 1024),
        };

        // Self-healing: stale data detection
        if (health.staleData) {
            const staleMins = Math.round((now - this.lastSuccessfulCollection) / 60000);
            console.warn(`[Health] Data stale for ${staleMins}min. Resetting sequence timing.`);
            this.lastFetch = 0; // Force immediate next sequence
        }

        // Self-healing: auto-reconnect CDP if remote down but circuit allows
        if (!health.cdpRemote && this.cdpCircuit.check() !== "REJECT") {
            console.log("[Health] Remote CDP down, triggering reconnect...");
            await this.reconnectRemote();
            health.cdpRemote = !!(this.clientRemote && this.clientRemote.isConnected());
        }

        // Self-healing: CDP stale connection probe (connected but unresponsive)
        if (health.cdpRemote && this.clientRemote.consecutiveTimeouts >= 1) {
            console.warn(`[Health] Remote CDP has ${this.clientRemote.consecutiveTimeouts} pending timeouts — forcing reconnect`);
            this.clientRemote.close();
            health.cdpRemote = false;
            await this.reconnectRemote();
            health.cdpRemote = !!(this.clientRemote && this.clientRemote.isConnected());
        }

        // Self-healing: memory warning
        if (health.memoryMB > 512) {
            console.warn(`[Health] High memory usage: ${health.memoryMB}MB. Reporting to backend.`);
            await this.reporter.sendReport("STATUS", { value: "W_HIGH_MEMORY", memoryMB: health.memoryMB });
        }

        console.log(`[Health] CDP-L:${health.cdpLocal} CDP-R:${health.cdpRemote} Mem:${health.memoryMB}MB Docker:${health.dockerCircuit.state} Stale:${health.staleData}`);
        return health;
    }

    async run() {
        console.log("[Nova-SM] Loop Started");
        while (this.running) {
            let nextDelay = this.delay;
            try {
                const localOk = this.clientLocal && this.clientLocal.isConnected();
                const remoteOk = this.clientRemote && this.clientRemote.isConnected();
                if (!localOk && !remoteOk) { console.warn("[Nova-SM] All CDPs disconnected."); break; }
                await this.pollCommandOnce();
                await this.processRefreshTokenCommand();

                const now = Date.now();
                if (remoteOk && (now - this.lastPopupCheck >= this.popupCheckInterval)) {
                    this.lastPopupCheck = now;
                    await this.popupKillerOnce();
                }
                // Periodic health check
                await this.runHealthCheck();

                // Periodic heartbeat PING — independent of data collection
                await this.sendPeriodicPing();

                switch (this.state) {
                    case StateMachine.STATES.INIT: await this.handleInit(); break;
                    case StateMachine.STATES.LOGIN: await this.handleLogin(); break;
                    case StateMachine.STATES.MAIN: await this.handleMainSequence(); break;
                }

                // Between long collection intervals, avoid high-frequency hot loop.
                if (this.state === StateMachine.STATES.MAIN && !this.forceRefreshCode) {
                    const remain = this.interval - (Date.now() - this.lastFetch);
                    if (remain > 0) {
                        nextDelay = Math.min(this.idleLoopDelay, Math.max(this.delay, remain));
                    }
                }
            } catch (e) {
                const errorType = ErrorClassifier.classify(e);
                console.error(`[Nova-SM] ${errorType} Error: ${e.message}`);
                if (errorType === ErrorClassifier.TYPES.FATAL) { console.error("[Nova-SM] Fatal error detected, stopping loop."); break; }
                if (e.message.includes("not open") || e.message.includes("closed")) { console.warn("[Nova-SM] Connection lost."); break; }
            }
            await sleep(nextDelay);
        }
        console.log("[Nova-SM] Loop Ended");
    }

    transition(newState) { if (this.state !== newState) { console.log(`[Nova-SM] ${this.state} -> ${newState}`); this.state = newState; } }
    stop() { this.running = false; }

    async popupKillerOnce() {
        try {
            const pkScript = `(function() { try { var targets = ['Dismiss', 'Later', 'OK', 'Got it', 'Close']; var buttons = document.querySelectorAll('button, [role="button"]'); for (var t = 0; t < targets.length; t++) { for (var i = 0; i < buttons.length; i++) { var btn = buttons[i]; var btnText = (btn.textContent || '').trim(); var rect = btn.getBoundingClientRect(); if (rect.width === 0 || rect.height === 0) continue; if (btnText.indexOf(targets[t]) !== -1) { if (targets[t] === 'Refresh' && btnText.length > 15) continue; btn.dispatchEvent(new MouseEvent('mousedown', {bubbles:true})); btn.dispatchEvent(new MouseEvent('mouseup', {bubbles:true})); btn.dispatchEvent(new MouseEvent('click', {bubbles:true})); return targets[t]; } } } return null; } catch(e) { return null; } })()`;
            const evalPromise = this.clientRemote.call("Runtime.evaluate", { expression: pkScript, returnByValue: true });
            const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("PK Timeout")), 2000));
            const result = await Promise.race([evalPromise, timeoutPromise]);
            if (result.result?.value === "Refresh") {
                const now = Date.now();
                this.lightningClicks.push(now);
                this.lightningClicks = this.lightningClicks.filter(t => now - t < 60000);
                if (this.lightningClicks.length > 5) this.reporter.sendReport("STATUS", { value: "BLOCKED_LIGHTNING" });
                await sleep(2000);
            }
        } catch (_e) {}
    }

    async handleInit() {
        try {
            if (this.clientRemote) {
                const { frameTree } = await this.clientRemote.call("Page.getFrameTree");
                this.handleNavigation(frameTree.frame.url);
            } else if (this.clientLocal) {
                // No Remote Target - go to MAIN with Local-only mode
                console.log("[Nova-SM] No Remote Target. Running Local-only mode.");
                this.transition(StateMachine.STATES.MAIN);
            }
        } catch (e) {
            console.error(`[Nova-SM] Init Error: ${e.message}`);
        }
    }

    async handleLogin() {
        if (!this.clientRemote || !this.clientRemote.isConnected()) {
            const reconnected = await this.reconnectRemote();
            if (!reconnected || !this.clientRemote || !this.clientRemote.isConnected()) return;
        }
        try {
            await this.refreshRemoteNavigationState();
            if (this.state === StateMachine.STATES.MAIN) return;
            const probeScript = `(function() { try { var bodyText = document.body.innerText.substring(0, 500); var hasSignIn = bodyText.indexOf('sign-in code') !== -1; var hasLogin = false; var buttons = document.querySelectorAll('button, [role="button"], a'); for(var i=0;i<buttons.length;i++) { var t = buttons[i].innerText.toLowerCase(); if(t.includes('log in') || t.includes('login')) hasLogin=true; } return JSON.stringify({ phase: hasSignIn ? 'CODE' : (hasLogin ? 'BUTTON' : 'UNKNOWN'), hint: bodyText.substring(0,200) }); } catch(e) { return JSON.stringify({phase:'ERROR'}); } })()`;
            const probePromise = this.clientRemote.call("Runtime.evaluate", { expression: probeScript, returnByValue: true });
            const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Login probe timeout")), 5000));
            const probeResult = await Promise.race([probePromise, timeoutPromise]);
            const probe = JSON.parse(probeResult.result?.value || "{}");
            console.log(`[Nova-SM] Login Phase: ${probe.phase}`);
            
            // Phase 1: UNKNOWN - Check if already logged in
            if (probe.phase === "UNKNOWN") {
                if (probe.hint.includes("Chat") && probe.hint.includes("π")) {
                    this.transition(StateMachine.STATES.MAIN);
                }
                return; // Wait for next loop
            }
            
            // Phase 2: BUTTON - Click Login and WAIT for page transition
            if (probe.phase === "BUTTON") {
                const findBtn = `(function() { try { var buttons = document.querySelectorAll('button, [role="button"], a'); for (var i = 0; i < buttons.length; i++) { var text = (buttons[i].textContent || '').trim().toLowerCase(); if (text.indexOf('log in') !== -1 || text.indexOf('login') !== -1 || text === 'sign in') { var rect = buttons[i].getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { return JSON.stringify({ found: true, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }); } } } return "{}"; } catch(e) { return "{}"; } })()`;
                const btnRes = JSON.parse(await this.evalRemote(findBtn) || "{}");
                if (btnRes.found) {
                    console.log(`[Nova-SM] Clicking Login Button at ${btnRes.x},${btnRes.y}`);
                    await this.clientRemote.call("Input.dispatchMouseEvent", { type: "mousePressed", x: btnRes.x, y: btnRes.y, button: "left", clickCount: 1 });
                    await sleep(50);
                    await this.clientRemote.call("Input.dispatchMouseEvent", { type: "mouseReleased", x: btnRes.x, y: btnRes.y, button: "left", clickCount: 1 });
                    console.log("[Nova-SM] Waiting for page transition...");
                    await sleep(3000); // Wait for page to load CODE screen
                }
                return; // CRITICAL: Don't run Partial Mode after clicking, wait for next loop
            }
            
            // Phase 3: CODE - Extract sign-in code from <p> elements
            if (probe.phase === "CODE") {
                if (this.forceRefreshCode) await this.processRefreshTokenCommand();

                const codeRes = await this.clientRemote.call("Runtime.evaluate", {
                    expression: `(function() { var c=null; document.querySelectorAll('p').forEach(el => { var t=(el.innerText||'').trim(); if(t.length===8 && /^[A-Z0-9]+$/.test(t)) c=t; }); return JSON.stringify({code:c}); })()`,
                    returnByValue: true
                });
                const c = JSON.parse(codeRes.result?.value || "{}").code;
                if (c && c !== this.lastSignInCode) {
                    this.lastSignInCode = c;
                    console.log(`[Nova-SM] Sign-in Code Detected: ${c}`);
                    const publicIp = await resolvePublicIp("");
                    await this.reporter.sendReport("SIGNIN_CODE", { value: c, externalIp: publicIp || "" });
                    await this.sendLoginPendingSnapshot();
                } else if (!c) {
                    console.log("[Nova-SM] Waiting for sign-in code to appear...");
                }
            }
        } catch (e) {
            console.error(`[Nova-SM] Login Error: ${e.message}`);
            // Don't transition to MAIN on error, stay in LOGIN and retry
        }
    }

    async handleMainSequence() {
        if (Date.now() - this.lastFetch < this.interval) return;

        // 每次采集任务开始前，先回到本地首页，避免停留在区块高度/排障页导致 API 采集异常
        await this.ensureLocalHomeBeforeCollection();
        
        // Auto-reconnect Remote if disconnected (e.g. by previous Local navigation)
        await this.reconnectRemote();
        
        console.log("Starting sequence...");
        console.log(`Action delay: ${this.delay}ms between steps`);
        this.sessionData = {
            localBlock: "0",
            latestBlock: "0",
            isSynced: false,
            apiEnabled: false,
            apiMiningTime: "0",
            apiData: {},
            dockerState: this.sessionData.dockerState || "unknown",
            dockerRestartCount: this.sessionData.dockerRestartCount || 0,
            piPageLatencyMs: this.sessionData.piPageLatencyMs,
            geoIp: this.sessionData.geoIp || "",
            geoCountry: this.sessionData.geoCountry || "",
            geoCity: this.sessionData.geoCity || "",
        };

        let step1Failed = false;
        let step2Failed = false;
        let step3Failed = false;

        try {
            // Step 0: Dependency Guard (no retry wrapper — has its own CircuitBreaker)
            await this.stepRefreshGeoIpInfo();
            await sleep(this.delay);

            // Step 0: Dependency Guard (no retry wrapper — has its own CircuitBreaker)
            await this.stepEnsureDockerContainer();
            await sleep(this.delay);

            // Step 0.5: Local - Check Pi Desktop web page connection latency
            await this.stepCheckPiPageLatency();
            await sleep(this.delay);

            // Step 1: Remote - Fetch API data incl. mining time (MUST be before Local ops!)
            if (this.clientRemote && this.clientRemote.isConnected()) {
                const s1 = await RetryPolicy.execute(
                    () => this.stepFetchApiData(),
                    "Step 1",
                    { onFail: (e, t) => console.warn(`Step 1 final fail [${t}]: ${e.message}`) }
                );
                if (!s1.success) {
                    step1Failed = true;
                    console.warn(`[Sequence] Step 1 failed (${s1.errorType}), continuing with partial data...`);
                    // If timeout caused connection to close, try reconnect for subsequent steps
                    if (!this.clientRemote.isConnected()) {
                        console.warn("[Sequence] Remote CDP lost after Step 1 failure, attempting reconnect...");
                        await this.reconnectRemote();
                    }
                }
                await sleep(this.delay);
            } else {
                step1Failed = true;
                console.warn("[Sequence] Step 1 skipped: Remote CDP not connected");
            }

            // Step 2: API - Get block height from local horizon endpoint
            const s2 = await RetryPolicy.execute(
                () => this.stepNodeStats(),
                "Step 2",
                { onFail: (e, t) => console.warn(`Step 2 final fail [${t}]: ${e.message}`) }
            );
            if (!s2.success) {
                step2Failed = true;
                console.warn(`[Sequence] Step 2 failed (${s2.errorType}), continuing...`);
            }
            await sleep(this.delay);

            // Step 3: API - Check local API availability
            const s3 = await RetryPolicy.execute(
                () => this.stepCheckApi(),
                "Step 3",
                { onFail: (e, t) => console.warn(`Step 3 final fail [${t}]: ${e.message}`) }
            );
            if (!s3.success) {
                step3Failed = true;
                console.warn(`[Sequence] Step 3 failed (${s3.errorType}), continuing...`);
            }
            await sleep(this.delay);

            // Always submit report — even with partial data
            await this.submitReport({ step1Failed, step2Failed, step3Failed });
            this.lastFetch = Date.now();
            if (!step1Failed && !step2Failed && !step3Failed) {
                this.lastSuccessfulCollection = Date.now();
                console.log("Sequence complete.");
            } else {
                console.log(`Sequence complete (partial: step1=${step1Failed ? 'FAIL' : 'OK'} step2=${step2Failed ? 'FAIL' : 'OK'} step3=${step3Failed ? 'FAIL' : 'OK'}).`);
            }
        } catch (e) { 
            const errorType = ErrorClassifier.classify(e);
            console.error(`${errorType} Error: ${e.message}`);
            // Even on unexpected errors, try to submit whatever data we have
            try {
                await this.submitReport({ step1Failed: true, step2Failed: true, step3Failed: true });
                this.lastFetch = Date.now();
            } catch (reportErr) {
                console.error(`Failed to submit partial report: ${reportErr.message}`);
            }
            if (errorType === ErrorClassifier.TYPES.TRANSIENT && (e.message.includes("not open") || e.message.includes("closed"))) {
                throw e; // Propagate connection loss to outer run() loop
            }
            // DEGRADED/RECOVERABLE errors: log and continue next cycle
        }
    }

    async ensureLocalHomeBeforeCollection() {
        if (!this.clientLocal || !this.clientLocal.isConnected()) return;

        try {
            console.log("采集前正在切回本地首页...");
            await this.clientLocal.call("Runtime.evaluate", {
                expression: "window.location.hash='/'",
            });
            // 给路由切换和页面稳定留一点时间，行为与初始化脚本一致
            await sleep(1000);
            console.log("已切回本地首页，开始采集");
        } catch (e) {
            console.warn(`切回本地首页失败，将继续采集: ${e.message}`);
        }
    }

    async runCommand(cmd, args = [], timeoutMs = 12000) {
        return new Promise((resolve) => {
            let done = false;
            const child = spawn(cmd, args, { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
            let stdout = "";
            let stderr = "";

            const timer = setTimeout(() => {
                if (done) return;
                done = true;
                try { child.kill(); } catch (_e) {}
                resolve({ ok: false, code: -1, stdout, stderr: `${stderr}\nTIMEOUT` });
            }, timeoutMs);

            child.stdout.on("data", (d) => { stdout += d.toString(); });
            child.stderr.on("data", (d) => { stderr += d.toString(); });

            child.on("error", (e) => {
                if (done) return;
                done = true;
                clearTimeout(timer);
                resolve({ ok: false, code: -1, stdout, stderr: `${stderr}\n${e.message}` });
            });

            child.on("close", (code) => {
                if (done) return;
                done = true;
                clearTimeout(timer);
                resolve({ ok: code === 0, code, stdout, stderr });
            });
        });
    }

    async discoverPiContainer() {
        if (this._cachedContainerName) return this._cachedContainerName;
        const result = await this.runCommand("docker", [
            "ps", "-a", "--format", "{{.Names}}\t{{.Image}}"
        ], 8000);
        if (!result.ok) {
            console.log("[Docker] Failed to list containers:", result.stderr);
            return null;
        }
        for (const line of result.stdout.trim().split("\n")) {
            const [name, image] = line.split("\t");
            if (image && image.startsWith("pinetwork/pi-node-docker")) {
                this._cachedContainerName = name.trim();
                console.log(`[Docker] Discovered Pi container: "${this._cachedContainerName}" (image: ${image.trim()})`);
                return this._cachedContainerName;
            }
        }
        console.log("[Docker] No container found with image pinetwork/pi-node-docker");
        return null;
    }


    async stepEnsureDockerContainer() {
        const action = this.dockerCircuit.check();
        if (action === "REJECT") {
            this.sessionData.dockerState = "circuit_open";
            const status = this.dockerCircuit.getStatus();
            console.log(`[Step 0] Docker circuit OPEN, ${Math.round(status.cooldownRemaining / 1000)}s remaining`);
            return;
        }
        const isProbe = action === "PROBE";
        if (isProbe) console.log("[Step 0] Docker circuit HALF_OPEN — probe attempt");

        const containerName = await this.discoverPiContainer();
        if (!containerName) {
            this.sessionData.dockerState = "not_found";
            console.log("[Step 0] Pi Node container not found (image: pinetwork/pi-node-docker)");
            if (isProbe) this.dockerCircuit.recordFailure();
            return;
        }

        const inspect = await this.runCommand("docker", ["inspect", "-f", "{{.State.Running}}", containerName], 8000);
        if (inspect.ok && inspect.stdout.trim().toLowerCase() === "true") {
            this.sessionData.dockerState = "running";
            this.dockerCircuit.recordSuccess();
            return;
        }

        console.log(`[Step 0] Docker container "${containerName}" not running, trying to start...`);
        const start = await this.runCommand("docker", ["start", containerName], 12000);
        if (start.ok) {
            this.sessionData.dockerState = "running";
            this.sessionData.dockerRestartCount = (this.sessionData.dockerRestartCount || 0) + 1;
            this.dockerCircuit.recordSuccess();
            console.log(`[Step 0] Docker container "${containerName}" started`);
            return;
        }

        const errMsg = start.stderr || inspect.stderr || 'unknown';
        const errorType = ErrorClassifier.classify(errMsg);
        this.sessionData.dockerState = "failed";
        console.log(`[Step 0] Docker start failed [${errorType}]: ${errMsg}`);

        const tripped = this.dockerCircuit.recordFailure();
        if (tripped) {
            this._cachedContainerName = null; // Clear cache on circuit break to allow re-discovery
            await this.reporter.sendReport("STATUS", {
                value: "E_DOCKER_RECOVERY_FAILED",
                circuitBreaker: this.dockerCircuit.getStatus(),
            });
        }
    }

    // Step 2: API - Get Block Height from http://localhost:31401/
    async stepNodeStats() {
        console.log("[Step 2] API: Getting Block Height from stellar-core http-command info...");
        try {
            const containerName = await this.discoverPiContainer();
            if (!containerName) {
                console.log("[Step 2] Pi Node container not found");
                this.sessionData.localBlock = "0";
                this.sessionData.latestBlock = "0";
                this.sessionData.isSynced = false;
                return;
            }

            const infoCommand = await this.runCommand("docker", [
                "exec",
                containerName,
                "sh",
                "-lc",
                "stellar-core http-command info"
            ], 12000);

            if (!infoCommand.ok) {
                console.log(`[Step 2] stellar-core info failed: ${infoCommand.stderr || infoCommand.stdout || 'unknown error'}`);
                this.sessionData.localBlock = "0";
                this.sessionData.latestBlock = "0";
                this.sessionData.isSynced = false;
                return;
            }

            const rawOutput = String(infoCommand.stdout || "");
            const jsonStart = rawOutput.indexOf('{');
            if (jsonStart === -1) {
                console.log("[Step 2] stellar-core info output missing JSON payload");
                this.sessionData.localBlock = "0";
                this.sessionData.latestBlock = "0";
                this.sessionData.isSynced = false;
                return;
            }

            const parsed = JSON.parse(rawOutput.slice(jsonStart));
            const info = parsed?.info || {};
            const ledgerNumber = Number(info?.ledger?.num || 0);
            const quorumLedger = Number(info?.quorum?.qset?.ledger || 0);
            const latestDisplayNumber = Math.max(ledgerNumber, quorumLedger);

            if (Number.isFinite(ledgerNumber) && ledgerNumber > 0) {
                this.sessionData.localBlock = String(ledgerNumber);
                this.sessionData.latestBlock = String(latestDisplayNumber);
                this.sessionData.isSynced = ledgerNumber > 0 && ledgerNumber === latestDisplayNumber;
                console.log(`[Step 2] Block(SCP): local=${this.sessionData.localBlock} latest=${this.sessionData.latestBlock} synced=${this.sessionData.isSynced}`);
            } else {
                console.log("[Step 2] ledger.num not found or invalid");
                this.sessionData.localBlock = "0";
                this.sessionData.latestBlock = "0";
                this.sessionData.isSynced = false;
            }
        } catch (e) {
            console.log(`[Step 2] API fetch failed: ${e.message}`);
            this.sessionData.localBlock = "0";
            this.sessionData.latestBlock = "0";
            this.sessionData.isSynced = false;
        }
    }

    // Step 3: API - Check local API availability
    async stepCheckApi() {
        console.log("[Step 3] API: Checking localhost:31401 availability...");
        try {
            const rootRes = await fetchWithTimeout("http://127.0.0.1:31401/", 5000);
            if (!rootRes.ok) {
                this.sessionData.apiEnabled = false;
                console.log(`[Step 3] API unavailable: HTTP ${rootRes.status}`);
                return;
            }

            const rootData = await rootRes.json();
            const hasCoreLedger = Number.isFinite(Number(rootData.core_latest_ledger || 0)) && Number(rootData.core_latest_ledger || 0) > 0;
            if (!hasCoreLedger) {
                this.sessionData.apiEnabled = false;
                console.log("[Step 3] API reachable but core_latest_ledger invalid");
                return;
            }

            const ledgersRes = await fetchWithTimeout("http://127.0.0.1:31401/ledgers?order=desc&limit=1", 5000);
            this.sessionData.apiEnabled = ledgersRes.ok;
            console.log(`[Step 3] API enabled: ${this.sessionData.apiEnabled}`);
        } catch (e) {
            this.sessionData.apiEnabled = false;
            console.log(`[Step 3] API check failed: ${e.message}`);
        }
    }

    // Step 3.3: Refresh public network location via api.ip.sb
    async stepRefreshGeoIpInfo() {
        console.log("[Step 3.3] Network: refreshing geoip...");
        try {
            const res = await fetchWithTimeout("https://api.ip.sb/geoip", 7000);
            if (!res.ok) {
                console.log(`[Step 3.3] geoip request failed: HTTP ${res.status}`);
                return;
            }
            const geo = await res.json();

            const ip = String(geo?.ip || "").trim();
            const country = String(geo?.country || geo?.country_name || "").trim();
            const city = String(geo?.city || "").trim();

            if (ip) this.sessionData.geoIp = ip;
            if (country) this.sessionData.geoCountry = country;
            if (city) this.sessionData.geoCity = city;

            console.log(`[Step 3.3] geoip: ip=${this.sessionData.geoIp || "-"}, country=${this.sessionData.geoCountry || "-"}, city=${this.sessionData.geoCity || "-"}`);
        } catch (e) {
            console.log(`[Step 3.3] geoip refresh failed: ${e.message}`);
        }
    }

    // Step 3.5: Local - Check Pi Desktop web page connection latency
    async stepCheckPiPageLatency() {
        console.log("[Step 3.5] Network: checking Pi Node latency via app-cdn.minepi.com...");
        const startedAt = Date.now();
        try {
            const res = await fetchWithTimeout("https://app-cdn.minepi.com", 8000);
            if (res.ok) {
                this.sessionData.piPageLatencyMs = Math.max(0, Date.now() - startedAt);
                console.log(`[Step 3.5] Pi Node latency: ${this.sessionData.piPageLatencyMs}ms`);
            } else {
                this.sessionData.piPageLatencyMs = null;
                console.log(`[Step 3.5] Pi Node latency unavailable: HTTP ${res.status}`);
            }
        } catch (e) {
            this.sessionData.piPageLatencyMs = null;
            console.log(`[Step 3.5] Pi Node latency check failed: ${e.message}`);
        }
    }

    // Step 1: Remote - Fetch API Data
    async stepFetchApiData() {
        console.log("[Step 1] Remote: Fetching API Data...");
        if (!this.clientRemote || !this.clientRemote.isConnected()) {
            console.log("[Step 1] Remote not connected, skipping API fetch");
            return;
        }
        const apiScript = `(async function() { 
            try { 
                var h = {}; 
                var t = localStorage.getItem('mobile-app-webview-ui_access-token'); 
                if(t) h['Authorization'] = 'Bearer ' + t; 
                var meRaw = await fetch('https://socialchain.app/api/me', {headers: h}).then(r => r.json()); 
                var st = await fetch('https://socialchain.app/api/mining_sessions/status', {headers: h}).then(r => r.json()); 
                var pi = await fetch('https://socialchain.app/api/pi', {headers: h}).then(r => r.json());
                return JSON.stringify({me: meRaw, status: st, pi: pi, token: t}); 
            } catch(e) { return JSON.stringify({error: e.message}); } 
        })()`;
        const result = await this.evalRemote(apiScript, true);
        this.sessionData.apiData = JSON.parse(result || "{}");
        if (this.sessionData.apiData.error) {
            console.error(`[Step 1] API Error: ${this.sessionData.apiData.error}`);
            return;
        }
        const me = this.sessionData.apiData.me || {};
        const st = this.sessionData.apiData.status || {};
        const pi = this.sessionData.apiData.pi || {};
        const profile = me.profile || {};
        const expiresAt = pi?.mining_status?.expires_at;
        if (expiresAt) {
            const ts = Date.parse(expiresAt);
            if (!Number.isNaN(ts)) {
                const remain = Math.max(0, Math.floor((ts - Date.now()) / 1000));
                const hh = String(Math.floor(remain / 3600)).padStart(2, "0");
                const mm = String(Math.floor((remain % 3600) / 60)).padStart(2, "0");
                const ss = String(remain % 60).padStart(2, "0");
                this.sessionData.apiMiningTime = `${hh}:${mm}:${ss}`;
            }
        }
        console.log(`[Step 1] User: ${profile.display_name || profile.username || 'Unknown'}, Rate: ${st.total_rate || 0}, Node Factor: ${st.node?.node_factor || 0}`);
    }

    async submitReport({ step1Failed = false, step2Failed = false, step3Failed = false } = {}) {
        const d = this.sessionData;
        const me = d.apiData.me || {};
        const st = d.apiData.status || {};
        const pi = d.apiData.pi || {};
        const profile = me.profile || {};

        const collectBalanceCandidates = (obj, out = [], depth = 0) => {
            if (!obj || typeof obj !== "object" || depth > 4) return out;
            for (const [k, v] of Object.entries(obj)) {
                const key = String(k || "").toLowerCase();
                if (key.includes("balance") && (typeof v === "number" || typeof v === "string")) {
                    out.push(v);
                } else if (v && typeof v === "object") {
                    collectBalanceCandidates(v, out, depth + 1);
                }
            }
            return out;
        };

        const normalizeBalance = (value) => {
            if (value === undefined || value === null) return null;
            if (typeof value === "number") {
                if (!Number.isFinite(value)) return null;
                return String(value);
            }
            const s = String(value).trim();
            return s.length > 0 ? s : null;
        };

        const balanceCandidates = [
            profile.available_balance,
            profile.balance,
            profile.wallet?.available_balance,
            profile.wallet?.balance,
            me.available_balance,
            me.balance,
            me.wallet_balance,
            me.wallet?.balance,
            me.account?.balance,
            st.available_balance,
            st.balance,
            st.user_balance,
            st.pending_balance,
            st.node?.available_balance,
            st.node?.balance,
            pi.available_balance,
            pi.balance,
            pi.wallet_balance,
            pi.user?.balance,
            pi.account?.balance,
            ...collectBalanceCandidates(profile),
            ...collectBalanceCandidates(me),
            ...collectBalanceCandidates(st),
            ...collectBalanceCandidates(pi),
        ];

        const normalizedBalances = balanceCandidates
            .map(normalizeBalance)
            .filter((v) => v !== null);
        const nonZeroBalance = normalizedBalances.find((v) => {
            const n = Number(String(v).replace(/,/g, ""));
            return Number.isFinite(n) && n > 0;
        });
        const resolvedBalance = nonZeroBalance || normalizedBalances[0] || "0";

        const externalIpCandidates = [
            profile.external_ip,
            profile.ip_address,
            profile.ip,
            me.external_ip,
            me.ip_address,
            me.ip,
            st.external_ip,
            st.ip_address,
            st.node?.external_ip,
            st.node?.ip,
        ];
        const rawIpCandidate = externalIpCandidates.find((v) => v !== undefined && v !== null && `${v}`.trim() !== "");
        const resolvedExternalIp = await resolvePublicIp(this.sessionData.geoIp || rawIpCandidate);
        const resolvedUsername = profile.display_name || profile.username || me.username || "Unknown";

        const resolvedMiningTime = d.apiMiningTime || "0";
        const tokenValue = String(d.apiData?.token || "").trim();
        const remoteUrl = String(this.lastUrl || "").trim().toLowerCase();
        const isSigninPage = remoteUrl.includes("node-signin");
        const hasStrongProfileSignals = !!resolvedUsername && resolvedUsername !== "Unknown" && resolvedUsername !== "-" && (Number(d.latestBlock || 0) > 1000 || Number(st.total_rate || 0) > 0 || resolvedMiningTime !== "0");
        const loginStatus = (tokenValue.length > 0 && !isSigninPage) || hasStrongProfileSignals;
        // pi_status: "online" = logged in, "degraded" = agent alive but API data unavailable, "starting" = login pending, "offline" = not logged in
        let piStatus;
        if (loginStatus) {
            piStatus = "online";
        } else if (isSigninPage) {
            piStatus = "starting";
        } else if (step1Failed && this.state === StateMachine.STATES.MAIN) {
            // Agent is running and in MAIN state, but API data couldn't be fetched
            piStatus = "degraded";
        } else {
            piStatus = "offline";
        }
        const externalNetworkStatus =
            Number.isFinite(Number(d.piPageLatencyMs)) ||
            String(d.geoIp || "").trim().length > 0;

        const payload = {
            username: resolvedUsername,
            balance: resolvedBalance,
            mining_rate: String((st.total_rate || 0) + " π/hr"),
            pi_status: piStatus,
            login_status: loginStatus,
            external_network_status: externalNetworkStatus,
            local_block_number: d.localBlock,
            is_synced: d.isSynced,
            node_name: resolvedUsername,
            ip_address: resolvedExternalIp || "",
            hashrate: String(st.total_rate || 0),
            rewards: String(st.node?.node_factor || 0),
            mining_time: resolvedMiningTime,
            block_height: d.latestBlock,
            docker_state: d.dockerState,
            docker_restart_count: d.dockerRestartCount,
            pi_page_latency_ms: Number.isFinite(Number(d.piPageLatencyMs)) ? Number(d.piPageLatencyMs) : null,
            ip_country: d.geoCountry ? d.geoCountry : null,
            ip_city: d.geoCity ? d.geoCity : null,
            timestamp: new Date().toISOString(),
            token: tokenValue
        };
        console.log("Report:", JSON.stringify(payload));
        await this.reporter.sendReport("MINING_DATA", payload);
    }

    async navigateRemote(url) { if (this.clientRemote) await this.clientRemote.call("Page.navigate", { url }); }
    async evalRemote(expression, awaitPromise = false) { if (!this.clientRemote) return null; const res = await this.clientRemote.call("Runtime.evaluate", { expression, awaitPromise, returnByValue: true }); return res.result?.value; }
    async evalLocal(expression, awaitPromise = false) { if (!this.clientLocal) return null; const res = await this.clientLocal.call("Runtime.evaluate", { expression, awaitPromise, returnByValue: true }); return res.result?.value; }
}
// ─────────────────────────────────────────────
// Helper: sleep
// ─────────────────────────────────────────────
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }

async function fetchWithTimeout(url, timeoutMs = 3000) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        return await fetch(url, { signal: controller.signal });
    } finally {
        clearTimeout(timer);
    }
}

async function resolvePublicIp(fallback = "") {
    const normalizedFallback = String(fallback || "").trim();
    if (normalizedFallback && normalizedFallback !== "127.0.0.1" && normalizedFallback !== "0.0.0.0") {
        return normalizedFallback;
    }

    const endpoints = [
        { url: "https://api.ipify.org?format=json", parse: async (r) => (await r.json()).ip },
        { url: "https://ifconfig.me/ip", parse: async (r) => (await r.text()).trim() },
        { url: "https://api64.ipify.org?format=json", parse: async (r) => (await r.json()).ip },
    ];

    for (const ep of endpoints) {
        try {
            const res = await fetchWithTimeout(ep.url, 3000);
            if (!res.ok) continue;
            const ip = String(await ep.parse(res) || "").trim();
            if (ip && ip !== "127.0.0.1" && ip !== "0.0.0.0") return ip;
        } catch (_e) {}
    }

    return "";
}

/**
 * TargetHunter: Locates BOTH the local Pi Desktop page and the remote minepi Webview
 */
class TargetHunter {
    constructor(endpoint = "http://127.0.0.1:8888/json") {
        this.endpoint = endpoint;
    }

    async findTargets() {
        try {
            const response = await fetch(this.endpoint);
            const targets = await response.json();

            if (!Array.isArray(targets)) return null;

            // 1. Local Target (Pi Desktop - file://)
            const local = targets.find(t => 
                (t.url && t.url.startsWith("file://") && t.title === "Pi Desktop") ||
                (t.type === "page" && t.title === "Pi Desktop")
            );

            // 2. Remote Target (Pi Webview - https://app-cdn.minepi.com)
            const remote = targets.find(t => 
                (t.url && t.url.includes("minepi.com") && t.type === "webview") ||
                (t.title === "Pi" && t.type === "webview")
            );

            if (local || remote) {
                console.log(`[Nova-Hunter] Targets Found: Local=${local ? 'YES' : 'NO'} Remote=${remote ? 'YES' : 'NO'}`);
                return { local, remote };
            }

            return null;
        } catch (error) {
            console.error(`[Nova-Hunter] Error fetching targets: ${error.message}`);
            return null;
        }
    }
}

/**
 * Entry Point: Main execution loop
 */
async function main() {
    console.log("Nova Core v3.8 - Adaptive Delay Mode");

    // 0. Parse CLI Arguments
    const args = process.argv.slice(2);
    let config = {
        cdpPort: 8888,
        apiPort: 13140,
        interval: 10 * 1000,
        delay: 3000
    };

    for (let i = 0; i < args.length; i++) {
        const arg = args[i];
        if (arg === '--port') {
            config.cdpPort = parseInt(args[++i]) || 8888;
        } else if (arg === '--api-port') {
            config.apiPort = parseInt(args[++i]) || 13140;
        } else if (arg.startsWith('--interval=')) {
            config.interval = parseInt(arg.split('=')[1]) * 60 * 1000;
        } else if (arg.startsWith('--delay=')) {
            config.delay = parseInt(arg.split('=')[1]) * 1000;
        }
    }

    console.log(`[Nova-Main] Config: CDP=${config.cdpPort}, API=${config.apiPort}, Interval=${config.interval}ms`);

    // 1. Wait for Security Key from STDIN
    const keyHex = await new Promise((resolve) => {
        console.log("[Nova-Main] Waiting for security key (64-char hex)...");
        const rl = require('readline').createInterface({
            input: process.stdin,
            terminal: false
        });
        
        const timeout = setTimeout(() => {
            console.error("[Nova-Main] Security key timeout");
            process.exit(1);
        }, 30000);

        rl.on('line', (line) => {
            const trimmed = line.trim();
            if (trimmed.length === 64) {
                clearTimeout(timeout);
                rl.close();
                resolve(trimmed);
            }
        });
    });

    const security = new SecurityLayer(keyHex);
    const reporter = new Reporter(security, `http://127.0.0.1:${config.apiPort}/report`);
    console.log("[Nova-Main] Secure Link Established");

    const hunter = new TargetHunter(`http://127.0.0.1:${config.cdpPort}/json`);
    
    let mainReconnectAttempt = 0;

    // 2. Main Reconnection Loop
    while (true) {
        let targets = null;
        let clientLocal = null;
        let clientRemote = null;

        try {
            // 2.1 Find Targets (Allow Partial)
            while (!targets) {
                console.log("[Nova-Main] Hunting for targets...");
                targets = await hunter.findTargets();
                if (!targets) await sleep(2000);
            }

            console.log(`[Nova-Main] Connecting... Local=${!!targets.local} Remote=${!!targets.remote}`);

            // 2.2 Connect Clients
            const promises = [];
            if (targets.local) {
                clientLocal = new CDPClient(targets.local.webSocketDebuggerUrl);
                promises.push(clientLocal.connect());
            }
            if (targets.remote) {
                clientRemote = new CDPClient(targets.remote.webSocketDebuggerUrl);
                promises.push(clientRemote.connect());
            }

            await Promise.all(promises);
            console.log("[Nova-Main] Connection(s) Established ✅");

            // 2.3 Initialize Managers
            let cmRemote = null;
            
            if (clientLocal) {
                await clientLocal.call("Page.enable");
                await clientLocal.call("Runtime.enable");
                await clientLocal.call("DOM.enable");
            }

            if (clientRemote) {
                cmRemote = new ContextManager(clientRemote);
                await clientRemote.call("Page.enable");
                await clientRemote.call("Runtime.enable");
                await clientRemote.call("DOM.enable");
            }

            // Pass clients (nullable) to StateMachine
            const sm = new StateMachine(clientLocal, clientRemote, reporter, cmRemote, hunter, {
                interval: config.interval,
                delay: config.delay,
                apiPort: config.apiPort,
                initialRemoteUrl: targets.remote?.url || null
            });
            sm.attachRemoteNavigationTracking(clientRemote);

            // 2.4 Send PING
            await reporter.sendReport("PING");

            // 2.5 Run Loop
            await sm.run();
            mainReconnectAttempt = 0; // Reset on successful loop exit (non-error)

        } catch (e) {
            const errorType = ErrorClassifier.classify(e);
            console.error(`[Nova-Main] ${errorType} Loop Error: ${e.message}`);
            if (errorType === ErrorClassifier.TYPES.FATAL) {
                console.error("[Nova-Main] Fatal error — stopping reconnection loop.");
                break;
            }
        } finally {
            if (clientLocal) clientLocal.close();
            if (clientRemote) clientRemote.close();
        }

        // Exponential backoff reconnection: 5s → 10s → 20s → max 60s
        const reconnectDelay = Math.min(5000 * Math.pow(2, mainReconnectAttempt), 60000);
        mainReconnectAttempt++;
        console.warn(`[Nova-Main] Connection lost. Reconnecting in ${Math.round(reconnectDelay / 1000)}s... (attempt ${mainReconnectAttempt})`);
        await sleep(reconnectDelay);
    }
}

main().catch(err => console.error("[Nova-Main] Critical Error:", err));
