"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.sequelizeCycleFieldModel = exports.addCycleFieldQuery = exports.nodeNotDownString = exports.nodeDownString = exports.cycleUpdatesName = exports.cycleDataName = void 0;
exports.init = init;
exports.reset = reset;
exports.getTxs = getTxs;
exports.validateRecordTypes = validateRecordTypes;
exports.dropInvalidTxs = dropInvalidTxs;
exports.updateRecord = updateRecord;
exports.parseRecord = parseRecord;
exports.sendRequests = sendRequests;
exports.apoptosizeSelf = apoptosizeSelf;
exports.isApopMarkedNode = isApopMarkedNode;
const debug_1 = require("../debug");
const logger_1 = require("../logger");
const ApoptosisProposalReq_1 = require("../types/ApoptosisProposalReq");
const ApoptosisProposalResp_1 = require("../types/ApoptosisProposalResp");
const utils_1 = require("../utils");
const getCallstack_1 = __importDefault(require("../utils/getCallstack"));
const nestedCounters_1 = require("../utils/nestedCounters");
const profiler_1 = require("../utils/profiler");
const Comms = __importStar(require("./Comms"));
const Context_1 = require("./Context");
const CycleCreator_1 = require("./CycleCreator");
const NodeList_1 = require("./NodeList");
const Self = __importStar(require("./Self"));
const Utils_1 = require("./Utils");
const TypeIdentifierEnum_1 = require("../types/enum/TypeIdentifierEnum");
const schemaDefintions_1 = require("../storage/utils/schemaDefintions");
const InternalRouteEnum_1 = require("../types/enum/InternalRouteEnum");
const lib_types_1 = require("@shardeum-foundation/lib-types");
const ResponseError_1 = require("../types/ResponseError");
const RequestErrorEnum_1 = require("../types/enum/RequestErrorEnum");
const Helpers_1 = require("../types/Helpers");
const promises_1 = require("../utils/functions/promises");
/** STATE */
// [TODO] - need to remove this after removing sequalize
exports.cycleDataName = 'apoptosized';
exports.cycleUpdatesName = 'apoptosis';
exports.nodeDownString = 'node is down';
exports.nodeNotDownString = 'node is not down';
const gossipRouteName = 'apoptosis';
let p2pLogger;
const proposals = {};
/** ROUTES */
const stopExternalRoute = {
    method: 'GET',
    name: 'stop',
    handler: (_req, res) => {
        if ((0, debug_1.isDebugMode)()) {
            res.json({ status: 'goodbye cruel world' });
            apoptosizeSelf('Apoptosis called at stopExternalRoute => src/p2p/Apoptosis.ts', 'Node stopped from `stop` route.');
        }
    },
};
const failExternalRoute = {
    method: 'GET',
    name: 'fail',
    handler: (_req, res) => {
        if ((0, debug_1.isDebugMode)()) {
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn('fail route invoked in Apoptosis; used to test unclean exit');
            //let fail_endpoint_debug = undefined
            //console.log(fail_endpoint_debug.forced)
            throw Error('fail_endpoint_debug');
        }
    },
};
// This route is expected to return "pass" or "fail" so that
//   the exiting node can know that some other nodes have
//   received the message and will send it to other nodes
const apoptosisInternalRoute = {
    name: InternalRouteEnum_1.InternalRouteEnum.apoptosize,
    handler: (payload, response, header, sign) => {
        const route = InternalRouteEnum_1.InternalRouteEnum.apoptosize;
        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.cApoptosisProposalReq);
            if (!requestStream) {
                errorHandler(RequestErrorEnum_1.RequestErrorEnum.InvalidRequest);
                return response((0, ResponseError_1.BadRequest)('Invalid apoptosisInternalRoute request stream'), ResponseError_1.serializeResponseError);
            }
            const req = (0, ApoptosisProposalReq_1.deserializeApoptosisProposalReq)(requestStream);
            const apopProposal = {
                id: req.id,
                when: req.when,
                sign: sign,
            };
            /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
                info(`Got Apoptosis proposal: ${lib_types_1.Utils.safeStringify(apopProposal)}`);
            let err = '';
            if (apopProposal.id === 'isDownCheck') {
                let down_msg = exports.nodeNotDownString;
                // IF we are not active or syncing then need to return fail. or not return at all?
                if (Self.isFailed === true) {
                    down_msg = exports.nodeDownString;
                }
                /* prettier-ignore */ nestedCounters_1.nestedCountersInstance.countEvent('p2p', `self-isDownCheck c:${CycleCreator_1.currentCycle} ${down_msg}`, 1);
                let resp = { s: down_msg, r: 1 };
                return response(resp, ApoptosisProposalResp_1.serializeApoptosisProposalResp);
            }
            const when = apopProposal.when;
            if (when > CycleCreator_1.currentCycle + 1 || when < CycleCreator_1.currentCycle - 1) {
                let resp = { s: 'fail', r: 2 };
                return response(resp, ApoptosisProposalResp_1.serializeApoptosisProposalResp);
            }
            //  check that the node which sent this is the same as the node that signed it, otherwise this is not original message so ignore it
            if (header.sender_id === apopProposal.id) {
                //  if (addProposal(payload)) p2p.sendGossipIn(gossipRouteName, payload)
                //  if (addProposal(payload)) Comms.sendGossip(gossipRouteName, payload)
                //  Omar - we must always accept the original apoptosis message regardless of quarter and save it to gossip next cycle
                //    but if we are in Q1 gossip it, otherwise save for Q1 of next cycle
                if (addProposal(apopProposal)) {
                    if (CycleCreator_1.currentQuarter === 1) {
                        // if it is Q1 we can try to gossip the message now instead of waiting for Q1 of next cycle
                        (0, promises_1.fireAndForget)(() => Comms.sendGossip(gossipRouteName, apopProposal));
                    }
                    let resp = { s: 'pass', r: 1 };
                    return response(resp, ApoptosisProposalResp_1.serializeApoptosisProposalResp);
                }
                else {
                    /* prettier-ignore */ if (logger_1.logFlags.error)
                        warn(`addProposal failed for payload: ${lib_types_1.Utils.safeStringify(apopProposal)}`);
                    let resp = { s: 'fail', r: 4 };
                    return response(resp, ApoptosisProposalResp_1.serializeApoptosisProposalResp);
                }
            }
            else {
                /* prettier-ignore */ if (logger_1.logFlags.error)
                    warn(`sender is not apop node: sender:${header.sender_id} apop:${apopProposal.id}`);
                let resp = { s: 'fail', r: 3 };
                return response(resp, ApoptosisProposalResp_1.serializeApoptosisProposalResp);
            }
        }
        catch (e) {
            nestedCounters_1.nestedCountersInstance.countEvent('internal', 'apoptosize-exception');
            /* prettier-ignore */ if (logger_1.logFlags.error)
                error(`apoptosize: Exception executing request: ${(0, utils_1.errorToStringFull)(e)}`);
        }
        finally {
            profiler_1.profilerInstance.scopedProfileSectionEnd('apoptosize');
        }
    },
};
const apoptosisGossipRoute = (payload, sender, tracker) => {
    profiler_1.profilerInstance.scopedProfileSectionStart('apoptosis');
    try {
        /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
            info(`Got Apoptosis gossip: ${lib_types_1.Utils.safeStringify(payload)}`);
        let err = '';
        err = (0, utils_1.validateTypes)(payload, { when: 'n', id: 's', sign: 'o' });
        if (err) {
            warn('apoptosisGossipRoute bad payload: ' + err);
            return;
        }
        err = (0, utils_1.validateTypes)(payload.sign, { owner: 's', sig: 's' });
        if (err) {
            warn('apoptosisGossipRoute bad payload.sign: ' + err);
            return;
        }
        if ([1, 2].includes(CycleCreator_1.currentQuarter)) {
            if (addProposal(payload)) {
                (0, promises_1.fireAndForget)(() => Comms.sendGossip(gossipRouteName, payload, tracker, Self.id, NodeList_1.byIdOrder, false)); // use Self.id so we don't gossip to ourself
            }
        }
    }
    finally {
        profiler_1.profilerInstance.scopedProfileSectionEnd('apoptosis');
    }
};
const routes = {
    external: [stopExternalRoute, failExternalRoute],
    internal: [],
    internalBinary: [apoptosisInternalRoute],
    gossip: {
        //    'gossip-join': gossipJoinRoute,
        [gossipRouteName]: apoptosisGossipRoute,
    },
};
/** FUNCTIONS */
function init() {
    p2pLogger = Context_1.logger.getLogger('p2p');
    // Init state
    reset();
    // Register routes
    for (const route of routes.external) {
        // [TODO] - Add Comms.registerExternalGet and Post that pass through to network.*
        //          so that we can always just use Comms.* instead of network.*
        Context_1.network._registerExternal(route.method, route.name, route.handler);
    }
    for (const route of routes.internal) {
        Comms.registerInternal(route.name, route.handler);
    }
    for (const route of routes.internalBinary) {
        Comms.registerInternalBinary(route.name, route.handler);
    }
    for (const [name, handler] of Object.entries(routes.gossip)) {
        Comms.registerGossipHandler(name, handler);
    }
}
function reset() {
    // only delete proposals where the node has been removed
    //   otherwise we will submit the proposal again in the next Q1
    for (const id of Object.keys(proposals)) {
        if (!NodeList_1.nodes.get(id)) {
            delete proposals[id];
        }
    }
}
function getTxs() {
    return {
        apoptosis: [...Object.values(proposals)],
    };
}
function validateRecordTypes(rec) {
    let err = (0, utils_1.validateTypes)(rec, { apoptosized: 'a' });
    if (err)
        return err;
    for (const item of rec.apoptosized) {
        if (typeof item !== 'string')
            return 'items of apoptosized array must be strings';
    }
    return '';
}
function dropInvalidTxs(txs) {
    const valid = txs.apoptosis.filter((request) => validateProposal(request));
    return { apoptosis: valid };
}
/*
Given the txs and prev cycle record mutate the referenced record
*/
function updateRecord(txs, record) {
    const apoptosized = [];
    for (const request of txs.apoptosis) {
        const publicKey = request.sign.owner;
        const node = NodeList_1.byPubKey.get(publicKey);
        if (node) {
            apoptosized.push(node.id);
        }
    }
    record.apoptosized = [...record.apoptosized, ...apoptosized].sort();
}
function parseRecord(record) {
    if (record.apoptosized.includes(Self.id)) {
        // This could happen if our Internet connection was bad.
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            error(`We got marked for apoptosis even though we didn't ask for it. Being nice and leaving.`);
        Self.emitter.emit('invoke-exit', 'node left active state due to un-refuted lost report', (0, getCallstack_1.default)(), `invoke-exit being called at parseRecord() => src/p2p/Apoptosis.ts: found our id in the apoptosis list`);
    }
    return {
        added: [],
        removed: record.apoptosized,
        updated: [],
    };
}
function sendRequests() {
    for (const id of Object.keys(proposals)) {
        // make sure node is still in the network, since it might
        //   have already been removed
        if (NodeList_1.nodes.get(id)) {
            (0, promises_1.fireAndForget)(() => Comms.sendGossip(gossipRouteName, proposals[id], '', null, NodeList_1.byIdOrder, true));
        }
    }
}
/* Module functions */
// [TODO] - We don't need the caller to pass us the list of nodes
//          remove this after changing references
async function apoptosizeSelf(message, userFriendlyMessage) {
    /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
        warn(`In apoptosizeSelf. ${message}`);
    // [TODO] - maybe we should shuffle this array
    const activeNodes = NodeList_1.activeByIdOrder;
    const proposal = createProposal();
    const apopProposalReq = {
        id: proposal.id,
        when: proposal.when,
    };
    await Comms.tellBinary(activeNodes, InternalRouteEnum_1.InternalRouteEnum.apoptosize, apopProposalReq, ApoptosisProposalReq_1.serializeApoptosisProposalReq, {});
    const qF = async (node) => {
        //  use ask instead of tell and expect the node to
        //          acknowledge it received the request by sending 'pass'
        if (node.id === Self.id)
            return null;
        try {
            const res = Comms.askBinary(node, InternalRouteEnum_1.InternalRouteEnum.apoptosize, apopProposalReq, ApoptosisProposalReq_1.serializeApoptosisProposalReq, ApoptosisProposalResp_1.deserializeApoptosisProposalResp, {});
            return res;
        }
        catch (err) {
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn(`qF: In apoptosizeSelf calling robustQuery proposal. ${message}`);
            /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
                warn(`Error: ${err}`);
            return null;
        }
    };
    const eF = (item1, item2) => {
        if (!item1 || !item2)
            return false;
        if (!item1.s || !item2.s)
            return false;
        if (item1.s === 'pass' && item2.s === 'pass')
            return true;
        return false;
    };
    // If we don't have any active nodes; means we are still joining
    if (activeNodes.length > 0) {
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            warn(`In apoptosizeSelf calling robustQuery proposal. ${message}`);
        let redunancy = 1;
        if (activeNodes.length > 5) {
            redunancy = 2;
        }
        if (activeNodes.length > 10) {
            redunancy = 3;
        }
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            warn(`Redunancy is ${redunancy}   ${message}`);
        await (0, Utils_1.robustQuery)(activeNodes, qF, eF, redunancy, true);
        /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
            warn(`Sent apoptosize-self proposal: ${lib_types_1.Utils.safeStringify(proposal)}   ${message}`);
    }
    // Omar - added the following line. Maybe we should emit an event when we apoptosize so other modules and app can clean up
    Self.emitter.emit('invoke-exit', userFriendlyMessage, (0, getCallstack_1.default)(), message); // we can pass true as a parameter if we want to be restarted
    // Omar - we should not add any proposal since we are exiting; we already sent our proposal to some nodes
    //  addProposal(proposal)
    /* prettier-ignore */ if (logger_1.logFlags.important_as_fatal)
        error(`We have been apoptosized. Exiting with status 1. Will not be restarted. ${message}`);
}
function createProposal() {
    const proposal = {
        //    id: p2p.id,
        id: Self.id,
        // Omar -  Maybe we should add a timestamp or cycle number to the message so it cannot be replayed
        when: CycleCreator_1.currentCycle,
    };
    //  return p2p.crypto.sign(proposal)
    return Context_1.crypto.sign(proposal);
}
function addProposal(proposal) {
    if (validateProposal(proposal) === false)
        return false;
    //  const publicKey = proposal.sign.owner
    const id = proposal.id;
    if (proposals[id])
        return false;
    proposals[id] = proposal;
    info(`Marked ${proposal.id} for apoptosis`);
    return true;
}
function validateProposal(payload) {
    // [TODO] Type checking
    if (!payload)
        return false;
    if (!payload.id)
        return false;
    if (!payload.when)
        return false;
    if (!payload.sign)
        return false;
    if (!isValidWhen(payload.when))
        return false;
    const proposal = payload;
    const id = proposal.id;
    // even joining nodes can send apoptosis message, so check all nodes list
    const node = NodeList_1.nodes.get(id);
    if (!node)
        return false;
    // Check if signature is valid and signed by expected node
    //  const valid = p2p.crypto.verify(proposal, node.publicKey)
    const valid = Context_1.crypto.verify(proposal, node.publicKey);
    if (!valid)
        return false;
    return true;
}
function isValidWhen(when) {
    if (typeof when !== 'number' || !Number.isInteger(when)) {
        return false;
    }
    const minAcceptableCycle = CycleCreator_1.currentCycle - 1;
    const maxAcceptableCycle = CycleCreator_1.currentCycle + 1;
    return when >= minAcceptableCycle && when <= maxAcceptableCycle;
}
function isApopMarkedNode(id) {
    let apopNode = false;
    if (proposals[id]) {
        // Check if the node is in the proposal list
        apopNode = true;
    }
    /* prettier-ignore */ if (logger_1.logFlags.p2pNonFatal)
        info('check if the node is apop marked node', id, apopNode);
    return apopNode;
}
function info(...msg) {
    const entry = `Apoptosis: ${msg.join(' ')}`;
    p2pLogger.info(entry);
}
function warn(...msg) {
    const entry = `Apoptosis: ${msg.join(' ')}`;
    p2pLogger.warn(entry);
}
function error(...msg) {
    const entry = `Apoptosis: ${msg.join(' ')}`;
    p2pLogger.error(entry);
}
/** STORAGE DATA */
/* Don't need this any more since we are not storing cycles in the database
 */
exports.addCycleFieldQuery = `ALTER TABLE cycles ADD ${exports.cycleDataName} JSON NULL`;
exports.sequelizeCycleFieldModel = {
    [exports.cycleDataName]: { type: schemaDefintions_1.SQLDataTypes.JSON, allowNull: true },
};
//# sourceMappingURL=Apoptosis.js.map