"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.waitForQ1SendRequests = exports.setRestartNetwork = exports.getIsFirst = exports.setIsFirst = exports.setp2pIgnoreJoinRequests = exports.setActive = exports.getThisNodeInfo = exports.getPublicNodeInfo = exports.getLatestCyclesFromArchiver = exports.getFullNodesFromArchiver = exports.contactArchiver = exports.updateNodeState = exports.getStatusHistoryCopy = exports.startupV2 = exports.init = exports.p2pIgnoreJoinRequests = exports.p2pSyncEnd = exports.p2pSyncStart = exports.p2pJoinTime = exports.isRestartNetwork = exports.port = exports.ip = exports.allowConnectionToFirstNode = exports.isFailed = exports.isActive = exports.isFirst = exports.id = exports.emitter = void 0;
const lib_types_1 = require("@shardeum-foundation/lib-types");
const events = __importStar(require("events"));
const logger_1 = require("../logger");
const network = __importStar(require("../network"));
const utils = __importStar(require("../utils"));
const checkIP_1 = require("../utils/functions/checkIP");
const nestedCounters_1 = require("../utils/nestedCounters");
const Archivers = __importStar(require("./Archivers"));
const Comms = __importStar(require("./Comms"));
const Context = __importStar(require("./Context"));
const CycleCreator = __importStar(require("./CycleCreator"));
const CycleCreator_1 = require("./CycleCreator");
const GlobalAccounts = __importStar(require("./GlobalAccounts"));
const Join = __importStar(require("./Join"));
const JoinV2 = __importStar(require("./Join/v2"));
const Acceptance = __importStar(require("./Join/v2/acceptance"));
const NodeList = __importStar(require("./NodeList"));
const Sync = __importStar(require("./Sync"));
const SyncV2 = __importStar(require("./SyncV2/"));
const Utils_1 = require("./Utils");
const CycleChain = __importStar(require("./CycleChain"));
const rfdc_1 = __importDefault(require("rfdc"));
const network_1 = require("../network");
const getCallstack_1 = __importDefault(require("../utils/getCallstack"));
const deepCopy = (0, rfdc_1.default)();
const debug_1 = require("../debug");
const standbyRefresh_1 = require("./Join/v2/standbyRefresh");
const Archivers_1 = require("./Archivers");
const CycleCreator_2 = require("./CycleCreator");
const lib_types_2 = require("@shardeum-foundation/lib-types");
const ServiceQueue = __importStar(require("./ServiceQueue"));
/** STATE */
const startTimestamp = Date.now();
exports.emitter = new events.EventEmitter();
let p2pLogger;
exports.isActive = false;
/** This flag shows that our node has failed and should not be in the network
 *  It could even be that the node has restart but is still in the active list
 *  and waiting to be removed first before it can join again
 */
