import { ExecutionStatus } from "@lodestar/fork-choice";
import { EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH } from "@lodestar/params";
import { computeEpochAtSlot, computeStartSlotAtEpoch, isExecutionCachedStateType, isMergeTransitionComplete, } from "@lodestar/state-transition";
import { ErrorAborted, prettyBytes, prettyBytesShort, sleep } from "@lodestar/utils";
import { ExecutionEngineState } from "../execution/index.js";
import { SyncState } from "../sync/index.js";
import { prettyTimeDiffSec } from "../util/time.js";
import { TimeSeries } from "../util/timeSeries.js";
/** Create a warning log whenever the peer count is at or below this value */
const WARN_PEER_COUNT = 1;
/**
 * Runs a notifier service that periodically logs information about the node.
 */
export async function runNodeNotifier(modules) {
    const { network, chain, sync, config, logger, signal } = modules;
    const headSlotTimeSeries = new TimeSeries({ maxPoints: 10 });
    const SLOTS_PER_SYNC_COMMITTEE_PERIOD = SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD;
    let hasLowPeerCount = false;
    let isFirstTime = true;
    try {
        while (!signal.aborted) {
            const connectedPeerCount = network.getConnectedPeerCount();
            if (connectedPeerCount <= WARN_PEER_COUNT) {
                // Only log once and prevent peer count warning on startup
                if (!hasLowPeerCount && !isFirstTime) {
                    logger.warn("Low peer count", { peers: connectedPeerCount });
                    hasLowPeerCount = true;
                }
            }
            else {
                hasLowPeerCount = false;
            }
            const clockSlot = chain.clock.currentSlot;
            const clockEpoch = computeEpochAtSlot(clockSlot);
            if (clockEpoch >= config.BELLATRIX_FORK_EPOCH && computeStartSlotAtEpoch(clockEpoch) === clockSlot) {
                if (chain.executionEngine.state === ExecutionEngineState.OFFLINE) {
                    logger.warn("Execution client is offline");
                }
                else if (chain.executionEngine.state === ExecutionEngineState.AUTH_FAILED) {
                    logger.error("Execution client authentication failed. Verify if the JWT secret matches on both clients");
                }
            }
            const headInfo = chain.forkChoice.getHead();
            const headState = chain.getHeadState();
            const finalizedEpoch = headState.finalizedCheckpoint.epoch;
            const finalizedRoot = headState.finalizedCheckpoint.root;
            const headSlot = headInfo.slot;
            headSlotTimeSeries.addPoint(headSlot, Math.floor(Date.now() / 1000));
            const peersRow = `peers: ${connectedPeerCount}`;
            const clockSlotRow = `slot: ${clockSlot}`;
            // Give info about empty slots if head < clock
            const skippedSlots = clockSlot - headInfo.slot;
            // headDiffInfo to have space suffix if its a non empty string
            const headDiffInfo = skippedSlots > 0 ? (skippedSlots > 1000 ? `${headInfo.slot} ` : `(slot -${skippedSlots}) `) : "";
            const headRow = `head: ${headDiffInfo}${prettyBytes(headInfo.blockRoot)}`;
            const executionInfo = getHeadExecutionInfo(config, clockEpoch, headState, headInfo);
            const finalizedCheckpointRow = `finalized: ${prettyBytes(finalizedRoot)}:${finalizedEpoch}`;
            let nodeState;
            switch (sync.state) {
                case SyncState.SyncingFinalized:
                case SyncState.SyncingHead: {
                    const slotsPerSecond = Math.max(headSlotTimeSeries.computeLinearSpeed(), 0);
                    const distance = Math.max(clockSlot - headSlot, 0);
                    const secondsLeft = distance / slotsPerSecond;
                    const timeLeft = Number.isFinite(secondsLeft) ? prettyTimeDiffSec(secondsLeft) : "?";
                    // Syncing - time left - speed - head - finalized - clock - peers
                    nodeState = [
                        "Syncing",
                        `${timeLeft} left`,
                        `${slotsPerSecond.toPrecision(3)} slots/s`,
                        clockSlotRow,
                        headRow,
                        ...executionInfo,
                        finalizedCheckpointRow,
                        peersRow,
                    ];
                    break;
                }
                case SyncState.Synced: {
                    // Synced - clock - head - finalized - peers
                    nodeState = ["Synced", clockSlotRow, headRow, ...executionInfo, finalizedCheckpointRow, peersRow];
                    break;
                }
                case SyncState.Stalled: {
                    // Searching peers - peers - head - finalized - clock
                    nodeState = ["Searching peers", peersRow, clockSlotRow, headRow, ...executionInfo, finalizedCheckpointRow];
                }
            }
            logger.info(nodeState.join(" - "));
            // Log important chain time-based events
            // Log sync committee change
            if (clockEpoch > config.ALTAIR_FORK_EPOCH && clockSlot % SLOTS_PER_SYNC_COMMITTEE_PERIOD === 0) {
                const period = Math.floor(clockEpoch / EPOCHS_PER_SYNC_COMMITTEE_PERIOD);
                logger.info(`New sync committee period ${period}`);
            }
            // Log halfway through each slot
            await sleep(timeToNextHalfSlot(config, chain, isFirstTime), signal);
            isFirstTime = false;
        }
    }
    catch (e) {
        if (e instanceof ErrorAborted) {
            return; // Ok
        }
        logger.error("Node notifier error", {}, e);
    }
}
function timeToNextHalfSlot(config, chain, isFirstTime) {
    const msPerSlot = config.SLOT_DURATION_MS;
    const msPerHalfSlot = msPerSlot / 2;
    const msFromGenesis = Date.now() - chain.genesisTime * 1000;
    const msToNextSlot = msFromGenesis < 0
        ? // For future genesis time, calculate time left in the slot
            -msFromGenesis % msPerSlot
        : // For past genesis time, calculate time until the next slot
            msPerSlot - (msFromGenesis % msPerSlot);
    if (isFirstTime) {
        // at the 1st time we may miss middle of the current clock slot
        return msToNextSlot > msPerHalfSlot ? msToNextSlot - msPerHalfSlot : msToNextSlot + msPerHalfSlot;
    }
    // after the 1st time always wait until middle of next clock slot
    return msToNextSlot + msPerHalfSlot;
}
function getHeadExecutionInfo(config, clockEpoch, headState, headInfo) {
    if (clockEpoch < config.BELLATRIX_FORK_EPOCH) {
        return [];
    }
    const executionStatusStr = headInfo.executionStatus.toLowerCase();
    // Add execution status to notifier only if head is on/post bellatrix
    if (isExecutionCachedStateType(headState)) {
        if (isMergeTransitionComplete(headState)) {
            const executionPayloadHashInfo = headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadBlockHash : "empty";
            const executionPayloadNumberInfo = headInfo.executionStatus !== ExecutionStatus.PreMerge ? headInfo.executionPayloadNumber : NaN;
            return [
                `exec-block: ${executionStatusStr}(${executionPayloadNumberInfo} ${prettyBytesShort(executionPayloadHashInfo)})`,
            ];
        }
        return [`exec-block: ${executionStatusStr}`];
    }
    return [];
}
//# sourceMappingURL=notifier.js.map