import { EventEmitter } from "node:events";
import { computeStartSlotAtEpoch } from "@lodestar/state-transition";
import { toRootHex } from "@lodestar/utils";
import { AttestationImportOpt } from "../../chain/blocks/index.js";
import { cacheByRangeResponses, downloadByRange } from "../utils/downloadByRange.js";
import { RangeSyncType, getRangeSyncTarget, rangeSyncTypes } from "../utils/remoteSyncType.js";
import { SyncChain } from "./chain.js";
import { updateChains } from "./utils/index.js";
export { RangeSyncEvent };
var RangeSyncEvent;
(function (RangeSyncEvent) {
    RangeSyncEvent["completedChain"] = "RangeSync-completedChain";
})(RangeSyncEvent || (RangeSyncEvent = {}));
export { RangeSyncStatus };
var RangeSyncStatus;
(function (RangeSyncStatus) {
    /** A finalized chain is being synced */
    RangeSyncStatus[RangeSyncStatus["Finalized"] = 0] = "Finalized";
    /** There are no finalized chains and we are syncing one more head chains */
    RangeSyncStatus[RangeSyncStatus["Head"] = 1] = "Head";
    /** There are no head or finalized chains and no long range sync is in progress */
    RangeSyncStatus[RangeSyncStatus["Idle"] = 2] = "Idle";
})(RangeSyncStatus || (RangeSyncStatus = {}));
/**
 * RangeSync groups peers by their `status` into static target `SyncChain` instances
 * Peers on each chain will be queried for batches until reaching their target.
 *
 * Not all SyncChain-s will sync at once, and are grouped by sync type:
 * - Finalized Chain Sync
 * - Head Chain Sync
 *
 * ### Finalized Chain Sync
 *
 * At least one peer's status finalized checkpoint is greater than ours. Then we'll form
 * a chain starting from our finalized epoch and sync up to their finalized checkpoint.
 * - Only one finalized chain can sync at a time
 * - The finalized chain with the largest peer pool takes priority
 * - As peers' status progresses we will switch to a SyncChain with a better target
 *
 * ### Head Chain Sync
 *
 * If no Finalized Chain Sync is active, and the peer's STATUS head is beyond
 * `SLOT_IMPORT_TOLERANCE`, then we'll form a chain starting from our finalized epoch and sync
 * up to their head.
 * - More than one head chain can sync in parallel
 * - If there are many head chains the ones with more peers take priority
 */
