import { computeEpochAtSlot } from "@lodestar/state-transition";
import { isGloasBeaconBlock } from "@lodestar/types";
import { toRootHex } from "@lodestar/utils";
import { JobItemQueue } from "../../util/queue/index.js";
import { RegenError, RegenErrorCode } from "./errors.js";
import { RegenCaller, RegenFnName, } from "./interface.js";
import { StateRegenerator } from "./regen.js";
const REGEN_QUEUE_MAX_LEN = 256;
// TODO: Should this constant be lower than above? 256 feels high
const REGEN_CAN_ACCEPT_WORK_THRESHOLD = 16;
/**
 * Regenerates states that have already been processed by the fork choice
 *
 * All requests are queued so that only a single state at a time may be regenerated at a time
 */
export class QueuedStateRegenerator {
    jobQueue;
    regen;
    forkChoice;
    blockStateCache;
    checkpointStateCache;
    metrics;
    logger;
    constructor(modules) {
        this.regen = new StateRegenerator(modules);
        this.jobQueue = new JobItemQueue(this.jobQueueProcessor, { maxLength: REGEN_QUEUE_MAX_LEN, signal: modules.signal }, modules.metrics ? modules.metrics.regenQueue : undefined);
        this.forkChoice = modules.forkChoice;
        this.blockStateCache = modules.blockStateCache;
        this.checkpointStateCache = modules.checkpointStateCache;
        this.metrics = modules.metrics;
        this.logger = modules.logger;
    }
    async init() {
        if (this.checkpointStateCache.init) {
            return this.checkpointStateCache.init();
        }
    }
    canAcceptWork() {
        return this.jobQueue.jobLen < REGEN_CAN_ACCEPT_WORK_THRESHOLD;
    }
    dropCache() {
        this.blockStateCache.clear();
        this.checkpointStateCache.clear();
    }
    dumpCacheSummary() {
        return [...this.blockStateCache.dumpSummary(), ...this.checkpointStateCache.dumpSummary()];
    }
    /**
     * Get a state from block state cache.
     */
    getStateSync(stateRoot) {
        return this.blockStateCache.get(stateRoot);
    }
    /**
     * Get state for block processing.
     */
    getPreStateSync(block) {
        const parentRoot = toRootHex(block.parentRoot);
        const parentBlock = isGloasBeaconBlock(block)
            ? this.forkChoice.getBlockHexAndBlockHash(parentRoot, toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash))
            : this.forkChoice.getBlockHexDefaultStatus(parentRoot);
        if (!parentBlock) {
            throw new RegenError({
                code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
                blockRoot: block.parentRoot,
            });
        }
        const parentEpoch = computeEpochAtSlot(parentBlock.slot);
        const blockEpoch = computeEpochAtSlot(block.slot);
        // Check the checkpoint cache (if the pre-state is a checkpoint state)
        if (parentEpoch < blockEpoch) {
            const checkpointState = this.checkpointStateCache.getLatest(parentRoot, blockEpoch);
            if (checkpointState && computeEpochAtSlot(checkpointState.slot) === blockEpoch) {
                return checkpointState;
            }
        }
        // Check the state cache, only if the state doesn't need to go through an epoch transition.
        // Otherwise the state transition may not be cached and wasted. Queue for regen since the
        // work required will still be significant.
        if (parentEpoch === blockEpoch) {
            const state = this.blockStateCache.get(parentBlock.stateRoot);
            if (state) {
                return state;
            }
        }
        return null;
    }
    async getCheckpointStateOrBytes(cp) {
        return this.checkpointStateCache.getStateOrBytes(cp);
    }
    /**
     * Get checkpoint state from cache
     */
    getCheckpointStateSync(cp) {
        return this.checkpointStateCache.get(cp);
    }
    /**
     * Get state closest to head
     */
    getClosestHeadState(head) {
        return this.checkpointStateCache.getLatest(head.blockRoot, Infinity) || this.blockStateCache.get(head.stateRoot);
    }
    pruneOnCheckpoint(finalizedEpoch, justifiedEpoch, headStateRoot) {
        this.checkpointStateCache.prune(finalizedEpoch, justifiedEpoch);
        this.blockStateCache.prune(headStateRoot);
    }
    pruneOnFinalized(finalizedEpoch) {
        this.checkpointStateCache.pruneFinalized(finalizedEpoch);
        this.blockStateCache.deleteAllBeforeEpoch(finalizedEpoch);
    }
    processState(blockRootHex, postState) {
        this.blockStateCache.add(postState);
        this.checkpointStateCache.processState(blockRootHex, postState).catch((e) => {
            this.logger.debug("Error processing block state", { blockRootHex, slot: postState.slot }, e);
        });
    }
    addCheckpointState(cp, item) {
        this.checkpointStateCache.add(cp, item);
    }
    updateHeadState(newHead, maybeHeadState) {
        const { stateRoot: newHeadStateRoot, blockRoot: newHeadBlockRoot, slot: newHeadSlot } = newHead;
        const maybeHeadStateRoot = toRootHex(maybeHeadState.hashTreeRoot());
        const logCtx = {
            newHeadSlot,
            newHeadBlockRoot,
            newHeadStateRoot,
            maybeHeadSlot: maybeHeadState.slot,
            maybeHeadStateRoot,
        };
        const headState = newHeadStateRoot === maybeHeadStateRoot ? maybeHeadState : this.blockStateCache.get(newHeadStateRoot);
        if (headState) {
            this.blockStateCache.setHeadState(headState);
        }
        else {
            // Trigger regen on head change if necessary
            this.logger.warn("Head state not available, triggering regen", logCtx);
            // for the old BlockStateCacheImpl only
            //     - head has changed, so the existing cached head state is no longer useful. Set strong reference to null to free
            //       up memory for regen step below. During regen, node won't be functional but eventually head will be available
            // for the new FIFOBlockStateCache, this has no affect
            this.blockStateCache.setHeadState(null);
            // for the new FIFOBlockStateCache, it's important to reload state to regen head state here if needed
            const allowDiskReload = true;
            this.regen.getState(newHeadStateRoot, RegenCaller.processBlock, allowDiskReload).then((headStateRegen) => this.blockStateCache.setHeadState(headStateRegen), (e) => this.logger.error("Error on head state regen", logCtx, e));
        }
    }
    updatePreComputedCheckpoint(rootHex, epoch) {
        return this.checkpointStateCache.updatePreComputedCheckpoint(rootHex, epoch);
    }
    /**
     * Get the state to run with `block`.
     * - State after `block.parentRoot` dialed forward to block.slot
     */
    async getPreState(block, opts, rCaller) {
        this.metrics?.regenFnCallTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getPreState });
        // First attempt to fetch the state from caches before queueing
        const cachedState = this.getPreStateSync(block);
        if (cachedState !== null) {
            return cachedState;
        }
        // The state is not immediately available in the caches, enqueue the job
        this.metrics?.regenFnQueuedTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getPreState });
        return this.jobQueue.push({ key: "getPreState", args: [block, opts, rCaller] });
    }
    /**
     * Get state of provided `blockRoot` and dial forward to `slot`
     * Use this api with care because we don't want the queue to be busy
     * For the context, gossip block validation uses this api so we want it to be as fast as possible
     * @returns
     */
    async getBlockSlotState(block, slot, opts, rCaller) {
        this.metrics?.regenFnCallTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getBlockSlotState });
        // The state is not immediately available in the caches, enqueue the job
        return this.jobQueue.push({ key: "getBlockSlotState", args: [block, slot, opts, rCaller] });
    }
    async getState(stateRoot, rCaller) {
        this.metrics?.regenFnCallTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getState });
        // First attempt to fetch the state from cache before queueing
        const state = this.blockStateCache.get(stateRoot);
        if (state) {
            return state;
        }
        // The state is not immediately available in the cache, enqueue the job
        this.metrics?.regenFnQueuedTotal.inc({ caller: rCaller, entrypoint: RegenFnName.getState });
        return this.jobQueue.push({ key: "getState", args: [stateRoot, rCaller] });
    }
    jobQueueProcessor = async (regenRequest) => {
        const metricsLabels = {
            caller: regenRequest.args.at(-1),
            entrypoint: regenRequest.key,
        };
        let timer;
        try {
            timer = this.metrics?.regenFnCallDuration.startTimer(metricsLabels);
            switch (regenRequest.key) {
                case "getPreState":
                    return await this.regen.getPreState(...regenRequest.args);
                case "getBlockSlotState":
                    return await this.regen.getBlockSlotState(...regenRequest.args);
                case "getState":
                    return await this.regen.getState(...regenRequest.args);
            }
        }
        catch (e) {
            this.metrics?.regenFnTotalErrors.inc(metricsLabels);
            throw e;
        }
        finally {
            if (timer)
                timer();
        }
    };
}
//# sourceMappingURL=queued.js.map