import { AggregateRoot } from '@easylayer/common/cqrs';
import { createEvmMempoolStateStore } from './mempool-state.store';
import { BatchSizer } from './components';
import { EvmMempoolInitializedEvent, EvmMempoolRefreshedEvent, EvmMempoolTransactionReplacedEvent, EvmMempoolSyncProcessedEvent, EvmMempoolSynchronizedEvent, EvmMempoolTxsConfirmedEvent, } from '../../events/mempool';
/**
 * EVM mempool aggregate.
 *
 * Domain/CQRS behavior stays in TypeScript. The heavy mutable structures are
 * delegated to `EvmMempoolStateStore`, which is Rust-backed in Node when the
 * native addon is available and falls back to a JS implementation in browser/tests.
 *
 * EVM-specific behavior:
 * - replacement detection is `(from, nonce)` based;
 * - refresh mode can be `snapshot` (`txpool_content`) or `additive` (`newPendingTransactions` WS);
 * - loaded tx payloads do not store calldata/input in the state store;
 * - all gas/wei fields are decimal strings in public normalized models.
 */
export class Mempool extends AggregateRoot {
    constructor({ aggregateId, blockHeight, minGasPrice = 1000000000n, maxPendingCount = 10000, pendingTxTtlMs = 30 * 60 * 1000, options, }) {
        super(aggregateId, blockHeight, options);
        this.batchSizer = new BatchSizer(100, 10, 1000);
        this.store = createEvmMempoolStateStore();
        this.lastUpdatedMs = Date.now();
        this.minGasPrice = minGasPrice;
        this.maxPendingCount = maxPendingCount;
        this.pendingTxTtlMs = pendingTxTtlMs;
    }
    effectiveGasPrice(meta) {
        const parse = (value) => {
            if (!value)
                return 0n;
            try {
                return BigInt(value);
            }
            catch {
                return 0n;
            }
        };
        return parse(meta.maxFeePerGas) || parse(meta.gasPrice);
    }
    meetsMinGasPrice(meta) {
        return this.effectiveGasPrice(meta) >= this.minGasPrice;
    }
    serializeUserState() {
        return {
            minGasPrice: this.minGasPrice.toString(),
            maxPendingCount: this.maxPendingCount,
            pendingTxTtlMs: this.pendingTxTtlMs,
            mempoolStore: this.store.exportSnapshot(),
        };
    }
    restoreUserState(state) {
        this.minGasPrice = state?.minGasPrice ? BigInt(state.minGasPrice) : 1000000000n;
        this.maxPendingCount = state?.maxPendingCount ?? 10000;
        this.pendingTxTtlMs = state?.pendingTxTtlMs ?? 30 * 60 * 1000;
        this.batchSizer = new BatchSizer(100, 10, 1000);
        this.prevDuration = undefined;
        this.store = createEvmMempoolStateStore();
        this.store.importSnapshot(state?.mempoolStore ?? state);
        Object.setPrototypeOf(this, Mempool.prototype);
    }
    async init({ requestId, height, logger, }) {
        this.apply(new EvmMempoolInitializedEvent({ aggregateId: this.aggregateId, requestId, blockHeight: height }, {}));
        logger?.log('Mempool successfully initialized', {
            module: 'mempool-model',
            args: {
                lastHeight: height,
            },
        });
    }
    async refresh({ requestId, height, perProvider, mode, logger, }) {
        const seen = new Set();
        const filtered = {};
        const replacements = [];
        for (const [provider, items] of Object.entries(perProvider)) {
            if (!Array.isArray(items) || items.length === 0)
                continue;
            const out = [];
            for (const { hash, metadata } of items) {
                if (!hash || seen.has(hash))
                    continue;
                if (!this.meetsMinGasPrice(metadata))
                    continue;
                seen.add(hash);
                if (mode === 'additive' && metadata.from && metadata.nonce !== undefined) {
                    const existing = this.store.getReplacementCandidate(metadata.from, metadata.nonce);
                    if (existing && existing.hash !== hash) {
                        const existingGas = this.effectiveGasPrice(existing.metadata);
                        const newGas = this.effectiveGasPrice(metadata);
                        if (newGas >= (existingGas * 110n) / 100n) {
                            replacements.push({
                                oldHash: existing.hash,
                                newHash: hash,
                                from: metadata.from,
                                nonce: metadata.nonce,
                                provider,
                            });
                        }
                        else {
                            continue;
                        }
                    }
                }
                out.push({ hash, metadata });
            }
            if (out.length > 0)
                filtered[provider] = out;
        }
        for (const replacement of replacements) {
            this.apply(new EvmMempoolTransactionReplacedEvent({ aggregateId: this.aggregateId, requestId, blockHeight: height }, {
                oldHash: replacement.oldHash,
                newHash: replacement.newHash,
                from: replacement.from,
                nonce: replacement.nonce,
                providerName: replacement.provider,
            }));
        }
        this.apply(new EvmMempoolRefreshedEvent({ aggregateId: this.aggregateId, requestId, blockHeight: height }, { aggregatedMetadata: filtered, mode }));
        logger?.log('Mempool refreshed', {
            module: 'mempool-model',
            args: { mode, providers: Object.keys(filtered).length },
        });
    }
    async sync({ requestId, service, logger, }) {
        const byProvider = new Map();
        for (const provider of this.store.providers()) {
            const pending = this.store.pendingHashes(provider, this.batchSizer.get(provider));
            if (pending.length > 0)
                byProvider.set(provider, pending);
        }
        if (byProvider.size === 0) {
            this.apply(new EvmMempoolSynchronizedEvent({ aggregateId: this.aggregateId, requestId, blockHeight: this.lastBlockHeight }, {}));
            logger?.log('Mempool synced', {
                module: 'mempool-model',
                args: { mempool: this.store.getStats() },
            });
            return;
        }
        const loaded = [];
        const batchDurations = {};
        const tasks = Array.from(byProvider.entries()).map(([providerName, hashes]) => (async () => {
            const t0 = Date.now();
            try {
                for (const hash of hashes) {
                    const metadata = await service.getPendingTransactionByHash(hash);
                    if (metadata)
                        loaded.push({ hash, metadata, providerName });
                }
            }
            catch (error) {
                logger?.debug('Mempool sync batch failed', { args: { providerName, error: error?.message } });
            }
            finally {
                const duration = Date.now() - t0;
                const previous = this.prevDuration?.get(providerName) ?? duration;
                if (!this.prevDuration)
                    this.prevDuration = new Map();
                this.prevDuration.set(providerName, duration);
                this.batchSizer.tune(providerName, duration / (previous || duration));
                batchDurations[providerName] = duration;
            }
        })());
        await Promise.allSettled(tasks);
        if (loaded.length > 0) {
            this.apply(new EvmMempoolSyncProcessedEvent({ aggregateId: this.aggregateId, requestId, blockHeight: this.lastBlockHeight }, { loadedTransactions: loaded, batchDurations }));
        }
    }
    async removeConfirmed({ requestId, hashes, height, }) {
        const toRemove = hashes.filter((hash) => this.store.hasTransaction(hash));
        if (toRemove.length === 0)
            return;
        this.apply(new EvmMempoolTxsConfirmedEvent({ aggregateId: this.aggregateId, requestId, blockHeight: height }, { confirmedHashes: toRemove }));
    }
    onEvmMempoolInitializedEvent(_) { }
    onEvmMempoolTransactionReplacedEvent({ payload }) {
        this.store.removeHash(payload.oldHash);
    }
    onEvmMempoolRefreshedEvent({ payload }) {
        const incoming = payload.aggregatedMetadata;
        if (payload.mode === 'snapshot') {
            this.store.applySnapshot(incoming);
            this.batchSizer.clear();
            this.prevDuration = undefined;
        }
        else {
            this.store.addTransactions(incoming, this.maxPendingCount);
        }
        this.store.pruneTtl(this.pendingTxTtlMs);
        this.lastUpdatedMs = Date.now();
    }
    onEvmMempoolSyncProcessedEvent({ payload }) {
        this.store.recordLoaded(payload.loadedTransactions || []);
    }
    onEvmMempoolSynchronizedEvent(_) { }
    onEvmMempoolTxsConfirmedEvent({ payload }) {
        this.store.removeHashes(payload.confirmedHashes);
    }
    hasTransaction(hash) {
        return this.store.hasTransaction(hash);
    }
    getTransactionMetadata(hash) {
        return this.store.getTransactionMetadata(hash);
    }
    isTransactionLoaded(hash) {
        return this.store.isTransactionLoaded(hash);
    }
    getStats() {
        return this.store.getStats();
    }
    getLastUpdatedMs() {
        return this.lastUpdatedMs;
    }
    /**
     * Iterates over loaded mempool entries — pairs of (hash, metadata) for every
     * transaction that has passed through `recordLoaded` after a sync phase.
     *
     * Delegates to the backing store (Rust native when available; JS fallback
     * otherwise). The native store returns an Array of `{ hash, metadata }`
     * objects in one NAPI call; arrays are iterable, so `yield*` works directly.
     */
    *iterLoadedTx() {
        yield* this.store.loadedEntries();
    }
    /**
     * Awaitable form of `iterLoadedTx`. The callback can be sync or async; awaited
     * sequentially so user code observing the mempool sees deterministic ordering.
     */
    async forEachLoadedTx(cb) {
        for (const entry of this.store.loadedEntries()) {
            await cb(entry);
        }
    }
    dispose() {
        this.store.dispose();
    }
}
