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

const Data = module.exports;

const Meta = require("../metadata");
const Core = require("./core");
const Util = require("../common-util");
const HK = require("../hk-util");

Data.getMetadataRaw = function (Env, channel /* channelName */, _cb) {
    const cb = Util.once(Util.mkAsync(_cb));
    if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); }
    if (channel.length !== HK.STANDARD_CHANNEL_LENGTH &&
        channel.length !== HK.ADMIN_CHANNEL_LENGTH &&
        channel.length !== HK.BLOB_ID_LENGTH) { return cb("INVALID_CHAN_LENGTH"); }

    // return synthetic metadata for admin broadcast channels as a safety net
    // in case anybody manages to write metadata
    if (channel.length === HK.ADMIN_CHANNEL_LENGTH) {
        return void cb(void 0, {
            channel: channel,
            creation: +new Date(),
            owners: Env.admins,
        });
    }

    var cached = Env.metadata_cache[channel];
    if (HK.isMetadataMessage(cached)) {
        Env.checkCache(channel);
        return void cb(void 0, cached);
    }

    Env.batchMetadata(channel, cb, function (done) {
        Env.computeMetadata(channel, function (err, meta) {
            if (!err && HK.isMetadataMessage(meta)) {
                Env.metadata_cache[channel] = meta;
                // clear metadata after a delay if nobody has joined the channel within 30s
                Env.checkCache(channel);
            }
            done(err, meta);
        });
    });
};

Data.getMetadata = function (Env, channel, cb, Server, netfluxId) {
    Data.getMetadataRaw(Env, channel, function (err, metadata) {
        if (err) { return void cb(err); }

        if (!(metadata && metadata.restricted)) {
            // if it's not restricted then just call back
            return void cb(void 0, metadata);
        }

        const session = HK.getNetfluxSession(Env, netfluxId);
        const allowed = HK.listAllowedUsers(metadata);

        if (!HK.isUserSessionAllowed(allowed, session)) {
            return void cb(void 0, {
                restricted: metadata.restricted,
                allowed: allowed,
                rejected: true,
            });
        }
        cb(void 0, metadata);
    });
};

/* setMetadata
    - write a new line to the metadata log if a valid command is provided
    - data is an object: {
        channel: channelId,
        command: metadataCommand (string),
        value: value
    }
*/
Data.setMetadata = function (Env, safeKey, data, cb, Server) {
    var unsafeKey = Util.unescapeKeyCharacters(safeKey);

    var channel = data.channel;
    var command = data.command;

    if (!channel || !Core.isValidId(channel)) { return void cb ('INVALID_CHAN'); }
    if (!command || typeof (command) !== 'string') { return void cb('INVALID_COMMAND'); }
    if (Meta.commands.indexOf(command) === -1) { return void cb('UNSUPPORTED_COMMAND'); }

    Env.queueMetadata(channel, function (next) {
        Data.getMetadataRaw(Env, channel, function (err, metadata) {
            if (err) {
                cb(err);
                return void next();
            }
            if (!Core.hasOwners(metadata)) {
                cb('E_NO_OWNERS');
                return void next();
            }

            // if you are a pending owner and not an owner
                // you can either ADD_OWNERS, or RM_PENDING_OWNERS
                    // and you should only be able to add yourself as an owner
                // everything else should be rejected
            // else if you are not an owner
                // you should be rejected
            // else write the command

            // Confirm that the channel is owned by the user in question
            // or the user is accepting a pending ownership offer
            if (Core.hasPendingOwners(metadata) &&
                Core.isPendingOwner(metadata, unsafeKey) &&
                        !Core.isOwner(metadata, unsafeKey)) {

                // If you are a pending owner, make sure you can only add yourelf as an owner
                if ((command !== 'ADD_OWNERS' && command !== 'RM_PENDING_OWNERS')
                        || !Array.isArray(data.value)
                        || data.value.length !== 1
                        || data.value[0] !== unsafeKey) {
                    cb('INSUFFICIENT_PERMISSIONS');
                    return void next();
                }
                // FIXME wacky fallthrough is hard to read
                // we could pass this off to a writeMetadataCommand function
                // and make the flow easier to follow
            } else if (!Core.isOwner(metadata, unsafeKey)) {
                cb('INSUFFICIENT_PERMISSIONS');
                return void next();
            }

            // Add the new metadata line
            var line = [command, data.value, +new Date()];
            var changed = false;
            try {
                changed = Meta.handleCommand(metadata, line);
            } catch (e) {
                cb(e);
                return void next();
            }

            // if your command is valid but it didn't result in any change to the metadata,
            // call back now and don't write any "useless" line to the log
            if (!changed) {
                cb(void 0, metadata);
                return void next();
            }
            Env.msgStore.writeMetadata(channel, JSON.stringify(line), function (e) {
                if (e) {
                    cb(e);
                    return void next();
                }

                // send the message back to the person who changed it
                // since we know they're allowed to see it
                cb(void 0, metadata);
                next();

                const metadata_cache = Env.metadata_cache;

                // update the cached metadata
                metadata_cache[channel] = metadata;
                Env.checkCache(channel);

                // it's easy to check if the channel is restricted
                const isRestricted = metadata.restricted;
                // and these values will be used in any case
                const s_metadata = JSON.stringify(metadata);
                const hk_id = Env.historyKeeper.id;

                if (!isRestricted) {
                    // pre-allow-list behaviour
                    // if it's not restricted, broadcast the new metadata to everyone
                    return void Server.channelBroadcast(channel, s_metadata, hk_id);
                }

                // otherwise derive the list of users (unsafeKeys) that are allowed to stay
                const allowed = HK.listAllowedUsers(metadata);
                // anyone who is not allowed will get the same error message
                const s_error = JSON.stringify({
                    error: 'ERESTRICTED',
                    channel: channel,
                });

                // iterate over the channel's userlist
                const toRemove = [];
                Server.getChannelUserList(channel).forEach(function (userId) {
                    const session = HK.getNetfluxSession(Env, userId);

                    // if the user is allowed to remain, send them the metadata
                    if (HK.isUserSessionAllowed(allowed, session)) {
                        return void Server.send(userId, [
                            0,
                            hk_id,
                            'MSG',
                            userId,
                            s_metadata
                        ], function () {});
                    }
                    // otherwise they are not in the list.
                    // send them an error and kick them out!
                    Server.send(userId, [
                        0,
                        hk_id,
                        'MSG',
                        userId,
                        s_error
                    ], function () {});
                });

                Server.removeFromChannel(channel, toRemove);
            });
        });
    });
};
