"use strict";
/**
 * [NOTE] [AS] To break this modules dependency on Wrapper, search for
 * all references to 'p2p' imported from Wrapper and replace them with
 * a direct call to whatever Wrapper.p2p was calling
 */
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.__clearForTest = exports.attemptCleanup = exports.processReceipt = exports.makeReceipt = exports.getGlobalTxReceipt = exports.awaitLocalReceiptInitiation = exports.createMakeReceiptHandle = exports.setGlobal = exports.init = void 0;
const logger_1 = require("../logger");
const shardFunctions_1 = __importDefault(require("../state-manager/shardFunctions"));
const utils = __importStar(require("../utils"));
const Comms = __importStar(require("./Comms"));
const Context = __importStar(require("./Context"));
const NodeList = __importStar(require("./NodeList"));
const Self = __importStar(require("./Self"));
const profiler_1 = require("../utils/profiler");
const network_1 = require("../network");
const InternalRouteEnum_1 = require("../types/enum/InternalRouteEnum");
const nestedCounters_1 = require("../utils/nestedCounters");
const RequestErrorEnum_1 = require("../types/enum/RequestErrorEnum");
const Helpers_1 = require("../types/Helpers");
const TypeIdentifierEnum_1 = require("../types/enum/TypeIdentifierEnum");
const MakeReceipReq_1 = require("../types/MakeReceipReq");
const lib_types_1 = require("@shardeum-foundation/lib-types");
const promises_1 = require("../utils/functions/promises");
/** ROUTES */
// [TODO] - need to add validattion of types to the routes
// const makeReceiptRoute: P2P.P2PTypes.Route<
//   P2P.P2PTypes.InternalHandler<P2P.GlobalAccountsTypes.SignedSetGlobalTx, unknown, string>
// > = {
//   name: 'make-receipt',
//   handler: (payload, respond, sender) => {
//     profilerInstance.scopedProfileSectionStart('make-receipt')
//     try {
//       makeReceipt(payload, sender)
//     } finally {
//       profilerInstance.scopedProfileSectionEnd('make-receipt')
//     }
//   },
// }
const makeReceiptBinaryHandler = {
    name: InternalRouteEnum_1.InternalRouteEnum.binary_make_receipt,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    handler: async (payload, respond, header, sign) => {
        const route = InternalRouteEnum_1.InternalRouteEnum.binary_make_receipt;
        nestedCounters_1.nestedCountersInstance.countEvent('internal', route);
        profiler_1.profilerInstance.scopedProfileSectionStart(route);
        const errorHandler = (errorType, opts) => (0, Helpers_1.requestErrorHandler)(route, errorType, header, opts);
        try {
            const requestStream = (0, Helpers_1.getStreamWithTypeCheck)(payload, TypeIdentifierEnum_1.TypeIdentifierEnum.cMakeReceiptReq);
            if (!requestStream) {
                return errorHandler(RequestErrorEnum_1.RequestErrorEnum.InvalidRequest);
            }
            const req = (0, MakeReceipReq_1.deserializeMakeReceiptReq)(requestStream);
            makeReceipt(req, header.sender_id);
        }
        catch (e) {
            nestedCounters_1.nestedCountersInstance.countEvent('internal', `${route}-exception`);
            /* prettier-ignore */ if (logger_1.logFlags.error)
                Context.logger.getLogger('p2p').error(`${route}: Exception executing request: ${utils.errorToStringFull(e)}`);
        }
        finally {
            profiler_1.profilerInstance.scopedProfileSectionEnd(route);
        }
    },
};
const setGlobalGossipRoute = {
    name: 'set-global',
    handler: (payload, sender, tracker) => {
        profiler_1.profilerInstance.scopedProfileSectionStart('set-global');
        try {
            if (validateReceipt(payload) === false)
                return;
            if (processReceipt(payload) === false)
                return;
            /** [TODO] [AS] Replace with Comms.sendGossip() */
            // p2p.sendGossipIn('set-global', payload)
            Comms.sendGossip('set-global', payload, tracker, sender, NodeList.byIdOrder, false);
        }
        finally {
            profiler_1.profilerInstance.scopedProfileSectionEnd('set-global');
        }
    },
};
/** STATE */
let lastClean = 0;
const receipts = new Map();
const trackers = new Map();
const localReceiptInitiationPromises = new Map();
const localReceiptResolvers = new Map();
/** FUNCTIONS */
function init() {
    // Register routes
    // Comms.registerInternal(makeReceiptRoute.name, makeReceiptRoute.handler)
    Comms.registerInternalBinary(makeReceiptBinaryHandler.name, makeReceiptBinaryHandler.handler);
    Comms.registerGossipHandler(setGlobalGossipRoute.name, setGlobalGossipRoute.handler);
}
exports.init = init;
/**
 * Sets a global account by creating and broadcasting a transaction.
 *
 * This function performs the following steps:
 * 1. Logs the initiation of the setGlobal process if console logging is enabled.
 * 2. Checks if the current node is active. If not, logs a message and returns.
 * 3. Creates a transaction (`tx`) for setting a global account with the provided parameters.
 * 4. Signs the transaction (`signedTx`).
 * 5. Checks if the state manager is available and logs the state.
 * 6. Determines the nodes to which the transaction will be broadcasted.
 * 7. Finds the home node and consensus group for the current node.
 * 8. Removes the current node from the consensus group.
 * 9. Sets up a timeout and receipt handling mechanism to process receipts or handle timeouts.
 * 10. Broadcasts the transaction to the consensus group to trigger the creation of a receipt collection.
 *
 * @param address - The address of the global account to set.
 * @param addressHash - The hash of the address.
 * @param value - The value to set for the global account.
 * @param when - The timestamp or condition when the value should be set.
 * @param source - The source node initiating the transaction.
 * @param afterStateHash - The state hash after the transaction is applied.
 */
