"use strict";
/**
 * `v2` houses some new state or functions introduced with Join Protocol v2.
 * TODO: Rename this module later?
 */
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.shutdown = exports.debugDumpJoinRequestList = exports.isOnStandbyList = exports.updateStandbyRefreshCounter = exports.getStandbyNodesInfoMap = exports.getLastHashedStandbyList = exports.getTxListHash = exports.getStandbyListHash = exports.computeNewStandbyListHash = exports.getSortedStandbyJoinRequests = exports.addStandbyJoinRequests = exports.drainNewJoinRequests = exports.saveJoinRequest = exports.deleteStandbyNodeFromMap = exports.init = exports.standbyNodesRefresh = exports.standbyNodesInfoHashes = exports.standbyNodesInfo = void 0;
const Context_1 = require("../../Context");
const CycleChain = __importStar(require("../../CycleChain"));
const Self = __importStar(require("../../Self"));
const rfdc_1 = __importDefault(require("rfdc"));
const select_1 = require("./select");
const Utils_1 = require("../../Utils");
const unjoin_1 = require("./unjoin");
const neverthrow_1 = require("neverthrow");
const acceptance_1 = require("./acceptance");
const stringifyReduce_1 = require("../../../utils/functions/stringifyReduce");
const logger_1 = require("../../../logger");
const lib_types_1 = require("@shardeum-foundation/lib-types");
const clone = (0, rfdc_1.default)();
/** The list of nodes that are currently on standby. */
exports.standbyNodesInfo = new Map();
exports.standbyNodesInfoHashes = new Map();
/** This list is a map of standby node public keys to refreshed time in seconds */
exports.standbyNodesRefresh = new Map();
/**
 * New join requests received during the node's current cycle. This list is
 * "drained" when the cycle is digested. Its entries are added to `standbyNodeList` as part of cycle...
 * digestion. appetizing!
 */
let newJoinRequests = [];
function init() {
    console.log('initializing join protocol v2');
    // set up event listeners for cycle quarters
    Self.emitter.on('cycle_q1_start', () => {
        if (Context_1.config.p2p.useJoinProtocolV2) {
            //TODO clean out the accepted route or is it still useful?
            //accepted endpoint does not return any more
            // The accepted flow is deprecated
            // notifyNewestJoinedConsensors().catch((e) => {
            //   console.error('failed to notify selected nodes:', e)
            // })
        }
    });
    Self.emitter.on('cycle_q2_start', () => {
        if (Context_1.config.p2p.useJoinProtocolV2)
            (0, select_1.executeNodeSelection)();
    });
}
exports.init = init;
function addJoinRequestToStandbyMap(joinRequest) {
    //this is the same as before.  add the join request to the map
    exports.standbyNodesInfo.set(joinRequest.nodeInfo.publicKey, joinRequest);
    //here we add the hash of the joinrequest to a different map
    exports.standbyNodesInfoHashes.set(joinRequest.nodeInfo.publicKey, Context_1.crypto.hash(joinRequest));
}
function deleteStandbyNodeFromMap(key) {
    if (exports.standbyNodesInfo.has(key)) {
        exports.standbyNodesInfo.delete(key);
        exports.standbyNodesInfoHashes.delete(key);
        return true;
    }
    return false;
}
exports.deleteStandbyNodeFromMap = deleteStandbyNodeFromMap;
/**
 * Pushes the join request onto the list of new join requests. Its node's info
 * will be added to the standby node list at the end of the cycle during cycle
 * digestion.
 *
 * @param joinRequest The join request to save.
 * @param persistImmediately If true, the node will be added to the standby node list immediately. This can be used for the first node in the network.
 */
function saveJoinRequest(joinRequest, persistImmediately = false) {
    if (logger_1.logFlags.verbose)
        console.log('saving join request:', joinRequest);
    // if first node, add to standby list immediately
    if (persistImmediately) {
        addJoinRequestToStandbyMap(joinRequest);
        return;
    }
    newJoinRequests.push(joinRequest);
}
exports.saveJoinRequest = saveJoinRequest;
/**
 * Returns the list of new standby join requests and empties the list.
 */
