import { ForkSeq } from "@lodestar/params";
import { callFnWhenAwait } from "@lodestar/utils";
import { isOptimisticBlock } from "../../util/forkChoice.js";
import { JobItemQueue, isQueueErrorAborted } from "../../util/queue/index.js";
import { ChainEvent } from "../emitter.js";
import { PROCESS_FINALIZED_CHECKPOINT_QUEUE_LENGTH } from "./constants.js";
import { HistoricalStateRegen } from "./historicalState/historicalStateRegen.js";
import { ArchiveMode } from "./interface.js";
import { FrequencyStateArchiveStrategy } from "./strategies/frequencyStateArchiveStrategy.js";
import { archiveBlocks } from "./utils/archiveBlocks.js";
import { archiveExecutionPayloadEnvelopes } from "./utils/archivePayloads.js";
import { pruneHistory } from "./utils/pruneHistory.js";
import { updateBackfillRange } from "./utils/updateBackfillRange.js";
export { ArchiveStoreTask };
var ArchiveStoreTask;
(function (ArchiveStoreTask) {
    ArchiveStoreTask["ArchiveBlocks"] = "archive_blocks";
    ArchiveStoreTask["ArchivePayloads"] = "archive_payloads";
    ArchiveStoreTask["PruneHistory"] = "prune_history";
    ArchiveStoreTask["OnFinalizedCheckpoint"] = "on_finalized_checkpoint";
    ArchiveStoreTask["MaybeArchiveState"] = "maybe_archive_state";
    ArchiveStoreTask["RegenPruneOnFinalized"] = "regen_prune_on_finalized";
    ArchiveStoreTask["ForkchoicePrune"] = "forkchoice_prune";
    ArchiveStoreTask["UpdateBackfillRange"] = "update_backfill_range";
})(ArchiveStoreTask || (ArchiveStoreTask = {}));
/**
 * Used for running tasks that depends on some events or are executed
 * periodically.
 */