function setGlobal(address, addressHash, value, when, source, afterStateHash) {
    if (logger_1.logFlags.console)
        console.log(`SETGLOBAL: WE ARE: ${Self.id.substring(0, 5)}`);
    // Only do this if you're active
    if (!Self.isActive) {
        if (logger_1.logFlags.console)
            console.log('setGlobal: Not active yet');
        return;
    }
    // Create a tx for setting a global account
    const txHash = Context.shardus.app.calculateTxId(value);
    const tx = {
        address,
        addressHash,
        value,
        when,
        source,
        txId: txHash,
        afterStateHash,
    };
    // Sign tx
    const signedTx = Context.crypto.sign(tx);
    if (Context.stateManager === null) {
        if (logger_1.logFlags.console)
            console.log('setGlobal: stateManager == null');
    }
    else {
        if (logger_1.logFlags.console)
            console.log('setGlobal: stateManager != null');
    }
    // Get the nodes that tx will be broadcasted to
    if (!Context.stateManager.currentCycleShardData) {
        if (logger_1.logFlags.console)
            console.log('stateManager.currentCycleShardData == null');
        return;
    }
    const homeNode = shardFunctions_1.default.findHomeNode(Context.stateManager.currentCycleShardData.shardGlobals, source, Context.stateManager.currentCycleShardData.parititionShardDataMap);
    const consensusGroup = [...homeNode.consensusNodeForOurNodeFull];
    if (logger_1.logFlags.console)
        console.log(`SETGLOBAL: CONSENSUS_GROUP: ${consensusGroup.map((n) => n.id.substring(0, 5))}`);
    /** [TODO] [AS] Replace p2p.id with Self.id */
    // const ourIdx = consensusGroup.findIndex(node => node.id === p2p.id)
    const ourIdx = consensusGroup.findIndex((node) => node.id === Self.id);
    if (ourIdx === -1)
        return; // Return if we're not in the consensusGroup
    consensusGroup.splice(ourIdx, 1); // Remove ourself from consensusGroup
    // Using makeReceipt API
    // Get ready to process receipts into a receiptCollection, or to timeout
    const timeout = 10000; // [TODO] adjust this to stop early timeouts
    const handle = createMakeReceiptHandle(txHash);
    const onTimeout = () => {
        if (logger_1.logFlags.console)
            console.log(`SETGLOBAL: TIMED OUT: ${txHash}`);
        /** [TODO] [AS] Replace with Self.emitter.removeListener */
        // p2p.removeListener(handle, onReceipt)
        Self.emitter.removeListener(handle, onReceipt);
        attemptCleanup();
    };
    const timer = setTimeout(onTimeout, timeout);
    const onReceipt = (receipt) => {
        if (logger_1.logFlags.console)
            console.log(`SETGLOBAL: GOT RECEIPT: ${txHash} ${lib_types_1.Utils.safeStringify(receipt)}`);
        clearTimeout(timer);
        // Gossip receipt to every node in network to apply to global account
        if (processReceipt(receipt) === false)
            return;
        /** [TODO] [AS] Replace with Comms.sendGossip */
        // p2p.sendGossipIn('set-global', receipt)
        (0, promises_1.fireAndForget)(() => Comms.sendGossip('set-global', receipt, '', null, NodeList.byIdOrder, true));
    };
    /** [TODO] [AS] Replace with Self.emitter.on() */
    // p2p.on(handle, onReceipt)
    Self.emitter.on(handle, onReceipt);
    // Broadcast tx to /makeReceipt of all nodes in source consensus group to trigger creation of receiptCollection
    /** [TODO] [AS] Replace with Self.id */
    // makeReceipt(signedTx, p2p.id) // Need this because internalRoute handler ignores messages from ourselves
    makeReceipt(signedTx, Self.id); // Need this because internalRoute handler ignores messages from ourselves
    /** [TODO] [AS] Replace with Comms.tell */
    // p2p.tell(consensusGroup, 'make-receipt', signedTx)
    // if (Context.config.p2p.useBinarySerializedEndpoints && Context.config.p2p.makeReceiptBinary) {
    const request = signedTx;
    (0, promises_1.fireAndForget)(() => Comms.tellBinary(consensusGroup, InternalRouteEnum_1.InternalRouteEnum.binary_make_receipt, request, MakeReceipReq_1.serializeMakeReceiptReq, {}));
    // } else {
    // Comms.tell(consensusGroup, 'make-receipt', signedTx)
    // }
}
exports.setGlobal = setGlobal;
function createMakeReceiptHandle(txHash) {
    return `receipt-${txHash}`;
}
exports.createMakeReceiptHandle = createMakeReceiptHandle;
/**
 * Waits for local receipt initiation to complete before attempting to access the receipt.
 * This function ensures that makeReceipt has completed its critical local setup.
 *
 * @param txHash - The transaction hash to wait for
 * @returns Promise that resolves when the receipt is ready or rejects on timeout/error
 */
