"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __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 });
const logger_1 = require("../logger");
const network_1 = require("../network");
const Context_1 = require("../p2p/Context");
const Archivers = __importStar(require("../p2p/Archivers"));
const Context = __importStar(require("../p2p/Context"));
const CycleAutoScale_1 = require("../p2p/CycleAutoScale");
const CycleChain = __importStar(require("../p2p/CycleChain"));
const Self = __importStar(require("../p2p/Self"));
const NodeList = __importStar(require("../p2p/NodeList"));
const package_json_1 = __importDefault(require("../../package.json"));
const debug_1 = require("../debug");
const nestedCounters_1 = require("../utils/nestedCounters");
const memoryReporting_1 = require("../utils/memoryReporting");
const debugUtils_1 = require("../utils/debugUtils");
const lib_types_1 = require("@shardeum-foundation/lib-types");
const Join_1 = require("../p2p/Join");
const CycleCreator_1 = require("../p2p/CycleCreator");
const process_1 = __importDefault(require("process"));
const promises_1 = require("../utils/functions/promises");
const http = require('../http');
const allZeroes64 = '0'.repeat(64);
class Reporter {
    constructor(config, logger, statistics, stateManager, profiler, loadDetection) {
        this.config = config;
        this.mainLogger = logger.getLogger('main');
        this.statistics = statistics;
        this.stateManager = stateManager;
        this.profiler = profiler;
        this.loadDetection = loadDetection;
        this.logger = logger;
        this.reportTimer = null;
        this.reportingInterval = null;
        this.socketReportInterval = null;
        this.lastTime = (0, network_1.shardusGetTime)();
        this.doConsoleReport = (0, debug_1.isDebugModeAnd)((config) => config.profiler);
        this.hasRecipient = this.config.recipient != null;
        this.stillNeedsInitialPatchPostActive = true;
        this.resetStatisticsReport();
    }
    resetStatisticsReport() {
        this.statisticsReport = {
            txInjected: 0,
            txApplied: 0,
            txRejected: 0,
            txProcessed: 0,
            txExpired: 0,
        };
    }
    collectStatisticToReport() {
        this.statisticsReport.txInjected += this.statistics ? this.statistics.getPreviousElement('txInjected') : 0;
        this.statisticsReport.txApplied += this.statistics ? this.statistics.getPreviousElement('txApplied') : 0;
        this.statisticsReport.txRejected += this.statistics ? this.statistics.getPreviousElement('txRejected') : 0;
        this.statisticsReport.txExpired += this.statistics ? this.statistics.getPreviousElement('txExpired') : 0;
        this.statisticsReport.txProcessed += this.statistics ? this.statistics.getPreviousElement('txProcessed') : 0;
    }
    async reportJoining(publicKey) {
        if (!this.hasRecipient) {
            return;
        }
        try {
            const nodeIpInfo = network_1.ipInfo;
            const appData = this.getAppData();
            await http.post(`${this.config.recipient}/joining`, {
                publicKey,
                nodeIpInfo,
                appData,
            });
        }
        catch (e) {
            if (logger_1.logFlags.error)
                this.mainLogger.error('reportJoining: ' + e.name + ': ' + e.message + ' at ' + e.stack);
            console.error(e);
        }
    }
    async reportJoined(nodeId, publicKey) {
        if (!this.hasRecipient) {
            return;
        }
        try {
            const nodeIpInfo = network_1.ipInfo;
            const appData = this.getAppData();
            await http.post(`${this.config.recipient}/joined`, {
                publicKey,
                nodeId,
                nodeIpInfo,
                appData,
            });
        }
        catch (e) {
            if (logger_1.logFlags.error)
                this.mainLogger.error('reportJoined: ' + e.name + ': ' + e.message + ' at ' + e.stack);
            console.error(e);
        }
    }
    async reportActive(nodeId) {
        if (!this.hasRecipient) {
            return;
        }
        try {
            await http.post(`${this.config.recipient}/active`, { nodeId });
        }
        catch (e) {
            if (logger_1.logFlags.error)
                this.mainLogger.error('reportActive: ' + e.name + ': ' + e.message + ' at ' + e.stack);
            console.error(e);
        }
    }
    async reportSyncStatement(nodeId, syncStatement) {
        if (!this.hasRecipient) {
            return;
        }
        try {
            await http.post(`${this.config.recipient}/sync-statement`, { nodeId, syncStatement });
        }
        catch (e) {
            if (logger_1.logFlags.error)
                this.mainLogger.error('reportSyncStatement: ' + e.name + ': ' + e.message + ' at ' + e.stack);
            console.error(e);
        }
    }
    async reportRemoved(nodeId) {
        if (!this.hasRecipient) {
            return;
        }
        try {
            await http.post(`${this.config.recipient}/removed`, { nodeId });
        }
        catch (e) {
            if (logger_1.logFlags.error)
                this.mainLogger.error('reportRemoved: ' + e.name + ': ' + e.message + ' at ' + e.stack);
            console.error(e);
        }
        // Omar added this, since, just clearing the timer did not work
        //   it was still sending one more heartbeat after sending a removed
        this.hasRecipient = false;
    }
    // Sends a report
    async _sendReport(data) {
        if (logger_1.logFlags.debug)
            this.mainLogger.debug(lib_types_1.Utils.safeStringify({ method: '_sendReport', script: 'reporter/index', hasRecipient: this.hasRecipient }));
        if (!this.hasRecipient) {
            return;
        }
        const nodeId = Self.id;
        if (logger_1.logFlags.debug)
            this.mainLogger.debug(lib_types_1.Utils.safeStringify({ method: '_sendReport', script: 'reporter/index', nodeId: nodeId }));
        if (!nodeId)
            throw new Error('No node ID available to the Reporter module.');
        const report = {
            nodeId,
            data,
        };
        if (logger_1.logFlags.debug) {
            try {
                this.mainLogger.debug(lib_types_1.Utils.safeStringify({ report: lib_types_1.Utils.safeStringify(report), method: `_sendReport`, script: '_sendReport' }));
            }
            catch (error) {
                console.error(`[reporter/index][_sendReport] ERROR while writing report object to log.`, error);
            }
        }
        try {
            await http.post(`${this.config.recipient}/heartbeat`, report);
        }
        catch (e) {
            if (logger_1.logFlags.error)
                this.mainLogger.error('_sendReport: ' + e.name + ': ' + e.message + ' at ' + e.stack);
            console.error(e);
        }
    }
    getReportInterval() {
        if (NodeList.activeByIdOrder.length >= 100) {
            return 10 * 1000;
        }
        else {
            return this.config.interval * 1000;
        }
    }
    getAppData() {
        if (typeof Context.shardus.app.getNodeInfoAppData === 'function') {
            const appData = Context.shardus.app.getNodeInfoAppData();
            return appData;
        }
        return 'unknown';
    }
    checkIsNodeLost(nodeId) {
        const lostNodeIds = CycleChain.getNewest().lost;
        if (lostNodeIds.length === 0)
            return false;
        const foundId = lostNodeIds.find((lostId) => lostId === nodeId);
        if (foundId)
            return true;
        return false;
    }
    checkIsNodeRefuted(nodeId) {
        const refutedNodeIds = CycleChain.getNewest().refuted;
        if (refutedNodeIds.length === 0)
            return false;
        const foundId = refutedNodeIds.find((refutedId) => refutedId === nodeId);
        if (foundId)
            return true;
        return false;
    }
    async report() {
        /*
        Stop calling getAccountsStateHash() since this is not of use in a sharded network, also expensive to compute.
          let appState = this.stateManager
            ? await this.stateManager.transactionQueue.getAccountsStateHash()
            : allZeroes64
        */
        if (logger_1.logFlags.debug)
            this.mainLogger.debug(lib_types_1.Utils.safeStringify({ method: 'report', script: 'reporter/index', cycleChainNewest: CycleChain.newest }));
        if (CycleChain.newest == null) {
            this.restartReportInterval();
            return;
        }
        let appState = allZeroes64; // monititor server will set color based on partition report
        const cycleMarker = CycleChain.newest.previous || ''; // [TODO] Replace with cycle creator
        const cycleCounter = CycleChain.newest.counter;
        const networkId = CycleChain.newest.networkId;
        const desiredNodes = (0, CycleAutoScale_1.getDesiredCount)();
        const lastScalingTypeRequested = CycleAutoScale_1.requestedScalingType;
        const lastScalingTypeWinner = CycleAutoScale_1.lastScalingType;
        const txInjected = this.statisticsReport.txInjected;
        const txApplied = this.statisticsReport.txApplied;
        const txRejected = this.statisticsReport.txRejected;
        const txExpired = this.statisticsReport.txExpired;
        const txProcessed = this.statisticsReport.txProcessed;
        const countedEvents = this.statistics.getAllCountedEvents();
        const reportInterval = this.getReportInterval();
        const nodeIpInfo = network_1.ipInfo;
        const appData = this.getAppData();
        let repairsStarted = 0;
        let repairsFinished = 0;
        // report only if we are active in te networks.
        // only knowingly report deltas.
        let partitionReport = null;
        let globalSync = null;
        let txCoverage = {};
        if (this.stateManager != null) {
            //todo need to get rid of / no-op partition report.  It can't scale with # of accounts. (at least not without some advanements in how handle hashing)
            //A report using trie hashes would be smarter / more usefull as a replacement.
            if (this.stateManager.partitionObjects != null) {
                partitionReport = this.stateManager.partitionObjects.getPartitionReport(true, true);
            }
            if (Context_1.config.mode === 'debug' && !Context_1.config.debug.disableTxCoverageReport) {
                txCoverage = { ...this.stateManager.transactionQueue.txCoverageMap };
            }
            globalSync = this.stateManager.isStateGood();
            repairsStarted = this.stateManager.dataRepairsStarted;
            repairsFinished = this.stateManager.dataRepairsCompleted;
            // Hack to code a green or red color for app state:
            appState = globalSync ? '00ff00ff' : 'ff0000ff';
        }
        let partitions = 0;
        let partitionsCovered = 0;
        if (this.stateManager != null) {
            /** @type {CycleShardData} */
            const shardData = this.stateManager.currentCycleShardData; //   getShardDataForCycle(cycleCounter)
            if (shardData != null) {
                partitions = shardData.shardGlobals.numPartitions;
                partitionsCovered = shardData.nodeShardData.storedPartitions.partitionsCovered;
            }
        }
        // Server load
        const currentNetworkLoad = this.loadDetection.getCurrentLoad();
        const currentNodeLoad = this.loadDetection.getCurrentNodeLoad();
        const queueLengthAll = this.statistics.getPreviousElement('queueLength');
        const executeQueueLength = this.statistics.getPreviousElement('executeQueueLength');
        const queueLength = executeQueueLength;
        const queueLengthBuckets = this.stateManager.transactionQueue.getQueueLengthBuckets();
        const txTimeInQueue = this.statistics.getPreviousElement('txTimeInQueue') / 1000; // ms to sec
        const maxTxTimeInQueue = this.statistics.getMax('txTimeInQueue') / 1000;
        const isNodeLost = this.checkIsNodeLost(Self.id);
        const isNodeRefuted = this.checkIsNodeRefuted(Self.id);
        const isDataSynced = !this.stateManager.accountPatcher.failedLastTrieSync;
        const cycleFinishedSyncing = Join_1.finishedSyncingCycle;
        if (CycleCreator_1.currentCycle > Join_1.finishedSyncingCycle && isDataSynced)
            this.stillNeedsInitialPatchPostActive = false;
        const stillNeedsInitialPatchPostActive = this.stillNeedsInitialPatchPostActive;
        let rareCounters = {};
        // convert nested Map to nested Object
        for (const [key, value] of nestedCounters_1.nestedCountersInstance.rareEventCounters) {
            rareCounters[key] = { ...value };
            rareCounters[key].subCounters = {};
            for (const [subKey, subValue] of value.subCounters) {
                rareCounters[key].subCounters[subKey] = subValue;
            }
        }
        // try to re-use the hashes from the newest cycle. if they don't
        // exist, compute them just-in-time
        const nodelistHash = NodeList.getNodeListHash();
        const archiverListHash = Archivers.getArchiverListHash();
        const lastInSyncResult = this.stateManager.accountPatcher.lastInSyncResult;
        try {
            await this._sendReport({
                repairsStarted,
                repairsFinished,
                isDataSynced,
                appState,
                cycleMarker,
                cycleCounter,
                nodelistHash,
                desiredNodes,
                lastScalingTypeWinner, // "up" "down" or null.  last scaling action decided by this node
                lastScalingTypeRequested, // "up" "down" or null.  last scaling action decided by this node
                timestamp: (0, network_1.shardusGetTime)() / 1000,
                txInjected,
                txApplied,
                txRejected,
                txExpired,
                txProcessed,
                reportInterval,
                networkId,
                nodeIpInfo,
                partitionReport,
                globalSync,
                partitions,
                partitionsCovered,
                countedEvents,
                currentLoad: {
                    networkLoad: currentNetworkLoad,
                    nodeLoad: currentNodeLoad,
                },
                queueLengthAll,
                queueLength,
                executeQueueLength,
                queueLengthBuckets,
                txTimeInQueue,
                maxTxTimeInQueue,
                rareCounters,
                txCoverage,
                isLost: isNodeLost,
                isRefuted: isNodeRefuted,
                shardusVersion: package_json_1.default.version,
                appData,
                archiverListHash,
                lastInSyncResult,
                cycleFinishedSyncing,
                stillNeedsInitialPatchPostActive,
                memory: process_1.default.memoryUsage(),
            });
            if (this.stateManager != null && Context_1.config.mode === 'debug' && !Context_1.config.debug.disableTxCoverageReport) {
                this.stateManager.transactionQueue.resetTxCoverageMap();
            }
            this.statistics.resetCountedEvents();
        }
        catch (e) {
            if (logger_1.logFlags.error)
                this.mainLogger.error('startReporting: ' + e.name + ': ' + e.message + ' at ' + e.stack);
            console.error(e);
        }
        this.resetStatisticsReport();
        this.restartReportInterval();
    }
    restartReportInterval() {
        if (this.reportTimer) {
            clearTimeout(this.reportTimer);
        }
        const reportInterval = this.getReportInterval();
        if (logger_1.logFlags.debug)
            this.mainLogger.debug(lib_types_1.Utils.safeStringify({ script: 'reporter/index', method: 'restartReportInterval', reportInterval }));
        this.reportTimer = setTimeout(() => {
            (0, promises_1.fireAndForget)(() => this.report());
        }, reportInterval);
    }
    startReporting() {
        const self = this;
        if (logger_1.logFlags.debug)
            this.mainLogger.debug(lib_types_1.Utils.safeStringify({
                script: 'reporter/index',
                method: 'startReporting',
                reportingIntervalDefined: this.reportingInterval !== undefined,
            }));
        if (this.reportingInterval) {
            return;
        }
        this.reportingInterval = setInterval(() => {
            if (logger_1.logFlags.debug)
                this.mainLogger.debug(lib_types_1.Utils.safeStringify({
                    script: 'reporter/index',
                    method: 'startReporting[reportingInterval]',
                    message: 'called',
                }));
            self.collectStatisticToReport();
            //temp mem debugging:
            if (logger_1.logFlags.important_as_error)
                this.mainLogger.info(memoryReporting_1.memoryReportingInstance.getMemoryStringBasic());
        }, 1000);
        //log a socket report every 5 minutes
        this.socketReportInterval = setInterval(async () => {
            if (logger_1.logFlags.debug)
                this.mainLogger.debug(lib_types_1.Utils.safeStringify({
                    script: 'reporter/index',
                    method: 'startReporting[socketReportInterval]',
                    logSocketReports: this.config.logSocketReports,
                }));
            if (this.config.logSocketReports) {
                const report = await (0, debugUtils_1.getSocketReport)();
                this.mainLogger.info(lib_types_1.Utils.safeStringify(report));
                const networkReport = memoryReporting_1.memoryReportingInstance.getShardusNetReport();
                this.mainLogger.info(lib_types_1.Utils.safeStringify(networkReport));
                if (report.error === true) {
                    clearInterval(self.socketReportInterval);
                }
            }
        }, 300000);
        // Creates and sends a report every `interval` seconds
        this.restartReportInterval();
    }
    consoleReport() {
        const time = (0, network_1.shardusGetTime)();
        let delta = time - this.lastTime;
        delta = delta * 0.001;
        const txInjected = this.statistics ? this.statistics.getPreviousElement('txInjected') : 0;
        const txApplied = this.statistics ? this.statistics.getPreviousElement('txApplied') : 0;
        const report = `Perf inteval ${delta}    ${txInjected} Injected @${txInjected / delta} per second.    ${txApplied} Applied @${txApplied / delta} per second`;
        this.lastTime = time;
        if (logger_1.logFlags.console)
            console.log(report);
        if (this.profiler) {
            //Note: turning this log on will make the perf endpoint math get reset
            //  one option would be to have a flag that gets set if anyone hits the perf endpoint
            //  if so, then just stop this logging.  for now i will leave this off.
            //      if (logFlags.console) console.log(this.profiler.printAndClearReport(delta))
            if (logger_1.logFlags.console)
                console.log('Current load', 'counter', CycleChain.newest.counter, this.loadDetection.getCurrentLoad());
        }
    }
    stopReporting() {
        if (logger_1.logFlags.debug)
            this.mainLogger.debug(lib_types_1.Utils.safeStringify({ script: 'reporter/index', method: 'stopReporting', message: 'called' }));
        this.mainLogger.info('Stopping statistics reporting...');
        clearTimeout(this.reportTimer);
        clearInterval(this.reportingInterval);
        clearInterval(this.socketReportInterval);
        this.reportingInterval = null;
    }
}
exports.default = Reporter;
//# sourceMappingURL=index.js.map