"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.BitcoinP2PWorker = void 0;
const events_1 = require("events");
const os = __importStar(require("os"));
const logger_1 = __importStar(require("../../logger"));
const block_1 = require("../../models/block");
const state_1 = require("../../models/state");
const transaction_1 = require("../../models/transaction");
const chain_state_1 = require("../../providers/chain-state");
const libs_1 = require("../../providers/libs");
const p2p_1 = require("../../services/p2p");
const utils_1 = require("../../utils");
const sync_1 = require("./sync");
class BitcoinP2PWorker extends p2p_1.BaseP2PWorker {
    constructor({ chain, network, chainConfig, blockModel = block_1.BitcoinBlockStorage }) {
        super({ chain, network, chainConfig, blockModel });
        this.blockModel = blockModel;
        this.chain = chain;
        this.network = network;
        this.bitcoreLib = libs_1.Libs.get(chain).lib;
        this.bitcoreP2p = libs_1.Libs.get(chain).p2p;
        this.chainConfig = chainConfig;
        this.events = new events_1.EventEmitter();
        this.isSyncing = false;
        this.initialSyncComplete = false;
        this.invCache = {};
        this.invCacheLimits = {
            [this.bitcoreP2p.Inventory.TYPE.BLOCK]: 100,
            [this.bitcoreP2p.Inventory.TYPE.TX]: 100000
        };
        this.messages = new this.bitcoreP2p.Messages({
            network: this.bitcoreLib.Networks.get(this.network)
        });
        this.pool = new this.bitcoreP2p.Pool({
            addrs: this.chainConfig.trustedPeers.map(peer => {
                return {
                    ip: {
                        v4: peer.host
                    },
                    port: peer.port
                };
            }),
            dnsSeed: false,
            listenAddr: false,
            network: this.network,
            messages: this.messages
        });
        this.multiThreadSync = new sync_1.UtxoMultiThreadSync({
            chain,
            network,
            config: chainConfig,
            callbacks: {
                getHeaders: () => this.getHeadersForSync(),
                processBlock: (block) => this.processBlock(block),
                getLocalTip: () => chain_state_1.ChainStateProvider.getLocalTip({ chain, network }),
                deserializeBlock: (rawHex) => new this.bitcoreLib.Block(Buffer.from(rawHex, 'hex'))
            }
        });
        this.multiThreadSync.once('INITIALSYNCDONE', async () => {
            this.initialSyncComplete = true;
            await state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true });
            this.events.emit('SYNCDONE');
        });
    }
    cacheInv(type, hash) {
        if (!this.invCache[type]) {
            this.invCache[type] = [];
        }
        if (this.invCache[type].length > this.invCacheLimits[type]) {
            this.invCache[type].shift();
        }
        this.invCache[type].push(hash);
    }
    isCachedInv(type, hash) {
        if (!this.invCache[type]) {
            this.invCache[type] = [];
        }
        return this.invCache[type].includes(hash);
    }
    setupListeners() {
        this.pool.on('peerready', peer => {
            logger_1.default.info(`${(0, logger_1.timestamp)()} | Connected to peer: ${peer.host}:${peer.port.toString().padEnd(5)} | Chain: ${this.chain} | Network: ${this.network}`);
        });
        this.pool.on('peerconnect', peer => {
            logger_1.default.info(`${(0, logger_1.timestamp)()} | Connected to peer: ${peer.host}:${peer.port.toString().padEnd(5)} | Chain: ${this.chain} | Network: ${this.network}`);
        });
        this.pool.on('peerdisconnect', peer => {
            logger_1.default.warn(`${(0, logger_1.timestamp)()} | Not connected to peer: ${peer.host}:${peer.port.toString().padEnd(5)} | Chain: ${this.chain} | Network: ${this.network}`);
        });
        this.pool.on('peertx', async (peer, message) => {
            try {
                const hash = message.transaction.hash;
                logger_1.default.debug('peer tx received: %o', {
                    peer: `${peer.host}:${peer.port}`,
                    chain: this.chain,
                    network: this.network,
                    hash
                });
                if (this.isSyncingNode && !this.isCachedInv(this.bitcoreP2p.Inventory.TYPE.TX, hash)) {
                    this.cacheInv(this.bitcoreP2p.Inventory.TYPE.TX, hash);
                    await this.processTransaction(message.transaction);
                    this.events.emit('transaction', message.transaction);
                }
            }
            catch (err) {
                logger_1.default.error('Error in peertx handler:', err);
            }
        });
        this.pool.on('peerblock', async (peer, message) => {
            try {
                const { block } = message;
                const { hash } = block;
                logger_1.default.debug('peer block received: %o', {
                    peer: `${peer.host}:${peer.port}`,
                    chain: this.chain,
                    network: this.network,
                    hash
                });
                const blockInCache = this.isCachedInv(this.bitcoreP2p.Inventory.TYPE.BLOCK, hash);
                if (!blockInCache) {
                    for (const transaction of block.transactions) {
                        this.cacheInv(this.bitcoreP2p.Inventory.TYPE.TX, transaction.hash);
                    }
                    this.cacheInv(this.bitcoreP2p.Inventory.TYPE.BLOCK, hash);
                }
                if (this.isSyncingNode && (!blockInCache || this.isSyncing)) {
                    this.events.emit(hash, message.block);
                    this.events.emit('block', message.block);
                    if (!this.isSyncing) {
                        this.sync();
                    }
                }
            }
            catch (err) {
                logger_1.default.error('Error in peerblock handler:', err);
            }
        });
        this.pool.on('peerheaders', (peer, message) => {
            logger_1.default.debug('peerheaders message received: %o', {
                peer: `${peer.host}:${peer.port}`,
                chain: this.chain,
                network: this.network,
                count: message.headers.length
            });
            this.events.emit('headers', message.headers);
        });
        this.pool.on('peerinv', (peer, message) => {
            if (this.isSyncingNode) {
                const filtered = message.inventory.filter(inv => {
                    const hash = this.bitcoreLib.encoding
                        .BufferReader(inv.hash)
                        .readReverse()
                        .toString('hex');
                    return !this.isCachedInv(inv.type, hash);
                });
                if (filtered.length) {
                    peer.sendMessage(this.messages.GetData(filtered));
                }
            }
        });
    }
    async connect() {
        this.setupListeners();
        this.pool.connect();
        this.connectInterval = setInterval(this.pool.connect.bind(this.pool), 5000);
        return new Promise(resolve => {
            this.pool.once('peerready', () => resolve());
        });
    }
    async disconnect() {
        this.pool.removeAllListeners();
        this.pool.disconnect();
        if (this.connectInterval) {
            clearInterval(this.connectInterval);
        }
    }
    async getHeaders(candidateHashes) {
        let received = false;
        return new Promise(async (resolve) => {
            this.events.once('headers', headers => {
                received = true;
                resolve(headers);
            });
            while (!received) {
                this.pool.sendMessage(this.messages.GetHeaders({ starts: candidateHashes }));
                await (0, utils_1.wait)(1000);
            }
        });
    }
    async getBlock(hash) {
        logger_1.default.debug('Getting block, hash:', hash);
        let received = false;
        return new Promise(async (resolve) => {
            this.events.once(hash, (block) => {
                logger_1.default.debug('Received block, hash: %o', hash);
                received = true;
                resolve(block);
            });
            while (!received) {
                this.pool.sendMessage(this.messages.GetData.forBlock(hash));
                await (0, utils_1.wait)(1000);
            }
        });
    }
    getBestPoolHeight() {
        let best = 0;
        for (const peer of Object.values(this.pool._connectedPeers)) {
            if (peer.bestHeight > best) {
                best = peer.bestHeight;
            }
        }
        return best;
    }
    async processBlock(block) {
        await this.blockModel.addBlock({
            chain: this.chain,
            network: this.network,
            forkHeight: this.chainConfig.forkHeight,
            parentChain: this.chainConfig.parentChain,
            initialSyncComplete: this.initialSyncComplete,
            block,
            initialHeight: this.chainConfig.syncStartHeight
        });
    }
    async processTransaction(tx) {
        const now = new Date();
        await transaction_1.TransactionStorage.batchImport({
            chain: this.chain,
            network: this.network,
            txs: [tx],
            height: -1 /* SpentHeightIndicators.pending */,
            mempoolTime: now,
            blockTime: now,
            blockTimeNormalized: now,
            initialSyncComplete: true
        });
    }
    async syncDone() {
        return new Promise(resolve => this.events.once('SYNCDONE', resolve));
    }
    useMultiThread() {
        if (this.chainConfig.threads == null) {
            // use multithread by default if there are >2 threads in the CPU
            return os.cpus().length > 2;
        }
        return this.chainConfig.threads > 0;
    }
    // Get headers using locator hashes (shared between sync modes)
    async getHeadersForSync() {
        const { chain, network } = this;
        let locators = await chain_state_1.ChainStateProvider.getLocatorHashes({ chain, network });
        if (locators.length === 1 && locators[0] === Array(65).join('0') && this.chainConfig.syncStartHash) {
            locators = [this.chainConfig.syncStartHash];
        }
        return this.getHeaders(locators);
    }
    // Handle genesis block fetch (needed when syncing from height 0)
    async handleGenesisBlock(headers) {
        if (headers[0]) {
            const block = await this.getBlock(headers[0].hash);
            if (block.header.prevHash) {
                const prevHash = Buffer.from(block.header.prevHash).reverse().toString('hex');
                const genesisBlock = await this.getBlock(prevHash);
                await this.processBlock(genesisBlock);
                return 1;
            }
        }
        return 0;
    }
    async sync() {
        if (this.isSyncing) {
            return false;
        }
        // Multi-thread mode check BEFORE DB read (matches EVM pattern).
        // The in-memory flag is set by INITIALSYNCDONE event after multi-thread sync finishes.
        // Reading from DB here would overwrite it back to false before the DB is updated.
        if (!this.initialSyncComplete && this.useMultiThread()) {
            this.isSyncing = true;
            const { chain, chainConfig, network } = this;
            const { parentChain, forkHeight } = chainConfig;
            let tip = await chain_state_1.ChainStateProvider.getLocalTip({ chain, network });
            if (parentChain && (!tip || tip.height < forkHeight)) {
                let parentTip = await chain_state_1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
                while (!parentTip || parentTip.height < forkHeight) {
                    logger_1.default.info(`Waiting until ${parentChain} syncs before ${chain} ${network}`);
                    await (0, utils_1.wait)(5000);
                    parentTip = await chain_state_1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
                }
            }
            // Handle genesis block before multi-thread sync
            tip = await chain_state_1.ChainStateProvider.getLocalTip({ chain, network });
            const currentHeight = tip?.height ?? (chainConfig.syncStartHeight || 0);
            if (currentHeight === 0) {
                const headers = await this.getHeadersForSync();
                if (headers.length > 0) {
                    await this.handleGenesisBlock(headers);
                }
            }
            logger_1.default.info(`${(0, logger_1.timestamp)()} | Starting multi-thread sync | Chain: ${chain} | Network: ${network}`);
            this.isSyncing = false;
            return this.multiThreadSync.sync();
        }
        // Single-thread mode with prefetching (for near-tip sync and post-initial sync)
        this.isSyncing = true;
        const { chain, chainConfig, network } = this;
        const { parentChain, forkHeight } = chainConfig;
        const state = await state_1.StateStorage.collection.findOne({});
        this.initialSyncComplete = state?.initialSyncComplete?.includes(`${chain}:${network}`);
        let tip = await chain_state_1.ChainStateProvider.getLocalTip({ chain, network });
        if (parentChain && (!tip || tip.height < forkHeight)) {
            let parentTip = await chain_state_1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
            while (!parentTip || parentTip.height < forkHeight) {
                logger_1.default.info(`Waiting until ${parentChain} syncs before ${chain} ${network}`);
                await (0, utils_1.wait)(5000);
                parentTip = await chain_state_1.ChainStateProvider.getLocalTip({ chain: parentChain, network });
            }
        }
        // Handle genesis block
        tip = await chain_state_1.ChainStateProvider.getLocalTip({ chain, network });
        const currentHeight = tip?.height ?? (chainConfig.syncStartHeight || 0);
        if (currentHeight === 0) {
            const genesisHeaders = await this.getHeadersForSync();
            if (genesisHeaders.length > 0) {
                await this.handleGenesisBlock(genesisHeaders);
            }
        }
        const prefetchSize = chainConfig.prefetchSize ?? 10;
        let headers = await this.getHeadersForSync();
        while (headers.length > 0) {
            tip = await chain_state_1.ChainStateProvider.getLocalTip({ chain, network });
            let height = tip?.height ?? (chainConfig.syncStartHeight || 0);
            const startingHeight = height;
            const startingTime = Date.now();
            let lastLog = startingTime;
            logger_1.default.info(`${(0, logger_1.timestamp)()} | Syncing ${headers.length} blocks | Chain: ${chain} | Network: ${network}` +
                (prefetchSize > 0 ? ` | Prefetch: ${prefetchSize}` : ''));
            // Prefetch: kick off parallel block downloads ahead of processing.
            // Blocks are still processed sequentially to preserve UTXO ordering.
            const prefetchMap = new Map();
            const initialBatch = Math.min(prefetchSize, headers.length);
            for (let i = 0; i < initialBatch; i++) {
                prefetchMap.set(headers[i].hash, this.getBlock(headers[i].hash));
            }
            for (let i = 0; i < headers.length; i++) {
                const header = headers[i];
                try {
                    const block = await (prefetchMap.get(header.hash) || this.getBlock(header.hash));
                    prefetchMap.delete(header.hash);
                    // Queue next block prefetch (sliding window)
                    const nextIdx = i + prefetchSize;
                    if (prefetchSize > 0 && nextIdx < headers.length) {
                        prefetchMap.set(headers[nextIdx].hash, this.getBlock(headers[nextIdx].hash));
                    }
                    await this.processBlock(block);
                    height++;
                    const now = Date.now();
                    const oneSecond = 1000;
                    if (now - lastLog > oneSecond) {
                        const blocksProcessed = height - startingHeight;
                        const elapsedMinutes = (now - startingTime) / (60 * oneSecond);
                        logger_1.default.info(`${(0, logger_1.timestamp)()} | Syncing... | Chain: ${chain} | Network: ${network} |${(blocksProcessed / elapsedMinutes)
                            .toFixed(2)
                            .padStart(8)} blocks/min | Height: ${height.toString().padStart(7)}`);
                        lastLog = now;
                    }
                }
                catch (err) {
                    logger_1.default.error(`${(0, logger_1.timestamp)()} | Error syncing | Chain: ${chain} | Network: ${network} | %o`, err);
                    this.isSyncing = false;
                    return this.sync();
                }
            }
            headers = await this.getHeadersForSync();
        }
        logger_1.default.info(`${(0, logger_1.timestamp)()} | Sync completed | Chain: ${chain} | Network: ${network}`);
        this.isSyncing = false;
        await state_1.StateStorage.collection.findOneAndUpdate({}, { $addToSet: { initialSyncComplete: `${chain}:${network}` } }, { upsert: true });
        this.events.emit('SYNCDONE');
        return true;
    }
    async stop() {
        this.stopping = true;
        this.multiThreadSync.stop();
        logger_1.default.debug(`Stopping worker for chain ${this.chain}`);
        for (const queuedRegistration of this.queuedRegistrations) {
            clearTimeout(queuedRegistration);
        }
        await this.unregisterSyncingNode();
        await this.disconnect();
    }
    async start() {
        logger_1.default.debug(`Started worker for chain ${this.chain}`);
        await this.connect();
        this.refreshSyncingNode();
    }
}
exports.BitcoinP2PWorker = BitcoinP2PWorker;
//# sourceMappingURL=p2p.js.map