import { getEvmNativeBindings } from '../../../native';
function canonicalHash(hash) {
    return hash.startsWith('0x') || hash.startsWith('0X')
        ? `0x${hash.slice(2).toLowerCase()}`
        : `0x${hash.toLowerCase()}`;
}
function nonceKey(from, nonce) {
    return `${from.toLowerCase()}:${nonce}`;
}
function effectiveGasPrice(meta) {
    if (!meta)
        return 0n;
    const parse = (value) => {
        if (!value)
            return 0n;
        try {
            return BigInt(value);
        }
        catch {
            return 0n;
        }
    };
    return parse(meta.maxFeePerGas) || parse(meta.gasPrice);
}
function convertUnits(units = 'MB') {
    if (units === 'B')
        return { unit: 'B', div: 1 };
    if (units === 'KB')
        return { unit: 'KB', div: 1024 };
    if (units === 'GB')
        return { unit: 'GB', div: 1024 * 1024 * 1024 };
    return { unit: 'MB', div: 1024 * 1024 };
}
export class JsEvmMempoolStateStore {
    constructor() {
        this.hashToId = new Map();
        this.idToHash = new Map();
        this.providerTx = new Map();
        this.metadataStore = new Map();
        this.loadTracker = new Map();
        this.nonceIndex = new Map();
        this.nextId = 0;
    }
    applySnapshot(perProvider) {
        const oldLoad = new Map();
        for (const [id, info] of this.loadTracker) {
            const hash = this.idToHash.get(id);
            if (hash)
                oldLoad.set(hash, info);
        }
        this.clearWithoutShrink();
        const seen = new Set();
        for (const [provider, items] of Object.entries(perProvider)) {
            if (!Array.isArray(items))
                continue;
            for (const { hash, metadata } of items) {
                const normalizedHash = canonicalHash(hash);
                if (seen.has(normalizedHash))
                    continue;
                seen.add(normalizedHash);
                const id = this.ensureId(normalizedHash);
                this.metadataStore.set(id, metadata);
                this.addProviderTx(provider, id);
                this.indexNonce(metadata, id);
                const previousLoad = oldLoad.get(normalizedHash);
                if (previousLoad)
                    this.loadTracker.set(id, previousLoad);
            }
        }
    }
    addTransactions(perProvider, maxPendingCount) {
        for (const [provider, items] of Object.entries(perProvider)) {
            if (!Array.isArray(items))
                continue;
            for (const { hash, metadata } of items) {
                const normalizedHash = canonicalHash(hash);
                if (this.hashToId.has(normalizedHash))
                    continue;
                if (maxPendingCount > 0 && this.hashToId.size >= maxPendingCount) {
                    this.evictLowestGas();
                }
                const id = this.ensureId(normalizedHash);
                this.metadataStore.set(id, metadata);
                this.addProviderTx(provider, id);
                this.indexNonce(metadata, id);
            }
        }
    }
    providers() {
        return Array.from(this.providerTx.keys());
    }
    pendingHashes(providerName, limit) {
        const ids = this.providerTx.get(providerName);
        if (!ids)
            return [];
        const out = [];
        for (const id of ids) {
            if (this.loadTracker.has(id) || !this.metadataStore.has(id))
                continue;
            const hash = this.idToHash.get(id);
            if (hash)
                out.push(hash);
            if (limit > 0 && out.length >= limit)
                break;
        }
        return out;
    }
    recordLoaded(loadedTransactions) {
        const timestamp = Date.now();
        for (const { hash, metadata, providerName } of loadedTransactions) {
            const id = this.hashToId.get(canonicalHash(hash));
            if (id === undefined || this.loadTracker.has(id))
                continue;
            this.loadTracker.set(id, {
                timestamp,
                effectiveGasPrice: effectiveGasPrice(metadata).toString(),
                providerName,
            });
        }
    }
    removeHash(hash) {
        const normalizedHash = canonicalHash(hash);
        const id = this.hashToId.get(normalizedHash);
        if (id === undefined)
            return false;
        const meta = this.metadataStore.get(id);
        if (meta?.from && meta.nonce !== undefined) {
            const key = nonceKey(meta.from, meta.nonce);
            if (this.nonceIndex.get(key) === id)
                this.nonceIndex.delete(key);
        }
        this.metadataStore.delete(id);
        this.loadTracker.delete(id);
        for (const set of this.providerTx.values())
            set.delete(id);
        this.hashToId.delete(normalizedHash);
        this.idToHash.delete(id);
        return true;
    }
    removeHashes(hashes) {
        let removed = 0;
        for (const hash of hashes) {
            if (this.removeHash(hash))
                removed++;
        }
        return removed;
    }
    getReplacementCandidate(from, nonce) {
        const id = this.nonceIndex.get(nonceKey(from, nonce));
        if (id === undefined)
            return undefined;
        const hash = this.idToHash.get(id);
        const metadata = this.metadataStore.get(id);
        return hash && metadata ? { hash, metadata } : undefined;
    }
    *hashes() {
        yield* this.idToHash.values();
    }
    *metadata() {
        yield* this.metadataStore.values();
    }
    *loadedEntries() {
        for (const id of this.loadTracker.keys()) {
            const hash = this.idToHash.get(id);
            const metadata = this.metadataStore.get(id);
            if (hash && metadata)
                yield { hash, metadata };
        }
    }
    hasTransaction(hash) {
        return this.hashToId.has(canonicalHash(hash));
    }
    isTransactionLoaded(hash) {
        const id = this.hashToId.get(canonicalHash(hash));
        return id !== undefined ? this.loadTracker.has(id) : false;
    }
    getTransactionMetadata(hash) {
        const id = this.hashToId.get(canonicalHash(hash));
        return id !== undefined ? this.metadataStore.get(id) : undefined;
    }
    getStats() {
        return {
            total: this.hashToId.size,
            loaded: this.loadTracker.size,
            providers: this.providerTx.size,
            nonceIndex: this.nonceIndex.size,
        };
    }
    pruneTtl(ttlMs, nowMs = Date.now()) {
        if (ttlMs <= 0)
            return 0;
        const cutoff = nowMs - ttlMs;
        const toRemove = [];
        for (const [id, entry] of this.loadTracker) {
            if (entry.timestamp < cutoff) {
                const hash = this.idToHash.get(id);
                if (hash)
                    toRemove.push(hash);
            }
        }
        return this.removeHashes(toRemove);
    }
    getMemoryUsage(units = 'MB') {
        const { unit, div } = convertUnits(units);
        const hashIndex = this.hashToId.size * (32 + 16);
        const metadata = this.metadataStore.size * 256;
        const loadTracker = this.loadTracker.size * 64;
        const providerTx = Array.from(this.providerTx.values()).reduce((sum, set) => sum + set.size * 4, 0);
        const nonceIndex = this.nonceIndex.size * 56;
        const total = hashIndex + metadata + loadTracker + providerTx + nonceIndex;
        const scale = (value) => value / div;
        return {
            unit,
            counts: {
                hashes: this.hashToId.size,
                metadata: this.metadataStore.size,
                loaded: this.loadTracker.size,
                providers: this.providerTx.size,
                nonceIndex: this.nonceIndex.size,
            },
            bytes: {
                hashIndex: scale(hashIndex),
                metadata: scale(metadata),
                loadTracker: scale(loadTracker),
                providerTx: scale(providerTx),
                nonceIndex: scale(nonceIndex),
                total: scale(total),
            },
        };
    }
    exportSnapshot() {
        const hashes = Array.from(this.idToHash.values());
        const hashById = (id) => this.idToHash.get(id);
        return {
            version: 2,
            hashes,
            providerTx: Array.from(this.providerTx.entries()).map(([provider, ids]) => [
                provider,
                Array.from(ids)
                    .map(hashById)
                    .filter((hash) => Boolean(hash)),
            ]),
            metadata: Array.from(this.metadataStore.entries())
                .map(([id, metadata]) => {
                const hash = hashById(id);
                return hash ? [hash, metadata] : undefined;
            })
                .filter((entry) => Boolean(entry)),
            loadTracker: Array.from(this.loadTracker.entries())
                .map(([id, info]) => {
                const hash = hashById(id);
                return hash ? [hash, info] : undefined;
            })
                .filter((entry) => Boolean(entry)),
            nonceIndex: Array.from(this.nonceIndex.entries())
                .map(([key, id]) => {
                const hash = hashById(id);
                return hash ? [key, hash] : undefined;
            })
                .filter((entry) => Boolean(entry)),
        };
    }
    importSnapshot(state) {
        this.dispose();
        const snapshot = normalizeSnapshot(state);
        for (const hash of snapshot.hashes)
            this.ensureId(canonicalHash(hash));
        for (const [hash, metadata] of snapshot.metadata) {
            const id = this.ensureId(canonicalHash(hash));
            this.metadataStore.set(id, metadata);
            this.indexNonce(metadata, id);
        }
        for (const [provider, hashes] of snapshot.providerTx) {
            for (const hash of hashes) {
                const id = this.ensureId(canonicalHash(hash));
                this.addProviderTx(provider, id);
            }
        }
        for (const [hash, info] of snapshot.loadTracker) {
            const id = this.ensureId(canonicalHash(hash));
            this.loadTracker.set(id, info);
        }
        for (const [key, hash] of snapshot.nonceIndex) {
            const id = this.hashToId.get(canonicalHash(hash));
            if (id !== undefined)
                this.nonceIndex.set(String(key).toLowerCase(), id);
        }
    }
    dispose() {
        this.clearWithoutShrink();
    }
    clearWithoutShrink() {
        this.hashToId.clear();
        this.idToHash.clear();
        this.providerTx.clear();
        this.metadataStore.clear();
        this.loadTracker.clear();
        this.nonceIndex.clear();
        this.nextId = 0;
    }
    ensureId(hash) {
        const existing = this.hashToId.get(hash);
        if (existing !== undefined)
            return existing;
        const id = this.nextId++;
        this.hashToId.set(hash, id);
        this.idToHash.set(id, hash);
        return id;
    }
    addProviderTx(provider, id) {
        let set = this.providerTx.get(provider);
        if (!set) {
            set = new Set();
            this.providerTx.set(provider, set);
        }
        set.add(id);
    }
    indexNonce(metadata, id) {
        if (metadata.from && metadata.nonce !== undefined) {
            this.nonceIndex.set(nonceKey(metadata.from, metadata.nonce), id);
        }
    }
    evictLowestGas() {
        let lowestId;
        let lowestGas;
        for (const [id, metadata] of this.metadataStore) {
            const gas = effectiveGasPrice(metadata);
            if (lowestGas === undefined || gas < lowestGas) {
                lowestGas = gas;
                lowestId = id;
            }
        }
        if (lowestId === undefined)
            return;
        const hash = this.idToHash.get(lowestId);
        if (hash)
            this.removeHash(hash);
    }
}
function normalizeSnapshot(state) {
    if (state?.version === 2) {
        return {
            version: 2,
            hashes: Array.isArray(state.hashes) ? state.hashes : [],
            providerTx: Array.isArray(state.providerTx) ? state.providerTx : [],
            metadata: Array.isArray(state.metadata) ? state.metadata : [],
            loadTracker: Array.isArray(state.loadTracker) ? state.loadTracker : [],
            nonceIndex: Array.isArray(state.nonceIndex) ? state.nonceIndex : [],
        };
    }
    // Legacy phase-1 shape used Map snapshots directly.
    const mapToEntries = (value) => {
        if (value instanceof Map)
            return Array.from(value.entries()).map(([key, val]) => [String(key), val]);
        if (Array.isArray(value))
            return value;
        return [];
    };
    const txEntries = mapToEntries(state?.txIndex);
    const idToHash = new Map();
    for (const [id, hash] of txEntries)
        idToHash.set(Number(id), canonicalHash(hash));
    const metadata = [];
    for (const [id, meta] of mapToEntries(state?.metaStore)) {
        const hash = idToHash.get(Number(id));
        if (hash)
            metadata.push([hash, meta]);
    }
    const loadTracker = [];
    for (const [id, info] of mapToEntries(state?.loadTracker)) {
        const hash = idToHash.get(Number(id));
        if (hash)
            loadTracker.push([hash, info]);
    }
    const providerTx = [];
    const rawProvider = state?.providerTx instanceof Map
        ? state.providerTx
        : new Map(Array.isArray(state?.providerTx) ? state.providerTx : []);
    for (const [provider, ids] of rawProvider.entries()) {
        const hashes = [];
        for (const id of ids instanceof Set ? ids : Array.isArray(ids) ? ids : []) {
            const hash = idToHash.get(Number(id));
            if (hash)
                hashes.push(hash);
        }
        providerTx.push([String(provider), hashes]);
    }
    return {
        version: 2,
        hashes: Array.from(idToHash.values()),
        providerTx,
        metadata,
        loadTracker,
        nonceIndex: [],
    };
}
export function createEvmMempoolStateStore() {
    const NativeEvmMempoolState = getEvmNativeBindings()?.NativeEvmMempoolState;
    if (NativeEvmMempoolState) {
        try {
            return new NativeEvmMempoolState();
        }
        catch {
            // fall through to JS store
        }
    }
    return new JsEvmMempoolStateStore();
}