async function awaitLocalReceiptInitiation(txHash) {
    const pendingPromise = localReceiptInitiationPromises.get(txHash);
    if (pendingPromise) {
        // Use configurable timeout, with fallback to 15 seconds if not configured
        const timeout = Context.config?.stateManager?.globalAccountsReceiptInitiationTimeout || 15000;
        if (logger_1.logFlags.verbose)
            console.log(`GlobalAccounts: Awaiting local receipt initiation for ${txHash} with timeout: ${timeout}ms`);
        try {
            await Promise.race([
                pendingPromise,
                new Promise((_, reject) => setTimeout(() => reject(new Error(`Receipt initiation timeout for ${txHash} after ${timeout}ms`)), timeout)),
            ]);
            if (logger_1.logFlags.verbose)
                console.log(`GlobalAccounts: Local receipt initiation completed for ${txHash}`);
        }
        catch (error) {
            if (logger_1.logFlags.error)
                console.error(`GlobalAccounts: Error awaiting receipt initiation for ${txHash}:`, error);
            throw error;
        }
        finally {
            // Clean up the promise after use
            localReceiptInitiationPromises.delete(txHash);
        }
    }
    else {
        if (logger_1.logFlags.verbose)
            console.log(`GlobalAccounts: No pending promise for ${txHash}, receipt might already exist`);
    }
}
exports.awaitLocalReceiptInitiation = awaitLocalReceiptInitiation;
async function getGlobalTxReceipt(txHash) {
    // For backward compatibility, still check for pending promises
    await awaitLocalReceiptInitiation(txHash);
    const receipt = receipts.get(txHash);
    if (!receipt)
        return null;
    return {
        signs: receipt.signs,
        tx: receipt.tx,
    };
}
exports.getGlobalTxReceipt = getGlobalTxReceipt;
function makeReceipt(signedTx, sender) {
    const txForHash = { ...signedTx };
    delete txForHash.sign;
    const txHash = Context.shardus.app.calculateTxId(txForHash.value);
    // Early exit and promise rejection if stateManager not ready
    if (!Context.stateManager) {
        if (logger_1.logFlags.console)
            console.log('GlobalAccounts: makeReceipt: stateManager not ready for tx', txHash);
        const promiseEntry = localReceiptResolvers.get(txHash);
        if (promiseEntry && sender === Self.id) {
            promiseEntry.reject(new Error('StateManager not ready in makeReceipt for tx ' + txHash));
            localReceiptInitiationPromises.delete(txHash);
            localReceiptResolvers.delete(txHash);
        }
        return;
    }
    const sign = signedTx.sign;
    const tx = { ...signedTx };
    delete tx.sign;
    console.log('makeReceipt', txHash);
    // Check if this is a new receipt and local initiation
    let isNewReceiptAndLocalInitiation = false;
    let currentPromiseResolvers = null;
    if (sender === Self.id && !receipts.has(txHash) && !localReceiptInitiationPromises.has(txHash)) {
        isNewReceiptAndLocalInitiation = true;
        // Create promise BEFORE creating receipt for atomic operation
        const promise = new Promise((resolve, reject) => {
            currentPromiseResolvers = { resolve, reject };
            localReceiptResolvers.set(txHash, currentPromiseResolvers);
        });
        localReceiptInitiationPromises.set(txHash, promise);
        if (logger_1.logFlags.verbose)
            console.log(`GlobalAccounts: Created promise for local receipt initiation ${txHash}`);
    }
    // Put into correct Receipt and Tracker
    let receipt = receipts.get(txHash);
    if (!receipt) {
        try {
            const consensusGroup = new Set(getConsensusGroupIds(tx.source));
            receipt = {
                signs: [],
                tx: null,
                consensusGroup,
            };
            receipts.set(txHash, receipt); // CRITICAL: Receipt is added to the map HERE
            if (logger_1.logFlags.verbose)
                console.log(`GlobalAccounts: receipts.set() called for ${txHash}`);
            if (isNewReceiptAndLocalInitiation && currentPromiseResolvers) {
                // Receipt successfully created, but don't resolve yet - wait for majority
                if (logger_1.logFlags.verbose)
                    console.log(`GlobalAccounts: Receipt created successfully for ${txHash}, waiting for majority`);
            }
        }
        catch (err) {
            if (logger_1.logFlags.error)
                console.error(`GlobalAccounts: Error during receipt creation for ${txHash}:`, err);
            if (isNewReceiptAndLocalInitiation && currentPromiseResolvers) {
                currentPromiseResolvers.reject(err); // Reject the promise
                localReceiptInitiationPromises.delete(txHash);
                localReceiptResolvers.delete(txHash);
            }
            throw err;
        }
        if (logger_1.logFlags.console)
            console.log(`SETGLOBAL: MAKERECEIPT CONSENSUS GROUP FOR ${txHash.substring(0, 5)}: ${lib_types_1.Utils.safeStringify([...receipt.consensusGroup].map((id) => id.substring(0, 5)))}`);
    }
    let tracker = trackers.get(txHash);
    if (!tracker) {
        tracker = createTracker(txHash);
    }
    // Ignore duplicate txs
    if (tracker.seen.has(sign.owner)) {
        if (logger_1.logFlags.verbose)
            console.log(`GlobalAccounts: Ignoring duplicate tx from ${sign.owner} for ${txHash}`);
        // Note: We don't reject the promise here as this is normal behavior
        return;
    }
    // Ignore if sender is not in consensus group
    if (!receipt.consensusGroup.has(sender)) {
        if (logger_1.logFlags.verbose)
            console.log(`GlobalAccounts: Sender ${sender} not in consensus group for ${txHash}`);
        // Note: We don't reject the promise here as this is normal behavior
        return;
    }
    receipt.signs.push(sign);
    receipt.tx = tx;
    tracker.seen.add(sign.owner);
    tracker.timestamp = tx.when;
    // When a majority (%60) is reached, emit the completion event for this txHash
    if (logger_1.logFlags.console)
        console.log(`SETGLOBAL: GOT SIGNED_SET_GLOBAL_TX FROM ${sender.substring(0, 5)}: ${txHash} ${lib_types_1.Utils.safeStringify(signedTx)}`);
    if (logger_1.logFlags.console)
        console.log(`SETGLOBAL: ${receipt.signs.length} RECEIPTS / ${receipt.consensusGroup.size} CONSENSUS_GROUP`);
    if (isReceiptMajority(receipt, receipt.consensusGroup)) {
        const handle = createMakeReceiptHandle(txHash);
        /** [TODO] [AS] Replace with Self.emitter.emit() */
        // p2p.emit(handle, receipt)
        Self.emitter.emit(handle, receipt);
        // Resolve the promise if we have majority
        const resolverEntry = localReceiptResolvers.get(txHash);
        if (resolverEntry) {
            resolverEntry.resolve();
            localReceiptResolvers.delete(txHash);
            // Promise will be cleaned up later in attemptCleanup
            if (logger_1.logFlags.verbose)
                console.log(`GlobalAccounts: Receipt reached majority, resolved promise for ${txHash}`);
        }
    }
}
exports.makeReceipt = makeReceipt;
function processReceipt(receipt) {
    const txHash = Context.crypto.hash(receipt.tx);
    const tracker = trackers.get(txHash) || createTracker(txHash);
    tracker.timestamp = receipt.tx.when;
    if (tracker.gossiped)
        return false;
    (0, promises_1.fireAndForget)(() => Context.shardus.put(receipt.tx.value, false, true));
    /* prettier-ignore */ if (logger_1.logFlags.console)
        console.log(`Processed set-global receipt: ${lib_types_1.Utils.safeStringify(receipt)} now:${(0, network_1.shardusGetTime)()}`);
    tracker.gossiped = true;
    attemptCleanup();
    return true;
}
exports.processReceipt = processReceipt;
function attemptCleanup() {
    const now = (0, network_1.shardusGetTime)();
    if (now - lastClean < 60000)
        return;
    lastClean = now;
    for (const [txHash, tracker] of trackers) {
        if (now - tracker.timestamp > 30000) {
            trackers.delete(txHash);
            receipts.delete(txHash);
            // Clean up any associated promises
            localReceiptInitiationPromises.delete(txHash);
            localReceiptResolvers.delete(txHash);
        }
    }
}
exports.attemptCleanup = attemptCleanup;
function validateReceipt(receipt) {
    if (Context.stateManager.currentCycleShardData === null) {
        // we may get this endpoint way before we are ready, so just log it can exit out
        if (logger_1.logFlags.console)
            console.log('validateReceipt: unable to validate receipt currentCycleShardData not ready');
        return false;
    }
    const consensusGroup = new Set(getConsensusGroupIds(receipt.tx.source));
    // Make sure receipt has enough signs
    if (isReceiptMajority(receipt, consensusGroup) === false) {
        if (logger_1.logFlags.console)
            console.log('validateReceipt: Receipt did not have majority');
        return false;
    }
    // Make a map of signs that overlap with consensusGroup
    const signsInConsensusGroup = [];
    const uniqueSignOwnerMap = {};
    for (const sign of receipt.signs) {
        const owner = sign.owner.toLowerCase();
        if (uniqueSignOwnerMap[owner]) {
            if (logger_1.logFlags.console)
                console.log(`validateReceipt: duplicate signatures for owner ${utils.stringifyReduce(sign.owner)}`);
            continue;
        }
        uniqueSignOwnerMap[owner] = true;
        /** [TODO] [AS] Replace with NodeList.byPubKey.get() */
        // const node = p2p.state.getNodeByPubKey(sign.owner)
        const node = NodeList.byPubKey.get(sign.owner);
        if (node === null) {
            if (logger_1.logFlags.console)
                console.log(`validateReceipt: node was null or not found ${utils.stringifyReduce(sign.owner)}`);
            /** [TODO] [AS] Replace with NodeList.nodes */
            if (logger_1.logFlags.console)
                console.log(
                // `validateReceipt: nodes: ${utils.stringifyReduce(p2p.state.getNodes())}`
                `validateReceipt: nodes: ${utils.stringifyReduce(NodeList.nodes)}`);
            continue;
        }
        const id = node.id;
        if (consensusGroup.has(id)) {
            signsInConsensusGroup.push(sign);
        }
        else {
            if (logger_1.logFlags.console)
                console.log(`validateReceipt: consensusGroup does not have id: ${id} ${utils.stringifyReduce(consensusGroup)}`);
        }
    }
    // Make sure signs and consensusGroup overlap >= %60
    if ((signsInConsensusGroup.length / consensusGroup.size) * 100 < 60) {
        if (logger_1.logFlags.console)
            console.log('validateReceipt: Receipt signature owners and consensus group did not overlap enough');
        return false;
    }
    // Verify the signs that overlap with consensusGroup are a majority
    let verified = 0;
    for (const sign of signsInConsensusGroup) {
        const signedTx = { ...receipt.tx, sign };
        if (Context.crypto.verify(signedTx))
            verified++;
    }
    if ((verified / consensusGroup.size) * 100 < 60) {
        if (logger_1.logFlags.console)
            console.log('validateReceipt: Receipt does not have enough valid signatures');
        return false;
    }
    if (logger_1.logFlags.console)
        console.log(`validateReceipt: success! ${utils.stringifyReduce(receipt)}`);
    return true;
}
function createTracker(txHash) {
    const tracker = {
        seen: new Set(),
        timestamp: 0,
        gossiped: false,
    };
    trackers.set(txHash, tracker);
    return tracker;
}
function getConsensusGroupIds(address) {
    const homeNode = shardFunctions_1.default.findHomeNode(Context.stateManager.currentCycleShardData.shardGlobals, address, Context.stateManager.currentCycleShardData.parititionShardDataMap);
    return homeNode.consensusNodeForOurNodeFull.map((node) => node.id);
}
function isReceiptMajority(receipt, consensusGroup) {
    return (receipt.signs.length / consensusGroup.size) * 100 >= 60;
}
function intersect(a, b) {
    const setB = new Set(b);
    return [...new Set(a)].filter((x) => setB.has(x));
}
function intersectCount(a, b) {
    return intersect(a, b).length;
}
function percentOverlap(a, b) {
    return (a.length / b.length) * 100;
}
// Export for testing purposes
function __clearForTest() {
    receipts.clear();
    trackers.clear();
}
exports.__clearForTest = __clearForTest;
//# sourceMappingURL=GlobalAccounts.js.map