"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.setNode = setNode;
exports.addNodeBack = addNodeBack;
exports.removeNodeBack = removeNodeBack;
const keys_js_1 = require("../core/security/keys.js");
const certificate_ts_1 = require("../security/certificate.js");
const validationWrapper_js_1 = require("../core/validation/validationWrapper.js");
const joi_1 = __importDefault(require("joi"));
const { pki } = require('node-forge');
const environmentManager_js_1 = require("../core/utility/environment/environmentManager.js");
const hdbTerms_ts_1 = require("../core/utility/hdbTerms.js");
const subscriptionManager_ts_1 = require("./subscriptionManager.js");
const knownNodes_ts_1 = require("./knownNodes.js");
const replicator_ts_1 = require("./replicator.js");
const nodeName_ts_1 = require("../core/server/nodeName.js");
const hdbLogger = __importStar(require("../core/utility/logging/harper_logger.js"));
const hdbError_js_1 = require("../core/utility/errors/hdbError.js");
const Server_ts_1 = require("../core/server/Server.js");
const { HTTP_STATUS_CODES } = hdbError_js_1.hdbErrors;
const validationSchema = joi_1.default.object({
    hostname: joi_1.default.string(),
    verify_tls: joi_1.default.boolean(),
    replicates: joi_1.default.boolean(),
    subscriptions: joi_1.default.array(),
    revoked_certificates: joi_1.default.array(),
    shard: joi_1.default.number(),
});
/**
 * Can add, update or remove a node from replication
 * @param req
 */
