/*
 * Copyright (C) 2012-2013 Anton Simonov <untone@gmail.com>
 * Copyright (C) 2014-2017 Vsevolod Stakhov <vsevolod@highsecure.ru>
 */

/* global require, Visibility */

define(["app/common", "app/icons", "bootstrap",
    "visibility"],
(common, icons, bootstrap) => {
    "use strict";
    const ui = {};

    const defaultAjaxTimeout = 20000;

    const ajaxTimeoutBox = ".popover #settings-popover #ajax-timeout";
    const graphs = {};
    let checked_server = "All SERVERS";
    const timer_id = [];
    let stickyTabsHandle = null;

    function ajaxSetup(ajax_timeout, setFieldValue, saveToLocalStorage) {
        const timeout = (ajax_timeout && ajax_timeout >= 0) ? ajax_timeout : defaultAjaxTimeout;
        if (saveToLocalStorage) localStorage.setItem("ajax_timeout", timeout);
        if (setFieldValue) document.querySelector(ajaxTimeoutBox).value = timeout;

        common.setAjaxTimeout(timeout);
    }

    function cleanCredentials() {
        sessionStorage.clear();
        document.getElementById("statWidgets").replaceChildren();
        const listMapsTbody = document.querySelector("#listMaps tbody");
        if (listMapsTbody) listMapsTbody.replaceChildren();
        document.getElementById("modalBody").replaceChildren();
    }

    function stopTimers() {
        for (const key in timer_id) {
            if (!{}.hasOwnProperty.call(timer_id, key)) continue;
            Visibility.stop(timer_id[key]);
        }
    }

    // Sticky Tabs: keep the active tab in sync with the URL hash so that the
    // selected tab survives reload and is shareable. Vanilla rewrite of the
    // former jquery.stickytabs plugin; uses the BS5 Tab API directly.
    function initStickyTabs(navSelector, initialTab) {
        const nav = document.querySelector(navSelector);
        function showByHash() {
            const selector = location.hash
                ? `a[href="${location.hash}"]`
                : initialTab;
            const link = selector ? nav.querySelector(selector) : null;
            if (link) bootstrap.Tab.getOrCreateInstance(link).show();
        }

        showByHash();
        window.addEventListener("hashchange", showByHash);

        function onClick(e) {
            const [, hash] = e.currentTarget.href.split("#");
            if (history.pushState) {
                history.pushState(null, "", location.pathname + location.search + "#" + hash);
            }
        }
        nav.querySelectorAll("a").forEach((link) => link.addEventListener("click", onClick));

        return {
            destroy() {
                window.removeEventListener("hashchange", showByHash);
                nav.querySelectorAll("a").forEach((link) => link.removeEventListener("click", onClick));
                nav.querySelectorAll(".nav-link.active").forEach((link) => link.classList.remove("active"));
            }
        };
    }

    function disconnect() {
        [graphs, common.tables].forEach((o) => {
            Object.keys(o).forEach((key) => {
                o[key].destroy();
                delete o[key];
            });
        });

        // Remove sticky-tabs listeners and reset active tab state
        if (stickyTabsHandle) {
            stickyTabsHandle.destroy();
            stickyTabsHandle = null;
        }

        stopTimers();
        cleanCredentials();
        ui.connect();
    }

    // Bootstrap disables non-button nav items (li/a) via the `disabled` attribute
    // + `.disabled` class together; buttons additionally honour the attribute.
    function disableNav(el) {
        el.setAttribute("disabled", "disabled");
        el.classList.add("disabled");
    }
    function enableNav(el) {
        el.removeAttribute("disabled");
        el.classList.remove("disabled");
    }
    // Active dropdown item's numeric data-value (jQuery .data() coerced to number).
    function activeMenuValue(menuClass) {
        const el = document.querySelector(".dropdown-menu a.active." + menuClass);
        return el ? parseInt(el.dataset.value, 10) : null;
    }

    function tabClick(id) {
        let tab_id = id;
        if (document.querySelector(id)?.hasAttribute("disabled")) return;
        let navBarControls = Array.from(document.querySelectorAll("#selSrv, #navBar li, #navBar a, #navBar button"));
        if (id !== "#autoRefresh") navBarControls.forEach(disableNav);

        stopTimers();

        if (id === "#refresh" || id === "#autoRefresh") {
            const active = document.querySelector(".nav-link.active");
            if (active) tab_id = "#" + active.id;
        }

        document.getElementById("autoRefresh").classList.add("invisible");
        document.getElementById("refresh").classList.add("radius-right");

        function setAutoRefresh(refreshInterval, timer, callback) {
            const refreshBtn = document.getElementById("refresh");
            function countdown(interval) {
                Visibility.stop(timer_id.countdown);
                if (!interval) {
                    document.getElementById("countdown").textContent = "--:--";
                    return;
                }

                let timeLeft = interval;
                document.getElementById("countdown").textContent = "00:00";
                timer_id.countdown = Visibility.every(1000, 1000, () => {
                    timeLeft -= 1000;
                    document.getElementById("countdown").textContent = new Date(timeLeft).toISOString().substr(14, 5);
                    if (timeLeft <= 0) Visibility.stop(timer_id.countdown);
                });
            }

            refreshBtn.classList.remove("radius-right");
            document.getElementById("autoRefresh").classList.remove("invisible");

            countdown(refreshInterval);
            if (!refreshInterval) return;
            timer_id[timer] = Visibility.every(refreshInterval, () => {
                countdown(refreshInterval);
                if (refreshBtn.hasAttribute("disabled")) return;
                disableNav(refreshBtn);
                callback();
            });
        }

        if (["#scan_nav", "#selectors_nav", "#disconnect"].indexOf(tab_id) !== -1) {
            document.getElementById("refresh").classList.add("invisible");
        } else {
            document.getElementById("refresh").classList.remove("invisible");
        }

        switch (tab_id) {
            case "#status_nav":
                require(["app/stats"], (module) => {
                    const refreshInterval = activeMenuValue("preset");
                    setAutoRefresh(refreshInterval, "status",
                        () => module.statWidgets(graphs, checked_server));
                    if (id !== "#autoRefresh") module.statWidgets(graphs, checked_server);

                    common.show(".preset");
                    common.hide(".history");
                    common.hide(".dynamic");
                });
                break;
            case "#throughput_nav":
                require(["app/graph"], (module) => {
                    const selData = common.getSelector("selData"); // Graph's dataset selector state
                    const step = {
                        day: 60000,
                        week: 300000
                    };
                    let refreshInterval = step[selData] || 3600000;
                    document.getElementById("dynamic-item").textContent = (refreshInterval / 60000) + " min";

                    if (!activeMenuValue("dynamic")) {
                        refreshInterval = null;
                    }
                    setAutoRefresh(refreshInterval, "throughput",
                        () => module.draw(graphs, common.neighbours, checked_server, selData));
                    if (id !== "#autoRefresh") module.draw(graphs, common.neighbours, checked_server, selData);

                    common.hide(".preset");
                    common.hide(".history");
                    common.show(".dynamic");
                });
                break;
            case "#configuration_nav":
                require(["app/config"], (module) => {
                    module.getActions();
                    module.getMaps();
                });
                break;
            case "#symbols_nav":
                require(["app/symbols"], (module) => module.getSymbols());
                break;
            case "#scan_nav":
                require(["app/upload"], (module) => {
                    module.getClassifiers();
                    module.getFuzzyStorages();
                });
                break;
            case "#selectors_nav":
                require(["app/selectors"], (module) => module.displayUI());
                break;
            case "#history_nav":
                require(["app/history"], (module) => {
                    function getHistoryAndErrors() {
                        module.getHistory();
                        module.getErrors();
                    }
                    const refreshInterval = activeMenuValue("history");
                    setAutoRefresh(refreshInterval, "history",
                        () => getHistoryAndErrors());
                    if (id !== "#autoRefresh") getHistoryAndErrors();

                    common.hide(".preset");
                    common.show(".history");
                    common.hide(".dynamic");

                    module.updateHistoryControlsState();
                });
                break;
            case "#disconnect":
                disconnect();
                break;
            default:
        }

        setTimeout(() => {
            // Do not enable Refresh button until AJAX requests to all neighbours are finished
            if (tab_id === "#history_nav") navBarControls = navBarControls.filter((el) => el.id !== "refresh");

            navBarControls.forEach(enableNav);
        }, (id === "#autoRefresh") ? 0 : 1000);
    }

    function saveCredentials(password) {
        sessionStorage.setItem("Password", password);
    }

    function displayUI() {
        // In many browsers local storage can only store string.
        // So when we store the boolean true or false, it actually stores the strings "true" or "false".
        common.read_only = sessionStorage.getItem("read_only") === "true";

        common.query("auth", {
            success: function (neighbours_status) {
                const selSrv = document.getElementById("selSrv");
                selSrv.replaceChildren();
                selSrv.append(common.el("option", {value: "All SERVERS", text: "All SERVERS"}));
                neighbours_status.forEach((e) => {
                    const selected = checked_server === e.name;
                    const disabled = !selected && !e.status;
                    selSrv.insertAdjacentHTML("beforeend",
                        '<option value="' + common.escapeHTML(e.name) + '"' +
                        (selected ? " selected" : "") + (disabled ? " disabled" : "") + ">" +
                        common.escapeHTML(e.name) + "</option>");
                });
            },
            complete: function () {
                ajaxSetup(localStorage.getItem("ajax_timeout"));

                if (require.defined("app/upload")) require(["app/upload"], (module) => module.getClassifiers());

                document.querySelectorAll(".ro-disable").forEach((el) => { el.disabled = common.read_only; });
                if (common.read_only) {
                    common.hide(".ro-hide");
                    common.show(".ro-show");
                } else {
                    common.show(".ro-hide");
                    common.hide(".ro-show");
                }

                document.getElementById("preloader").classList.add("d-none");
                document.querySelectorAll("#navBar, #mainUI").forEach((el) => el.classList.remove("d-none"));

                stickyTabsHandle = initStickyTabs(".nav-tabs-sticky", "#status_nav");
            },
            errorMessage: "Cannot get server status",
            server: "All SERVERS"
        });
    }

    // Show the connect (login) dialog and wire its form submission.
    function showConnectDialog() {
        const connectDialog = document.getElementById("connectDialog");
        const connectForm = document.getElementById("connectForm");
        const connectPassword = document.getElementById("connectPassword");

        function clearFeedback() {
            connectPassword.oninput = null;
            connectPassword.classList.remove("is-invalid");
            common.hide("#authInvalidCharFeedback,#authUnauthorizedFeedback");
        }

        connectDialog.addEventListener("show.bs.modal", () => clearFeedback(), {once: true});
        connectDialog.addEventListener("shown.bs.modal", () => connectPassword.focus(), {once: true});
        bootstrap.Modal.getOrCreateInstance(connectDialog).show();

        connectForm.onsubmit = (e) => {
            e.preventDefault();
            const password = connectPassword.value;

            function invalidFeedback(tooltip) {
                connectPassword.classList.add("is-invalid");
                connectPassword.oninput = () => clearFeedback();
                common.show(tooltip);
            }

            if (!(/^[ -~]*$/).test(password)) {
                invalidFeedback("#authInvalidCharFeedback");
                connectPassword.focus();
                return;
            }

            common.query("auth", {
                headers: {
                    Password: password
                },
                success: function (json) {
                    const [{data}] = json;
                    connectPassword.value = "";
                    if (data.auth === "ok") {
                        sessionStorage.setItem("read_only", data.read_only);
                        saveCredentials(password);
                        connectForm.onsubmit = null;
                        bootstrap.Modal.getOrCreateInstance(connectDialog).hide();
                        displayUI();
                    }
                },
                error: function (_result, xhr, textStatus) {
                    if (xhr.statusText === "Unauthorized") {
                        invalidFeedback("#authUnauthorizedFeedback");
                    } else {
                        common.alertMessage("alert-modal alert-danger",
                            textStatus === "timeout" ? "Request timeout" : xhr.statusText);
                    }
                    connectPassword.value = "";
                    connectPassword.focus();
                },
                params: {
                    global: false,
                },
                server: "local"
            });
        };
    }


    // Public functions

    ui.connect = function () {
        // Prevent locking out of the WebUI if timeout is too low.
        let timeout = localStorage.getItem("ajax_timeout");
        if (timeout < defaultAjaxTimeout) timeout = defaultAjaxTimeout;
        ajaxSetup(timeout);

        // Query "/stat" to check if user is already logged in or client ip matches "secure_ip"
        const xhr = new XMLHttpRequest();
        xhr.open("GET", "stat", true);
        const ajaxTimeout = common.getAjaxTimeout();
        if (ajaxTimeout > 0) xhr.timeout = ajaxTimeout;
        xhr.onload = () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                try {
                    const data = JSON.parse(xhr.responseText);
                    sessionStorage.setItem("read_only", data.read_only);
                    displayUI();
                } catch (err) {
                    // A 2xx /stat whose body isn't JSON means a broken response
                    // (truncated by a proxy, a captive-portal page, etc.): show
                    // the login dialog rather than an empty, half-loaded UI.
                    showConnectDialog();
                }
            } else {
                showConnectDialog();
            }
        };
        xhr.onerror = () => showConnectDialog();
        xhr.ontimeout = () => showConnectDialog();
        xhr.send();
    };

    function updateThemeIcon(theme) {
        const iconMap = {light: "fa-sun", dark: "fa-moon", auto: "fa-display"};
        const icon = document.getElementById("theme-icon");
        if (icon) {
            icons.setIcon(icon, iconMap[theme] || "fa-display");
        }
    }

    // Check the radio within `selector` whose value matches.
    function checkRadio(selector, value) {
        document.querySelectorAll(selector).forEach((radio) => {
            radio.checked = radio.value === value;
        });
    }

    (function initSettings() {
        let selected_locale = null;
        let custom_locale = null;
        const localeTextbox = ".popover #settings-popover #locale";
        const historyCountDef = 1000;
        const historyCountSelector = ".popover #settings-popover #settings-history-count";

        function validateLocale(saveToLocalStorage) {
            const localeInput = document.querySelector(localeTextbox);
            function toggle_form_group_class(remove, add) {
                localeInput.classList.remove("is-" + remove);
                localeInput.classList.add("is-" + add);
            }

            const now = new Date();

            if (custom_locale.length) {
                try {
                    now.toLocaleString(custom_locale);

                    if (saveToLocalStorage) localStorage.setItem("custom_locale", custom_locale);
                    common.locale = (selected_locale === "custom") ? custom_locale : null;
                    toggle_form_group_class("invalid", "valid");
                } catch (err) {
                    common.locale = null;
                    toggle_form_group_class("valid", "invalid");
                }
            } else {
                if (saveToLocalStorage) localStorage.setItem("custom_locale", null);
                common.locale = null;
                localeInput.classList.remove("is-valid", "is-invalid");
            }

            // Display date example
            document.querySelector(".popover #settings-popover #date-example").textContent =
                (common.locale)
                    ? now.toLocaleString(common.locale)
                    : now.toLocaleString();
        }

        const settingsBtn = document.getElementById("settings");
        bootstrap.Popover.getOrCreateInstance(settingsBtn, {
            container: "body",
            placement: "bottom",
            html: true,
            sanitize: false,
            content: function () {
                // Using .clone() has the side-effect of producing elements with duplicate id attributes.
                return document.getElementById("settings-popover").cloneNode(true);
            }
        });
        // Restore the tooltip of the element that the popover is attached to.
        const originalTitle = settingsBtn.getAttribute("data-original-title");
        if (originalTitle !== null) settingsBtn.setAttribute("title", originalTitle);
        settingsBtn.addEventListener("click", (e) => {
            e.preventDefault();
        });
        settingsBtn.addEventListener("inserted.bs.popover", () => {
            selected_locale = localStorage.getItem("selected_locale") || "browser";
            custom_locale = localStorage.getItem("custom_locale") || "";
            validateLocale();

            checkRadio('.popover #settings-popover input[type="radio"][name="locale"]', selected_locale);
            document.querySelector(localeTextbox).value = custom_locale;

            ajaxSetup(localStorage.getItem("ajax_timeout"), true);

            document.querySelector(historyCountSelector).value =
                parseInt(localStorage.getItem("historyCount"), 10) || historyCountDef;

            // Restore theme selection
            const savedTheme = localStorage.getItem("theme") || "auto";
            checkRadio('.popover #settings-popover input[type="radio"][name="theme"]', savedTheme);
        });
        common.delegate(document, "change", '.popover #settings-popover input[type="radio"][name="locale"]',
            (event, target) => {
                selected_locale = target.value;
                localStorage.setItem("selected_locale", selected_locale);
                validateLocale();
            });
        common.delegate(document, "input", localeTextbox, (event, target) => {
            custom_locale = target.value;
            validateLocale(true);
        });
        common.delegate(document, "change", '.popover #settings-popover input[type="radio"][name="theme"]',
            (event, target) => {
                const theme = target.value;
                if (window.rspamd && window.rspamd.theme) {
                    window.rspamd.theme.applyPreference(theme);
                }
                updateThemeIcon(theme || "auto");
            });
        updateThemeIcon(localStorage.getItem("theme") || "auto");
        common.delegate(document, "input", ajaxTimeoutBox, (event, target) => {
            ajaxSetup(target.value, false, true);
        });
        common.delegate(document, "click", ".popover #settings-popover #ajax-timeout-restore", () => {
            ajaxSetup(null, true, true);
        });

        common.delegate(document, "input", historyCountSelector, (event, target) => {
            const v = parseInt(target.value, 10);
            if (v > 0) {
                localStorage.setItem("historyCount", v);
                target.classList.remove("is-invalid");
                const historyCount = document.getElementById("history-count");
                historyCount.value = v;
                historyCount.dispatchEvent(new Event("change", {bubbles: true}));
            } else {
                target.classList.add("is-invalid");
            }
        });
        common.delegate(document, "click", ".popover #settings-popover #settings-history-count-restore",
            () => {
                localStorage.removeItem("historyCount");
                document.querySelector(historyCountSelector).value = historyCountDef;
            });

        // Dismiss Bootstrap popover by clicking outside
        document.body.addEventListener("click", (e) => {
            document.querySelectorAll(".popover").forEach((popover) => {
                const triggerBtn = e.target.closest("button");
                if (popover.contains(e.target) ||
                    (triggerBtn && triggerBtn.getAttribute("aria-describedby") === popover.id)) return;
                bootstrap.Popover.getOrCreateInstance(settingsBtn).hide();
            });
        });
    }());

    document.getElementById("selData").addEventListener("change", () => {
        tabClick("#throughput_nav");
    });

    function refreshSpinStart() {
        document.querySelectorAll("#refresh > svg").forEach((el) => el.classList.add("fa-spin"));
    }
    function refreshSpinStop() {
        setTimeout(() => {
            document.querySelectorAll("#refresh > svg").forEach((el) => el.classList.remove("fa-spin"));
        }, 1000);
    }

    // Drive the refresh spinner from common.query (XHR) requests.
    common.onAjaxStart(refreshSpinStart);
    common.onAjaxComplete(refreshSpinStop);

    document.querySelectorAll('a[data-bs-toggle="tab"]').forEach((link) => {
        link.addEventListener("shown.bs.tab", (e) => {
            tabClick("#" + e.currentTarget.id);
        });
    });
    document.querySelectorAll("#refresh, #disconnect").forEach((btn) => {
        btn.addEventListener("click", (e) => {
            e.preventDefault();
            tabClick("#" + e.currentTarget.id);
        });
    });
    document.querySelectorAll(".dropdown-menu a").forEach((item) => {
        item.addEventListener("click", (e) => {
            e.preventDefault();
            const match = (/\b(?:dynamic|history|preset)\b/).exec(e.currentTarget.className);
            if (!match) return;
            const [menuClass] = match;
            document.querySelectorAll(".dropdown-menu a.active." + menuClass)
                .forEach((el) => el.classList.remove("active"));
            e.currentTarget.classList.add("active");
            tabClick("#autoRefresh");
        });
    });

    document.getElementById("theme-toggle").addEventListener("click", (e) => {
        e.preventDefault();
        const currentTheme = localStorage.getItem("theme") || "auto";
        // Cycle through: light -> dark -> auto -> light
        const themeMap = {light: "dark", dark: "auto", auto: "light"};
        const newTheme = themeMap[currentTheme] || "light";

        if (window.rspamd && window.rspamd.theme) {
            window.rspamd.theme.applyPreference(newTheme);
        }
        updateThemeIcon(newTheme);

        // Update radio button in settings popover if it's open
        checkRadio('.popover #settings-popover input[type="radio"][name="theme"]', newTheme);
    });

    document.getElementById("selSrv").addEventListener("change", (e) => {
        checked_server = e.currentTarget.value;
        if (checked_server === "All SERVERS") {
            document.getElementById("learnServers").classList.remove("invisible");
        } else {
            document.getElementById("learnServers").classList.add("invisible");
        }
        const active = document.querySelector("#tablist > .nav-item > .nav-link.active");
        if (active) tabClick("#" + active.id);
    });

    // Radio buttons
    common.delegate(document, "click", 'input[type="radio"][name="clusterName"]', (event, target) => {
        if (!target.disabled) {
            checked_server = target.value;
            tabClick("#status_nav");
        }
    });

    document.getElementById("loading").classList.add("d-none");

    return ui;
});
