"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var BlockchainProviderService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BlockchainProviderService = void 0;
const common_1 = require("@nestjs/common");
const normalizer_1 = require("./normalizer");
const value_normalization_1 = require("./value-normalization");
const trie_verifier_1 = require("./providers/trie-verifier");
const block_payload_codec_1 = require("./codecs/block-payload-codec");
/**
 * BlockchainProviderService — main EVM blockchain operations interface.
 *
 * Architecture:
 * - NetworkConnectionManager: block/tx fetching with auto-failover
 * - MempoolConnectionManager: pending tx stream (optional, WS-based)
 * - Unified API for ethersjs/web3js providers
 */
let BlockchainProviderService = BlockchainProviderService_1 = class BlockchainProviderService {
    networkManager;
    mempoolManager;
    networkConfig;
    log = new common_1.Logger(BlockchainProviderService_1.name);
    normalizer;
    constructor(networkManager, mempoolManager, networkConfig) {
        this.networkManager = networkManager;
        this.mempoolManager = mempoolManager;
        this.networkConfig = networkConfig;
        this.normalizer = new normalizer_1.BlockchainNormalizer(networkConfig);
    }
    get config() {
        return this.networkConfig;
    }
    get connectionManager() {
        return this.networkManager;
    }
    /** @returns true if mempool providers are configured and connected */
    get isMempoolAvailable() {
        return this.mempoolManager.isAvailable;
    }
    // ===== INTERNAL =====
    async execNetwork(methodName, op) {
        try {
            const provider = await this.networkManager.getActiveProvider();
            return await op(provider);
        }
        catch (error) {
            try {
                const curr = await this.networkManager.getActiveProvider();
                const recovered = await this.networkManager.handleProviderFailure(curr.uniqName, error, methodName);
                return await op(recovered);
            }
            catch (recoveryError) {
                this.log.warn('Provider recovery failed', { args: { methodName, recoveryError } });
                throw recoveryError;
            }
        }
    }
    // ===== BLOCK HEIGHT =====
    /** Get current block height from network */
    async getCurrentBlockHeight() {
        return this.execNetwork('getCurrentBlockHeight', (p) => p.getBlockHeight());
    }
    /** Alias used by crawler for consistency with bitcoin package */
    async getCurrentBlockHeightFromMempool() {
        return this.getCurrentBlockHeight();
    }
    async getCurrentBlockHeightFromNetwork() {
        return this.getCurrentBlockHeight();
    }
    // ===== BLOCK METHODS =====
    async getOneBlockByHeight(height, fullTransactions = false, verifyTrie = false) {
        return this.execNetwork('getOneBlockByHeight', async (provider) => {
            const h = Number(height);
            const rawBlocks = await provider.getManyBlocksByHeights([h], fullTransactions, verifyTrie);
            if (!rawBlocks?.length || !rawBlocks[0])
                return null;
            const raw = rawBlocks[0];
            if (raw.blockNumber == null)
                raw.blockNumber = h;
            return this.normalizer.normalizeBlock(raw);
        });
    }
    async getManyBlocksByHeights(heights, fullTransactions = false, verifyTrie = false) {
        return this.execNetwork('getManyBlocksByHeights', async (provider) => {
            const numeric = heights.map(Number);
            const rawBlocks = await provider.getManyBlocksByHeights(numeric, fullTransactions, verifyTrie);
            return rawBlocks
                .filter((b) => b !== null)
                .map((raw) => this.normalizer.normalizeBlock(raw));
        });
    }
    async getOneBlockByHash(hash, fullTransactions = false) {
        return this.execNetwork('getOneBlockByHash', async (provider) => {
            const rawBlocks = await provider.getManyBlocksByHashes([hash], fullTransactions);
            if (!rawBlocks?.length || !rawBlocks[0])
                return null;
            const raw = rawBlocks[0];
            if (raw.blockNumber == null) {
                raw.blockNumber = await provider.getBlockHeight();
            }
            return this.normalizer.normalizeBlock(raw);
        });
    }
    async getManyBlocksByHashes(hashes, fullTransactions = false) {
        return this.execNetwork('getManyBlocksByHashes', async (provider) => {
            const rawBlocks = await provider.getManyBlocksByHashes(hashes, fullTransactions);
            const needHeight = rawBlocks.some((b) => b && b.blockNumber == null);
            const currentHeight = needHeight ? await provider.getBlockHeight() : 0;
            return rawBlocks
                .filter((b) => b !== null)
                .map((raw) => {
                if (raw.blockNumber == null)
                    raw.blockNumber = currentHeight;
                return this.normalizer.normalizeBlock(raw);
            });
        });
    }
    async getOneBlockWithReceipts(height, fullTransactions = false, verifyTrie = false) {
        return this.execNetwork('getOneBlockWithReceipts', async (provider) => {
            const h = Number(height);
            const blocks = await provider.getManyBlocksWithReceipts([h], fullTransactions, verifyTrie);
            if (!blocks?.length || !blocks[0])
                return null;
            const raw = blocks[0];
            if (raw.blockNumber == null)
                raw.blockNumber = h;
            return this.normalizer.normalizeBlock(raw);
        });
    }
    async getManyBlocksWithReceipts(heights, fullTransactions = false, verifyTrie = false) {
        return this.execNetwork('getManyBlocksWithReceipts', async (provider) => {
            const numeric = heights.map(Number);
            const rawBlocks = await provider.getManyBlocksWithReceipts(numeric, fullTransactions, verifyTrie);
            return rawBlocks
                .filter((b) => b !== null)
                .map((raw) => this.normalizer.normalizeBlock(raw));
        });
    }
    async getManyBlocksStatsByHeights(heights) {
        return this.execNetwork('getManyBlocksStatsByHeights', async (provider) => {
            const stats = await provider.getManyBlocksStatsByHeights(heights.map(Number));
            return stats.filter((s) => s !== null);
        });
    }
    async mergeReceiptsIntoBlocks(blocks, receipts) {
        let receiptIndex = 0;
        for (const block of blocks) {
            const txCount = block.transactions?.length || 0;
            block.receipts = txCount > 0 ? receipts.slice(receiptIndex, receiptIndex + txCount) : [];
            receiptIndex += txCount;
            // Re-normalize size fields using the normalizer (which uses BlockSizeCalculator internally)
            const renormalized = this.normalizer.normalizeBlock({ ...block });
            block.size = renormalized.size;
            block.sizeWithoutReceipts = renormalized.sizeWithoutReceipts;
        }
        return blocks;
    }
    /**
     * Subscribes to new blocks via WebSocket.
     * Returns Subscription (Promise<void> & { unsubscribe() }).
     */
    subscribeToNewBlocks(callback, fullTransactions = true, verifyTrie = false) {
        let resolve;
        let reject;
        const promise = new Promise((res, rej) => {
            resolve = res;
            reject = rej;
        });
        this.networkManager
            .getActiveProvider()
            .then((provider) => {
            if (!provider.hasWebSocketSupport || !provider.isWebSocketConnected) {
                reject(new Error('Provider does not support WebSocket'));
                return;
            }
            const sub = provider.subscribeToNewBlocks(async (blockNumber) => {
                try {
                    const block = await this.getOneBlockWithReceipts(blockNumber, fullTransactions, verifyTrie);
                    if (block)
                        callback(block);
                }
                catch (e) {
                    sub.unsubscribe();
                    reject(e);
                }
            });
            promise.unsubscribe = () => {
                sub.unsubscribe();
                resolve();
            };
        })
            .catch((e) => reject(e));
        return promise;
    }
    // ===== TRACE METHODS =====
    /**
     * Parse raw bytes back into a Block object.
     * EVM path: MessagePack bytes → Block, with JSON fallback for tests/browser compatibility.
     * If verifyTrie is true, validates transactionsRoot after parse.
     * Called by the queue iterator after getBatchUpToSize.
     */
    async parseBlock(bytes, _height, verifyTrie = false) {
        const block = (0, block_payload_codec_1.decodeEvmBlockPayload)(bytes);
        if (verifyTrie && block.transactions?.length && block.transactionsRoot) {
            const valid = await trie_verifier_1.EvmTrieVerifier.verifyTransactionsRoot(block.transactions, block.transactionsRoot);
            if (!valid) {
                throw new Error(`Transactions root verification failed for block ${block.hash} at height ${block.blockNumber}`);
            }
        }
        return block;
    }
    async assertTraceSupport() {
        if (!this.networkConfig.supportsTraces) {
            throw new Error(`Trace API is disabled by network config for chainId ${this.networkConfig.chainId}`);
        }
        await this.execNetwork('assertTraceSupport', (provider) => provider.assertTraceSupport());
    }
    /**
     * Loads traces for a block by height.
     * Tries debug_traceBlockByNumber (Geth) then trace_block (Erigon/OE).
     * Fails fast if traces are disabled or unsupported.
     * Should only be called when tracesEnabled=true in config.
     */
    async getTracesByBlockHeight(height) {
        if (!this.networkConfig.supportsTraces) {
            throw new Error(`Trace API is disabled by network config for chainId ${this.networkConfig.chainId}`);
        }
        return this.execNetwork('getTracesByBlockHeight', async (provider) => {
            const universalTraces = await provider.getTracesByBlockNumber(height);
            return universalTraces.map((t) => ({
                transactionHash: t.transactionHash || '',
                transactionPosition: t.transactionPosition || 0,
                type: t.type || 'call',
                action: t.action || {},
                result: t.result,
                error: t.error,
                subtraces: t.subtraces || 0,
                traceAddress: t.traceAddress || [],
            }));
        });
    }
    /**
     * Loads traces for a single transaction.
     */
    async getTracesByTransactionHash(hash) {
        if (!this.networkConfig.supportsTraces) {
            throw new Error(`Trace API is disabled by network config for chainId ${this.networkConfig.chainId}`);
        }
        return this.execNetwork('getTracesByTransactionHash', async (provider) => {
            const universalTraces = await provider.getTracesByTxHash(hash);
            return universalTraces.map((t) => ({
                transactionHash: t.transactionHash || hash,
                transactionPosition: t.transactionPosition || 0,
                type: t.type || 'call',
                action: t.action || {},
                result: t.result,
                error: t.error,
                subtraces: t.subtraces || 0,
                traceAddress: t.traceAddress || [],
            }));
        });
    }
    // ===== MEMPOOL METHODS =====
    /**
     * Subscribe to pending transaction hashes via WebSocket.
     * Uses MempoolConnectionManager provider.
     * Returns Subscription object.
     */
    subscribeToPendingTransactions(callback) {
        const mempoolProvider = this.mempoolManager.getActiveProvider();
        if (!mempoolProvider) {
            const promise = Promise.resolve();
            promise.unsubscribe = () => { };
            this.log.warn('Mempool not available — subscribeToPendingTransactions is no-op');
            return promise;
        }
        let resolve;
        const promise = new Promise((res) => {
            resolve = res;
        });
        const sub = mempoolProvider.subscribeToPendingTransactions(callback);
        promise.unsubscribe = () => {
            sub.unsubscribe();
            resolve();
        };
        return promise;
    }
    /**
     * Fetch pending tx metadata by hash.
     * Returns null if not found (already confirmed or dropped).
     */
    async getPendingTransactionByHash(hash) {
        const mempoolProvider = this.mempoolManager.getActiveProvider();
        if (!mempoolProvider)
            return null;
        try {
            const raw = await mempoolProvider.getTransactionByHash(hash);
            if (!raw)
                return null;
            return {
                hash: (0, value_normalization_1.normalizeHex)(raw.hash || hash),
                from: (0, value_normalization_1.normalizeAddress)(raw.from),
                to: raw.to ? (0, value_normalization_1.normalizeAddress)(raw.to) : null,
                nonce: (0, value_normalization_1.quantityToNumber)(raw.nonce),
                value: (0, value_normalization_1.quantityToDecimalString)(raw.value),
                gas: (0, value_normalization_1.quantityToNumber)(raw.gas),
                gasPrice: raw.gasPrice ? (0, value_normalization_1.quantityToDecimalString)(raw.gasPrice) : undefined,
                maxFeePerGas: raw.maxFeePerGas ? (0, value_normalization_1.quantityToDecimalString)(raw.maxFeePerGas) : undefined,
                maxPriorityFeePerGas: raw.maxPriorityFeePerGas ? (0, value_normalization_1.quantityToDecimalString)(raw.maxPriorityFeePerGas) : undefined,
                type: raw.type ? (0, value_normalization_1.normalizeHex)(raw.type) : undefined,
            };
        }
        catch {
            return null;
        }
    }
    /**
     * Fetch entire txpool_content (Geth/Erigon).
     * Returns per-provider raw mempool. May return empty if not supported.
     */
    async getRawMempoolFromAll() {
        const mempoolProvider = this.mempoolManager.getActiveProvider();
        if (!mempoolProvider)
            return [];
        try {
            const rawMempool = await mempoolProvider.getRawMempool();
            const normalized = {};
            for (const [hash, raw] of Object.entries(rawMempool)) {
                if (!raw || typeof raw !== 'object')
                    continue;
                normalized[(0, value_normalization_1.normalizeHex)(raw.hash || hash)] = {
                    hash: (0, value_normalization_1.normalizeHex)(raw.hash || hash),
                    from: (0, value_normalization_1.normalizeAddress)(raw.from),
                    to: raw.to ? (0, value_normalization_1.normalizeAddress)(raw.to) : null,
                    nonce: (0, value_normalization_1.quantityToNumber)(raw.nonce),
                    value: (0, value_normalization_1.quantityToDecimalString)(raw.value),
                    gas: (0, value_normalization_1.quantityToNumber)(raw.gas),
                    gasPrice: raw.gasPrice ? (0, value_normalization_1.quantityToDecimalString)(raw.gasPrice) : undefined,
                    maxFeePerGas: raw.maxFeePerGas ? (0, value_normalization_1.quantityToDecimalString)(raw.maxFeePerGas) : undefined,
                    maxPriorityFeePerGas: raw.maxPriorityFeePerGas
                        ? (0, value_normalization_1.quantityToDecimalString)(raw.maxPriorityFeePerGas)
                        : undefined,
                    type: raw.type ? (0, value_normalization_1.normalizeHex)(raw.type) : undefined,
                };
            }
            return [{ providerName: mempoolProvider.uniqName, value: normalized }];
        }
        catch {
            return [];
        }
    }
    // ===== NETWORK INFO =====
    isFeatureSupported(feature) {
        switch (feature) {
            case 'eip1559':
                return this.networkConfig.hasEIP1559;
            case 'withdrawals':
                return this.networkConfig.hasWithdrawals;
            case 'blobTransactions':
                return this.networkConfig.hasBlobTransactions;
            case 'traces':
                return this.networkConfig.supportsTraces;
            default:
                return false;
        }
    }
    getNativeCurrencySymbol() {
        return this.networkConfig.nativeCurrencySymbol;
    }
    getNativeCurrencyDecimals() {
        return this.networkConfig.nativeCurrencyDecimals;
    }
    getChainId() {
        return this.networkConfig.chainId;
    }
};
exports.BlockchainProviderService = BlockchainProviderService;
exports.BlockchainProviderService = BlockchainProviderService = BlockchainProviderService_1 = __decorate([
    (0, common_1.Injectable)(),
    __metadata("design:paramtypes", [Function, Function, Object])
], BlockchainProviderService);