exports.isFailed = false;
exports.allowConnectionToFirstNode = false;
exports.isRestartNetwork = false;
exports.p2pJoinTime = 0;
exports.p2pSyncStart = 0;
exports.p2pSyncEnd = 0;
exports.p2pIgnoreJoinRequests = true;
let joinRequestCounter;
let mode = null;
/*
  Records the state of the node (INITIALIZING -> STANDBY -> SYNCING -> ACTIVE)
  INITIALIZING -> STANDBY = Node is trying to get its join request accepted
  STANDBY -> SYNCING = Node has accepted its join request and is syncing
  SYNCING -> ACTIVE = Node has synced and is now active
*/
let state = lib_types_1.P2P.P2PTypes.NodeStatus.INITIALIZING;
let firstTimeJoiningLoop = true;
let isFirstRefresh = true;
let cyclesElapsedSinceRefresh = 0;
const idErrorMessage = `id did not match the cycle record info`;
const nodeMatch = (node) => node.externalIp === network.ipInfo.externalIp && node.externalPort === network.ipInfo.externalPort;
/** ROUTES */
/** FUNCTIONS */
function init() {
    // Setup our IP and port so modules like Sync can use it
    exports.ip = network.ipInfo.externalIp;
    exports.port = network.ipInfo.externalPort;
    // Create a logger for yourself
    p2pLogger = Context.logger.getLogger('p2p');
    if ((0, debug_1.isServiceMode)()) {
        info('p2p/Self/init disabled: Starting in service mode.');
        return;
    }
    // Init submodules
    Comms.init();
    Archivers.init();
    CycleCreator.init();
    GlobalAccounts.init();
    NodeList.init();
    Sync.init();
    CycleChain.init();
    // initialize SyncV2 if enabled
    if (Context.config.p2p.useSyncProtocolV2) {
        SyncV2.init();
    }
    // initialize JoinV2 if enabled
    if (Context.config.p2p.useJoinProtocolV2) {
        JoinV2.init();
    }
    // Create a logger for yourself
    p2pLogger = Context.logger.getLogger('p2p');
    // kick-start the status history. debug end-point is /status-history
    updateNodeState(lib_types_1.P2P.P2PTypes.NodeStatus.INITIALIZING); // requires p2pLogger through warn()
}
exports.init = init;
function startupV2(shardus) {
    const promise = new Promise((resolve, reject) => {
        if ((0, debug_1.isServiceMode)()) {
            info('p2p/Self/startup disabled: Starting in service mode.');
            return true;
        }
        const publicKey = Context.crypto.getPublicKey();
        let attemptJoiningTimer = null;
        let attemptJoiningRunning = false;
        let cycleDuration = Context.config.p2p.cycleDuration;
        // Function to set node's status to SYNCING, perform sync functions, and finish startup
        const enterSyncingState = async () => {
            try {
                // set status SELECTED?
                updateNodeState(lib_types_1.P2P.P2PTypes.NodeStatus.SELECTED);
                //robust query to get cycle number in case we want to send gossip before p2p sync
                // if (!isFirst) {
                //   // do robust query to get cycle number
                //   let robustCycleNumber = null
                //   const MAX_TRIES = 5
                //   let gotCycleNumber = false
                //   let currentTry = 0
                //   const archiver = getRandomAvailableArchiver()
                //   while (currentTry < MAX_TRIES && !gotCycleNumber) {
                //     try {
                //       const activeNodesResult = await getActiveNodesFromArchiver(archiver)
                //       if (!activeNodesResult) {
                //         throw Error(`couldn't get active nodes`)
                //       }
                //       const activeNodes = activeNodesResult.nodeList
                //       for (let i = 0; i < 3; i++) {
                //         const node = utils.getRandom(activeNodes, 1)[0]
                //         const cycleNumber = await http.get(`${node.ip}:${node.port}/cyclenumber`)
                //         // do some checking
                //       }
                //       if (gotCycleNumber) break
                //       else robustCycleNumber = null
                //       currentTry++
                //     } catch (e) {
                //       throw Error(`submitSyncStarted: Error in try ${currentTry} posting syncStarted request: ` + e)
                //     }
                //   }
                //   if (robustCycleNumber) {
                //     let payload = {
                //       nodeId: id,
                //       cycleNumber: robustCycleNumber,
                //     }
                //     payload = Context.crypto.sign(payload)
                //     submitSyncStarted(payload)
                //   }
                // }
                exports.p2pSyncStart = (0, network_1.shardusGetTime)();
                if (logger_1.logFlags.p2pNonFatal)
                    info('Emitting `joined` event.');
                // Should fire after being accepted into the network
                exports.emitter.emit('joined', exports.id, publicKey);
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', 'joined');
                // Sync cycle chain from network
                await syncCycleChain(exports.id, shardus);
                await Join.queueStartedSyncingRequest();
                // Enable internal routes
                Comms.setAcceptInternal(true);
                // Start creating cycle records
                await CycleCreator.startCycles();
                exports.p2pSyncEnd = (0, network_1.shardusGetTime)();
                exports.p2pJoinTime = (exports.p2pSyncEnd - exports.p2pSyncStart) / 1000;
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `sync time ${exports.p2pJoinTime} seconds`);
                if (logger_1.logFlags.p2pNonFatal)
                    info('Emitting `initialized` event.' + exports.p2pJoinTime);
                exports.emitter.emit('initialized');
                // Break loop
                return resolve(true);
            }
            catch (err) {
                if (err.message === idErrorMessage) {
                    nestedCounters_1.nestedCountersInstance.countEvent('p2p', idErrorMessage);
                    /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                        console.log(`startupV2 ${idErrorMessage}`);
                    console.log(`self:startupV2.  ${idErrorMessage}.`);
                    exports.emitter.emit('invoke-exit', `id did not match`, (0, getCallstack_1.default)(), idErrorMessage, true);
                }
                // Log syncing error and abort startup
                /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                    console.log('error in startupV2 > enterSyncingState: ', utils.formatErrorMessage(err));
                /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                    warn('Error while syncing to network:');
                /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                    warn(utils.formatErrorMessage(err));
                //warn(err.stack)
                throw new Error('Fatal: Error while syncing to network:' + err.message);
            }
        };
        // This funciton will be called repeatedly until the node is accepted to become an active node
        // It has two phases the first phase is to wait for isOnStandbyList to be come true
        // which indicates we are now on the standby list (per the consenced cycle record)
        // Then the node will wait for and "id" the presense of an id indicates we have been selected to become an active node
        const attemptJoining = async () => {
            // Prevent scheduler from running multiple times
            if (attemptJoiningRunning) {
                return;
            }
            attemptJoiningRunning = true;
            // Clear existing scheduler timer
            if (attemptJoiningTimer) {
                clearTimeout(attemptJoiningTimer);
            }
            try {
                // Get active nodes from Archiver
                info(`startupV2: attemptJoining enter`);
                // this name is confusing, as the node is not actually active yet
                const activeNodes = await contactArchiver('startupV2:attemptJoining');
                info(`startupV2: got active nodes: ${activeNodes.length}`);
                // Determine if you're the first node
                if (utils.isUndefined(exports.isFirst)) {
                    exports.isFirst = discoverNetwork(activeNodes);
                    if (exports.isFirst) {
                        // Join your own network and give yourself an ID
                        exports.id = await Join.firstJoin();
                        // set status SYNCING
                        await enterSyncingState();
                        attemptJoiningRunning = false;
                        return;
                    }
                }
                // Remove yourself from activeNodes if you are present in them
                const ourIdx = activeNodes.findIndex((node) => node.ip === network.ipInfo.externalIp && node.port === network.ipInfo.externalPort);
                if (ourIdx > -1) {
                    if (activeNodes.length === 1)
                        exports.isFirst = undefined; // looks like I'm  in the list;
                    activeNodes.splice(ourIdx, 1);
                }
                // Get latest cycle record from active nodes
                const latestCycle = await Sync.getNewestCycle(activeNodes);
                cycleDuration = latestCycle.duration;
                mode = latestCycle.mode || null;
                // Query network for node status
                const resp = await Join.fetchJoinedV2(activeNodes);
                info(`startupV2: resp ${lib_types_2.Utils.safeStringify(resp)}`);
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `fetchJoinedV2: isOnStandbyList: ${resp.isOnStandbyList} id: ${resp.id}`);
                // note the list below is in priority order of what operation is the most important
                // mainly this matters on something like our node being selected to join but also on the
                // same cycle could be cut due to version
                // If we see and id with a real value then our node has been selected to go active
                // start the syncing process and stop looping the attemptJoining function
                if (resp?.id) {
                    //detect if we are a zombie node (bounce and network think we are still active)
                    //if we never even tried to join yet but the network thinks we are active
                    //then we are a zombie node
                    if (firstTimeJoiningLoop === true) {
                        exports.isFailed = true;
                        /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `detected self as zombie ${latestCycle.counter} waiting ${Context.config.p2p.delayZombieRestartSec} before exiting`, 1);
                        utils.sleep(Context.config.p2p.delayZombieRestartSec * 1000 /*sec to ms*/).then(() => {
                            //give the network a chance to see we are not active
                            //TODO in the future if we are more confident in our ablility to shut down the node from functioning
                            // we could have a shutdown wait. (but there is a lot of surface area)
                            // the other method would be to request to be shut down but that is tricky and may not be possible
                            // in all cases  i.e. our key changed
                            const message = `node detected as zombie node, waited ${Context.config.p2p.delayZombieRestartSec} seconds before restart`;
                            exports.emitter.emit('invoke-exit', `node restarted ungracefully, needs to restart`, (0, getCallstack_1.default)(), message, false //force shutdown for now in a way that requires the user to restart again
                            );
                        });
                        attemptJoiningRunning = false;
                        return;
                    }
                    exports.id = resp.id;
                    await enterSyncingState();
                    attemptJoiningRunning = false;
                    return;
                }
                firstTimeJoiningLoop = false;
                // If we see that isOnStandbyList is true then we are on standby.
                // If our state is not STANDBY yet set it to STANDBY (this is mainly for operator CLI purposes)
                // Note that attemptJoining isn't just to get on the standby list, but also
                // we will be checking above to see when our node is selected to go active
                if (resp?.isOnStandbyList === true) {
                    //nestedCountersInstance.countEvent('p2p', 'startupV2: on standby list')
                    if (state !== lib_types_1.P2P.P2PTypes.NodeStatus.STANDBY) {
                        updateNodeState(lib_types_1.P2P.P2PTypes.NodeStatus.STANDBY);
                    }
                    if (isFirstRefresh) {
                        if (latestCycle.counter >=
                            joinRequestCounter + Context.config.p2p.standbyListCyclesTTL - Context.config.p2p.cyclesToRefreshEarly) {
                            isFirstRefresh = false;
                            //info(`startupV2: submitStandbyRefresh first ${latestCycle.counter}`)
                            (0, standbyRefresh_1.submitStandbyRefresh)(publicKey);
                            nestedCounters_1.nestedCountersInstance.countEvent('p2p', `submitted StandbyRefreshRequest request - first time`);
                            /* prettier-ignore */ if (logger_1.logFlags.verbose)
                                console.log(`submitted StandbyRefreshRequest request - first time`);
                            cyclesElapsedSinceRefresh = 0;
                        }
                    }
                    else if (cyclesElapsedSinceRefresh >= Context.config.p2p.standbyListCyclesTTL) {
                        //info(`startupV2: submitStandbyRefresh ${latestCycle.counter}`)
                        (0, standbyRefresh_1.submitStandbyRefresh)(publicKey);
                        nestedCounters_1.nestedCountersInstance.countEvent('p2p', `submitted StandbyRefreshRequest request`);
                        /* prettier-ignore */ if (logger_1.logFlags.verbose)
                            console.log(`submitted StandbyRefreshRequest request`);
                        cyclesElapsedSinceRefresh = 0;
                    }
                    cyclesElapsedSinceRefresh += Context.config.p2p.attemptJoiningWaitMultiplier;
                    // Call scheduler after 5 cycles... does this mean it may be 5 cycles before we realized we were
                    // accepted to go active?
                    // Looks like that is the worst case
                    //
                    attemptJoiningTimer = setTimeout(() => {
                        attemptJoining();
                    }, Context.config.p2p.attemptJoiningWaitMultiplier * cycleDuration * 1000);
                    attemptJoiningRunning = false;
                    return;
                }
                // If we are in state stanby but suddenly isOnStandbyList becomes false again
                // then we have been kicked from the stanby list.  The node should exit with error
                if (state === lib_types_1.P2P.P2PTypes.NodeStatus.STANDBY) {
                    if (resp?.isOnStandbyList === false) {
                        nestedCounters_1.nestedCountersInstance.countEvent('p2p', 'detected standby list removal of our node');
                        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                            console.log('startupV2 our node has been removed from the standby list and will restart');
                        console.log(`self:startupV2.  standby=>not standby.  restarting`);
                        //  todo this may not be the correct UX
                        const message = `validator removed from standby list`;
                        exports.emitter.emit('invoke-exit', `removed from standby list`, (0, getCallstack_1.default)(), message, true);
                        attemptJoiningRunning = false;
                        return;
                    }
                }
                // If we see that isOnStandbyList is false then we are not on standby.
                // we should call joinNetworkV2 to try to get on the standby list
                // note this may not actually result in a message to the network, as the dapp
                // may need to check stain and take other actions before it isReadyToJoin
                if (resp?.isOnStandbyList === false) {
                    await joinNetworkV2(activeNodes);
                    // Call scheduler after 2 cycles
                    attemptJoiningTimer = setTimeout(() => {
                        attemptJoining();
                    }, 2 * cycleDuration * 1000);
                    attemptJoiningRunning = false;
                    return;
                }
                // iff we need to ever jump out of standby ??
                // if (state === P2P.P2PTypes.NodeStatus.STANDBY) {
                //   //check our own version against the latest version
                //   //quite if we are wrong.
                // }
                //this should help us feel safer that attemptJoining will not finish until we are ready for it to do so
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', 'attemptJoining: error got too far without an action');
                throw new Error('Should not reach this point. Throwing non-fatal error which will restart attemptJoining');
            }
            catch (err) {
                // Log joining error
                /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                    console.log(`error in startupV2 > attemptJoining:`, utils.formatErrorMessage(err));
                /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                    warn(`Error while joining network:`);
                /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                    warn(utils.formatErrorMessage(err));
                //warn(err.stack)
                // Abort startup if error is fatal
                if (err.message.startsWith('Fatal:')) {
                    attemptJoiningRunning = false;
                    /* prettier-ignore */ if (logger_1.logFlags.fatal)
                        warn(`Fatal error while joining network. re-throw to cause shutdown`);
                    throw err;
                }
                // Schedule another attempt to join
                /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                    info(`Trying to join again in ${cycleDuration} seconds...`);
                attemptJoiningTimer = setTimeout(() => {
                    attemptJoining();
                }, cycleDuration * 1000);
            }
            finally {
                attemptJoiningRunning = false;
            }
        };
        // If startInWitness config is set to true, start witness mode and end
        if (Context.config.p2p.startInWitnessMode) {
            if (logger_1.logFlags.p2pNonFatal)
                info('Emitting `witnessing` event.');
            exports.emitter.emit('witnessing', publicKey);
            return resolve(true);
        }
        // Emit the joining event
        if (logger_1.logFlags.p2pNonFatal)
            info('Emitting `joining` event.');
        exports.emitter.emit('joining', publicKey);
        // register listener for acceptance
        // The accepted flow is deprecated
        // Acceptance.getEventEmitter().on('accepted', () => {
        //   if (state === P2P.P2PTypes.NodeStatus.SYNCING || state === P2P.P2PTypes.NodeStatus.ACTIVE) {
        //     return
        //   }
        //   attemptJoining()
        // })
        // Start by joining the network
        attemptJoining();
    });
    return promise;
}
exports.startupV2 = startupV2;
const statusHistory = [];
function getStatusHistoryCopy() {
    // return a copy so it cannot be mutated
    return deepCopy(statusHistory);
}
exports.getStatusHistoryCopy = getStatusHistoryCopy;
function updateNodeState(updatedState, because = '') {
    state = updatedState;
    const pubKey = (Context.crypto && Context.crypto.getPublicKey()) || null;
    const entry = {
        moduleStatus: state,
        nodeListStatus: (pubKey && NodeList.byPubKey && NodeList.byPubKey.get(pubKey) && NodeList.byPubKey.get(pubKey).status) || null,
        timestamp: (0, network_1.shardusGetTime)(),
        isoDateTime: new Date().toISOString(),
        uptime: utils.readableDuration(startTimestamp),
        newestCycleCounter: (CycleChain.getNewest() && CycleChain.getNewest().counter) || null,
        quarter: CycleCreator.currentQuarter,
        because: because,
    };
    /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `stateupdate: ${updatedState} c:${entry.newestCycleCounter} q:${entry.quarter}`);
    // changing status is infrequent, so log it always
    /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
        warn(`Node status changed to ${updatedState}:\n${JSON.stringify(entry, null, 2)}`);
    statusHistory.push(entry);
}
exports.updateNodeState = updateNodeState;
async function joinNetworkV2(activeNodes) {
    info(`joinNetworkV2: enter`);
    /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `joinNetworkV2:enter`);
    // Get latest cycle record from active nodes
    const latestCycle = await Sync.getNewestCycle(activeNodes);
    mode = latestCycle.mode || null;
    info(`joinNetworkV2: got latest cycle :${mode}`);
    const publicKey = Context.crypto.getPublicKey();
    try {
        const isReadyToJoin = await Context.shardus.app.isReadyToJoin(latestCycle, publicKey, activeNodes, mode);
        if (!isReadyToJoin) {
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `joinNetworkV2:isReadyToJoin:false`);
            // Wait for Context.config.p2p.cycleDuration and try again
            throw new Error('Node not ready to join');
        }
        else {
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `joinNetworkV2:isReadyToJoin:true`);
        }
    }
    catch (ex) {
        /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `joinNetworkV2:isReadyToJoin:crashed: ${ex?.message}`);
        warn(`joinNetworkV2: isReadyToJoin crashed :${utils.formatErrorMessage(ex)}`);
        return;
    }
    // Create join request from latest cycle
    const request = await Join.createJoinRequest(latestCycle);
    joinRequestCounter = request.nodeInfo.refreshedCounter;
    //we can't use allowBogon lag yet because its value is detected later.
    //it is possible to throw out any invalid IPs at this point
    if (Context.config.p2p.rejectBogonOutboundJoin || Context.config.p2p.forceBogonFilteringOn) {
        if ((0, checkIP_1.isInvalidIP)(request.nodeInfo.externalIp)) {
            throw new Error(`Fatal: Node cannot join with invalid external IP: ${request.nodeInfo.externalIp}`);
        }
    }
    // Figure out when Q1 is from the latestCycle
    const { startQ1 } = (0, CycleCreator_1.calcIncomingTimes)(latestCycle);
    /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
        info(`Next cycles Q1 start ${startQ1}; Currently ${(0, network_1.shardusGetTime)()}`);
    // Wait until a Q1 then send join request to active nodes
    let untilQ1 = startQ1 - (0, network_1.shardusGetTime)();
    //make untilQ1 in the future if needed
    while (untilQ1 < 0) {
        untilQ1 += latestCycle.duration * 1000;
    }
    let offsetTime = 500;
    //random in between 0 and 2000.  trying to debug why we are rejected from dapps
    offsetTime = Math.floor(Math.random() * Context.config.p2p.randomJoinRequestWait); //TODO make config and set default value back to 2000ms or lower
    /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
        info(`Waiting ${untilQ1} + ${offsetTime} ms for Q1 before sending join...`);
    await utils.sleep(untilQ1 + offsetTime); // Not too early
    info(`joinNetworkV2: submitJoinV2`);
    // send join request
    await Join.submitJoinV2(activeNodes, request);
}
// async function joinNetwork(
//   activeNodes: P2P.P2PTypes.Node[],
//   firstTime: boolean
// ): Promise<{ isFirst: boolean; id: string }> {
//   // Check if you're the first node
//   const isFirst = discoverNetwork(activeNodes)
//   if (isFirst) {
//     // Join your own network and give yourself an ID
//     const id = await Join.firstJoin()
//     // Return id and isFirst
//     return { isFirst, id }
//   }
//   // Remove yourself from activeNodes if you are present in them
//   const ourIdx = activeNodes.findIndex(
//     (node) => node.ip === network.ipInfo.externalIp && node.port === network.ipInfo.externalPort
//   )
//   if (ourIdx > -1) {
//     activeNodes.splice(ourIdx, 1)
//   }
//   // Check joined before trying to join, if not first time
//   if (firstTime === false) {
//     // Check if joined by trying to set our node ID
//     const id = await Join.fetchJoined(activeNodes)
//     if (id) {
//       return { isFirst: false, id }
//     }
//   }
//   // Get latest cycle record from active nodes
//   const latestCycle = await Sync.getNewestCycle(activeNodes)
//   mode = latestCycle.mode || null
//   const publicKey = Context.crypto.getPublicKey()
//   const isReadyToJoin = await Context.shardus.app.isReadyToJoin(latestCycle, publicKey, activeNodes, mode)
//   if (!isReadyToJoin) {
//     // Wait for Context.config.p2p.cycleDuration and try again
//     throw new Error('Node not ready to join')
//   }
//   // Create join request from latest cycle
//   const request = await Join.createJoinRequest(latestCycle.previous)
//   //we can't use allowBogon lag yet because its value is detected later.
//   //it is possible to throw out any invalid IPs at this point
//   if (Context.config.p2p.rejectBogonOutboundJoin || Context.config.p2p.forceBogonFilteringOn) {
//     if (isInvalidIP(request.nodeInfo.externalIp)) {
//       throw new Error(`Fatal: Node cannot join with invalid external IP: ${request.nodeInfo.externalIp}`)
//     }
//   }
//   // Figure out when Q1 is from the latestCycle
//   const { startQ1, startQ4 } = calcIncomingTimes(latestCycle)
//   if (logFlags.important_as_fatal) info(`Next cycles Q1 start ${startQ1}; Currently ${shardusGetTime()}`)
//   // create the Promise that we will `await` to wait for the 'accepted' event,
//   // in case of Join v2. this registers the listener ahead of time
//   const trigger = acceptedTrigger()
//   // only submit join requests if we are using the old protocol or if we have not yet successfully submitted a join request
//   if (!Context.config.p2p.useJoinProtocolV2 || !Join.getHasSubmittedJoinRequest()) {
//     // Wait until a Q1 then send join request to active nodes
//     let untilQ1 = startQ1 - shardusGetTime()
//     while (untilQ1 < 0) {
//       untilQ1 += latestCycle.duration * 1000
//     }
//     if (logFlags.important_as_fatal) info(`Waiting ${untilQ1 + 500} ms for Q1 before sending join...`)
//     await utils.sleep(untilQ1 + 500) // Not too early
//     await Join.submitJoin(activeNodes, request)
//   }
//   if (Context.config.p2p.useJoinProtocolV2) {
//     // if using join protocol v2, simply wait for the 'accepted' event to fire
//     await trigger
//     // then, we can fetch the id from the network and return
//     const id = await Join.fetchJoined(activeNodes)
//     return { isFirst, id }
//   } else {
//     // otherwise, wait until a Q4 before we loop ..
//     // This is a bit faster than before and should allow nodes to try joining
//     // without skipping a cycle
//     if (logFlags.p2pNonFatal) info('Waiting approx. one cycle then checking again...')
//     let untilQ4 = startQ4 - shardusGetTime()
//     while (untilQ4 < 0) {
//       untilQ4 += latestCycle.duration * 1000
//     }
//     await utils.sleep(untilQ4 + 500)
//   }
//   return {
//     isFirst: undefined,
//     id: undefined,
//   }
// }
async function syncCycleChain(selfId, shardus) {
    // You're already synced if you're first
    if (exports.isFirst) {
        // If you're the first node in a restart network, you only need to sync the network generated tx list
        if (exports.isRestartNetwork) {
            // retry as needed to get the TX list
            for (let i = 0; i < 100; i++) {
                try {
                    nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: syncTxListFromArchiver: getting tx list from archiver`);
                    await ServiceQueue.syncTxListFromArchiver();
                    nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: syncTxListFromArchiver: got tx list from archiver: ${i}`);
                    break;
                }
                catch (e) {
                    nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: syncTxListFromArchiver: getting tx list from archiver failed:${i}`);
                    info(`syncCycleChain: syncTxListFromArchiver: error getting tx list from archiver ${e.message}`);
                    await utils.sleep(1000);
                }
            }
        }
        return;
    }
    let synced = false;
    while (!synced) {
        // Once joined, sync to the network
        try {
            if (logger_1.logFlags.p2pNonFatal)
                info('Getting activeNodes from archiver to sync to network...');
            const activeNodes = await contactArchiver('syncCycleChain');
            // Remove yourself from activeNodes if you are present in them
            const ourIdx = activeNodes.findIndex(nodeMatch);
            if (ourIdx > -1) {
                activeNodes.splice(ourIdx, 1);
            }
            if (logger_1.logFlags.p2pNonFatal)
                info('Attempting to sync to network...');
            if (Context.config.p2p.useSyncProtocolV2) {
                // attempt syncing with the v2 protocol and handle the result. the first
                // callback will run if the result is `Ok`, the second if it is `Err`
                // TODO this can be very very expensive.  In a local test it was getting called repeatedly due to
                // a local error.  we may need some limits on how many times we try to sync
                await SyncV2.syncV2(activeNodes, shardus).match(() => (synced = true), (err) => {
                    throw err;
                });
            }
            else {
                synced = await Sync.sync(activeNodes);
            }
        }
        catch (err) {
            synced = false;
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: ex: ${err.message}`);
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn('syncCycleChain:', utils.formatErrorMessage(err));
            if (logger_1.logFlags.p2pNonFatal)
                info('Trying again in 2 sec...');
            await utils.sleep(2000);
        }
    }
    //check if the id matches the cycle record info
    await checkNodeId(nodeMatch, selfId);
}
async function checkNodeId(nodeMatch, selfId) {
    const newestCycle = CycleChain.getNewest();
    let node = newestCycle.joinedConsensors.find(nodeMatch);
    // for nodes joining the network in some cases the correct cycle to check is the previous one that is not in the cycle chain of the node
    // query the archiver for the latest cycles if we can't find the node in the current cycle
    if (!node) {
        //check the latest 4 cycles from the archiver
        info('syncCycleChain: checkNodeId: Getting last 4 cycles from archiver check node id');
        let latestCycles = undefined;
        for (let i = 0; i < 30; i++) {
            try {
                const archiver = (0, Utils_1.getRandomAvailableArchiver)();
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: checkNodeId: getting latest cycles from archiver`);
                latestCycles = await getLatestCyclesFromArchiver(6, archiver);
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: checkNodeId: got latest cycles from archiver: ${i}`);
                break;
            }
            catch (e) {
                nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: checkNodeId: getting latest cycles from archiver failed:${i}`);
                info(`syncCycleChain: checkNodeId: error getting latest cycles from archiver ${e.message}`);
                await utils.sleep(1000);
            }
        }
        for (const cycle of latestCycles) {
            node = cycle.joinedConsensors.find(nodeMatch);
            info(`syncCycleChain: checkNodeId: cycle ${cycle.counter} node ${node?.id}`);
            if (node) {
                break;
            }
        }
    }
    if (node == null) {
        info(`syncCycleChain: checkNodeId: checkNodeId node not found in joinedConsensors ${newestCycle.joinedConsensors.length}`);
        /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: checkNodeId: checkNodeId node not found in joinedConsensors for a recent cycle`);
        // throw new Error(`checkNodeId node not found in joinedConsensors for a recent cycle`)
        throw new Error(idErrorMessage); //keeping the same basic error as it is used in control flow, and do not have time to redo that part
    }
    if (node.id !== selfId) {
        info(`syncCycleChain: checkNodeId: ${idErrorMessage} ${node.id} != ${selfId}`);
        /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `syncCycleChain: checkNodeId: ${idErrorMessage} ${node.id} != ${selfId}`);
        throw new Error(idErrorMessage);
    }
    if (logger_1.logFlags.p2pNonFatal)
        info('Node passed id check');
}
async function contactArchiver(dbgContex) {
    const maxRetries = 10;
    let retry = maxRetries;
    const failArchivers = [];
    let archiver;
    let activeNodesSigned;
    info(`contactArchiver: enter archivers:${(0, Archivers_1.getNumArchivers)()}`);
    while (retry > 0) {
        try {
            retry--;
            archiver = (0, Utils_1.getRandomAvailableArchiver)();
            info(`contactArchiver: communicate with:${archiver?.ip}`);
            if (!failArchivers.includes(archiver.ip + ':' + archiver.port)) {
                failArchivers.push(archiver.ip + ':' + archiver.port);
            }
            activeNodesSigned = await getActiveNodesFromArchiver(archiver);
            if (activeNodesSigned == null || activeNodesSigned.nodeList == null || activeNodesSigned.nodeList.length === 0) {
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `contactArchiver: no nodes in nodelist yet. ${dbgContex}`, 1);
                info(`contactArchiver: no nodes in nodelist yet, or seedlist null ${lib_types_2.Utils.safeStringify(activeNodesSigned)}`);
                await utils.sleep(1000); // no nodes in nodelist yet so please take a breather. would be smarter to ask each archiver only once but
                // but do not want to refactor that much right now
                if (retry === 1) {
                    /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `contactArchiver: no nodes in nodelist yet out of retries. ${dbgContex}`, 1);
                    throw Error(`contactArchiver: nodelist null or empty after ${maxRetries} retries:`);
                }
                continue;
            }
            if (!Context.crypto.verify(activeNodesSigned, archiver.publicKey)) {
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `contactArchiver: verify failed. ${dbgContex}`, 1);
                info(`contactArchiver:  seedlist failed verification ${lib_types_2.Utils.safeStringify(activeNodesSigned)}`);
                throw Error(`Fatal: _getSeedNodes seed list was not signed by archiver!. Archiver: ${archiver.ip}:${archiver.port}, signature: ${activeNodesSigned.sign}`);
            }
            break; // To stop this loop if it gets the response without failing
        }
        catch (e) {
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `contactArchiver: out of retries. ${dbgContex}`, 1);
            info(`contactArchiver: failed ${archiver.ip} ${utils.formatErrorMessage(e)} retry:${retry}`);
            if (retry === 1) {
                throw Error(`Could not get seed list from seed node server ${failArchivers} after ${maxRetries} retries:`);
            }
        }
        //move retry-- to top so that we dont retry forever!
        //retry--
    }
    info(`contactArchiver: passed ${archiver.ip} retry:${retry}`);
    info(`contactArchiver: activeNodesSigned:${lib_types_2.Utils.safeStringify(activeNodesSigned?.joinRequest)} restartCycleRecord:${lib_types_2.Utils.safeStringify(activeNodesSigned?.restartCycleRecord)}`);
    const joinRequest = activeNodesSigned.joinRequest;
    if (joinRequest) {
        const accepted = Archivers.addArchiverJoinRequest(joinRequest);
        if (accepted.success === false) {
            throw Error('Fatal: _getSeedNodes archivers join request not accepted by us!');
        }
        if (Context.config.p2p.experimentalSnapshot && Context.config.features.archiverDataSubscriptionsUpdate) {
            const firstNodeDataRequest = {
                dataRequestCycle: activeNodesSigned.dataRequestCycle,
            };
            Archivers.addDataRecipient(joinRequest.nodeInfo, firstNodeDataRequest);
            // Using this flag due to isFirst check is not working as expected yet in the first consensor-archiver connection establishment
            exports.allowConnectionToFirstNode = true;
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `contactArchiver: got valid nodelist ${dbgContex} joinRequest count:${activeNodesSigned.nodeList.length}`, 1);
            return activeNodesSigned.nodeList;
        }
    }
    const restartCycleRecord = activeNodesSigned.restartCycleRecord;
    if (restartCycleRecord) {
        // The archiver is sending a cycle record with shutdown mode from previous network
        // TODO - Make sure the cycle record is valid
        // use minNodes since we are below baseline for safety and looking to upsize to get into processing mode
        restartCycleRecord.desired = Context.config.p2p.minNodes;
        restartCycleRecord.duration = Context.config.p2p.cycleDuration;
        CycleChain.prepend(restartCycleRecord);
        setRestartNetwork(true);
        if (Context.config.p2p.experimentalSnapshot && Context.config.features.archiverDataSubscriptionsUpdate) {
            const firstNodeDataRequest = {
                dataRequestCycle: activeNodesSigned.dataRequestCycle,
            };
            // The list of archivers are not added in the shutdown mode cycle record yet. Thus, using the contacted archiver to add the data recipient
            Archivers.addDataRecipient(archiver, firstNodeDataRequest, firstNodeDataRequest.dataRequestCycle);
            for (const archiverInfo of restartCycleRecord.archiversAtShutdown) {
                Archivers.archivers.set(archiverInfo.publicKey, archiverInfo);
            }
            // Using this flag due to isFirst check is not working as expected yet in the first consensor-archiver connection establishment
            exports.allowConnectionToFirstNode = true;
            /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `contactArchiver: got valid nodelist ${dbgContex} restartCycleRecord count:${activeNodesSigned.nodeList.length}`, 1);
            return activeNodesSigned.nodeList;
        }
    }
    const dataRequestCycle = activeNodesSigned.dataRequestCycle;
    const dataRequestStateMetaData = activeNodesSigned.dataRequestStateMetaData;
    const dataRequest = [];
    if (dataRequestCycle) {
        dataRequest.push(dataRequestCycle);
    }
    if (dataRequestStateMetaData) {
        dataRequest.push(dataRequestStateMetaData);
    }
    if (joinRequest && dataRequest.length > 0) {
        Archivers.addDataRecipient(joinRequest.nodeInfo, dataRequest);
    }
    /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `contactArchiver: got valid nodelist ${dbgContex}  joinRequest:${joinRequest != null} restartCycleRecord:${restartCycleRecord != null} count:${activeNodesSigned.nodeList.length} `, 1);
    return activeNodesSigned.nodeList;
}
exports.contactArchiver = contactArchiver;
function discoverNetwork(seedNodes) {
    // Check if we are first seed node
    const isFirstSeed = checkIfFirstSeedNode(seedNodes);
    if (!isFirstSeed) {
        if (logger_1.logFlags.p2pNonFatal)
            info('You are not the first seed node...');
        return false;
    }
    if (logger_1.logFlags.p2pNonFatal)
        info('You are the first seed node!');
    return true;
}
function checkIfFirstSeedNode(seedNodes) {
    if (!seedNodes.length)
        throw new Error('Fatal: No seed nodes in seed list!');
    if (seedNodes.length > 1)
        return false;
    const seed = seedNodes[0];
    if (network.ipInfo.externalIp === seed.ip && network.ipInfo.externalPort === seed.port) {
        return true;
    }
    return false;
}
async function getActiveNodesFromArchiver(archiver) {
    const nodeInfo = getPublicNodeInfo();
    const seedListResult = await Archivers.postToArchiver(archiver, 'nodelist', Context.crypto.sign({
        nodeInfo,
    }), 10000);
    if (seedListResult.isErr()) {
        const e = seedListResult.error;
        const nodeListUrl = `http://${archiver.ip}:${archiver.port}/nodelist`;
        /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countRareEvent('archiver_nodelist', 'Could not get seed list from seed node server 1');
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            warn(`Could not get seed list from seed node server 1 ${nodeListUrl}: ` + e.message);
        throw Error(e.message);
    }
    const seedListSigned = seedListResult.value;
    if (logger_1.logFlags.p2pNonFatal)
        info(`Got signed seed list: ${lib_types_2.Utils.safeStringify(seedListSigned)}`);
    return seedListSigned;
}
async function getFullNodesFromArchiver(archiver = Context.config.p2p.existingArchivers[0]) {
    const fullNodeListResult = await Archivers.getFromArchiver(archiver, 'full-nodelist');
    if (fullNodeListResult.isErr()) {
        const nodeListUrl = `http://${archiver.ip}:${archiver.port}/full-nodelist`;
        throw Error(`Fatal: Could not get seed list from seed node server ${nodeListUrl}: ` + fullNodeListResult.error.message);
    }
    const fullNodeList = fullNodeListResult.value;
    if (logger_1.logFlags.p2pNonFatal)
        info(`Got signed full node list: ${lib_types_2.Utils.safeStringify(fullNodeList)}`);
    return fullNodeList;
}
exports.getFullNodesFromArchiver = getFullNodesFromArchiver;
async function getLatestCyclesFromArchiver(cycleCounter, archiver = Context.config.p2p.existingArchivers[0]) {
    const endpoint = `cycleinfo/${cycleCounter}`;
    const cyclesListResult = await Archivers.getFromArchiver(archiver, endpoint, undefined, 10000);
    if (cyclesListResult.isErr()) {
        const nodeListUrl = `http://${archiver.ip}:${archiver.port}/${endpoint}`;
        throw Error(`Fatal: Could not get latest cycles from ${nodeListUrl}: ` + cyclesListResult.error.message);
    }
    return cyclesListResult.value.cycleInfo;
}
exports.getLatestCyclesFromArchiver = getLatestCyclesFromArchiver;
function getPublicNodeInfo(reportIntermediateStatus = false) {
    const publicKey = Context.crypto.getPublicKey();
    const curvePublicKey = Context.crypto.convertPublicKeyToCurve(publicKey);
    const status = { status: getNodeStatus(publicKey, reportIntermediateStatus) };
    const nodeInfo = Object.assign({ id: exports.id, publicKey, curvePublicKey }, network.ipInfo, status);
    return nodeInfo;
}
exports.getPublicNodeInfo = getPublicNodeInfo;
function getNodeStatus(pubKey, reportIntermediateStatus = false) {
    const current = NodeList.byPubKey;
    if (current.get(pubKey))
        return current.get(pubKey).status;
    return reportIntermediateStatus ? state : null;
}
function getThisNodeInfo() {
    const { externalIp, externalPort, internalIp, internalPort } = network.ipInfo;
    const publicKey = Context.crypto.getPublicKey();
    // TODO: Change this to actual selectable address
    const address = publicKey;
    const joinRequestTimestamp = utils.getTime('s');
    const activeTimestamp = 0;
    const activeCycle = 0;
    const syncingTimestamp = 0;
    const readyTimestamp = 0;
    const nodeInfo = {
        publicKey,
        externalIp,
        externalPort,
        internalIp,
        internalPort,
        address,
        joinRequestTimestamp,
        activeTimestamp,
        activeCycle,
        syncingTimestamp,
        readyTimestamp,
    };
    if (logger_1.logFlags.p2pNonFatal)
        info(`Node info of this node: ${lib_types_2.Utils.safeStringify(nodeInfo)}`);
    return nodeInfo;
}
exports.getThisNodeInfo = getThisNodeInfo;
function setActive() {
    exports.isActive = true;
}
exports.setActive = setActive;
function setp2pIgnoreJoinRequests(value) {
    exports.p2pIgnoreJoinRequests = value;
}
exports.setp2pIgnoreJoinRequests = setp2pIgnoreJoinRequests;
function info(...msg) {
    const entry = `Self: ${msg.join(' ')}`;
    p2pLogger.info(entry);
}
function warn(...msg) {
    const entry = `Self: ${msg.join(' ')}`;
    p2pLogger.warn(entry);
}
// debug functions
function setIsFirst(val) {
    exports.isFirst = val;
}
exports.setIsFirst = setIsFirst;
function getIsFirst() {
    return exports.isFirst;
}
exports.getIsFirst = getIsFirst;
function setRestartNetwork(val) {
    info(`setRestartNetwork: ${val}`);
    nestedCounters_1.nestedCountersInstance.countEvent('restore', `setRestartNetwork: ${val} ${(0, network_1.shardusGetTime)()}`);
    exports.isRestartNetwork = val;
}
exports.setRestartNetwork = setRestartNetwork;
function acceptedTrigger() {
    return new Promise((resolve) => {
        Acceptance.getEventEmitter().once('accepted', () => {
            resolve();
        });
    });
}
/**
 * Wait for currentQuarter to equal 1 and q1SendRequests to be true
 *
 * q1SendRequests is a flag that has a slight delay at the start of q1 to make sure
 * that requests are not impacted by timestamp variation which could cause half of the network
 * to think it is still Q4
 * @returns
 */
function waitForQ1SendRequests() {
    return new Promise((resolve) => {
        const intervalId = setInterval(() => {
            if (CycleCreator_2.currentQuarter === 1 && CycleCreator_1.q1SendRequests === true) {
                clearInterval(intervalId);
                resolve();
            }
        }, Context.config.p2p.secondsToCheckForQ1);
    });
}
exports.waitForQ1SendRequests = waitForQ1SendRequests;
//# sourceMappingURL=Self.js.map