async function setNode(req) {
    if (req.node_name && !req.hostname)
        req.hostname = req.node_name;
    if (req.verify_tls !== undefined)
        req.rejectUnauthorized = req.verify_tls;
    let { url, hostname } = req;
    if (!url)
        url = (0, nodeName_ts_1.hostnameToUrl)(hostname);
    else if (!hostname)
        hostname = req.hostname = (0, replicator_ts_1.urlToNodeName)(url);
    const validation = (0, validationWrapper_js_1.validateBySchema)(req, validationSchema);
    if (validation) {
        throw (0, hdbError_js_1.handleHDBError)(validation, validation.message, HTTP_STATUS_CODES.BAD_REQUEST, undefined, undefined, true);
    }
    if (req.operation === 'remove_node') {
        if (!url && !hostname)
            throw new hdbError_js_1.ClientError('url or hostname is required for remove_node operation');
        const nodeRecordId = hostname;
        const hdbNodes = (0, knownNodes_ts_1.getHDBNodeTable)();
        const record = await hdbNodes.get(nodeRecordId);
        if (!record)
            throw new hdbError_js_1.ClientError(nodeRecordId + ' does not exist');
        try {
            // we delete record and req that other node also deletes record (or mark itself as non-replicating)
            // we do not wait for the other node to respond, it may not even be online anymore
            await (0, replicator_ts_1.sendOperationToNode)({ url: record.url }, {
                operation: 'remove_node_back',
                name: record?.subscriptions?.length > 0
                    ? (0, nodeName_ts_1.getThisNodeName)() // if we are doing a removal with explicit subscriptions, we want to the other node to remove the record for this node
                    : nodeRecordId, // if we are doing a removal with full replication, we want the other node to remove its own record to indicate it is not replicating
            }, undefined);
        }
        catch (err) {
            hdbLogger.warn(`Error removing node from target node ${nodeRecordId}, if it is offline and we be online in the future, you may need to clean up this node manually, or retry:`, err);
        }
        await hdbNodes.delete(nodeRecordId);
        return `Successfully removed '${nodeRecordId}' from cluster`;
    }
    if (!url)
        throw new hdbError_js_1.ClientError('url required for this operation');
    const thisUrl = (0, nodeName_ts_1.getThisNodeUrl)();
    if (thisUrl == null)
        throw new hdbError_js_1.ClientError('replication url is missing from harperdb-config.yaml');
    let rep;
    let csr;
    let csrPrivateKeyName;
    let cert_auth;
    if (url?.startsWith('wss:')) {
        rep = await (0, certificate_ts_1.getReplicationCert)();
        const caRecord = await (0, certificate_ts_1.getReplicationCertAuth)();
        if (!rep)
            throw new Error('Unable to find a certificate to use for replication');
        if (rep.options.is_self_signed || req.force_signing) {
            // Create the certificate signing request that will be sent to the other node
            const csrResult = await (0, certificate_ts_1.createCsr)();
            csr = csrResult.pem;
            csrPrivateKeyName = csrResult.privateKeyName;
            hdbLogger.info('Sending CSR to target node:', url);
        }
        else if (caRecord) {
            cert_auth = caRecord.certificate;
            hdbLogger.info('Sending CA named', caRecord.name, 'to target node', url);
        }
    }
    // This is the record that will be added to the other nodes hdbNodes table
    const targetAddNodeObj = {
        operation: 'add_node_back',
        hostname: (0, nodeName_ts_1.getThisNodeName)(),
        target_hostname: hostname,
        url: thisUrl,
        csr,
        cert_auth,
        authorization: req.retain_authorization ? req.authorization : null,
    };
    if ((0, environmentManager_js_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SHARD) !== undefined)
        targetAddNodeObj.shard = (0, environmentManager_js_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SHARD);
    if (req.subscriptions) {
        targetAddNodeObj.subscriptions = req.subscriptions.map(reverseSubscription);
    }
    else
        targetAddNodeObj.subscriptions = null;
    if (req.hasOwnProperty('subscribe') || req.hasOwnProperty('publish')) {
        const rev = reverseSubscription(req);
        targetAddNodeObj.subscribe = rev.subscribe;
        targetAddNodeObj.publish = rev.publish;
    }
    if (req?.authorization?.username && req?.authorization?.password) {
        req.authorization =
            'Basic ' + Buffer.from(req.authorization.username + ':' + req.authorization.password).toString('base64');
    }
    let targetNodeResponse;
    let targetNodeResponseError;
    hdbLogger.debug('sending add_node_back operation', targetAddNodeObj);
    try {
        targetNodeResponse = await (0, replicator_ts_1.sendOperationToNode)({ url }, targetAddNodeObj, req);
    }
    catch (err) {
        err.message = `Error returned from ${url}: ` + err.message;
        hdbLogger.warn('Error adding node:', url, 'to cluster:', err);
        targetNodeResponseError = err;
    }
    if (csr && (!targetNodeResponse?.certificate || !targetNodeResponse?.certificate?.includes?.('BEGIN CERTIFICATE'))) {
        if (targetNodeResponseError) {
            targetNodeResponseError.message += ' and connection was required to sign certificate';
            throw targetNodeResponseError;
        }
        throw new Error(`Unexpected certificate signature response from node ${url} response: ${JSON.stringify(targetNodeResponse)}`);
    }
    if (csr) {
        hdbLogger.info('CSR response received from node:', url, 'saving certificate and CA in hdb_certificate');
        await (0, keys_js_1.setCertTable)({
            name: pki.certificateFromPem(targetNodeResponse.signingCA).issuer.getField('CN').value,
            certificate: targetNodeResponse.signingCA,
            is_authority: true,
        });
        if (targetNodeResponse.certificate) {
            await (0, keys_js_1.setCertTable)({
                // give it a different name so we don't interfere with existing certificates
                name: `${(0, nodeName_ts_1.getThisNodeName)()}-replication`,
                uses: ['replication'],
                certificate: targetNodeResponse.certificate,
                private_key_name: csrPrivateKeyName ?? rep?.options?.key_file,
                is_authority: false,
                is_self_signed: false,
            });
        }
        cert_auth = targetNodeResponse.signingCA;
    }
    const nodeRecord = { url, ca: targetNodeResponse?.usingCA };
    if (req.hostname)
        nodeRecord.name = req.hostname;
    if (req.subscriptions)
        nodeRecord.subscriptions = req.subscriptions;
    else
        nodeRecord.replicates = true;
    if (req.start_time) {
        nodeRecord.start_time = typeof req.start_time === 'string' ? new Date(req.start_time).getTime() : req.start_time;
    }
    if (req.retain_authorization)
        nodeRecord.authorization = req.authorization;
    if (req.revoked_certificates)
        nodeRecord.revoked_certificates = req.revoked_certificates;
    if (targetNodeResponse?.shard !== undefined)
        nodeRecord.shard = targetNodeResponse.shard;
    else if (req.shard !== undefined)
        nodeRecord.shard = req.shard;
    if (nodeRecord.replicates) {
        const thisNode = {
            url: thisUrl,
            ca: cert_auth,
            replicates: true,
            subscriptions: null,
        };
        if ((0, environmentManager_js_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SHARD) !== undefined)
            thisNode.shard = (0, environmentManager_js_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SHARD);
        if (req.retain_authorization)
            thisNode.authorization = req.authorization;
        if (req.start_time)
            thisNode.start_time = req.start_time;
        await (0, subscriptionManager_ts_1.ensureNode)((0, nodeName_ts_1.getThisNodeName)(), thisNode);
    }
    await (0, subscriptionManager_ts_1.ensureNode)(targetNodeResponse ? targetNodeResponse.nodeName : (nodeRecord.name ?? (0, replicator_ts_1.urlToNodeName)(url)), nodeRecord);
    let message;
    if (req.operation === 'update_node') {
        message = `Successfully updated '${url}'`;
    }
    else
        message = `Successfully added '${url}' to cluster`;
    if (targetNodeResponseError)
        message += ' but there was an error updating target node: ' + targetNodeResponseError.message;
    return message;
}
/**
 * Is called by other node when an add_node operation is requested
 * @param req
 */