function drainNewJoinRequests() {
    if (logger_1.logFlags.verbose)
        console.log('draining new standby info:', newJoinRequests);
    const tmp = newJoinRequests;
    newJoinRequests = [];
    return tmp;
}
exports.drainNewJoinRequests = drainNewJoinRequests;
/**
 * Adds nodes to the standby node list.
 */
function addStandbyJoinRequests(nodes, logErrors = false) {
    if (logger_1.logFlags.verbose)
        console.log('adding standby nodes:', nodes);
    //TODO proper input validation
    for (const joinRequest of nodes) {
        if (getStandbyNodesInfoMap().size >= Context_1.config.p2p.maxStandbyCount) {
            /* prettier-ignore */ if (logErrors && logger_1.logFlags.important_as_fatal)
                console.error('standby nodes list is max capacity reached. Cannot add more nodes.');
            return;
        }
        if (joinRequest == null) {
            /* prettier-ignore */ if (logErrors && logger_1.logFlags.important_as_fatal)
                console.error('null node in standby list');
            continue;
        }
        if (joinRequest.nodeInfo == null) {
            /* prettier-ignore */ if (logErrors && logger_1.logFlags.important_as_fatal)
                console.error('null node.nodeInfo in standby list: ' + lib_types_1.Utils.safeStringify(joinRequest));
            continue;
        }
        addJoinRequestToStandbyMap(joinRequest);
    }
}
exports.addStandbyJoinRequests = addStandbyJoinRequests;
let lastHashedList = [];
/**
 * Returns the list of standby nodes, sorted by their public keys.
 */
function getSortedStandbyJoinRequests() {
    if (logger_1.logFlags.verbose)
        console.log('getting sorted standby node list');
    return [...exports.standbyNodesInfo.values()].sort((a, b) => 
    // using mathematical comparison in case localeCompare is inconsistent.
    // we will use a simple ternary statement for this that doesn't account for
    // equality. this should be fine as no two public keys should be the same.
    a.nodeInfo.publicKey > b.nodeInfo.publicKey ? 1 : -1);
}
exports.getSortedStandbyJoinRequests = getSortedStandbyJoinRequests;
/** Calculates and returns a hash based on the list of standby nodes, sorted by public key. This will also update the recorded `lastHashedList` of nodes, which can be retrieved via `getLastHashedStandbyList`. */
function computeNewStandbyListHash() {
    if (Context_1.config.p2p.standbyListFastHash) {
        //this field must be udpated as it is used by other functions
        lastHashedList = Array.from(getSortedStandbyJoinRequests());
        //sort hashes by value.  could sort by ID, but this is a bit faster
        const hashes = Array.from(exports.standbyNodesInfoHashes.values());
        hashes.sort();
        const hash = Context_1.crypto.hash(hashes);
        return hash;
    }
    // set the lastHashedList to the current list by pubkey, then hash.
    lastHashedList = Array.from(getSortedStandbyJoinRequests());
    const hash = Context_1.crypto.hash(lastHashedList);
    if (logger_1.logFlags.verbose) {
        console.log(`computing new standby list hash: ${hash} number of nodes: ${lastHashedList.length}`);
        //use map to convert lastHashedList to a list of public keys
        const publicKeyList = lastHashedList.map((node) => node.nodeInfo.publicKey);
        console.log(`{standby_public_key_list: ${(0, stringifyReduce_1.stringifyReduce)(publicKeyList)}}`);
    }
    return hash;
}
exports.computeNewStandbyListHash = computeNewStandbyListHash;
/**
 * Returns the standby node list hash from the last complete cycle, if available. If you
 * want to compute a new hash instead, use `computeNewStandbyListHash`.
 */
function getStandbyListHash() {
    if (logger_1.logFlags.verbose)
        console.log('getting standby list hash');
    return CycleChain.newest?.standbyNodeListHash;
}
exports.getStandbyListHash = getStandbyListHash;
/**
 * Returns the txList hash from the last complete cycle, if available
 */
