// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later

const Util = require("./common-util");

const Core = require("./commands/core");
const Admin = require("./commands/admin-rpc");
const Pinning = require("./commands/pin-rpc");
const Quota = require("./commands/quota");
const Metadata = require("./commands/metadata");
const Channel = require("./commands/channel");
const Upload = require("./commands/upload");
const HK = require("./hk-util");

var RPC = module.exports;

const UNAUTHENTICATED_CALLS = {
    GET_FILE_SIZE: Pinning.getFileSize,
    GET_MULTIPLE_FILE_SIZE: Pinning.getMultipleFileSize,
    GET_DELETED_PADS: Pinning.getDeletedPads,
    IS_CHANNEL_PINNED: Pinning.isChannelPinned, // FIXME drop this RPC
    IS_NEW_CHANNEL: Channel.isNewChannel,
    WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
    DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
    GET_METADATA: Metadata.getMetadata,
    IS_PREMIUM: Pinning.isPremium,
    ADD_FIRST_ADMIN: Admin.addFirstAdmin
};

var isUnauthenticateMessage = function (msg) {
    return msg && msg.length === 2 && typeof(UNAUTHENTICATED_CALLS[msg[0]]) === 'function';
};

var handleUnauthenticatedMessage = function (Env, msg, respond, Server, netfluxId) {
    Env.Log.silly('LOG_RPC', msg[0]);

    Env.plugins?.MONITORING?.increment(`rpc_${msg[0]}`);

    var method = UNAUTHENTICATED_CALLS[msg[0]];
    method(Env, msg[1], function (err, value) {
        if (err) {
            Env.WARN(err, msg[1]);
            return void respond(err);
        }
        respond(err, [null, value, null]);
    }, Server, netfluxId);
};

const AUTHENTICATED_USER_TARGETED = {
    RESET: Pinning.resetUserPins,
    PIN: Pinning.pinChannel,
    UNPIN: Pinning.unpinChannel,
    CLEAR_OWNED_CHANNEL: Channel.clearOwnedChannel,
    REMOVE_OWNED_CHANNEL: Channel.removeOwnedChannel,
    TRIM_HISTORY: Channel.trimHistory,
    UPLOAD_STATUS: Upload.status,
    UPLOAD: Upload.upload,
    UPLOAD_COMPLETE: Upload.complete,
    UPLOAD_CANCEL: Upload.cancel,
    OWNED_UPLOAD_COMPLETE: Upload.complete_owned,
    ADMIN: Admin.command,
    SET_METADATA: Metadata.setMetadata,
};

const AUTHENTICATED_USER_SCOPED = {
    GET_HASH: Pinning.getHash,
    GET_TOTAL_SIZE: Pinning.getTotalSize,
    UPDATE_LIMITS: Quota.getUpdatedLimit,
    GET_LIMIT: Pinning.getLimit,
    EXPIRE_SESSION: Core.expireSessionAsync,
    REMOVE_PINS: Pinning.removePins,
    TRIM_PINS: Pinning.trimPins,
    COOKIE: Core.haveACookie,
};

var isAuthenticatedCall = function (call) {
    if (call === 'UPLOAD') { return false; }
    return typeof(AUTHENTICATED_USER_TARGETED[call] || AUTHENTICATED_USER_SCOPED[call]) === 'function';
};

var handleAuthenticatedMessage = function (Env, unsafeKey, msg, respond, Server) {
    /*  If you have gotten this far, you have signed the message with the
        public key which you provided.
    */

    var safeKey = Util.escapeKeyCharacters(unsafeKey);

    var Respond = function (e, value) {
        var session = Env.Sessions[safeKey];
        var token = session? session.tokens.slice(-1)[0]: '';
        var cookie = Core.makeCookie(token).join('|');
        respond(e ? String(e): e, [cookie].concat(typeof(value) !== 'undefined' ?value: []));
    };

    msg.shift();
    // discard validated cookie from message
    if (!msg.length) {
        return void Respond('INVALID_MSG');
    }

    var TYPE = msg[0];

    Env.Log.silly('LOG_RPC', TYPE);

    if (typeof(AUTHENTICATED_USER_TARGETED[TYPE]) === 'function') {
        return void AUTHENTICATED_USER_TARGETED[TYPE](Env, safeKey, msg[1], function (e, value) {
            Env.WARN(e, value);
            return void Respond(e, value);
        }, Server);
    }

    if (typeof(AUTHENTICATED_USER_SCOPED[TYPE]) === 'function') {
        return void AUTHENTICATED_USER_SCOPED[TYPE](Env, safeKey, function (e, value) {
            if (e) {
                Env.WARN(e, safeKey);
                return void Respond(e);
            }
            Respond(e, value);
        });
    }

    return void Respond('UNSUPPORTED_RPC_CALL', msg);
};

