"use strict";
/**
 * MidnightNodeProvider, Substrate RPC WebSocket Client
 *
 * Connects directly to a Midnight Node via Substrate JSON-RPC 2.0 over WebSocket.
 * Primary data source for the active crawler. Independent of the hosted Midnight Indexer.
 *
 * Protocol: JSON-RPC 2.0 over WebSocket (ws://node:9944)
 * Reference: Polkadot/Substrate RPC specification
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MidnightNodeProvider = void 0;
const ws_1 = __importDefault(require("ws"));
// ============================================================================
// Midnight Node Provider
// ============================================================================
class MidnightNodeProvider {
    ws = null;
    requestId = 0;
    pendingRequests = new Map();
    subscriptions = new Map();
    connected = false;
    reconnecting = false;
    reconnectAttempts = 0;
    config;
    onReconnectCallback = null;
    reconnectTimer = null;
    constructor(config) {
        this.config = {
            nodeUrl: config.nodeUrl,
            requestTimeout: config.requestTimeout || 30000,
            reconnectInterval: config.reconnectInterval || 5000,
            maxReconnectAttempts: config.maxReconnectAttempts || 10
        };
    }
    // ========================================================================
    // Connection Management
    // ========================================================================
    async connect() {
        return new Promise((resolve, reject) => {
            try {
                this.ws = new ws_1.default(this.config.nodeUrl);
                this.ws.on('open', () => {
                    this.connected = true;
                    this.reconnecting = false;
                    this.reconnectAttempts = 0;
                    console.log(`[MidnightNode] Connected to ${this.config.nodeUrl}`);
                    resolve();
                });
                this.ws.on('message', (data) => {
                    this.handleMessage(data.toString());
                });
                this.ws.on('error', (error) => {
                    console.error('[MidnightNode] WebSocket error:', error.message);
                    if (!this.connected) {
                        reject(error);
                    }
                });
                this.ws.on('close', () => {
                    const wasConnected = this.connected;
                    this.connected = false;
                    this.rejectAllPending('Connection closed');
                    this.subscriptions.clear();
                    if (!wasConnected) {
                        // Socket closed before 'open', reject the connect() promise
                        reject(new Error(`WebSocket closed before connection established to ${this.config.nodeUrl}`));
                        return;
                    }
                    if (!this.reconnecting) {
                        console.warn('[MidnightNode] Connection lost, attempting reconnect...');
                        this.attemptReconnect();
                    }
                });
            }
            catch (error) {
                reject(error);
            }
        });
    }
    async disconnect() {
        this.reconnecting = false;
        // Clear any pending reconnect timer
        if (this.reconnectTimer) {
            clearTimeout(this.reconnectTimer);
            this.reconnectTimer = null;
        }
        this.rejectAllPending('Disconnecting');
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
        this.connected = false;
        this.subscriptions.clear();
        console.log('[MidnightNode] Disconnected');
    }
    isConnected() {
        return this.connected;
    }
    /**
     * Register a callback to invoke after successful reconnect.
     * Used by the Crawler to re-establish subscriptions.
     */
    setOnReconnect(callback) {
        this.onReconnectCallback = callback;
    }
    attemptReconnect() {
        if (this.reconnecting)
            return;
        if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
            console.error(`[MidnightNode] Max reconnect attempts (${this.config.maxReconnectAttempts}) reached`);
            return;
        }
        this.reconnecting = true;
        this.reconnectAttempts++;
        const delay = this.config.reconnectInterval * Math.min(this.reconnectAttempts, 5);
        console.log(`[MidnightNode] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
        this.reconnectTimer = setTimeout(async () => {
            this.reconnectTimer = null;
            try {
                await this.connect();
                console.log('[MidnightNode] Reconnected successfully');
                // Notify crawler to re-establish subscriptions
                if (this.onReconnectCallback) {
                    try {
                        await this.onReconnectCallback();
                    }
                    catch (cbErr) {
                        console.error('[MidnightNode] Reconnect callback failed:', cbErr.message);
                    }
                }
            }
            catch (err) {
                console.error('[MidnightNode] Reconnect failed:', err.message);
                this.reconnecting = false;
                this.attemptReconnect();
            }
        }, delay);
    }
    // ========================================================================
    // JSON-RPC 2.0 Core
    // ========================================================================
    async rpc(method, params = []) {
        if (!this.ws || !this.connected) {
            throw new Error('Not connected to Midnight Node');
        }
        const id = ++this.requestId;
        return new Promise((resolve, reject) => {
            const timeout = setTimeout(() => {
                this.pendingRequests.delete(id);
                reject(new Error(`RPC timeout: ${method} (${this.config.requestTimeout}ms)`));
            }, this.config.requestTimeout);
            this.pendingRequests.set(id, { resolve, reject, timeout });
            const request = {
                jsonrpc: '2.0',
                id,
                method,
                params
            };
            this.ws.send(JSON.stringify(request));
        });
    }
    /**
     * Send multiple RPCs in a single JSON-RPC 2.0 batch request.
     *
     * One WSS frame out, one frame in. The server processes the calls in
     * parallel internally and returns a response array; we resolve each
     * caller's promise as the matching id arrives in `handleMessage`.
     *
     * Returns results in the SAME ORDER as the input requests. Throws if any
     * single sub-request errors out (matches the rpc() semantics).
     */
    async rpcBatch(requests) {
        if (!this.ws || !this.connected) {
            throw new Error('Not connected to Midnight Node');
        }
        if (requests.length === 0)
            return [];
        const ids = [];
        const promises = [];
        for (const req of requests) {
            const id = ++this.requestId;
            ids.push(id);
            promises.push(new Promise((resolve, reject) => {
                const timeout = setTimeout(() => {
                    this.pendingRequests.delete(id);
                    reject(new Error(`RPC timeout: ${req.method} (${this.config.requestTimeout}ms)`));
                }, this.config.requestTimeout);
                this.pendingRequests.set(id, { resolve, reject, timeout });
            }));
        }
        const payload = requests.map((req, i) => ({
            jsonrpc: '2.0',
            id: ids[i],
            method: req.method,
            params: req.params ?? []
        }));
        this.ws.send(JSON.stringify(payload));
        return Promise.all(promises);
    }
    handleMessage(raw) {
        let parsed;
        try {
            parsed = JSON.parse(raw);
        }
        catch {
            console.warn('[MidnightNode] Invalid JSON message received');
            return;
        }
        // JSON-RPC 2.0 batch response: a top-level array of response objects.
        // Iterate and dispatch each as a normal message.
        if (Array.isArray(parsed)) {
            for (const msg of parsed) {
                this.handleSingleMessage(msg);
            }
            return;
        }
        this.handleSingleMessage(parsed);
    }
    handleSingleMessage(message) {
        // Subscription notification
        if (message.method && message.params?.subscription) {
            const callback = this.subscriptions.get(message.params.subscription);
            if (callback) {
                try {
                    const callbackResult = callback(message.params.result);
                    if (callbackResult && typeof callbackResult.then === 'function') {
                        void callbackResult.catch((err) => {
                            const errMsg = err instanceof Error ? err.message : String(err);
                            console.error('[MidnightNode] Subscription callback failed:', errMsg);
                        });
                    }
                }
                catch (err) {
                    const errMsg = err instanceof Error ? err.message : String(err);
                    console.error('[MidnightNode] Subscription callback failed:', errMsg);
                }
            }
            return;
        }
        // Regular RPC response
        if (message.id !== undefined) {
            const pending = this.pendingRequests.get(message.id);
            if (pending) {
                clearTimeout(pending.timeout);
                this.pendingRequests.delete(message.id);
                if (message.error) {
                    pending.reject(new Error(`RPC error ${message.error.code}: ${message.error.message}`));
                }
                else {
                    pending.resolve(message.result);
                }
            }
        }
    }
    rejectAllPending(reason) {
        for (const [id, pending] of this.pendingRequests) {
            clearTimeout(pending.timeout);
            pending.reject(new Error(reason));
        }
        this.pendingRequests.clear();
    }
    // ========================================================================
    // Chain RPC Methods
    // ========================================================================
    /**
     * Get the latest block header
     */
    async getLatestHeader() {
        return this.rpc('chain_getHeader');
    }
    /**
     * Get a block header by hash
     */
    async getHeader(hash) {
        return this.rpc('chain_getHeader', hash ? [hash] : []);
    }
    /**
     * Get a full signed block by hash
     */
    async getBlock(hash) {
        return this.rpc('chain_getBlock', [hash]);
    }
    /**
     * Get the block hash for a given height
     */
    async getBlockHash(height) {
        return this.rpc('chain_getBlockHash', [height]);
    }
    /**
     * Get the finalized block head hash
     */
    async getFinalizedHead() {
        return this.rpc('chain_getFinalizedHead');
    }
    // ========================================================================
    // State RPC Methods
    // ========================================================================
    /**
     * Query runtime storage at a given key (optionally at a specific block)
     */
    async getStorage(key, blockHash) {
        return this.rpc('state_getStorage', blockHash ? [key, blockHash] : [key]);
    }
    /**
     * Get the runtime version
     */
    async getRuntimeVersion(blockHash) {
        return this.rpc('state_getRuntimeVersion', blockHash ? [blockHash] : []);
    }
    /**
     * Get runtime metadata (SCALE-encoded)
     */
    async getMetadata(blockHash) {
        return this.rpc('state_getMetadata', blockHash ? [blockHash] : []);
    }
    // ========================================================================
    // System RPC Methods
    // ========================================================================
    /**
     * Get node health status
     */
    async health() {
        return this.rpc('system_health');
    }
    /**
     * Get the chain name
     */
    async chain() {
        return this.rpc('system_chain');
    }
    /**
     * Get the node name
     */
    async name() {
        return this.rpc('system_name');
    }
    /**
     * Get the node version
     */
    async version() {
        return this.rpc('system_version');
    }
    // ========================================================================
    // Subscriptions
    // ========================================================================
    /**
     * Subscribe to new block headers (finalized)
     */
    async subscribeNewHeads(callback) {
        const subscriptionId = await this.rpc('chain_subscribeNewHeads', []);
        this.subscriptions.set(subscriptionId, callback);
        return subscriptionId;
    }
    /**
     * Subscribe to finalized block headers
     */
    async subscribeFinalizedHeads(callback) {
        const subscriptionId = await this.rpc('chain_subscribeFinalizedHeads', []);
        this.subscriptions.set(subscriptionId, callback);
        return subscriptionId;
    }
    /**
     * Unsubscribe from new heads
     */
    async unsubscribeNewHeads(subscriptionId) {
        this.subscriptions.delete(subscriptionId);
        return this.rpc('chain_unsubscribeNewHeads', [subscriptionId]);
    }
    /**
     * Unsubscribe from finalized heads
     */
    async unsubscribeFinalizedHeads(subscriptionId) {
        this.subscriptions.delete(subscriptionId);
        return this.rpc('chain_unsubscribeFinalizedHeads', [subscriptionId]);
    }
    // ========================================================================
    // Utility
    // ========================================================================
    /**
     * Parse a hex-encoded block number to integer
     */
    static parseBlockNumber(hex) {
        const n = parseInt(hex, 16);
        if (isNaN(n)) {
            throw new Error(`Invalid block number hex: "${hex}"`);
        }
        return n;
    }
    /**
     * Get pending request count (for health monitoring)
     */
    getPendingRequestCount() {
        return this.pendingRequests.size;
    }
    /**
     * Get active subscription count
     */
    getSubscriptionCount() {
        return this.subscriptions.size;
    }
}
exports.MidnightNodeProvider = MidnightNodeProvider;