async function addNodeBack(req) {
    hdbLogger.trace('addNodeBack received request:', req);
    let certs = {};
    // If the add_node req has a CSR attached, return the CA that was used to issue the CSR,
    // else return whatever CA this node is using for replication
    let originCa;
    if (!req.csr) {
        // If there is no CSR in the request there should be a CA, use this CA in the hdbNodes record for origin node
        originCa = req?.cert_auth;
        hdbLogger.info('addNodeBack received CA from node:', req.url);
    }
    else {
        certs = await (0, certificate_ts_1.signCertificate)(req);
        originCa = certs.signingCA;
        hdbLogger.info('addNodeBack received CSR from node:', req.url, 'this node will use and respond with CA that was used to issue CSR');
    }
    const nodeRecord = { url: req.url, ca: originCa };
    if (req.subscriptions)
        nodeRecord.subscriptions = req.subscriptions;
    else {
        nodeRecord.replicates = true;
        nodeRecord.subscriptions = null;
    }
    if (req.start_time)
        nodeRecord.start_time = req.start_time;
    if (req.authorization)
        nodeRecord.authorization = req.authorization;
    if (req.shard !== undefined)
        nodeRecord.shard = req.shard;
    const repCa = await (0, certificate_ts_1.getReplicationCertAuth)();
    if (nodeRecord.replicates) {
        const thisNode = {
            url: (0, nodeName_ts_1.getThisNodeUrl)(),
            ca: repCa?.certificate,
            replicates: true,
            subscriptions: null,
        };
        if ((0, environmentManager_js_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SHARD) !== undefined) {
            thisNode.shard = (0, environmentManager_js_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.REPLICATION_SHARD);
            certs.shard = thisNode.shard;
        }
        if (req.start_time)
            thisNode.start_time = req.start_time;
        if (req.authorization)
            thisNode.authorization = req.authorization;
        await (0, subscriptionManager_ts_1.ensureNode)((0, nodeName_ts_1.getThisNodeName)(), thisNode);
    }
    await (0, subscriptionManager_ts_1.ensureNode)(req.hostname, nodeRecord);
    certs.nodeName = (0, nodeName_ts_1.getThisNodeName)();
    certs.usingCA = repCa?.certificate; // in addition to the signed CA, we need to return the CA that is being used for the active certificate
    hdbLogger.info('addNodeBack responding to:', req.url, 'with CA named:', repCa?.name);
    return certs;
}
/**
 * Is called by other node when remove_node is requested and
 * system tables are not replicating
 */
async function removeNodeBack(req) {
    hdbLogger.trace('removeNodeBack received request:', req);
    const hdbNodes = (0, knownNodes_ts_1.getHDBNodeTable)();
    //  delete the record
    await hdbNodes.delete(req.name);
}
function reverseSubscription(subscription) {
    const { subscribe, publish } = subscription;
    return { ...subscription, subscribe: publish, publish: subscribe };
}
Server_ts_1.server.registerOperation?.({
    name: 'set_node',
    execute: setNode,
    httpMethod: 'PUT',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
Server_ts_1.server.registerOperation?.({
    name: 'add_node',
    execute: setNode,
    httpMethod: 'PUT',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
Server_ts_1.server.registerOperation?.({
    name: 'add_node_back',
    execute: addNodeBack,
    httpMethod: 'PUT',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
Server_ts_1.server.registerOperation?.({
    name: 'remove_node_back;',
    execute: removeNodeBack,
    httpMethod: 'DELETE',
});
Server_ts_1.server.registerOperation?.({
    name: 'remove_node',
    execute: setNode,
    httpMethod: 'DELETE',
    parametersSchema: [{ name: 'hostname', in: 'path', schema: { type: 'string' } }],
});
//# sourceMappingURL=setNode.js.map