var rpc = function (Env, Server, userId, data, respond) {
    if (!Array.isArray(data)) {
        Env.Log.debug('INVALID_ARG_FORMET', data);
        return void respond('INVALID_ARG_FORMAT');
    }

    if (!data.length) {
        return void respond("INSUFFICIENT_ARGS");
    } else if (data.length !== 1) {
        Env.Log.debug('UNEXPECTED_ARGUMENTS_LENGTH', data);
    }

    var msg = data[0].slice(0);

    if (!Array.isArray(msg)) {
        return void respond('INVALID_ARG_FORMAT');
    }

    if (isUnauthenticateMessage(msg)) {
        return handleUnauthenticatedMessage(Env, msg, respond, Server, userId);
    }

    var signature = msg.shift();
    var publicKey = msg.shift();
    var safeKey = Util.escapeKeyCharacters(publicKey);
    var hadSession = Boolean(Env.Sessions[safeKey]);

    // make sure a user object is initialized in the cookie jar
    if (publicKey) {
        Core.getSession(Env.Sessions, publicKey);
    } else {
        Env.Log.debug("NO_PUBLIC_KEY_PROVIDED", publicKey);
    }

    var cookie = msg[0];
    if (!Core.isValidCookie(Env.Sessions, publicKey, cookie)) {
        // no cookie is fine if the RPC is to get a cookie
        if (msg[1] !== 'COOKIE') {
            return void respond('NO_COOKIE');
        }
    }

    var serialized = JSON.stringify(msg);

    if (!(serialized && typeof(publicKey) === 'string')) {
        return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY');
    }

    var command = msg[1];

    Env.plugins?.MONITORING?.increment(`rpc_${command}`);

    if (command === 'UPLOAD') {
        // UPLOAD is a special case that skips signature validation
        // intentional fallthrough behaviour
        return void handleAuthenticatedMessage(Env, publicKey, msg, respond, Server);
    }
    if (isAuthenticatedCall(command)) {
        // check the signature on the message
        // refuse the command if it doesn't validate
        return void Env.checkSignature(serialized, signature, publicKey, function (err) {
            if (err) {
                return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY");
            }
            if (command === 'COOKIE' && !hadSession && Env.logIP) {
                Env.Log.info('NEW_RPC_SESSION', {userId: userId, publicKey: publicKey});
            }
            HK.authenticateNetfluxSession(Env, userId, publicKey);
            return void handleAuthenticatedMessage(Env, publicKey, msg, respond, Server);
        });
    }
    Env.Log.warn('INVALID_RPC_CALL', command);
    return void respond("INVALID_RPC_CALL");
};

RPC.create = function (Env, cb) {
    var Sessions = Env.Sessions;

    var pingAccountsDaily = function () {
        Quota.pingAccountsDaily(Env, function (e) {
            if (e) {
                Env.WARN('dailyPing', e);
            }
        });
    };
    pingAccountsDaily();
    Env.intervals.dailyPing = setInterval(pingAccountsDaily, 24*3600*1000);

    var updateLimitInterval = function () {
        Quota.updateCachedLimits(Env, function (e) {
            // failure is expected if they have not specified a quota API endpoint
            if (!Env.accounts_api) { return; }
            if (e) {
                Env.WARN('LIMIT_UPDATE', e);
            }
        });
    };
    Quota.applyCustomLimits(Env);
    updateLimitInterval();
    if (Env.accounts_api) {
        Env.intervals.quotaUpdate = setInterval(updateLimitInterval, 3600*1000);
    }

    // expire old sessions once per minute
    Env.intervals.sessionExpirationInterval = setInterval(function () {
        Core.expireSessions(Sessions);
    }, Core.SESSION_EXPIRATION_TIME);

    cb(void 0, function (Server, userId, data, respond) {
        try {
            return rpc(Env, Server, userId, data, respond);
        } catch (e) {
            console.log("Error from RPC with data " + JSON.stringify(data));
            console.log(e.stack);
        }
    });
};