export class ArchiveStore {
    archiveMode;
    jobQueue;
    archiveDataEpochs;
    statesArchiverStrategy;
    chain;
    db;
    logger;
    metrics;
    opts;
    signal;
    historicalStateRegen;
    constructor(modules, opts, signal) {
        this.chain = modules.chain;
        this.db = modules.db;
        this.logger = modules.logger;
        this.metrics = modules.metrics;
        this.opts = opts;
        this.signal = signal;
        this.archiveMode = opts.archiveMode;
        this.archiveDataEpochs = opts.archiveDataEpochs;
        this.jobQueue = new JobItemQueue(this.processFinalizedCheckpoint, {
            maxLength: PROCESS_FINALIZED_CHECKPOINT_QUEUE_LENGTH,
            signal,
        });
        if (opts.archiveMode === ArchiveMode.Frequency) {
            this.statesArchiverStrategy = new FrequencyStateArchiveStrategy(this.chain.regen, this.db, this.logger, opts, this.chain.bufferPool);
        }
        else {
            throw new Error(`State archive strategy "${opts.archiveMode}" currently not supported.`);
        }
        if (!opts.disableArchiveOnCheckpoint) {
            this.chain.emitter.on(ChainEvent.forkChoiceFinalized, this.onFinalizedCheckpoint);
            this.chain.emitter.on(ChainEvent.checkpoint, this.onCheckpoint);
            this.signal.addEventListener("abort", () => {
                this.chain.emitter.off(ChainEvent.forkChoiceFinalized, this.onFinalizedCheckpoint);
                this.chain.emitter.off(ChainEvent.checkpoint, this.onCheckpoint);
            }, { once: true });
        }
    }
    async init() {
        if (this.opts.pruneHistory) {
            // prune ALL stale data before starting
            this.logger.info("Pruning historical data");
            await callFnWhenAwait(pruneHistory(this.chain.config, this.db, this.logger, this.metrics, this.opts.anchorState.finalizedCheckpoint.epoch, this.chain.clock.currentEpoch), () => this.logger.info("Still pruning historical data, please wait..."), 30_000, this.signal);
        }
        if (this.opts.serveHistoricalState) {
            this.historicalStateRegen = await HistoricalStateRegen.init({
                opts: {
                    genesisTime: this.chain.clock.genesisTime,
                    dbLocation: this.opts.dbName,
                },
                config: this.chain.config,
                metrics: this.metrics,
                logger: this.logger,
                signal: this.signal,
            });
        }
    }
    async close() {
        await this.historicalStateRegen?.close();
    }
    async scrapeMetrics() {
        return this.historicalStateRegen?.scrapeMetrics() ?? "";
    }
    async getHistoricalStateBySlot(slot) {
        const finalizedBlock = this.chain.forkChoice.getFinalizedBlock();
        if (slot >= finalizedBlock.slot) {
            return null;
        }
        // request for finalized state using historical state regen
        const stateSerialized = await this.historicalStateRegen?.getHistoricalState(slot);
        if (!stateSerialized) {
            return null;
        }
        return { state: stateSerialized, executionOptimistic: isOptimisticBlock(finalizedBlock), finalized: true };
    }
    /**
     * Archive latest finalized state
     * */
    async persistToDisk() {
        return this.statesArchiverStrategy.archiveState(this.chain.forkChoice.getFinalizedCheckpoint());
    }
    //-------------------------------------------------------------------------
    // Event handlers
    //-------------------------------------------------------------------------
    onFinalizedCheckpoint = (finalized) => {
        this.jobQueue.push(finalized).catch((e) => {
            if (!isQueueErrorAborted(e)) {
                this.logger.error("Error queuing finalized checkpoint", { epoch: finalized.epoch }, e);
            }
        });
    };
    onCheckpoint = () => {
        const headStateRoot = this.chain.forkChoice.getHead().stateRoot;
        this.chain.regen.pruneOnCheckpoint(this.chain.forkChoice.getFinalizedCheckpoint().epoch, this.chain.forkChoice.getJustifiedCheckpoint().epoch, headStateRoot);
        this.statesArchiverStrategy.onCheckpoint(headStateRoot, this.metrics).catch((err) => {
            this.logger.error("Error during state archive", { archiveMode: this.archiveMode }, err);
        });
    };
    processFinalizedCheckpoint = async (finalized) => {
        try {
            const finalizedEpoch = finalized.epoch;
            const finalizedFork = this.chain.config.getForkSeqAtEpoch(finalizedEpoch);
            this.logger.verbose("Start processing finalized checkpoint", { epoch: finalizedEpoch, rootHex: finalized.rootHex });
            let timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
            await archiveBlocks(this.chain.config, this.db, this.chain.forkChoice, this.chain.lightClientServer, this.logger, finalized, this.chain.clock.currentEpoch, this.archiveDataEpochs, this.chain.opts.persistOrphanedBlocks, this.chain.opts.persistOrphanedBlocksDir);
            timer?.({ source: ArchiveStoreTask.ArchiveBlocks });
            if (finalizedFork >= ForkSeq.gloas) {
                timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
                await archiveExecutionPayloadEnvelopes(this.chain, finalized);
                timer?.({ source: ArchiveStoreTask.ArchivePayloads });
            }
            if (this.opts.pruneHistory) {
                timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
                await pruneHistory(this.chain.config, this.db, this.logger, this.metrics, finalizedEpoch, this.chain.clock.currentEpoch);
                timer?.({ source: ArchiveStoreTask.PruneHistory });
            }
            timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
            await this.statesArchiverStrategy.onFinalizedCheckpoint(finalized, this.metrics);
            timer?.({ source: ArchiveStoreTask.OnFinalizedCheckpoint });
            // should be after ArchiveBlocksTask to handle restart cleanly
            timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
            await this.statesArchiverStrategy.maybeArchiveState(finalized, this.metrics);
            timer?.({ source: ArchiveStoreTask.MaybeArchiveState });
            timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
            this.chain.regen.pruneOnFinalized(finalizedEpoch);
            timer?.({ source: ArchiveStoreTask.RegenPruneOnFinalized });
            // tasks rely on extended fork choice
            timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
            const prunedBlocks = this.chain.forkChoice.prune(finalized.rootHex);
            timer?.({ source: ArchiveStoreTask.ForkchoicePrune });
            timer = this.metrics?.processFinalizedCheckpoint.durationByTask.startTimer();
            await updateBackfillRange({ chain: this.chain, db: this.db, logger: this.logger }, finalized);
            timer?.({ source: ArchiveStoreTask.UpdateBackfillRange });
            this.logger.verbose("Finish processing finalized checkpoint", {
                epoch: finalizedEpoch,
                rootHex: finalized.rootHex,
                prunedBlocks: prunedBlocks.length,
            });
        }
        catch (e) {
            if (!this.signal.aborted) {
                this.logger.error("Error processing finalized checkpoint", { epoch: finalized.epoch }, e);
            }
        }
    };
}
//# sourceMappingURL=archiveStore.js.map