"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RpcProviderStrategy = void 0;
const block_payload_codec_1 = require("../../../blockchain-provider/codecs/block-payload-codec");
const load_strategy_interface_1 = require("./load-strategy.interface");
class RpcProviderStrategy {
    log;
    blockchainProvider;
    queue;
    name = load_strategy_interface_1.StrategyNames.RPC;
    _maxRequestBlocksBatchSize;
    _preloadedItemsQueue = [];
    _maxPreloadCount;
    _lastLoadReceiptsDuration = 0;
    _previousLoadReceiptsDuration = 0;
    tracesEnabled;
    verifyTrie;
    constructor(log, blockchainProvider, queue, config) {
        this.log = log;
        this.blockchainProvider = blockchainProvider;
        this.queue = queue;
        this._maxRequestBlocksBatchSize = config.maxRequestBlocksBatchSize;
        this._maxPreloadCount = config.basePreloadCount;
        this.tracesEnabled = config.tracesEnabled ?? false;
        this.verifyTrie = config.verifyTrie ?? false;
    }
    async load(currentNetworkHeight) {
        while (this.queue.lastHeight < currentNetworkHeight) {
            if (this.queue.isMaxHeightReached)
                return;
            if (this.queue.isQueueFull) {
                throw new Error('The queue is full, waiting before retry');
            }
            if (this._preloadedItemsQueue.length === 0) {
                await this.preloadBlocksWithTransactions(currentNetworkHeight);
            }
            if (this._preloadedItemsQueue.length > 0) {
                await this.loadReceiptsAndEnqueueBlocks();
            }
        }
    }
    async stop() {
        this._preloadedItemsQueue = [];
    }
    async preloadBlocksWithTransactions(currentNetworkHeight) {
        if (this._previousLoadReceiptsDuration > 0 && this._lastLoadReceiptsDuration > 0) {
            const ratio = this._lastLoadReceiptsDuration / this._previousLoadReceiptsDuration;
            if (ratio > 1.2)
                this._maxPreloadCount = Math.round(this._maxPreloadCount * 1.25);
            else if (ratio < 0.8)
                this._maxPreloadCount = Math.max(1, Math.round(this._maxPreloadCount * 0.75));
        }
        const lastHeight = this.queue.lastHeight;
        const remaining = currentNetworkHeight - lastHeight;
        const count = Math.min(this._maxPreloadCount, remaining);
        if (count <= 0)
            return;
        const heights = Array.from({ length: count }, (_, i) => lastHeight + 1 + i);
        this._preloadedItemsQueue = await this.blockchainProvider.getManyBlocksByHeights(heights, true, this.verifyTrie);
    }
    async loadReceiptsAndEnqueueBlocks() {
        const startTime = Date.now();
        const receiptsBatches = this.createOptimalBatchesForReceipts();
        const completeBlocks = [];
        const batch = receiptsBatches[0];
        if (batch) {
            const loaded = await this.loadBlocksWithReceipts(batch.blocks, 3);
            completeBlocks.push(...loaded);
        }
        this._preloadedItemsQueue = [];
        if (this.tracesEnabled && completeBlocks.length > 0) {
            await this.attachTraces(completeBlocks);
        }
        await this.enqueueBlocks(completeBlocks);
        this._previousLoadReceiptsDuration = this._lastLoadReceiptsDuration;
        this._lastLoadReceiptsDuration = Date.now() - startTime;
    }
    createOptimalBatchesForReceipts() {
        const batches = [];
        this._preloadedItemsQueue.sort((a, b) => a.blockNumber - b.blockNumber);
        let currentBatch = [];
        let currentEstimatedSize = 0;
        for (const block of this._preloadedItemsQueue) {
            const estimated = this.estimateReceiptsSize(block);
            if (currentEstimatedSize + estimated <= this._maxRequestBlocksBatchSize || currentBatch.length === 0) {
                currentBatch.push(block);
                currentEstimatedSize += estimated;
            }
            else {
                batches.push({ blocks: [...currentBatch], maxPossibleSize: currentEstimatedSize });
                currentBatch = [block];
                currentEstimatedSize = estimated;
            }
        }
        if (currentBatch.length > 0) {
            batches.push({ blocks: currentBatch, maxPossibleSize: currentEstimatedSize });
        }
        return batches;
    }
    estimateReceiptsSize(block) {
        if (!block.transactions?.length)
            return 0;
        const txCount = block.transactions.length;
        const blockSize = block.sizeWithoutReceipts || 0;
        if (blockSize > 2 * 1024 * 1024)
            return txCount * 2000;
        if (blockSize > 500 * 1024)
            return txCount * 1000;
        return txCount * 500;
    }
    async loadBlocksWithReceipts(blocks, maxRetries) {
        let attempt = 0;
        while (attempt < maxRetries) {
            try {
                const heights = blocks.map((b) => b.blockNumber);
                return await this.blockchainProvider.getManyBlocksWithReceipts(heights, true, this.verifyTrie);
            }
            catch (error) {
                attempt++;
                if (attempt >= maxRetries) {
                    this.log.warn('Max retries exceeded for receipts batch', { args: { count: blocks.length, error } });
                    throw error;
                }
                await new Promise((r) => setTimeout(r, 50 * attempt));
            }
        }
        throw new Error('Failed to fetch receipts batch');
    }
    async attachTraces(blocks) {
        for (const block of blocks) {
            block.traces = await this.blockchainProvider.getTracesByBlockHeight(block.blockNumber);
        }
    }
    getRequiredBlockSize(block) {
        if (typeof block.size !== 'number' || !Number.isFinite(block.size) || block.size < 0) {
            throw new Error(`EVM block ${block.blockNumber} (${block.hash}) is missing required size`);
        }
        return block.size;
    }
    async enqueueBlocks(blocks) {
        const sorted = [...blocks].sort((a, b) => a.blockNumber - b.blockNumber);
        for (const block of sorted) {
            if (block.blockNumber <= this.queue.lastHeight) {
                this.log.debug('Skipping block below lastHeight', { args: { blockNumber: block.blockNumber } });
                continue;
            }
            const bytes = (0, block_payload_codec_1.encodeEvmBlockPayload)(block);
            await this.queue.enqueue({
                hash: block.hash,
                height: block.blockNumber,
                size: this.getRequiredBlockSize(block),
                bytes,
            });
        }
    }
}
exports.RpcProviderStrategy = RpcProviderStrategy;
