"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 });
exports.calculatePartitionBlock = calculatePartitionBlock;
exports.createNetworkHash = createNetworkHash;
exports.updateStateHashesByCycleMap = updateStateHashesByCycleMap;
exports.updateReceiptHashesByCycleMap = updateReceiptHashesByCycleMap;
exports.updateSummaryHashesByCycleMap = updateSummaryHashesByCycleMap;
exports.savePartitionAndNetworkHashes = savePartitionAndNetworkHashes;
exports.saveReceiptAndNetworkHashes = saveReceiptAndNetworkHashes;
exports.saveSummaryAndNetworkHashes = saveSummaryAndNetworkHashes;
exports.readOldCycleRecord = readOldCycleRecord;
exports.readOldNetworkHash = readOldNetworkHash;
exports.readOldPartitionHashes = readOldPartitionHashes;
exports.calculateOldDataMap = calculateOldDataMap;
exports.copyOldDataToDataToMigrate = copyOldDataToDataToMigrate;
exports.getMissingPartitions = getMissingPartitions;
exports.registerDownloadRoutes = registerDownloadRoutes;
exports.downloadDataFromNode = downloadDataFromNode;
exports.convertMapToObj = convertMapToObj;
const got_1 = __importDefault(require("got"));
const stream_1 = __importDefault(require("stream"));
const zlib_1 = __importDefault(require("zlib"));
const logger_1 = require("../logger");
const Context = __importStar(require("../p2p/Context"));
const NodeList = __importStar(require("../p2p/NodeList"));
const Self = __importStar(require("../p2p/Self"));
const shardFunctions_1 = __importDefault(require("../state-manager/shardFunctions"));
const index_1 = require("./index");
const lib_types_1 = require("@shardeum-foundation/lib-types");
const fakeReceipMap = new Map();
function calculatePartitionBlock(shard) {
    const partitionToReceiptMap = new Map();
    for (const partition of shard.ourStoredPartitions) {
        partitionToReceiptMap.set(partition, fakeReceipMap);
    }
    // set receiptMap for global partition
    partitionToReceiptMap.set(-1, fakeReceipMap);
    return partitionToReceiptMap;
}
function createNetworkHash(hashes) {
    let hashArray = [];
    for (const [, hash] of hashes) {
        hashArray.push(hash);
    }
    hashArray = hashArray.sort();
    const hash = Context.crypto.hash(hashArray);
    return hash;
}
function updateStateHashesByCycleMap(counter, stateHash, stateHashesByCycle) {
    const newStateHashByCycle = new Map(stateHashesByCycle);
    const transformedStateHash = {
        ...stateHash,
        partitionHashes: convertMapToObj(stateHash.partitionHashes),
    };
    newStateHashByCycle.set(counter, transformedStateHash);
    if (newStateHashByCycle.size > 100 && counter > 100) {
        const limit = counter - 100;
        for (const [key] of newStateHashByCycle) {
            if (key < limit) {
                newStateHashByCycle.delete(key);
            }
        }
    }
    return newStateHashByCycle;
}
function updateReceiptHashesByCycleMap(counter, receiptHash, receiptHashesByCycle) {
    const newReceiptHashesByCycle = new Map(receiptHashesByCycle);
    const transformedStateHash = {
        ...receiptHash,
        receiptMapHashes: convertMapToObj(receiptHash.receiptMapHashes),
    };
    newReceiptHashesByCycle.set(counter, transformedStateHash);
    if (newReceiptHashesByCycle.size > 100 && counter > 100) {
        const limit = counter - 100;
        for (const [key] of newReceiptHashesByCycle) {
            if (key < limit) {
                newReceiptHashesByCycle.delete(key);
            }
        }
    }
    return newReceiptHashesByCycle;
}
function updateSummaryHashesByCycleMap(counter, summaryHashes, summaryHashesByCycle) {
    const newSummaryHashesByCycle = new Map(summaryHashesByCycle);
    const transformedSummaryHash = {
        ...summaryHashes,
        summaryHashes: convertMapToObj(summaryHashes.summaryHashes),
    };
    newSummaryHashesByCycle.set(counter, transformedSummaryHash);
    if (newSummaryHashesByCycle.size > 100 && counter > 100) {
        const limit = counter - 100;
        for (const [key] of newSummaryHashesByCycle) {
            if (key < limit) {
                newSummaryHashesByCycle.delete(key);
            }
        }
    }
    return newSummaryHashesByCycle;
}
async function savePartitionAndNetworkHashes(shard, partitionHashes, networkHash) {
    for (const [partitionId, hash] of partitionHashes) {
        await Context.storage.addPartitionHash({
            partitionId,
            cycleNumber: shard.cycleNumber,
            hash,
        });
    }
    await Context.storage.addNetworkState({
        cycleNumber: shard.cycleNumber,
        hash: networkHash,
    });
}
async function saveReceiptAndNetworkHashes(shard, receiptMapHashes, networkReceiptHash) {
    for (const [partitionId, hash] of receiptMapHashes) {
        await Context.storage.addReceiptMapHash({
            partitionId,
            cycleNumber: shard.cycleNumber,
            hash,
        });
    }
    await Context.storage.addNetworkReceipt({
        cycleNumber: shard.cycleNumber,
        hash: networkReceiptHash,
    });
}
async function saveSummaryAndNetworkHashes(shard, summaryHashes, summaryReceiptHash) {
    for (const [partitionId, hash] of summaryHashes) {
        await Context.storage.addSummaryHash({
            partitionId,
            cycleNumber: shard.cycleNumber,
            hash,
        });
    }
    await Context.storage.addNetworkSummary({
        cycleNumber: shard.cycleNumber,
        hash: summaryReceiptHash,
    });
}
async function readOldCycleRecord() {
    const oldCycles = await Context.storage.listOldCycles();
    if (oldCycles && oldCycles.length > 0)
        return oldCycles[0];
}
async function readOldNetworkHash() {
    try {
        const networkStateHash = await Context.storage.getLastOldNetworkHash();
        log('Read Old network state hash', networkStateHash);
        if (networkStateHash && networkStateHash.length > 0)
            return networkStateHash[0];
    }
    catch (e) {
        index_1.snapshotLogger.error('Unable to read old network state hash');
    }
}
async function readOldPartitionHashes() {
    try {
        const partitionHashes = await Context.storage.getLastOldPartitionHashes();
        log('Read Old partition_state_hashes', partitionHashes);
        return partitionHashes;
    }
    catch (e) {
        index_1.snapshotLogger.error('Unable to read old partition hashes');
    }
}
async function calculateOldDataMap(shardGlobals, nodeShardDataMap, oldPartitionHashMap, lastSnapshotCycle) {
    const partitionShardDataMap = new Map();
    const oldDataMap = new Map();
    shardFunctions_1.default.computePartitionShardDataMap(shardGlobals, partitionShardDataMap, 0, shardGlobals.numPartitions);
    /**
     * [NOTE] [AS] Need to do this because type of 'cycleJoined' field differs
     * between ShardusTypes.Node (number) and P2P/Node (string)
     */
    const nodes = NodeList.byIdOrder;
    shardFunctions_1.default.computeNodePartitionDataMap(shardGlobals, nodeShardDataMap, nodes, partitionShardDataMap, nodes, true, false // this is not the active node list.  Perf will be slower so we may want to
    // rework this calculation
    );
    // If we have old data, figure out which partitions we have and put into OldDataMap
    for (const [partitionId, partitonObj] of partitionShardDataMap) {
        try {
            const lowAddress = partitonObj.homeRange.low;
            const highAddress = partitonObj.homeRange.high;
            const oldAccountCopiesInPartition = await Context.storage.getOldAccountCopiesByCycleAndRange(lastSnapshotCycle, lowAddress, highAddress);
            if (oldAccountCopiesInPartition) {
                const existingHash = oldPartitionHashMap.get(partitionId);
                const oldAccountsWithoutCycleNumber = oldAccountCopiesInPartition.map((acc) => {
                    return {
                        accountId: acc.accountId,
                        data: acc.data,
                        timestamp: acc.timestamp,
                        hash: acc.hash,
                        isGlobal: acc.isGlobal,
                    };
                });
                const computedHash = Context.crypto.hash(oldAccountsWithoutCycleNumber);
                log(`old accounts in partition: ${partitionId}: `, oldAccountCopiesInPartition);
                log(computedHash, existingHash);
                log('partition: ', partitionId);
                log('existing hash: ', existingHash);
                log('computed hash: ', computedHash);
                // make sure that we really have correct data only if hashes match
                if (computedHash === existingHash) {
                    oldDataMap.set(partitionId, oldAccountCopiesInPartition);
                }
            }
        }
        catch (e) {
            console.log(e);
        }
    }
    // check if we have global account in old DB
    try {
        const oldGlobalAccounts = await Context.storage.getOldGlobalAccountCopies(lastSnapshotCycle);
        if (oldGlobalAccounts) {
            const existingGlobalHash = oldPartitionHashMap.get(-1);
            const oldGlobalAccWithoutCycleNumber = oldGlobalAccounts.map((acc) => {
                return {
                    accountId: acc.accountId,
                    data: acc.data,
                    timestamp: acc.timestamp,
                    hash: acc.hash,
                    isGlobal: acc.isGlobal,
                };
            });
            const computedGlobalHash = Context.crypto.hash(oldGlobalAccWithoutCycleNumber);
            log('existing global hash', existingGlobalHash);
            log('computed global hash', computedGlobalHash);
            // make sure that we really have correct data only if hashes match
            if (computedGlobalHash === existingGlobalHash) {
                oldDataMap.set(-1, oldGlobalAccounts);
            }
        }
    }
    catch (e) {
        console.log(e);
    }
    return oldDataMap;
}
function copyOldDataToDataToMigrate(oldDataMap, dataToMigrate) {
    for (const [key, value] of oldDataMap) {
        if (!dataToMigrate.has(key)) {
            dataToMigrate.set(key, value);
        }
    }
}
function getMissingPartitions(shardGlobals, oldDataMap) {
    log('Checking missing partitions...');
    const missingPartitions = [];
    const { homePartition } = shardFunctions_1.default.addressToPartition(shardGlobals, Self.id);
    log(`Home partition for us is: ${homePartition}`);
    const { partitionStart, partitionEnd } = shardFunctions_1.default.calculateStoredPartitions2(shardGlobals, homePartition);
    log('partition start: ', partitionStart);
    log('partition end: ', partitionEnd);
    const partitionsToCheck = [];
    if (partitionStart < partitionEnd) {
        for (let i = partitionStart; i <= partitionEnd; i++) {
            partitionsToCheck.push(i);
        }
    }
    else if (partitionStart > partitionEnd) {
        const largestPartition = index_1.safetyModeVals.safetyNum - 1;
        for (let i = partitionStart; i <= largestPartition; i++) {
            partitionsToCheck.push(i);
        }
        for (let i = 0; i <= partitionEnd; i++) {
            partitionsToCheck.push(i);
        }
    }
    log('Partitions to check: ', partitionsToCheck);
    log('oldDataMap', oldDataMap);
    for (const partitionId of partitionsToCheck) {
        if (!oldDataMap.has(partitionId)) {
            missingPartitions.push(partitionId);
        }
    }
    // check for virtual global partiton
    if (!oldDataMap.has(-1)) {
        missingPartitions.push(-1);
    }
    return missingPartitions;
}
function registerDownloadRoutes(network, oldDataMap, oldPartitionHashMap) {
    let dataToSend = {};
    for (const [partitionId] of oldDataMap) {
        // eslint-disable-next-line security/detect-object-injection
        dataToSend[partitionId] = {
            data: oldDataMap.get(partitionId),
            hash: oldPartitionHashMap.get(partitionId),
        };
    }
    dataToSend = lib_types_1.Utils.safeStringify(dataToSend);
    if (logger_1.logFlags.console)
        console.log('Registering download route', typeof dataToSend, dataToSend);
    network.registerExternalGet('download-snapshot-data', (_req, res) => {
        const readerStream = stream_1.default.Readable.from([dataToSend]);
        const gzip = zlib_1.default.createGzip();
        res.set('content-disposition', 'attachment; filename="snapshot-data"');
        res.set('content-type', 'application/gzip');
        readerStream.on('error', (err) => console.log('rs Error', err));
        gzip.on('error', (err) => console.log('gzip Error', err));
        res.on('error', (err) => console.log('res Error', err));
        readerStream
            .pipe(gzip)
            .pipe(res)
            .on('end', () => {
            res.end({ success: true });
        });
    });
}
async function downloadDataFromNode(url) {
    log('Downloading snapshot data from server...', url);
    const res = await (0, got_1.default)(url, {
        timeout: 1000, //  Omar - setting this to 1 sec
        retry: 0, // Omar - setting this to 0.
        decompress: true,
        encoding: null,
        headers: {
            'Content-Encoding': 'gzip',
        },
    });
    return new Promise((resolve, reject) => {
        zlib_1.default.unzip(res.body, (err, result) => {
            if (err) {
                reject(err);
            }
            else {
                try {
                    const parsedData = lib_types_1.Utils.safeJsonParse(result.toString());
                    resolve(parsedData);
                }
                catch (e) {
                    resolve(null);
                }
            }
        });
    });
}
/** If necessary, convert `inputMap` to a plan object. If the value passed to
 * this function is a `Map`, it will be converted. if it is not a `Map`,
 * we'll just return the value as it was passed. */
function convertMapToObj(inputMap) {
    if (inputMap instanceof Map) {
        const obj = {};
        for (const [key, value] of inputMap) {
            // eslint-disable-next-line security/detect-object-injection
            obj[key] = value;
        }
        return obj;
    }
    else {
        return inputMap;
    }
}
function log(...things) {
    if (logger_1.logFlags.console)
        console.log('DBG', 'SNAPSHOT', ...things);
}
//# sourceMappingURL=snapshotFunctions.js.map