export class RangeSync extends EventEmitter {
    chain;
    network;
    metrics;
    config;
    logger;
    /** There is a single chain per type, 1 finalized sync, 1 head sync */
    chains = new Map();
    opts;
    constructor(modules, opts) {
        super();
        const { chain, network, metrics, config, logger } = modules;
        this.chain = chain;
        this.network = network;
        this.metrics = metrics;
        this.config = config;
        this.logger = logger;
        this.opts = opts;
        if (metrics) {
            metrics.syncStatus.addCollect(() => this.scrapeMetrics(metrics));
        }
    }
    /** Throw / return all AsyncGenerators inside every SyncChain instance */
    close() {
        for (const chain of this.chains.values()) {
            chain.remove();
        }
    }
    /**
     * A peer with a relevant STATUS message has been found, which also is advanced from us.
     * Add this peer to an existing chain or create a new one. The update the chains status.
     */
    addPeer(peerId, localStatus, peerStatus) {
        // Compute if we should do a Finalized or Head sync with this peer
        const { syncType, startEpoch, target } = getRangeSyncTarget(localStatus, peerStatus, this.chain);
        this.logger.debug("Sync peer joined", {
            peer: peerId,
            syncType,
            startEpoch,
            targetSlot: target.slot,
            targetRoot: toRootHex(target.root),
            localHeadSlot: localStatus.headSlot,
            earliestAvailableSlot: peerStatus.earliestAvailableSlot ?? Infinity,
        });
        // If the peer existed in any other chain, remove it.
        // re-status'd peers can exist in multiple finalized chains, only one sync at a time
        if (syncType === RangeSyncType.Head) {
            this.removePeer(peerId);
        }
        this.addPeerOrCreateChain(startEpoch, target, peerId, syncType);
        this.update(localStatus.finalizedEpoch);
    }
    /**
     * Remove this peer from all head and finalized chains. A chain may become peer-empty and be dropped
     */
    removePeer(peerId) {
        for (const syncChain of this.chains.values()) {
            syncChain.removePeer(peerId);
        }
    }
    /**
     * Compute the current RangeSync state, not cached
     */
    get state() {
        const syncingHeadTargets = [];
        for (const chain of this.chains.values()) {
            if (chain.isSyncing) {
                if (chain.syncType === RangeSyncType.Finalized) {
                    return { status: RangeSyncStatus.Finalized, target: chain.target };
                }
                syncingHeadTargets.push(chain.target);
            }
        }
        if (syncingHeadTargets.length > 0) {
            return { status: RangeSyncStatus.Head, targets: syncingHeadTargets };
        }
        return { status: RangeSyncStatus.Idle };
    }
    /** Full debug state for lodestar API */
    getSyncChainsDebugState() {
        return Array.from(this.chains.values())
            .map((syncChain) => syncChain.getDebugState())
            .reverse(); // Newest additions first
    }
    /** Convenience method for `SyncChain` */
    processChainSegment = async (blocks, syncType) => {
        // Not trusted, verify signatures
        const flags = {
            // Only skip importing attestations for finalized sync. For head sync attestation are valuable.
            // Importing attestations also triggers a head update, see https://github.com/ChainSafe/lodestar/issues/3804
            // TODO: Review if this is okay, can we prevent some attacks by importing attestations?
            importAttestations: syncType === RangeSyncType.Finalized ? AttestationImportOpt.Skip : undefined,
            // Ignores ALREADY_KNOWN or GENESIS_BLOCK errors, and continues with the next block in chain segment
            ignoreIfKnown: true,
            // Ignore WOULD_REVERT_FINALIZED_SLOT error, continue with the next block in chain segment
            ignoreIfFinalized: true,
            // We won't attest to this block so it's okay to ignore a SYNCING message from execution layer
            fromRangeSync: true,
            // when this runs, syncing is the most important thing and gossip is not likely to run
            // so we can utilize worker threads to verify signatures
            blsVerifyOnMainThread: false,
        };
        if (this.opts?.disableProcessAsChainSegment) {
            // Should only be used for debugging or testing
            for (const block of blocks)
                await this.chain.processBlock(block, flags);
        }
        else {
            await this.chain.processChainSegment(blocks, flags);
        }
    };
    downloadByRange = async (peer, batch) => {
        const batchBlocks = batch.getBlocks();
        const { result, warnings } = await downloadByRange({
            config: this.config,
            network: this.network,
            logger: this.logger,
            peerIdStr: peer.peerId,
            batchBlocks,
            peerDasMetrics: this.chain.metrics?.peerDas,
            ...batch.getRequestsForPeer(peer),
        });
        const cached = cacheByRangeResponses({
            cache: this.chain.seenBlockInputCache,
            peerIdStr: peer.peerId,
            responses: result,
            batchBlocks,
        });
        return { result: cached, warnings };
    };
    pruneBlockInputs = (blocks) => {
        for (const block of blocks) {
            this.chain.seenBlockInputCache.prune(block.blockRootHex);
        }
    };
    /** Convenience method for `SyncChain` */
    reportPeer = (peer, action, actionName) => {
        this.network.reportPeer(peer, action, actionName);
    };
    getConnectedPeerSyncMeta = (peerId) => {
        return this.network.getConnectedPeerSyncMeta(peerId);
    };
    /** Convenience method for `SyncChain` */
    onSyncChainEnd = (err, target) => {
        this.update(this.chain.forkChoice.getFinalizedCheckpoint().epoch);
        this.emit(RangeSyncEvent.completedChain);
        if (err === null && target !== null) {
            this.metrics?.syncRange.syncChainHighestTargetSlotCompleted.set(target.slot);
        }
    };
    addPeerOrCreateChain(startEpoch, target, peer, syncType) {
        let syncChain = this.chains.get(syncType);
        if (!syncChain) {
            syncChain = new SyncChain(startEpoch, target, syncType, {
                processChainSegment: this.processChainSegment,
                downloadByRange: this.downloadByRange,
                reportPeer: this.reportPeer,
                getConnectedPeerSyncMeta: this.getConnectedPeerSyncMeta,
                pruneBlockInputs: this.pruneBlockInputs,
                onEnd: this.onSyncChainEnd,
            }, {
                config: this.config,
                clock: this.chain.clock,
                logger: this.logger,
                custodyConfig: this.chain.custodyConfig,
                metrics: this.metrics,
            });
            this.chains.set(syncType, syncChain);
            this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "add" });
            this.logger.debug("SyncChain added", {
                syncType,
                firstEpoch: syncChain.firstBatchEpoch,
                targetSlot: syncChain.target.slot,
                targetRoot: toRootHex(syncChain.target.root),
                peer,
            });
        }
        syncChain.addPeer(peer, target);
    }
    update(localFinalizedEpoch) {
        const localFinalizedSlot = computeStartSlotAtEpoch(localFinalizedEpoch);
        // Remove chains that are out-dated, peer-empty, completed or failed
        for (const [id, syncChain] of this.chains.entries()) {
            // Checks if a Finalized or Head chain should be removed
            if (
            // Sync chain has completed syncing or encountered an error
            syncChain.isRemovable ||
                // Sync chain has no more peers to download from
                syncChain.peers === 0 ||
                // Outdated: our chain has progressed beyond this sync chain
                syncChain.target.slot < localFinalizedSlot ||
                this.chain.forkChoice.hasBlock(syncChain.target.root)) {
                syncChain.remove();
                this.chains.delete(id);
                this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "remove" });
                this.logger.debug("SyncChain removed", {
                    id: syncChain.logId,
                    localFinalizedSlot,
                    lastValidatedSlot: syncChain.lastValidatedSlot,
                    firstEpoch: syncChain.firstBatchEpoch,
                    targetSlot: syncChain.target.slot,
                    targetRoot: toRootHex(syncChain.target.root),
                    validatedEpochs: syncChain.validatedEpochs,
                });
                // Re-status peers from successful chain. Potentially trigger a Head sync
                this.network
                    .reStatusPeers(syncChain.getPeers())
                    .catch((e) => this.logger.error("Error resyncing peers", {}, e));
            }
        }
        const { toStop, toStart } = updateChains(Array.from(this.chains.values()));
        for (const syncChain of toStop) {
            syncChain.stopSyncing();
            if (syncChain.isSyncing) {
                this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "stop" });
            }
        }
        for (const syncChain of toStart) {
            syncChain.startSyncing(localFinalizedEpoch);
            if (!syncChain.isSyncing) {
                this.metrics?.syncRange.syncChainsEvents.inc({ syncType: syncChain.syncType, event: "start" });
            }
        }
    }
    scrapeMetrics(metrics) {
        metrics.syncRange.syncChainsPeers.reset();
        const syncChainsByType = {
            [RangeSyncType.Finalized]: 0,
            [RangeSyncType.Head]: 0,
        };
        for (const chain of this.chains.values()) {
            metrics.syncRange.syncChainsPeers.observe({ syncType: chain.syncType }, chain.peers);
            syncChainsByType[chain.syncType]++;
        }
        for (const syncType of rangeSyncTypes) {
            metrics.syncRange.syncChains.set({ syncType }, syncChainsByType[syncType]);
        }
    }
}
//# sourceMappingURL=range.js.map