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

(() => {
const factory = (Crypto, CPNetflux, Netflux, Util,
            Hash, Realtime, NetConfig, Cache, Pinpad, nThen) => {
    var finish = function (S, err, doc) {
        if (S.done) { return; }
        S.cb((err && err.error), doc, err);
        S.done = true;

        if (!S.hasNetwork) {
            var disconnect = Util.find(S, ['network', 'disconnect']);
            if (typeof(disconnect) === 'function') { disconnect(); }
        }
        if (S.realtime && S.realtime.stop) {
            try {
                S.realtime.stop();
            } catch (e) { console.error(e); }
        }
        var abort = Util.find(S, ['session', 'realtime', 'abort']);
        if (typeof(abort) === 'function') {
            S.session.realtime.sync();
            abort();
        }
    };

    var makeNetwork = function (cb) {
        var wsUrl = NetConfig.getWebsocketURL();
        Netflux.connect(wsUrl).then(function (network) {
            cb(null, network);
        }, function (err) {
            cb(err);
        });
    };

    var start = function (Session, config) {
        // Create a network and authenticate with all our keys if necessary,
        // then start chainpad-netflux
        nThen(function (waitFor) {
            if (Session.hasNetwork) { return; }
            makeNetwork(waitFor(function (err, network) {
                if (err) { return; }
                config.network = network;
            }));
        }).nThen(function () {
            Session.realtime = CPNetflux.start(config);
        });
    };

    var onRejected = function (config, Session, data, cb) {
        // Check if we can authenticate
        if (!Array.isArray(data) || !data.length || data[0].length !== 16) {
            return void cb(true);
        }
        if (!Array.isArray(Session.accessKeys)) { return void cb(true); }

        // Authenticate
        config.network.historyKeeper = data[0];
        nThen(function (waitFor) {
            Session.accessKeys.forEach(function (obj) {
                Pinpad.create(config.network, obj, waitFor(function (e) {
                    console.log('done', obj);
                    if (e) { console.error(e); }
                }));
            });
        }).nThen(function () {
            cb();
        });
    };

    var makeConfig = function (hash, opt) {
        var secret;
        if (typeof(hash) === 'string') {
        // We can't use cryptget with a file or a user so we can use 'pad' as hash type
            secret = Hash.getSecrets('pad', hash, opt.password);
        } else if (typeof(hash) === 'object') {
            // we may want to just supply options directly
            // and this is the easiest place to do it
            secret = hash;
        }
        if (!secret.keys) { secret.keys = secret.key; } // support old hashses
        var config = {
            websocketURL: NetConfig.getWebsocketURL(opt.origin),
            channel: secret.channel,
            validateKey: secret.keys.validateKey || undefined,
            crypto: Crypto.createEncryptor(secret.keys),
            logLevel: 0,
            initialState: opt.initialState,
            Cache: Cache
        };
        return config;
    };

    var isObject = function (o) {
        return typeof(o) === 'object';
    };

    var overwrite = function (a, b) {
        if (!(isObject(a) && isObject(b))) { return; }
        Object.keys(b).forEach(function (k) { a[k] = b[k]; });
    };

    var get = function (hash, cb, opt, progress) {
        if (typeof(cb) !== 'function') {
            throw new Error('Cryptget expects a callback');
        }
        opt = opt || {};
        progress = progress || function () {};

        var config = makeConfig(hash, opt);
        var Session = {
            cb: cb,
            accessKeys: opt.accessKeys,
            hasNetwork: Boolean(opt.network)
        };

        config.onRejected = function (data, cb) {
            onRejected(config, Session, data, cb);
        };

        config.onReady = function (info) {
            var rt = Session.session = info.realtime;
            Session.network = info.network;
            progress(1);
            finish(Session, void 0, rt.getUserDoc());
        };

        config.onError = function (info) {
            console.warn(info);
            finish(Session, info);
        };
        config.onChannelError = function (info) {
            console.error(info);
            finish(Session, info);
        };

        config.onCacheReady = opt.onCacheReady;

        // We use the new onMessage handler to compute the progress:
        // we should receive 2 checkpoints max, so 100 messages max
        // We're going to consider that 1 message = 1%, and we'll send 100%
        // at the end
        var i = 0;
        config.onMessage = function () {
            i++;
            progress(Math.min(0.99, i/100));
        };

        overwrite(config, opt);

        start(Session, config);
    };

    var put = function (hash, doc, cb, opt) {
        if (typeof(cb) !== 'function') {
            throw new Error('Cryptput expects a callback');
        }
        opt = opt || {};

        var config = makeConfig(hash, opt);
        var Session = {
            cb: cb,
            accessKeys: opt.accessKeys,
            hasNetwork: Boolean(opt.network)
        };

        config.onRejected = function (data, cb) {
            onRejected(config, Session, data, cb);
        };

        config.onReady = function (info) {
            var realtime = Session.session = info.realtime;
            Session.network = info.network;

            realtime.contentUpdate(doc);

            var to = setTimeout(function () {
                cb(new Error("Timeout"));
            }, 15000);

            Realtime.whenRealtimeSyncs(realtime, function () {
                clearTimeout(to);
                var doc = realtime.getAuthDoc();
                realtime.abort();
                finish(Session, void 0, doc);
            });
        };

        config.onChannelError = function (info) {
            finish(Session, info);
        };

        overwrite(config, opt);

        start(Session, config);
    };

    return {
        get: get,
        put: put,
    };
};

if (typeof(module) !== 'undefined' && module.exports) {
    module.exports = factory(
        require('chainpad-crypto'),
        require('chainpad-netflux'),
        require('netflux-websocket'),
        require('./common-util'),
        require('./common-hash'),
        require('./common-realtime'),
        require('./network-config'),
        require('./cache-store'),
        require('./pinpad'),
        require('nthen'),
        require('chainpad')
    );
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
    define([
        '/components/chainpad-crypto/crypto.js',
        'chainpad-netflux',
        'netflux-client',
        '/common/common-util.js',
        '/common/common-hash.js',
        '/common/common-realtime.js',
        '/common/network-config.js',
        '/common/cache-store.js',
        '/common/pinpad.js',
        '/components/nthen/index.js',
        '/components/chainpad/chainpad.dist.js',
    ], factory);
} else {
    // unsupported initialization
}
})();

