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

define([
    'jquery',
    '/components/chainpad-crypto/crypto.js',
    '/common/toolbar.js',
    'json.sortify',
    '/common/common-util.js',
    '/components/nthen/index.js',
    '/common/sframe-common.js',
    '/common/common-interface.js',
    '/common/common-hash.js',
    '/common/common-constants.js',
    '/common/hyperscript.js',
    '/common/clipboard.js',
    '/api/config',
    '/common/common-realtime.js',
    '/customize/messages.js',
    '/customize/application_config.js',
    '/common/common-ui-elements.js',
    '/debug/chainpad.dist.js',

    'css!/components/bootstrap/dist/css/bootstrap.min.css',
    'css!/components/components-font-awesome/css/font-awesome.min.css',
    'less!/debug/app-debug.less',
], function (
    $,
    Crypto,
    Toolbar,
    JSONSortify,
    Util,
    nThen,
    SFCommon,
    UI,
    Hash,
    Constants,
    h,
    Clipboard,
    ApiConfig,
    CommonRealtime,
    Messages,
    AppConfig,
    UIElements,
    ChainWalk)
{
    var APP = window.APP = {
        $: $,
        AppConfig: AppConfig,
        SFCommon: SFCommon,
        Crypto: Crypto,
        ApiConfig: ApiConfig
    };

    var toolbar;
    var common;

    nThen(function (waitFor) {
        $(waitFor(function () {
            UI.addLoadingScreen();
        }));
        SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
    }).nThen(function (/*waitFor*/) {
        var initializing = true;
        var $bar = $('#cp-toolbar');
        var Title;
        var cpNfInner;
        var metadataMgr;
        var readOnly = true;
        var sframeChan = common.getSframeChannel();

        var getHrefsTable = function (chainpad, length, cb, progress) {
            var priv = metadataMgr.getPrivateData();
            var origin = priv.origin;
            var edPublic = priv.edPublic;

            var pads = {};
            var channelByHref = {};
            var isOwned = function (data) {
                data = data || {};
                return data && data.owners && Array.isArray(data.owners) && data.owners.indexOf(edPublic) !== -1;
            };
            var parseBlock = function (block, doc) {
                var c = block.getContent(doc).doc;
                if (!c) { return void console.error(block); }
                var p;
                try {
                    p = JSON.parse(c);
                    if (!p.metadata) {
                        p = p.drive || {};
                    }
                } catch (e) {
                    console.error(e);
                    p = {};
                }

                // Get pads from the old storage key
                var old = p[Constants.oldStorageKey];
                var ids = p[Constants.storageKey];
                var pad, parsed, chan, href;
                if (old && Array.isArray(old)) {
                    for (var i = 0; i<old.length; i++) {
                        try {
                            pad = old[i];
                            href = (pad.href && pad.href.indexOf('#') !== -1) ? pad.href : pad.roHref;
                            chan = channelByHref[href];
                            if (!chan && href) {
                                parsed = Hash.parsePadUrl(href);
                                chan = parsed.hashData && Util.base64ToHex(parsed.hashData.channel || '');
                                channelByHref[href] = chan;
                            }
                            if (chan && (!pads[chan] || pads[chan].atime < pad.atime)) {
                                pads[chan] = {
                                    atime: +new Date(pad.atime),
                                    href: href,
                                    title: pad.title,
                                    owned: isOwned(pad),
                                    expired: pad.expire && pad.expire < (+new Date())
                                };
                            }
                        } catch (e) {}
                    }
                }
                // Get pads from the new storage key
                if (ids) {
                    for (var id in ids) {
                        try {
                            pad = ids[id];
                            href = (pad.href && pad.href.indexOf('#') !== -1) ? pad.href : pad.roHref;
                            chan = pad.channel || channelByHref[href];
                            if (!chan) {
                                if (href) {
                                    parsed = Hash.parsePadUrl(href);
                                    chan = (parsed.hashData && Util.base64ToHex(parsed.hashData.channel || '')) ||
                                           (Hash.getSecrets(parsed.type, parsed.hash, pad.password) || {}).channel;
                                    channelByHref[href] = chan;
                                }
                            }
                            if (chan && (!pads[chan] || pads[chan].atime < pad.atime)) {
                                pads[chan] = {
                                    atime: +new Date(pad.atime),
                                    href: href,
                                    title: pad.title,
                                    owned: isOwned(pad),
                                    expired: pad.expire && pad.expire < (+new Date())
                                };
                            }
                        } catch (e) {}
                    }
                }
                return c;
            };

            var allChannels;
            var deleted;

            nThen(function (W) {
                var nt = nThen;
                // Safely get all the pads from all the states
                var i = 0;
                var next = function (block, doc) {
                    nt = nt(W(function (waitFor) {
                        i++;
                        var doc2 = parseBlock(block, doc);
                        progress(Math.min(i/length, 1));
                        var c = block.getChildren();
                        setTimeout(waitFor(), 1);
                        c.forEach(function (b) {
                            next(b, doc2);
                        });
                    })).nThen;
                };

                var root = chainpad.getRootBlock();
                next(root);
            }).nThen(function (waitFor) {
                // Make the table
                allChannels = Object.keys(pads);
                sframeChan.query('Q_DRIVE_GETDELETED', {list:allChannels}, waitFor(function (err, data) {
                    deleted = data;
                }));
            }).nThen(function () {
                // Current status
                try {
                    var parsed = JSON.parse(chainpad.getUserDoc());
                    var drive = parsed.metadata ? parsed : parsed.drive;
                    var channels = Object.keys(drive[Constants.storageKey] || {}).map(function (id) {
                        return drive[Constants.storageKey][id].channel;
                    });
                } catch (e) {
                    console.error(e);
                }

                // Header
                var rows = [h('tr', [// TODO
                    h('th', '#'),
                    h('th', 'Title'),
                    h('th', 'URL'),
                    h('th', 'Last visited'),
                    h('th', 'Owned'),
                    h('th', 'CryptDrive status'),
                    h('th', 'Server status'),
                ])];
                // Body
                var body = allChannels;
                body.sort(function (a, b) {
                    return pads[a].atime - pads[b].atime;
                });
                body.forEach(function (id, i) {
                    var p = pads[id];
                    var del = deleted.indexOf(id) !== -1;
                    var removed = channels.indexOf(id) === -1;
                    rows.push(h('tr', [
                        h('td', String(i+1)),
                        h('td', {
                            title: p.title
                        }, p.title),
                        h('td', h('a', {
                            href: origin+p.href,
                            target: '_blank'
                        }, p.href)),
                        h('td', new Date(p.atime).toLocaleString()),
                        h('td', p.owned ? 'Yes' : 'No'),
                        h('td'+(p.expired || removed ?'.cp-debug-nok':'.cp-debug-ok'),
                            p.expired ? 'Expired' :
                                        (!removed ? 'Stored' : 'Deleted')),// TODO
                        h('td'+(del?'.cp-debug-nok':'.cp-debug-ok'), del ? 'Missing' : 'Available'),// TODO
                    ]));
                });
                // Table
                var t = h('table', rows);
                cb(t);
            });
        };

        var getGraph = function (chainpad, cb) {
            var hashes = metadataMgr.getPrivateData().hashes;
            var hash = hashes.editHash || hashes.viewHash;
            var chan = Hash.hrefToHexChannelId('/drive/#'+hash);

            var makeGraph = function () {
                var out = [
                    chan + ' digraph {'
                ];
                var parseBlock = function (x) {
                    var c = x.getChildren();
                    var label = x.hashOf.slice(0,8) + ' (' + x.parentCount + ' - ' + x.recvOrder + ')';
                    var p = x.getParent();
                    if (p && p.getChildren().length === 1 && c.length === 1) {
                        label = '...';
                        var gc = c;
                        while (gc.length === 1) {
                            c = gc;
                            gc = c[0].getChildren();
                        }
                    }
                    var nodeInfo = ['  p' + x.hashOf + '[label="' + label + '"'];
                    if (x.isCheckpoint && label !== '...') { nodeInfo.push(',color=red,weight=0.5'); }
                    nodeInfo.push(']');
                    out.push(nodeInfo.join(''));
                    c.forEach(function (child) {
                        out.push('  p' + x.hashOf + ' -> p' + child.hashOf);
                        parseBlock(child);
                    });
                };
                parseBlock(chainpad.getRootBlock());
                out.push('}');
                return out.join('\n');
            };

            cb(makeGraph());
        };

        var getFullChainpad = function (history, length, cb, progress) {
            var chainpad = ChainWalk.create({
                userName: 'debug',
                initialState: '',
                logLevel: 0,
                noPrune: true
            });

            var nt = nThen;
            history.forEach(function (msg, i) {
                nt = nt(function (waitFor) {
                    chainpad.message(msg);
                    progress(Math.min(i/length, 1));
                    setTimeout(waitFor());
                }).nThen;
            });
            nt(function () {
                cb(chainpad);
            });
        };

        var fullHistoryCalled = false;
        var getFullHistory = function () {
            var priv = metadataMgr.getPrivateData();
            if (fullHistoryCalled) { return; }
            fullHistoryCalled = true;

            // Set spinner
            var content = h('div#cp-app-debug-loading', [
                h('h2', 'Step 1/3'),
                h('p', 'Loading history from the server...'),
                h('span.fa.fa-circle-o-notch.fa-spin.fa-3x.fa-fw')
            ]);
            $('#cp-app-debug-content').html('').append(content);

            // Update progress bar
            var decrypting = false;
            var length = 0;
            var decryptProgress = h('span', '0%');
            sframeChan.on('EV_FULL_HISTORY_STATUS', function (progress) {
                if (!decrypting) {
                    // Add the progress bar the first time
                    decrypting = true;
                    var content = h('div.cp-app-debug-progress.cp-loading-progress', [
                        h('h2', 'Step 2/3'),
                        h('p', 'Decrypting your history...'),
                        h('span.fa.fa-circle-o-notch.fa-spin.fa-3x.fa-fw'),
                        h('br'),
                        decryptProgress
                    ]);
                    $('#cp-app-debug-content').html('').append(content);
                }
                length++;
                decryptProgress.innerHTML = (progress*100).toFixed(2) + '%';
            });

            // Get full history
            sframeChan.query('Q_GET_FULL_HISTORY', null, function (err, data) {
                // History is ready.
                // Display the graph code, and if the doc is a drive, display the button to list all the pads

                // Graph
                var graph = h('div.cp-app-debug-content-graph');

                var seeAllButton = h('button.btn.btn-success', 'Get the list');
                var hrefs = h('div.cp-app-debug-content-hrefs', [
                    h('h2', 'List all the pads ever stored in your CryptDrive'), // TODO
                ]);

                var parseProgress = h('span', '0%');
                var content = h('div#cp-app-debug-loading', [
                    h('h2', 'Step 3/3'),
                    h('p', 'Parsing history...'),// TODO
                    h('span.fa.fa-circle-o-notch.fa-spin.fa-3x.fa-fw'),
                    h('br'),
                    parseProgress
                ]);
                $('#cp-app-debug-content').html('').append(content);

                getFullChainpad(data, length, function (chainpad) {
                    var content = h('div.cp-app-debug-content', [
                        graph,
                        priv.debugDrive ? hrefs : ''
                    ]);
                    $('#cp-app-debug-content').html('').append(content);

                    // Table
                    if (priv.debugDrive) {
                        var clicked = false;
                        $(seeAllButton).click(function () {
                            if (clicked) { return; }
                            clicked = true;
                            $(seeAllButton).remove();
                            // Make the table
                            var progress = h('span', '0%');
                            var loading = h('div', [
                                'Loading data...',
                                h('br'),
                                progress
                            ]);
                            hrefs.append(loading);
                            getHrefsTable(chainpad, length, function (table) {
                                loading.innerHTML = '';
                                hrefs.append(table);
                            }, function (p) {
                                progress.innerHTML = (p*100).toFixed(2) + '%';
                            });
                        }).appendTo(hrefs);
                    }

                    // Graph
                    var code = h('code');
                    getGraph(chainpad, function (graphVal) {
                        code.innerHTML = graphVal;
                        $(graph).append(h('h2', 'Graph')); // TODO
                        $(graph).append(code);
                    });
                }, function (p) {
                    parseProgress.innerHTML = (p*100).toFixed(2) + '%';
                });
            }, {timeout: 2147483647}); // Max 32-bit integer
        };

        var replayFullHistory = function () {
            // Set spinner
            var content = h('div#cp-app-debug-loading', [
                h('p', 'Loading history from the server...'),
                h('span.fa.fa-circle-o-notch.fa-spin.fa-3x.fa-fw')
            ]);
            $('#cp-app-debug-content').html('').append(content);
            var makeChainpad = function () {
                return window.ChainPad.create({
                    userName: 'debug',
                    initialState: '',
                    logLevel: 2,
                    noPrune: true,
                    validateContent: function (content) {
                        try {
                            JSON.parse(content);
                            return true;
                        } catch (e) {
                            console.log('Failed to parse, rejecting patch');
                            return false;
                        }
                    },
                });
            };
            sframeChan.query('Q_GET_FULL_HISTORY', {
                debug: true,
            }, function (err, data) {
                var start = 0;
                var replay, input, left, right;
                var content = h('div.cp-app-debug-progress.cp-loading-progress', [
                    h('p', [
                        left = h('span.fa.fa-chevron-left'),
                        h('label', 'Start'),
                        start = h('input', {type: 'number', value: 0}),
                        h('label', 'State'),
                        input = h('input', {type: 'number', min: 1}),
                        right = h('span.fa.fa-chevron-right'),
                    ]),
                    h('br'),
                    replay = h('pre.cp-debug-replay'),
                ]);
                var $start = $(start);
                var $input = $(input);
                var $left = $(left);
                var $right = $(right);

                $('#cp-app-debug-content').html('').append(content);
                var chainpad = makeChainpad();
                console.warn(chainpad);

                var i = 0;
                var play = function (_i) {
                    if (_i < (start+1)) { _i = start + 1; }
                    if (_i > data.length - 1) { _i = data.length - 1; }
                    if (_i < i) {
                        chainpad.abort();
                        chainpad = makeChainpad();
                        console.warn(chainpad);
                        i = 0;
                    }
                    var messages = data.slice(i, _i);
                    i = _i;
                    $start.val(start);
                    $input.val(i);
                    messages.forEach(function (obj) {
                        chainpad.message(obj);
                    });
                    if (messages.length) {
                        var hashes = Object.keys(chainpad._.messages);
                        var currentHash = hashes[hashes.length - 1];
                        var best = chainpad.getAuthBlock();
                        var current = chainpad.getBlockForHash(currentHash);
                        if (best.hashOf === currentHash) {
                            console.log("Best", best);
                        } else {
                            console.warn("Current", current);
                            console.log("Best", best);
                        }
                    }
                    if (!chainpad.getUserDoc()) {
                        $(replay).text('');
                        return;
                    }
                    $(replay).text(JSON.stringify(JSON.parse(chainpad.getUserDoc()), 0, 2));
                };
                play(1);
                $left.click(function () {
                    play(i-1);
                });
                $right.click(function () {
                    play(i+1);
                });

                $input.keydown(function (e) {
                    if ([37, 38, 39, 40].indexOf(e.which) !== -1) {
                        e.preventDefault();
                    }
                });
                $input.keyup(function (e) {
                    var val = Number($input.val());
                    if (e.which === 37 || e.which === 40) { // Left or down
                        e.preventDefault();
                        play(val - 1);
                        return;
                    }
                    if (e.which === 38 || e.which === 39) { // Up or right
                        e.preventDefault();
                        play(val + 1);
                        return;
                    }
                    if (e.which !== 13) { return; }
                    if (!val) {
                        $input.val(1);
                        return;
                    }
                    play(Number(val));
                });

                // Initial state
                $start.keydown(function (e) {
                    if ([37, 38, 39, 40].indexOf(e.which) !== -1) {
                        e.preventDefault();
                    }
                });
                $start.keyup(function (e) {
                    var val = Number($start.val());
                    e.preventDefault();
                    if ([37, 38, 39, 40, 13].indexOf(e.which) !== -1) {
                        chainpad.abort();
                        chainpad = makeChainpad();
                    }
                    if (e.which === 37 || e.which === 40) { // Left or down
                        start = Math.max(0, val - 1);
                        i = start;
                        play(i);
                        return;
                    }
                    if (e.which === 38 || e.which === 39) { // Up or right
                        start = Math.min(data.length - 1, val + 1);
                        i = start;
                        play(i);
                        return;
                    }
                    if (e.which !== 13) { return; }
                    start = Number(val);
                    if (!val) { start = 0; }
                    i = start;
                    play(i);
                });
            }, {timeout: 2147483647}); // Max 32-bit integer
        };

        var getContent = function () {
            if ($('#cp-app-debug-content').is(':visible')) {
                $('#cp-app-debug-content').hide();
                $('#cp-app-debug-history').show();
                $('#cp-app-debug-get-content').removeClass('cp-toolbar-button-active');
                return;
            }
            $('#cp-app-debug-content').css('display', 'flex');
            $('#cp-app-debug-history').hide();
            $('#cp-app-debug-get-content').addClass('cp-toolbar-button-active');
        };
        var setInitContent = function () {
            var button = h('button.btn.btn-success', 'Load history');
            var buttonReplay = h('button.btn.btn-success', 'Replay');
            $(button).click(getFullHistory);
            $(buttonReplay).click(replayFullHistory);
            var content = h('p.cp-app-debug-init', [
                'To get better debugging tools, we need to load the entire history of the document. This make take some time.', // TODO
                h('br'),
                button,
                buttonReplay
            ]);
            $('#cp-app-debug-content').html('').append(content);
        };
        setInitContent();

        var config = APP.config = {
            readOnly: readOnly,
            // cryptpad debug logging (default is 1)
            // logLevel: 0,
            validateContent: function (content) {
                try {
                    JSON.parse(content);
                    return true;
                } catch (e) {
                    console.log("Failed to parse, rejecting patch");
                    return false;
                }
            }
        };

        var history = false;

        var setHistory = function (bool, update) {
            history = bool;
            if (!bool && update) { config.onRemote(); }
            else {
                setTimeout(cpNfInner.metadataMgr.refresh);
            }
            return true;
        };

        var displayDoc = function (doc) {
            $('#cp-app-debug-history').text(JSON.stringify(doc, 0, 2));
            console.log(doc);
        };

        var extractMetadata = function (content) {
            if (Array.isArray(content)) {
                var m = content[content.length - 1];
                if (typeof(m.metadata) === 'object') {
                    // pad
                    return m.metadata;
                }
            } else if (typeof(content.metadata) === 'object') {
                return content.metadata;
            }
            return;
        };

        // Get the realtime metadata when in history mode
        var getLastMetadata = function () {
            var newContentStr = cpNfInner.chainpad.getUserDoc();
            var newContent = JSON.parse(newContentStr);
            var meta = extractMetadata(newContent);
            return meta;
        };
        var setLastMetadata = function (md) {
            var newContentStr = cpNfInner.chainpad.getAuthDoc();
            var newContent = JSON.parse(newContentStr);
            if (Array.isArray(newContent)) {
                newContent[3] = {
                    metadata: md
                };
            } else {
                newContent.metadata = md;
            }
            try {
                cpNfInner.chainpad.contentUpdate(JSONSortify(newContent));
                return true;
            } catch (e) {
                console.error(e);
                return false;
            }
        };

        var toRestore;
        config.onLocal = function (a, restore) {
            if (!toRestore || !restore) { return; }
            cpNfInner.chainpad.contentUpdate(toRestore);
        };


        config.onInit = function (info) {
            Title = common.createTitle({});

            var configTb = {
                displayed: ['pad'],
                title: Title.getTitleConfig(),
                metadataMgr: metadataMgr,
                readOnly: 1,
                realtime: info.realtime,
                sfCommon: common,
                $container: $bar,
                $contentContainer: $('#cp-app-debug')
            };
            toolbar = APP.toolbar = Toolbar.create(configTb);
            Title.setToolbar(toolbar);

            /* add a history button */
            var histConfig = {
                onLocal: function () {
                    // The following lines allow us to restore an old version from the debug app
                    // without affecting the snapshots.
                    // It's parsing, updating and stringifying text data which is not a clean way
                    // to change metadata, so we're disabling it by default.
                    if (window.cp_snapshots) {
                        var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
                        var _snapshots = md.snapshots;
                        var newContent = JSON.parse(toRestore);
                        try {
                            if (Array.isArray(newContent)) {
                                newContent[3].metadata.snapshots = _snapshots;
                            } else {
                                newContent.metadata.snapshots = _snapshots;
                            }
                        } catch (e) { console.error(e); }
                        toRestore = JSONSortify(newContent);
                    }
                    config.onLocal(null, true);
                },
                onRemote: config.onRemote,
                setHistory: setHistory,
                extractMetadata: extractMetadata,
                getLastMetadata: getLastMetadata, // get from authdoc
                setLastMetadata: setLastMetadata, // set to userdoc/authdoc
                applyVal: function (val) {
                    toRestore = val;
                    var newContent = JSON.parse(val);
                    var meta = extractMetadata(newContent);
                    cpNfInner.metadataMgr.updateMetadata(meta);
                    displayDoc(JSON.parse(val) || {});
                },
                $toolbar: $bar,
                debug: true
            };
            var $hist = common.createButton('history', true, {histConfig: histConfig});
            $hist.addClass('cp-hidden-if-readonly');
            var $histEntry = UIElements.getEntryFromButton($hist);
            toolbar.$drawer.append($histEntry);

            var $content = common.createButton(null, true, {
                icon: 'fa-clock-o',
                title: 'Get debugging graph', // TODO
                name: 'graph',
                text: 'Replay',
                id: 'cp-app-debug-get-content'
            });
            $content.click(getContent);
            var $contentEntry = UIElements.getEntryFromButton($content);
            toolbar.$drawer.append($contentEntry);

            var priv = metadataMgr.getPrivateData();
            if (priv.debugDrive) {
                var $drive = common.createButton(null, true, {
                    icon: 'fa-hdd-o',
                    title: 'Get Shared Folder content', // TODO
                    text: 'SF channel list',
                    id: 'cp-app-debug-get-channels'
                });
                $drive.click(() => {
                    let p = JSON.parse(info.realtime.getUserDoc());
                    const fd = p?.drive?.filesData || p?.filesData;
                    let all = Object.keys(fd).map(id => {
                        return fd[id]?.channel;
                    });
                    console.error(all);
                    Clipboard.copy(all.join('\n'), (err) => {
                        if (err) { return UI.warn(Messages.error); }
                        UI.log(Messages.genericCopySuccess);
                    });
                });
                var $driveEntry = UIElements.getEntryFromButton($drive);
                toolbar.$drawer.append($driveEntry);
            }
        };

        config.onReady = function (info) {
            if (APP.realtime !== info.realtime) {
                APP.realtime = info.realtime;
            }

            var userDoc = APP.realtime.getUserDoc();
            if (userDoc !== "") {
                var hjson = JSON.parse(userDoc);

                if (Array.isArray(hjson)) {
                    metadataMgr.updateMetadata(hjson[3]);
                } else if (hjson && hjson.metadata) {
                    metadataMgr.updateMetadata(hjson.metadata);
                }
                displayDoc(hjson);
            }

            //metadataMgr.updateTitle('');

            initializing = false;
            $('#cp-app-debug-history').show();
            UI.removeLoadingScreen();
        };

        config.onRemote = function () {
            if (initializing) { return; }
            if (history) { return; }
            var userDoc = APP.realtime.getUserDoc();

            var json = JSON.parse(userDoc);
            if (Array.isArray(json)) {
                metadataMgr.updateMetadata(json[3]);
            } else if (json && json.metadata) {
                metadataMgr.updateMetadata(json.metadata);
            }
            displayDoc(json);
        };

        config.onAbort = function () {
            console.log('onAbort');
        };

        config.onConnectionChange = function (info) {
            console.log('onConnectionChange', info.state);
        };

        cpNfInner = APP.cpNfInner = common.startRealtime(config);
        metadataMgr = APP.metadataMgr = cpNfInner.metadataMgr;

        cpNfInner.onInfiniteSpinner(function () {
            console.error('infinite spinner');
        });
    });

});