function getTxListHash() {
    if (logger_1.logFlags.verbose)
        console.log('getting tx list hash');
    return CycleChain.newest?.txlisthash;
}
exports.getTxListHash = getTxListHash;
/** Returns the last list of standby information that had its hash computed. */
function getLastHashedStandbyList() {
    if (logger_1.logFlags.verbose)
        console.log('getting last hashed standby list');
    return lastHashedList;
}
exports.getLastHashedStandbyList = getLastHashedStandbyList;
/** Returns the map of standby information. */
function getStandbyNodesInfoMap() {
    if (logger_1.logFlags.verbose)
        console.log('getting standby nodes info map');
    return exports.standbyNodesInfo;
}
exports.getStandbyNodesInfoMap = getStandbyNodesInfoMap;
function updateStandbyRefreshCounter(updatedJoinRequest) {
    const originalJoinRequest = exports.standbyNodesInfo.get(updatedJoinRequest.nodeInfo.publicKey);
    // TODO: Ask pod1 if we need to updated lastHashedList here
    if (areJoinRequestsIdenticalExceptRefreshCounter(originalJoinRequest, updatedJoinRequest)) {
        if (exports.standbyNodesRefresh.has(updatedJoinRequest.nodeInfo.publicKey))
            exports.standbyNodesInfo.set(updatedJoinRequest.nodeInfo.publicKey, updatedJoinRequest);
        if (exports.standbyNodesInfoHashes.has(updatedJoinRequest.nodeInfo.publicKey))
            exports.standbyNodesInfoHashes.set(updatedJoinRequest.nodeInfo.publicKey, Context_1.crypto.hash(updatedJoinRequest));
    }
    else {
        console.error('Trying to update Join request fields other than refreshedCounter. Ignoring the update.');
    }
}
exports.updateStandbyRefreshCounter = updateStandbyRefreshCounter;
function areJoinRequestsIdenticalExceptRefreshCounter(original, updated) {
    // Clone the objects to avoid mutating the original ones
    const originalCopy = lib_types_1.Utils.safeJsonParse(lib_types_1.Utils.safeStringify(original));
    const updatedCopy = lib_types_1.Utils.safeJsonParse(lib_types_1.Utils.safeStringify(updated));
    delete originalCopy.refreshedCounter;
    delete updatedCopy.refreshedCounter;
    // Compare the two objects without the refreshedCounter field
    return deepEqual(originalCopy, updatedCopy);
}
function deepEqual(obj1, obj2) {
    if (obj1 === obj2) {
        return true; // Identical references or primitive values
    }
    if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
        return false; // One of them is not an object or is null
    }
    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);
    if (keys1.length !== keys2.length) {
        return false; // Different number of properties
    }
    for (const key of keys1) {
        if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
            return false; // Different keys or values
        }
    }
    return true; // Everything matched
}
function isOnStandbyList(publicKey) {
    if (exports.standbyNodesInfo.has(publicKey)) {
        return true;
    }
    else {
        return false;
    }
}
exports.isOnStandbyList = isOnStandbyList;
function debugDumpJoinRequestList(list, message) {
    list.sort((a, b) => (a.nodeInfo.publicKey > b.nodeInfo.publicKey ? 1 : -1));
    //let getSortedStandbyNodeList = JoinV2.getSortedStandbyJoinRequests()
    const result = list.map((node) => ({
        pubKey: node.nodeInfo.publicKey,
        //ip: node.nodeInfo.externalIp,
        port: node.nodeInfo.externalPort,
    }));
    console.log(`Standby list:${list.length} `, message, (0, stringifyReduce_1.stringifyReduce)(result));
}
exports.debugDumpJoinRequestList = debugDumpJoinRequestList;
/**
 * Handles unjoining from the network.
 */
async function shutdown() {
    // if not using join protocol v2, unjoining isn't needed
    if (!Context_1.config.p2p.useJoinProtocolV2)
        return;
    const unjoinResult = await neverthrow_1.ResultAsync.fromPromise((0, Utils_1.attempt)(async () => (0, unjoin_1.submitUnjoin)(), {
        delay: 1000,
        maxRetries: 5,
    }), (err) => err).andThen((result) => result);
    // reset acceptance state
    (0, acceptance_1.reset)();
    if (unjoinResult.isErr()) {
        console.error('Failed send unjoin request:', unjoinResult.error);
    }
    else {
        if (logger_1.logFlags.verbose)
            console.log('Unjoin request sent');
    }
}
exports.shutdown = shutdown;
//# sourceMappingURL=index.js.map