/// <reference path="..\..\GSMyAdmin\WebRoot\Scripts\UI.js" />
/// <reference path="..\..\GSMyAdmin\WebRoot\Scripts\API.js" />
/// <reference path="..\..\GSMyAdmin\WebRoot\Scripts\Knockout-3.5.1.js" />

/* global API,UI,PluginHandler,ko */
/* eslint eqeqeq: "off", curly: "error", "no-extra-parens": "off" */
const nextSpecialNotice = 3;

this.plugin = {
    PreInit: function () {
        //Called prior to the plugins initialisation, before the tabs are loaded.
        //This method must not invoke any module/plugin specific API calls.
    },

    PostInit: function () {
        //The tabs have been loaded. You should wire up any event handlers here.
        //You can use module/specific API calls here.
        $("#tab_ADSModule_InstanceContextMenu").on("click", function () { UI.HideWizard(); });
    },

    SettingChanged: function (node, value) {
        //Invoked whenever a setting is changed. Only applies to changes in the current session rather
        //than made by other users.
    },

    AMPDataLoaded: async function () {
        //Any data you might need has been loaded, you should wire up event handlers which use data such as settings here.
        if (GetSetting("ADSModule.ADS.Mode") == 0) {
            API.Core.GetUpdates.clearInterval();
            firstTimeSetup();
            return;
        }

        UI.GetSideMenuItem("tab_console")?.title("Deployment Log");
        UI.GetSideMenuItem("tab_LocalFileBackupPlugin_Backups")?.visible(false);
        UI.GetSideMenuItem("tab_settings")?.children()?.find(c => c.tab() == "#tab_settings_loaded_Updates, #tab_settings")?.visible(false);
        UI.GetSideMenuItem("tab_settings")?.children()?.find(c => c.tab() == "#tab_settings_loaded_Backups, #tab_settings")?.visible(false);

        UI.HideStatusTab();

        ADSinit();
        checkKeyLengthValid();

        let cont = $("<div>", { "id": "remoteContainer" });
        cont.load("/Plugins/ADSModule/RemoteInstance.html");
        $("#mainBody").append(cont);

        Features.Search.RegisterSearchProvider(instanceSearchResults);

        $("#tab_ADSModule_Instances .splitViewBody").on("click", () => $("#selectedInstanceInfo").removeClass("visible"));

        if (API.WebsocketsEnabled()) { API.Core.GetUpdates.clearInterval(); }

        const ampinstmgrVersion = viewModels.support.toolsVersion();
        const minVersion = "2.4.5.0";
        if (Version.parse(ampinstmgrVersion).olderThan(minVersion) && ampinstmgrVersion != "0.0") {
            const showUpdateInfoResult = await UI.ShowModalAsync("Outdated System Tools", `The AMP instance manager command line tools are out of date. You are currently running ${ampinstmgrVersion} but the minimum required version is ${minVersion}. Upgrading is required to continue using AMP due to changes in how AMP handles updates and manages instances. Continuing to use AMP without upgrading your system tools carries a high risk of causing damage to your installation.`, UI.Icons.Exclamation, [
                new UI.ModalAction("Show update instructions", true, "bgGreen"),
                new UI.ModalAction("Ignore warning", false, "bgRed"),
            ]);
            if (showUpdateInfoResult) {
                window.open("https://discourse.cubecoders.com/docs?topic=2297?utm_source=toolsupdatewarning#updating-the-instance-manager-3", "_blank");
            }
        }

        if (userHasPermission("Settings.Core.AMP.LastSpecialNoticeID") &&
            GetSetting("Core.AMP.LastSpecialNoticeID") < nextSpecialNotice &&
            GetSetting("ADSModule.ADS.Mode") != 30) {
            specialNotice();
        }

        if (GetSetting("ADSModule.ADS.AlertTitle")?.trim() != "" && GetSetting("ADSModule.ADS.AlertMessage")?.trim() != "") {
            UI.CreateLocalAnnouncement(GetSetting("ADSModule.ADS.AlertTitle"), GetSetting("ADSModule.ADS.AlertMessage"), true);
        }
    },

    PushedMessage: function (source, message, parameters) {
        //Invoked when your associated plugin invokes IPluginMessagePusher.Push(message, parameters) - you only receive messages pushed
        //from your plugin and not from other plugins/modules.  
        switch (message.toLocaleLowerCase()) {
            case "refresh":
                updateInstanceList();
                break;
            case "refreshgroup":
                manageInstancesVM.refreshGroup(parameters);
                break;
            case "instupdate":
                {
                    let instance = manageInstancesVM.findInstance(parameters.Id);
                    if (instance == null) {
                        manageInstancesVM.refreshGroup(parameters.Target);
                    }
                    else {
                        instance?.refresh(parameters.Instance);
                    }
                    break;
                }
            case "instdelete":
                {
                    let instance = manageInstancesVM.findInstance(parameters);
                    instance?.group.instances.remove(instance);
                    break;
                }
            case "refreshapplist":
                updateNewAppInfo();
                break;
            case "metrics":
                {
                    let instance = manageInstancesVM.findInstance(parameters.InstanceID);
                    if (instance != null) {
                        instance.refreshMetrics(parameters.Metrics);
                        instance.appState(parameters.State);
                    }
                    break;
                }
            case "remotepairstatus":
                manageInstancesVM.notifyPairComplete(parameters);
                break;
            default:
                break;
        }
    }
};

this.tabs = [
    {
        File: "Instances.html",
        ExternalTab: false,
        ShortName: "Instances",
        Name: "Instances",
        Icon: "dns",
        Light: false,
        Category: "",
        Order: -100,
        Click: () => {
            if (remoteInstanceId != null) {
                closeRemote();
            }
        },
        IsDefault: true,
        ViewModel: "InstanceManagementVM",
        BodyClass: "",
        PopHandler: handleInstancesNavigatePopstate,
    },
    {
        File: "Templates.html",
        ShortName: "Templates",
        Name: "Templates",
        Icon: "app_registration",
        Order: -10,
        Click: () => { manageTemplatesVM.refresh(true); },
        RequiredPermission: "ADSModule.TemplateManagement.ManageTemplates",
        BodyClass: "noPaddingTab",
        FeatureSet: "Templating",
        ViewModel: "TemplateManagementVM",
        PopHandler: (t, s) => { manageTemplatesVM.handlePop(t, s); },
    },
    {
        File: "Datastores.html",
        ShortName: "Datastores",
        Name: "Datastores",
        Icon: "storage",
        Order: -11,
        Click: () => { manageDatastoresVM.refresh(); },
        RequiredPermission: "ADSModule.DatastoreManagement.ManageDatastores",
    },
    {
        File: "DatastoreAddEditPopup.html",
        ShortName: "DatastoreInfoPopup",
        Name: "DatastoreInfoPopup",
        IsWizard: true,
    },
    {
        File: "CreateInstanceWizard.html",
        ExternalTab: false,
        ShortName: "CreateInstanceWizard",
        Name: "Create New Instance",
        Icon: "",
        Light: false,
        Category: "",
        IsWizard: true
    },
    {
        File: "CloneInstanceWizard.html",
        ExternalTab: false,
        ShortName: "CloneInstanceWizard",
        Name: "Clone Instance",
        Icon: "",
        Light: false,
        Category: "",
        IsWizard: true
    },
    {
        File: "DeployTemplateWizard.html",
        ExternalTab: false,
        ShortName: "DeployTemplateWizard",
        Name: "Deploy Template",
        Icon: "",
        Light: false,
        Category: "",
        IsWizard: true
    },
    {
        File: "InstanceInfoPopup.html",
        ShortName: "InstanceInfoPopup",
        Name: "InstanceInfoPopup",
        Category: "Settings",
        IsWizard: true
    },
    {
        File: "TargetInfoPopup.html",
        ShortName: "TargetInfoPopup",
        Name: "TargetInfoPopup",
        Icon: "",
        Light: false,
        Category: "Settings",
        IsWizard: true
    },
    {
        File: "PairTargetPopup.html",
        ShortName: "PairTargetPopup",
        Name: "PairTargetPopup",
        Icon: "",
        Light: false,
        Category: "Settings",
        IsWizard: true
    }
];

this.features = {
    PropagateAuthServer: function () { alert("lol"); },
    RunSetup: firstTimeSetup,
    ShowNotice: specialNotice
};

this.stylesheet = "StyleSheet.css";    //Styles for tab-specific styles

const ADSModes = {
    Controller: "10",
    Hybrid: "20",
    Target: "30",
    Standalone: "100"
};

async function checkKeyLengthValid() {
    const setting = GetSetting("ADSModule.Defaults.NewInstanceKey");
    if (setting == null) { return; }
    const keyLen = setting.length;
    if ((keyLen < 32 || keyLen > 36) && keyLen != 0) {
        //Licence key has possibly been lost.
        const msg = "Licence key was wrong length. Expected between 32 and 36 or 0, got " + keyLen.toString();
        await UI.ShowModalAsync("Reactivation Required", "AMP has lost its licence key due to a hardware or system configuration change, please re-enter your licence key. See Configuration > Instance Deployment > Deployment Defaults > Licence Key. You may also need to reactivate your existing instances on this host.", UI.Icons.Exclamation, UI.OKActionOnly, null, null, msg);
        currentSettings["ADSModule.Defaults.NewInstanceKey"].highlight();
    }
}

async function specialNotice() {
    let SpecialNoticeVM = new function () {
        this.page = ko.observable(0);
        this.newTheme = ko.observable(false);
        this.showThemeChange = userHasPermission("Settings.Core.AMP.Theme");

        this.next = function () {
            if (this.page() < 1) {
                this.page(this.page() + 1);
            }
            else {
                this.finish();
            }
        };

        this.finish = async function () {
            currentSettings["Core.AMP.LastSpecialNoticeID"].value(nextSpecialNotice);
            if (this.newTheme() === true) {
                currentSettings["Core.AMP.Theme"].value("Deimos");
                await API.Core.SetConfigAsync("Core.AMP.Theme", "Deimos");
                $("#themeLink").attr("href", "/theme?" + Date.now())
            }
            $(".snVisible").removeClass("snVisible");
            setTimeout(() => $(".specialNoticeBg").parent().remove(), 2000);
        }
    };

    const contents = $("<div>").load("/Plugins/ADSModule/SpecialNotice.html", function () {
        RegisterViewmodel(SpecialNoticeVM, "SpecialNoticeVM");
        setTimeout(function () { $(".specialNoticeBg").addClass("snVisible"); }, 100);
    });
    $("#mainBody").append(contents);
}

async function firstTimeSetup() {
    let FirstStartVM = new function () {
        const self = this;
        this.privErrorReports = ko.observable(true);
        this.privAnalytics = ko.observable(true);
        this.privLicenceReporting = ko.observable(true);
        this.denyAll = async function () {
            self.privErrorReports(false);
            self.privAnalytics(false);
            self.privLicenceReporting(false);
        };
        this.page = ko.observable(0);
        this.mode = ko.observable("100");
        this.modeName = ko.computed(() => {
            switch (self.mode()) {
                case "10": return "Controller";
                case "20": return "Hybrid";
                case "30": return "Target";
                case "100": return "Standalone";
            }
        });
        this.url = ko.observable(document.location.hostname == "localhost" ? "http://localhost:" + document.location.port : document.location.origin);
        this.controllerPairingCode = ko.observable("");
        this.friendlyName = ko.observable(document.location.host);
        this.licenceKey = ko.observable("");
        this.allowsControllerTarget = ko.observable(false);
        this.showControls = ko.observable(true);
        this.offerAuthnSetup = navigator.credentials !== undefined;
        this.setupWebauthn = async function () {
            await viewModels.userinfo.setupWebauthn();
            self.nextPage();
        }
        this.setupTwoFactor = async function () {
            await viewModels.userinfo.enableTwoFactor();
            self.nextPage();
        }
        this.pageStack = [];
        this.nextPage = function () {
            self.pageStack.push(self.page());

            const currentPage = self.page();

            switch (currentPage) {
                case 0:
                    if (viewModels.support.diagdata.Virtualization == "Docker" || viewModels.support.diagdata.Virtualization == "Podman") {
                        self.page(101);
                        break;
                    }
                    self.page(1);
                    break;
                case 1:
                    if (self.licenceKey() == "") {
                        self.page(3);
                        break;
                    }
                    self.checkLicenceKey();
                    self.page(2);
                    break;
                case 3:
                    {
                        let nextPage = -1;
                        switch (self.mode()) {
                            case ADSModes.Controller: //Fall
                            case ADSModes.Standalone: //Fall
                            case ADSModes.Hybrid: nextPage = 6; break;
                            case ADSModes.Target:
                                nextPage = 4;
                                self.url("");
                                if (self.friendlyName().includes("localhost")) {
                                    self.friendlyName("");
                                }
                                break;
                        }

                        if (nextPage > -1) {
                            self.page(nextPage);
                        }
                        break;
                    }
                case 4:
                    self.pairWithController();
                    break;
                case 6:
                    self.finish();
                    break;
                case 7:
                    self.page(this.offerAuthnSetup ? 8 : 9);
                    break;
                case 101:
                    self.page(1);
                    break;
                default:
                    self.page(currentPage + 1);
                    break;
            }

            switch (self.page()) {
                case 2:
                    $("#ftsFriendlyName").trigger("focus");
                    break;
                case 4:
                    $("#enterLicenceKey").trigger("focus");
                    break;
            }
        };
        this.prevPage = () => {
            const newPage = self.pageStack.pop();
            self.page(newPage);
        };
        this.checkLicenceKey = async function () {
            const keyCheckResult = await API.Core.ActivateAMPLicenceAsync(self.licenceKey());
            if (!keyCheckResult.Status) {
                UI.ShowModalAsync("Failed to activate AMP", keyCheckResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
                self.page(1);
            }
            else if (keyCheckResult.Result.GradeName == "Developer") {
                UI.ShowModalAsync("Incorrect Key Type", "Developer licences cannot be used to run AMP. Please enter a valid licence key.", UI.Icons.Exclamation, UI.OKActionOnly);
                self.page(1);
            }
            else {
                self.allowsControllerTarget(keyCheckResult.Result.GradeName != "Standard")
                self.page(3);
            }
        };
        this.restartAMP = async () => {
            self.showControls(false);
            self.page(10);
            localStorage.SavedToken = null;
            localStorage.SavedUsername = null;
            await API.Core.RestartAMP();
            await sleepAsync(10000);
            API.Core.GetModuleInfo.setInterval(5000, function (data) { location.reload(); });
        };
        this.pairWithController = async function () {
            if (self.url() == "" || self.controllerPairingCode() == "") {
                return;
            }

            self.page(5);
            self.showControls(false);

            const result = await API.ADSModule.RegisterTargetWithCodeAsync(self.url, document.location.toString(), self.controllerPairingCode, self.friendlyName);

            if (result.Status === true) {
                self.showControls(true);
                self.page(6);
            }
            else {
                await UI.ShowModalAsync("Failed to pair with controller", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
                self.showControls(true);
                self.page(4);
            }
        };
        this.finish = async function () {
            self.page(7);
            self.showControls(false);

            await sleepAsync(1000);

            const defaultInstanceSettings = {
                "Core.Privacy.AutoReportFatalExceptions": self.privErrorReports(),
                "Core.Privacy.AllowAnalytics": self.privAnalytics(),
                "Core.Privacy.EnhancedLicenceReporting": self.privLicenceReporting()
            };

            let useAuthServer;
            if (self.mode() == ADSModes.Target) {
                useAuthServer = self.url();
            } else if (self.mode() == ADSModes.Standalone) {
                useAuthServer = `http://localhost:${viewModels.support.basePort()}`;
            } else {
                useAuthServer = document.location.origin;
            }

            const applySettings = {
                "ADSModule.ADS.Mode": self.mode(),
                "ADSModule.Limits.CreateLocalInstances": self.mode() != ADSModes.Controller,
                "Core.Security.EnablePassthruAuth": self.mode() != ADSModes.Target,
                "Core.AMP.MapAllPluginStores": true,
                "Core.Login.UseAuthServer": self.mode() == ADSModes.Target,
                "Core.Login.AuthServerURL": useAuthServer,
                "Core.Privacy.AutoReportFatalExceptions": self.privErrorReports(),
                "Core.Privacy.AllowAnalytics": self.privAnalytics(),
                "Core.Privacy.EnhancedLicenceReporting": self.privLicenceReporting(),
                "ADSModule.Defaults.DefaultSettings": JSON.stringify(defaultInstanceSettings),
                "ADSModule.Network.DefaultIPBinding": "127.0.0.1",
                "ADSModule.Defaults.DefaultAuthServerURL": useAuthServer,
                "Core.AMP.LastSpecialNoticeID": nextSpecialNotice,
            };

            if (self.licenceKey() !== "") {
                applySettings["ADSModule.Defaults.NewInstanceKey"] = self.licenceKey().trim();
            }

            const setConfigResult = await API.Core.SetConfigsAsync(applySettings);

            if (self.privAnalytics()) {
                loadAnalytics(true);
                const systemData = await API.Core.GetDiagnosticsInfoAsync();
                plausible("Install", {
                    props: {
                        "SetupMode": self.modeName(),
                        "WebauthnAvailable": self.offerAuthnSetup,
                        "KeyProvided": self.licenceKey() !== "",
                        "OS": systemData.OS,
                        "Platform": systemData.Platform,
                        "Arch": systemData["System Type"],
                        "Virtualization": systemData.Virtualization,
                        "Secure": window.location.protocol === "https:",
                        "AMPVersion": viewModels.support.installedVersion(),
                        "CPUModel": systemData["CPU Model"],
                        "InstalledRAM": systemData["Installed RAM"],
                        "ReleaseStream": systemData["Release Stream"]
                    }
                })
            }

            if (setConfigResult === true) {
                self.showControls(true);
                self.nextPage();
            }
            else {
                await UI.ShowModalAsync("Something went wrong", "Failed to apply configuration changes.", UI.Icons.Exclamation, UI.OKActionOnly);
                self.showControls(true);
                self.page(3);
            }
        };
    }();

    $("#mainBodyArea, #sideMenuContainer, #barTop").remove();

    if (parent != window) {
        await UI.ShowModalAsync("Unable to configure", "ADS cannot be configured while embedded or managed from a controller. Please open this ADS instance in its own browser tab directly to configure.", UI.Icons.Exclamation, [new UI.ModalAction("OK")]);
        parent.closeRemote();
        return;
    }

    $("#mainBody").css("top", 0);
    $("#mainBody").load("/Plugins/ADSModule/FirstTimeSetup.html", function () {
        RegisterViewmodel(FirstStartVM, "FirstTimeSetupVM");
    });
    API.Core.GetUpdates.clearInterval();
}

window.nextWindowReady ??= {};
let remoteInstanceId = null;
let lastRemoteInstanceId = null;

window.NotifyRemoteReady = (id) => {
    if (window.nextWindowReady[id] == null) {
        console.log(`NextWindowReady ${id} isn't set, but we got a ready request for it.`)
        return;
    }
    window.nextWindowReady[id]();
    window.nextWindowReady[id] = null;
    delete window.nextWindowReady[id];
};

function loadInstance(id) {
    const target = `${document.location.origin}/instance/${id}`;

    const p = new Promise(resolve => window.nextWindowReady[id] = resolve);

    $("#remoteInstance").attr("src", target);

    lastRemoteInstanceId = id;

    return p;
}

function closeRemote() {
    $("#remoteInstance").attr("src", "about:blank");
    $("#remoteInstance, #remoteContainer").hide();
    $("#tabTitle").text("Instances");
    $("#returnADS").hide();
    remoteInstanceId = null;
    UI.NavigateTo("/instances");
}

function PortUsageVM() {
    const self = this;
    this._OriginalPortNumber = 0;
    this.PortNumber = ko.observable(0);
    this.Protocol = 0;
    this.ProvisionNodeName = "";
    this.Verified = false;
    this.Description = "";
    this.IsUserDefined = false;
    // Child ports are derived (parent.Port + Offset): shown read-only, never sent as their own mapping.
    this.IsChild = false;
    this.ParentRef = "";
    this.ParentNode = ""; // full provision-node of the parent; backend-emitted, matched in linkChildPorts
    this.Offset = 0;
    this.parent = null; // set by linkChildPorts after the list is mapped
    this.vm = null;

    this.PortNumber.subscribe((newValue) => {
        if (self._OriginalPortNumber != 0) { return; }
        self._OriginalPortNumber = newValue;
    });
    this.IsDirty = ko.computed(() => !self.IsChild && self.PortNumber() != self._OriginalPortNumber);

    // What the UI shows for the port. For a child it tracks its parent live (parent.Port + Offset) so it
    // updates as the parent is edited; for everything else it's just the editable PortNumber.
    this.DisplayPort = ko.computed(() => self.parent ? Number(self.parent.PortNumber()) + self.Offset : self.PortNumber());

    this.Delete = async function () {
        const deleteMappingResult = await API.ADSModule.ModifyCustomFirewallRuleAsync(self.vm.id, self.PortNumber, 1, self.Protocol, self.Description, false);
        if (!deleteMappingResult.Status) {
            await UI.ShowModalAsync("Unable to delete port exemption", deleteMappingResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
        else {
            self.vm.editPortMappings(true);
        }
    };
}

// Point each child port VM at its parent VM (matched by the backend-emitted ParentNode against the parent's
// ProvisionNodeName) so its read-only DisplayPort tracks the parent live as it's edited.
function linkChildPorts(portMappings) {
    for (const child of portMappings) {
        if (!child.IsChild || !child.ParentNode) { continue; }
        child.parent = portMappings.find(p => !p.IsChild && p.ProvisionNodeName === child.ParentNode) || null;
    }
}

function fixedOrWhole(num, decimals) {
    const fixed = num.toFixed(decimals);
    if (Number.parseFloat(fixed) === Math.floor(num)) {
        return Math.floor(num).toString();
    }
    return fixed;
}

function InstanceMetricVM(name, color, value, maxValue, units, shortName) {
    const self = this;
    this.name = name;
    this.value = ko.observable(value || 0);
    this.maxValue = maxValue || 100;
    this.percentage = ko.computed(() => (self.value() / self.maxValue) * 100);
    this.color = color || "#fff";
    this.units = units;
    this.shortName = shortName;
    if (shortName == "" || shortName == undefined) {
        this.shortName = name.replace(" Usage", "").replace("Active ", "");
    }
    this.icon = ko.computed(() => {
        const iconHints = {
            "CPU Usage": "memory",
            "Memory Usage": "memory_alt",
            "Active Users": "group",
            "TPS": "speed",
        }

        return iconHints.hasOwnProperty(self.name) ? iconHints[self.name] : "bar_chart";
    });
    this.text = ko.computed(() => {

        if (self.units === "%") {
            return `${self.value()}%`;
        } else if (self.units === "MB" && (self.value() > 1024 || self.maxValue > 1024)) {
            const gbValue = fixedOrWhole(self.value() / 1024, 2);
            const gbMaxValue = fixedOrWhole(self.maxValue / 1024, 2);
            return `${gbValue}/${gbMaxValue} GB`;
        } else {
            return `${self.value()}/${self.maxValue} ${self.units}`;
        }
    });
}

async function ManageInstance(id, name, friendlyname, targetURL, instance, fromViewState = false) {
    instance.busy(true);
    setTimeout(() => instance.busy(false), 10000);

    const [manageResult] = await Promise.all([
        API.ADSModule.ManageInstanceAsync(id),
        loadInstance(id)
    ]);

    if (!manageResult.Status) {
        instance.busy(false);
        closeRemote();
        switch (manageResult.SupportTitle) {
            case "VERMISMATCH_ADS":
                {
                    //Instance is newer than ADS
                    const ADSUpgradeResult = await UI.ShowModalAsync("ADS upgrade required", `This ADS installation needs to be upgraded access this instance, as this instance is a newer version than this ADS installation. ADS is API version ${viewModels.support.apiVersion()}, but the instance is version ${instance.installedVersion()}. Please update ADS to manage this instance.`, UI.Icons.Info, [
                        new UI.ModalAction("Upgrade ADS", true, "bgGreen slideIcon icons_download"),
                        new UI.ModalAction("Cancel", false)
                    ]);

                    if (ADSUpgradeResult) {
                        viewModels.support.upgradeAMP();
                    }
                    return;
                }
            case "VERMISMATCH_INST":
                {
                    //ADS is newer than instance
                    const instanceUpgradeResult = await UI.ShowModalAsync("Instance upgrade required", `This instance needs to be upgraded to be accessed from this ADS instance. It is currently ${instance.installedVersion()}, but ADS is API version ${viewModels.support.apiVersion()}. Please update this instance to manage it from ADS.`, UI.Icons.Info, [
                        new UI.ModalAction("Upgrade Instance", true, "bgGreen slideIcon icons_download"),
                        new UI.ModalAction("Cancel", false)
                    ]);

                    if (instanceUpgradeResult) {
                        instance.update();
                    }
                    return;
                }
        }

        UI.ShowModalAsync("Cannot manage this instance", manageResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        return;
    }

    const token = manageResult.Result;

    if (token == null || token == "") {
        instance.busy(false);
        UI.ShowModalAsync("Cannot manage this instance", "ADS failed to provide a login token.", UI.Icons.Exclamation, UI.OKActionOnly);
        return;
    }
    const caption = `${friendlyname} (${name})`;
    const connectViaTarget = currentSettings['ADSModule.Network.AccessMode'].value() == 20;
    const shortId = id.substr(0, 8);
    const popState = ["instances", shortId];
    const contentWindow = document.getElementById("remoteInstance").contentWindow;

    contentWindow.performADSLogin(id, localStorage.SavedUsername, token, name, API.GetSessionID(), closeRemote, caption, targetURL, connectViaTarget, function (success, result, resultReason) {
        instance.busy(false);
        if (!success) {
            closeRemote();
            const msg = UI.GetLoginText(result);
            UI.ShowModalAsync("Failed to login to remote instance - " + msg.title, msg.message + " - " + resultReason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
        else {
            $("#remoteInstance, #remoteContainer").show();
            remoteInstanceId = shortId;
            if (!fromViewState) {
                UI.NavigateTo("/" + popState.join("/"));
            }
        }
    }, instance.DisplayImageURI(), handleInstancePopstate, popState, queryPopstate);
}

function handleInstancePopstate(parts) {
    UI.NavigateTo("/" + parts.join("/"));
}

function queryPopstate() { return window.location.pathname; }

function handleInstancesNavigatePopstate(_, parts) {
    if (parts.length < 2 && remoteInstanceId != null) {
        closeRemote();
    }
    const instanceToManage = parts[1];

    const inst = manageInstancesVM.findInstanceShort(instanceToManage);
    if (inst == null) { return; }

    if (remoteInstanceId != null && remoteInstanceId != instanceToManage) {
        closeRemote();
    }

    if (remoteInstanceId == instanceToManage) {
        document.getElementById("remoteInstance").contentWindow.UI.RootViewchange("/" + parts.join("/"));
    }
    else {
        inst.manage(null, null, true);
    }
}

function GetDisplayImageURI(imageSource) {
    const source = /^([a-z]+?):(.+?)$/.exec(imageSource);

    if (source == null) { return `/Plugins/ADSModule/Images/UnknownApp.png`; }

    const sourceType = source[1];
    const data = source[2];

    if (data == "") { return `/Plugins/ADSModule/Images/UnknownApp.png`; }

    switch (sourceType) {
        case "url": return data;
        case "steam": return `https://cdn.cloudflare.steamstatic.com/steam/apps/${data}/header.jpg`;
        case "internal": return `/Plugins/ADSModule/Images/${data}.jpg`;
        default: return `/Plugins/ADSModule/Images/UnknownApp.png`;
    }
}

function MountBindingVM(key, value) {
    const self = this;
    this.isReadOnly = ko.observable(value.endsWith(":ro"));
    value = value.replace(/:ro$/, "");
    this.key = ko.observable(key);
    this.value = ko.observable(value);
    this.originalKey = key;
    this.originalValue = value;
    this.originalIsReadOnly = this.isReadOnly();
    this.isDirty = ko.computed(() => self.key() != self.originalKey || self.value() != self.originalValue || self.isReadOnly() != self.originalIsReadOnly);
}

const retiredModules = new Set(["Factorio", "StarBound", "Terraria", "TheForest", "SevenDays", "SpaceEngineers", "Arma3", "ARK", "srcds", "FiveM"])

function AMPInstanceVM(instance, group) {
    const self = this;
    this.group = group;

    if (instance.Group == "null" || instance.Group == null) {
        instance.Group = "";
    }

    this.id = instance.InstanceID;
    this.name = instance.InstanceName;
    this.friendlyname = ko.observable(instance.FriendlyName || "");
    this.description = ko.observable(instance.Description || "");
    this.welcomeMessage = ko.observable(instance.WelcomeMessage || "");
    this.module = instance.Module;
    this.isRetired = retiredModules.has(this.module);
    this.displayModule = instance.ModuleDisplayName ?? instance.Module;
    this.running = ko.observable(instance.Running);
    this.selected = ko.observable(false);
    this.ip = ko.observable(instance.IP);
    this.port = ko.observable(instance.Port);
    this.https = ko.observable(instance.isHTTPS);
    this.managementMode = ko.observable(instance.ManagementMode);
    this.startOnBoot = ko.observable(instance.DaemonAutostart);
    this.excludeFromFirewall = ko.observable(instance.ExcludeFromFirewall || false);
    this.useHostModeNetwork = ko.observable(instance.UseHostModeNetwork || false);
    this.suspended = ko.observable(instance.Suspended);
    this.selected = ko.observable(false);
    this.metrics = ko.observableArray(); //of InstanceMetricVM
    this.binding = ko.computed(() => `${self.ip()}:${self.port()} ${self.https() ? " (https)" : ""}`);
    this.endpoint = instance.ApplicationEndpoints.length > 0 ? instance.ApplicationEndpoints[0].Endpoint : "Unknown";
    this.endpointURI = instance.ApplicationEndpoints.length > 0 ? instance.ApplicationEndpoints[0].Uri : null;
    this.portMappings = ko.observableArray(); //of PortUsageVM
    this.displayImageSource = instance.DisplayImageSource;
    this.versionObject = ko.observable(Version.fromObject(instance.AMPVersion || instance.InstalledVersion));
    this.installedVersion = ko.computed(() => self.versionObject()?.toString() ?? "");
    this.compatVersion = ko.computed(() => self.versionObject()?.toString(3) ?? "");
    this.appState = ko.observable(instance.AppState);
    this.displayGroup = ko.observable(instance.Group);
    this.busy = ko.observable(false);
    this.infoTab = ko.observable(0);
    this.mountBindings = ko.observableArray(); //of MountBindingVM
    this.extraPackages = ko.observableArray(instance.ExtraContainerPackages); //of String
    this.appIsMultiIPAware = ko.observable(instance.IsMultiIPAware);
    this.applicationIp = ko.observable(instance.ApplicationIP);
    this._originalApplicationIp = instance.ApplicationIP;
    this._originalSuspended = instance.Suspended;
    this.advertisedAddress = ko.observable((instance?.DeploymentArgs["Core.AMP.AdvertisedAddress"]) || "");
    this._originalAdvertisedAddress = this.advertisedAddress();
    this.advertisedAddressIncludesPort = ko.observable(!(instance?.DeploymentArgs["Core.AMP.AdvertisedAddressIncludesPort"] === "false"));
    this._originalAdvertisedAddressIncludesPort = this.advertisedAddressIncludesPort();

    //Set up the mount bindings
    for (const [key, value] of Object.entries(instance.CustomMountBinds)) {
        self.mountBindings.push(new MountBindingVM(key, value));
    }

    this.showReplaceWarning = function () {
        window.open("https://discourse.cubecoders.com/t/retiring-amp-legacy-modules/32447", "_blank");
    }

    this.displayGroup.subscribe(async function (newValue) {
        if (newValue != -1) { return; }

        const result = await UI.PromptAsync("Set Group", "Please set a group for this instance.");

        if (result) {
            if (result == "-1") {
                UI.ShowModalAsync("Reserved Value", "The group ID -1 is reserved for internal use and cannot be used.", UI.Icons.Exclamation, UI.OKActionOnly);
                return;
            }
            self.displayGroup(result);
        }
        else {
            self.displayGroup("");
        }
    });

    this.hasPermissionTo = (permission) => ko.computed(() => userHasPermission(`Instances.${self.id}.${permission}`), this);

    this.versionMismatch = ko.computed(function () { return self.compatVersion() !== viewModels.support.compatVersion() });

    this._OriginalRunInContainer = instance.IsContainerInstance;
    this.runInContainer = ko.observable(instance.IsContainerInstance);
    this.containerMemoryPolicy = ko.observable(instance.ContainerMemoryPolicy);
    this.containerMemory = ko.observable(instance.ContainerMemoryMB);
    this.containerSwap = ko.observable(instance.ContainerSwapMB);
    this.containerMaxCPU = ko.observable(instance.ContainerCPUs);
    this.containerCPUSet = ko.observable(instance.ContainerCPUSetCPUs ?? "");
    this.containerImage = ko.observable(instance.SpecificDockerImage ?? "");

    this.longStateText = ko.computed(function () {
        if (!self.running()) { return Locale.l("Start this instance to configure it and run the application within it."); }

        switch (self.appState()) {
            case 5:
            case 7:
            case 10:
            case 70:
            case 75:
                return Locale.l("Manage this instance to view its progress.");
            case 80:
                return Locale.l("Manage this instance to provide required information.");
            case 100: return Locale.l("A fault is preventing the application from starting. Manage the instance to examine the logs.");
            default: return Locale.l("Manage this instance to start or configure the application.");
        }
    });

    this.shortStateText = ko.computed(function () {
        if (self.suspended()) { return Locale.l("Suspended"); }
        if (!self.running()) { return Locale.l("Offline"); }

        switch (self.appState()) {
            case -1: return Locale.l("Waiting");
            case 0: return Locale.l("Idle");
            case 5: return Locale.l("Starting");
            case 7: return Locale.l("Configuring");
            case 10: return Locale.l("Starting");
            case 20: return Locale.l("Running");
            case 30: return Locale.l("Restarting");
            case 40: return Locale.l("Stopping");
            case 45: return Locale.l("Sleeping");
            case 50: return Locale.l("Sleeping");
            case 60: return Locale.l("Waiting");
            case 70: return Locale.l("Installing");
            case 75: return Locale.l("Updating");
            case 80: return Locale.l("Waiting for user input");
            case 100: return Locale.l("Error");
            case 200: return Locale.l("Suspended");
            case 250: return Locale.l("Unavailable");
            case 999: return Locale.l("Running");
            default: return `unknown (${state})`;
        }
    });

    this.stateIcon = ko.computed(function () {
        if (self.suspended()) { return "pause_circle"; }
        if (!self.running()) { return "stop_circle" };
        switch (self.appState()) {
            case -1:
            case 10:
            case 30:
            case 5:
            case 7:
            case 60:
            case 70:
            case 75:
                return "pending"
            case 20:
            case 50:
                return "play_circle";
            case 0:
            case 40:
                return "stop_circle";
            case 80:
                return "help"
            case 100:
            case 200:
            case 250:
            case 999:
            default:
                return "error";
        }
    });

    this.stateColor = ko.computed(function () {
        if (self.suspended()) { return ""; }
        if (!self.running()) { return "bgRed" };
        switch (self.appState()) {
            case 10:
            case 30:
                return "bgAmber";
            case -1:
            case 5:
            case 7:
            case 60:
            case 70:
            case 75:
            case 80:
                return "bgInfo"
            case 20:
            case 50:
                return "bgGreen";
            case 0:
                return "bgGray";
            case 40:
            case 100:
            case 200:
            case 250:
            case 999:
            default:
                return "bgRed";
        }
    });

    this.infoClass = ko.computed(function () {
        if (self.suspended()) { return "statusSuspended"; }
        if (!self.running()) { return "statusOffline" };
        switch (self.appState()) {
            case 10:
            case 30:
                return "statusBusy";
            case -1:
            case 5:
            case 7:
            case 60:
            case 70:
            case 75:
                return "statusNotice";
            case 20:
            case 50:
                return "statusRunning";
            case 0:
                return "statusIdle";
            case 40:
            case 80:
                return "statusNotice";
            case 100:
            case 200:
            case 250:
            case 999:
            default:
                return "statusFail";
        }
    });

    this.touchStartTime = 0;

    this.touchStart = function () {
        self.touchStartTime = new Date();
    };

    this.touchEnd = function (f, e) {
        self.click();
    };

    this.DisplayImageURI = ko.computed(function () {
        return GetDisplayImageURI(self.displayImageSource);
    });

    this.connectToApplication = function () {
        window.open(self.endpointURI);
    };


    this.editPortMappings = async function (refreshOnly) {
        const NetworkInfo = await API.ADSModule.GetInstanceNetworkInfoAsync(self.id);
        self.portMappings.removeAll();
        const data = ko.quickmap.to(PortUsageVM, NetworkInfo, false, { vm: self });
        linkChildPorts(data);
        ko.utils.arrayPushAll(self.portMappings, data);
        if (refreshOnly !== false) {
            UI.ShowWizard("#tab_ADSModule_InstanceNetworkInfoPopup");
        }
    };

    this.newPortDescription = ko.observable("");
    this.newPortNumber = ko.observable(1025);
    this.newPortProtocol = ko.observable("0");
    this.addCustomPortMapping = async function () {
        const addMappingResult = await API.ADSModule.ModifyCustomFirewallRuleAsync(self.id, self.newPortNumber, 1, self.newPortProtocol, self.newPortDescription(), true);
        if (!addMappingResult.Status) {
            await UI.ShowModalAsync("Unable to add port mapping", addMappingResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
        else {
            self.editPortMappings(true);
        }
    };

    this.saveNetworkChanges = async function () {
        const newMappings = {};
        for (const portUsage of self.portMappings()) {
            // Child ports are derived (parent.Port + Offset) - read-only, never sent as their own mapping.
            if (portUsage.IsChild) { continue; }

            if (portUsage.PortNumber() < 1025 || portUsage.PortNumber() > 65535) {
                UI.ShowModalAsync("Unable to reconfigure instance", portUsage.PortNumber().toString() + " is not a valid port number.", UI.Icons.Exclamation, UI.OKActionOnly);
                return false;
            }

            // A parent must leave room for its linked child ports (each sits at parent + offset).
            const maxChildOffset = self.portMappings()
                .filter(p => p.IsChild && p.parent === portUsage)
                .reduce((m, c) => Math.max(m, c.Offset), 0);
            if (Number(portUsage.PortNumber()) + maxChildOffset > 65535) {
                UI.ShowModalAsync("Unable to reconfigure instance", `Port ${portUsage.PortNumber()} leaves no room for its linked ports (needs up to +${maxChildOffset}).`, UI.Icons.Exclamation, UI.OKActionOnly);
                return false;
            }

            newMappings[portUsage.ProvisionNodeName] = portUsage.PortNumber();
        }

        const result = await API.ADSModule.SetInstanceNetworkInfoAsync(self.id, newMappings, self.applicationIp(), false, self.advertisedAddress(), self.advertisedAddressIncludesPort());

        if (result.Status) {
            await self.group.refresh();
            API.SetTaskComplete(result.Id, self.group.refresh);
            return true;
        }
        else {
            UI.ShowModalAsync("Unable to reconfigure instance", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
            return false;
        }
    };

    this.newMountBindKey = ko.observable("");
    this.newMountBindValue = ko.observable("");
    this.newMountBindReadOnly = ko.observable(false);
    this.addMountBind = function () {
        if (self.newMountBindKey() == "" || self.newMountBindValue() == "") {
            return;
        }
        const value = this.newMountBindValue() + (this.newMountBindReadOnly() ? ":ro" : "");
        this.mountBindings.push(new MountBindingVM(this.newMountBindKey(), value));
        this.newMountBindKey("");
        this.newMountBindValue("");
        this.newMountBindReadOnly(false);
    };

    this.deleteMountBind = function (bind) {
        this.mountBindings.remove(bind);
    };

    this.addPackage = async function () {
        const result = await UI.PromptAsync("Add Package", "Please enter the name of the Debian package you wish to add to this instance.");
        if (result == null) { return; }

        //Check the packages list doesn't already contain the result
        if (self.extraPackages().includes(result)) {
            UI.ShowModalAsync("Package already added", "The package " + result + " is already in the list of extra packages for this instance.", UI.Icons.Exclamation, UI.OKActionOnly);
            return;
        }

        self.extraPackages.push(result);
    };

    this.removePackage = function (packageName) {
        self.extraPackages.remove(function (pkg) {
            return pkg === packageName;
        });
    };

    this.editSettings = async function () {
        await self.group.refresh();
        self.infoTab(0);
        UI.ShowWizard("#tab_ADSModule_InstanceInfoPopup");
        let NetworkInfo = await API.ADSModule.GetInstanceNetworkInfoAsync(self.id);
        NetworkInfo.sort((a, b) => a.PortNumber - b.PortNumber);
        self.portMappings.removeAll();
        const data = ko.quickmap.to(PortUsageVM, NetworkInfo, false, { vm: self });
        linkChildPorts(data);
        ko.utils.arrayPushAll(self.portMappings, data);
    };

    this.closeInstanceInfo = async function () {
        UI.HideWizard();
        await self.group.refresh();
    };

    this.saveInstanceChanges = async function () {
        const hasDirtyPorts = self.portMappings().some(p => p.IsDirty());
        const ipDirty = self.applicationIp() != null && self._originalApplicationIp != self.applicationIp();
        const advertisedDirty = self._originalAdvertisedAddress != self.advertisedAddress()
            || self._originalAdvertisedAddressIncludesPort != self.advertisedAddressIncludesPort();
        //const restartWillBeRequired = hasDirtyPorts || ipDirty || (this._OriginalRunInContainer != this.runInContainer());
        //let restartPrompt = true;

        //if (this.running() && restartWillBeRequired) { //The instance needs to be restarted for generic instances to get their new deployment settings. It's messy inside the methods below, so until cleaned up this is needed.
        if (self.running()) {
            const restartPrompt = await UI.ShowModalAsync("Confirm Restart", "You have changed settings that require this instance to be restarted in order to take effect. This will also restart the game server inside it. Are you sure you wish to continue?", UI.Icons.Question, [
                new UI.ModalAction("Restart Now", true),
                //new UI.ModalAction("Restart Later", false),
                new UI.ModalAction("Cancel", false),

            ]);

            if (restartPrompt === false) {
                UI.HideWizard();
                return;
            }
        }

        if (self.suspended() && !self._originalSuspended) {
            const suspendReason = await UI.PromptAsync("Suspend Reason", "Please enter a reason for suspending this instance. This is optional.");
            if (suspendReason === null) {
                self.suspended(false);
                return;
            }

            const confirmResult = await UI.ShowModalAsync("Confirm Suspend", { text: "Are you sure you wish to suspend the following instance? This will make it inaccessible but will not delete the associated data.", subtitle: self.friendlyname() }, UI.Icons.Exclamation, [
                new UI.ModalAction("Suspend Server", true, "bgRed slideIcon icons_stop"),
                UI.CancelAction()
            ]);

            if (!confirmResult) {
                self.suspended(false);
                return;
            }
        }

        if (hasDirtyPorts || ipDirty || advertisedDirty) {
            const changeResult = await self.saveNetworkChanges();
            if (!changeResult) {
                await self.group.refresh();
                return;
            }
        }

        const mountBindsObject = {};
        for (const bind of self.mountBindings()) {
            mountBindsObject[bind.key()] = bind.value() + (bind.isReadOnly() ? ":ro" : "");
        }

        const result = await API.ADSModule.UpdateInstanceInfoAsync(self.id, self.friendlyname, self.description, self.startOnBoot, self.suspended, self.excludeFromFirewall, self.runInContainer, self.containerMemory, self.containerSwap, self.containerMemoryPolicy, self.containerMaxCPU, self.containerCPUSet, self.containerImage, self.welcomeMessage, self.displayGroup() || "", mountBindsObject, self.extraPackages, self.appIsMultiIPAware, self.useHostModeNetwork);

        if (!result.Status) {
            UI.ShowModalAsync("Unable to reconfigure instance", result.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
            return;
        }

        UI.HideWizard();

        //if (restartPrompt) {
        //    await self.restart();
        //}

        self._originalApplicationIp = self.applicationIp();
        self._OriginalRunInContainer = self.runInContainer();
        self._originalSuspended = self.suspended(); // ← track the new state
        await self.group.refresh();
    };

    this.restart = async function () {
        await API.ADSModule.RestartInstanceAsync(self.id);
    }

    this.refresh = function (instance) {
        this.running(instance.Running);
        this.ip(instance.IP);
        this.port(instance.Port);
        this.https(instance.isHTTPS);
        this.suspended(instance.Suspended);
        this.managementMode(instance.ManagementMode);
        this.startOnBoot(instance.DaemonAutostart);
        this.friendlyname(instance.FriendlyName);
        this.welcomeMessage(instance.WelcomeMessage);
        this.versionObject(instance.InstalledVersion);
        this.applicationIp(instance.ApplicationIP);

        if (instance.Metrics != null) {
            self.refreshMetrics(instance.Metrics);
        }
    };

    this.refreshMetrics = function (newMetrics) {
        self.metrics.removeAll();
        for (const key of Object.keys(newMetrics)) {
            const metric = newMetrics[key];
            const metricVM = new InstanceMetricVM(key, metric.Color, metric.RawValue, metric.MaxValue, metric.Units, metric.ShortName);
            self.metrics.push(metricVM);
        }
    }

    this.start = async function (data, event) {
        event?.preventDefault();
        event?.stopImmediatePropagation();
        const startResult = await API.ADSModule.StartInstanceAsync(self.id);

        if (!startResult.Status) {
            UI.ShowModalAsync("Unable to start instance", startResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
    };

    this.stop = async function (noConfirm) {
        if (noConfirm) {
            API.ADSModule.StopInstance(self.id);
            return;
        }

        const result = await UI.ShowModalAsync("Confirm Stop", { text: "Are you sure you wish to stop the following instance?", subtitle: self.friendlyname() }, UI.Icons.Exclamation, [
            new UI.ModalAction("Stop Server", true, "bgRed slideIcon icons_stop"),
            UI.CancelAction()
        ]);

        if (result === true) { API.ADSModule.StopInstance(self.id); }
    };

    this.delInstance = async function () {
        const result = await UI.ShowModalAsync("Confirm Deletion", { text: "Are you sure you want to delete the selected instance? This will permently remove all of its associated data.", subtitle: self.friendlyname() }, UI.Icons.Exclamation, [
            new UI.ModalAction("Delete Instance Forever", true, "bgRed slideIcon icons_stop"),
            UI.CancelAction()
        ]);

        if (result !== true) { return; }

        const confirmation = await UI.PromptAsync("Confirm Deletion", `Please enter the name of the instance you wish to delete: ${self.name}`);

        if (confirmation == null) { return; }

        if (confirmation != self.name) {
            UI.ShowModalAsync("Confirmation Failed", "The instance name you entered did not match the name of this instance. The instance will not be deleted.", UI.Icons.Exclamation, UI.OKActionOnly);
            return;
        }

        const deleteResult = await API.ADSModule.DeleteInstanceTaskAsync(self.id);
        if (deleteResult.Status) {
            deleteResult.onComplete(updateInstanceList);
        }
    };

    this.shareInstance = function () {
        window.open("/c/" + self.id.substr(0, 8), "_blank");
    }

    this.update = async function (noConfirm) {
        if (noConfirm) {
            API.ADSModule.UpgradeInstance(self.id);
            return;
        }

        if (self.running()) {
            const result = await UI.ShowModalAsync("Confirm Shutdown", { text: "Upgrading this instance requires that it is shut down, along with the application within it. Do you want to proceed with the upgrade?", subtitle: self.friendlyname() }, UI.Icons.Exclamation, [
                new UI.ModalAction("Shutdown and Update", true, "bgRed slideIcon icons_stop"),
                UI.CancelAction()
            ]);

            if (result === false) {
                return;
            }
        }
        API.ADSModule.UpgradeInstance(self.id);
    }

    this.reactivateInstance = async function () {
        if (self.running()) {
            const result = await UI.ShowModalAsync("Confirm Shutdown", { text: "Reactivating this instance requires that it is shut down, along with the application within it. Do you want to proceed with the reactivation?", subtitle: self.friendlyname() }, UI.Icons.Exclamation, [
                new UI.ModalAction("Shutdown and Reactivate", true, "bgRed slideIcon icons_stop"),
                UI.CancelAction()
            ]);

            if (result === false) {
                return;
            }
        }
        API.ADSModule.ReactivateInstance(self.id);
    }

    this.manage = async function (_, event, fromViewState = false) {
        event?.preventDefault();
        event?.stopImmediatePropagation();
        if (self.managementMode() != 10 && self.module !== "ADS") {
            UI.ShowModalAsync("Cannot manage this instance", "Unmanaged instances cannot be accessed from within ADS. You must browse directly to it's address in a separate tab.", UI.Icons.Exclamation, UI.OKActionOnly);
            return;
        }

        if (!self.running()) {
            await API.ADSModule.StartInstanceAsync(self.id);
            await sleepAsync(self.runInContainer() ? 20 : 10);
        }

        if (self.group.showGuide()) {
            localStorage.guide_firstTime = true;
            self.group.showGuide(false);
            plausible("GuideFinished");
        }

        ManageInstance(self.id, self.name, self.friendlyname(), self.group.url(), self, fromViewState);
    };

    this.manageNewTab = function () {
        const id = self.id.substr(0, 8);
        const connectViaTarget = currentSettings['ADSModule.Network.AccessMode'].value() == 20;
        const baseUrl = connectViaTarget ? self.group.url() : document.location.origin;
        const url = `${baseUrl}/remote/${id}`;
        window.open(url);
    };

    this.click = function () {
        const currentlySelected = self.group.vm.selectedInstance();
        if (currentlySelected != null) { currentlySelected.selected(false); }
        self.group.vm.selectedInstance(this);
        self.selected(true);
        $("#selectedInstanceInfo").toggleClass("visible");
    };

    this.viewLogs = function () {
        UI.NavigateTo((group.isRemote ? `/instances/${group.id.substr(0, 8)}/` : '/') + `filemanager/__VDS__${self.name}/AMP_Logs/`, true);
    };

    this.browseDatastore = function () {
        UI.NavigateTo((group.isRemote ? `/instances/${group.id.substr(0, 8)}/` : '/') + `filemanager/__VDS__${self.name}/`, true);
    };

    this.cloneInstance = function () {
        cloneInstanceVM.sourceInstance(self);
        cloneInstanceVM.newFriendlyName(self.friendlyname() + ' (Clone)');
        cloneInstanceVM.selectedTarget(self.group);
        cloneInstanceVM.datastore(null);
        cloneInstanceVM.deleteSource(false);
        cloneInstanceVM.copyFiles(false);
        cloneInstanceVM.afterCreation(Number.parseInt(GetSetting("ADSModule.Defaults.DefaultPostCreate") || 0));
        UI.ShowWizard("#tab_ADSModule_CloneInstanceWizard");
    };

    self.refreshMetrics(instance.Metrics);
}

const emptyPlatformInfo = {
    PlatformName: "Unknown",
    OS: -1,
    InstalledRAMMB: 1,
    CPUInfo: { Sockets: 1, Cores: 1, Threads: 1, ModelName: "Unknown" }
};

function instanceSearchResults(query) {
    const vm = new SearchResultCategoryVM("Instances", "Your current AMP instances. Click an instance to manage it.");

    for (const group of manageInstancesVM.groups()) {
        for (const instance of group.instances()) {
            if (instance.name.toLocaleLowerCase().includes(query) ||
                instance.description().toLocaleLowerCase().includes(query) ||
                instance.friendlyname().toLocaleLowerCase().includes(query) ||
                instance.module.toLocaleLowerCase().includes(query)) {

                const source = `Targets > ${group.name()} > ${instance.name} (${instance.friendlyname()})`;

                const resultVM = new SearchResultVM(instance.name, instance.friendlyname(), source, 0, null, instance.manage);
                vm.items.push(resultVM);
            }
        }
    }

    if (vm.items().length == 0) { return null; }

    return vm;
}

class DatastoreSummaryVM {
    constructor() {
        this.Id = ko.observable(0);
        this.FriendlyName = ko.observable("");
    }
}

function InstanceDisplayGroupVM(groupName, instances) {
    const self = this;
    this.name = ko.observable(groupName || Locale.l('No Group'));
    this.instances = ko.observableArray(instances); //of AMPInstanceVM
    this.sortedInstances = ko.computed(() => { 
        const sortCriteria = manageInstancesVM.sortMode(); //Member of sortModes
        const instances = self.instances().slice(0); //Clone the array so we don't modify the original
        instances.sort((a, b) => instanceSort(a, b, sortCriteria));
        return instances;
    });
    this.order = ko.observable(0);

    this.refresh = function () {
        self.instances.removeAll();
        for (const group of manageInstancesVM.groups()) {
            for (const instance of group.instances()) {
                if (instance.displayGroup() == self.name()) {
                    self.instances.push(instance);
                }
            }
        }
    };

    this.canStart = ko.computed(() => self.instances().some(x => x.group.canStart()));
    this.canStop = ko.computed(() => self.instances().some(x => x.group.canStop()));
    this.canUpgrade = ko.computed(() => self.instances().some(x => x.group.canUpgrade()));
    this.canCreate = ko.computed(() => self.instances().some(x => x.group.canCreate()));

    this.createInstance = async function () {
        createInstanceVM.groupName(self.name());
        createInstanceVM.selectedTarget(createInstanceVM.availableTargets()[0]);
        showCreateInstance();
    };

    this.startAll = async function () {
        for (const instance of self.instances()) {
            instance.start();
        }
    };
    this.stopAll = async function () {
        const result = await UI.ShowModalAsync("Confirm Stop", { text: "Are you sure you wish to stop all instances in this group?", subtitle: self.name() }, UI.Icons.Exclamation, [
            new UI.ModalAction("Stop Servers", true, "bgRed slideIcon icons_stop"),
            UI.CancelAction()
        ]);

        if (!result) { return; }

        for (const instance of self.instances()) {
            instance.stop(true);
        }
    };
    this.updateAll = async function () {
        const anyInstanceIsRunning = self.instances().some(x => x.running());

        if (anyInstanceIsRunning) {
            const result = await UI.ShowModalAsync("Confirm Shutdown", { text: "Some instances in this group are running. Updating an instance requires that it is shut down, along with the application within it. Do you want to proceed with the upgrade?", subtitle: self.name() }, UI.Icons.Exclamation, [
                new UI.ModalAction("Shutdown and Update", true, "bgRed slideIcon icons_stop"),
                UI.CancelAction()
            ]);

            if (result === false) {
                return;
            }
        }

        const instances = self.instances();
        for (const instance of instances) {
            instance.update(true);
        }
    };
}

function instanceSort(a, b, sortMode) {
    const aPort = Number.parseInt(a.endpoint.split(":")[1] || "0");
    const bPort = Number.parseInt(b.endpoint.split(":")[1] || "0");
    switch (sortMode) {
        case sortModes.NameAsc:
            return a.friendlyname().localeCompare(b.friendlyname());
        case sortModes.NameDesc:
            return b.friendlyname().localeCompare(a.friendlyname());
        case sortModes.AppAsc:
            return a.displayModule.localeCompare(b.displayModule);
        case sortModes.AppDesc:
            return b.displayModule.localeCompare(a.displayModule);
        case sortModes.PortAsc:
            return aPort - bPort;
        case sortModes.PortDesc:
            return bPort - aPort;
        default:
            return 0;
    }
}

function InstanceGroupVM(group, vm) {
    const self = this;
    this.vm = vm; //InstanceManagementVM
    this.name = ko.observable(group.FriendlyName,);
    this.description = ko.observable(group.Description || "");
    this.id = group.InstanceId;
    this.platform = ko.observable(group.Platform || emptyPlatformInfo);
    this.cpuName = ko.computed(() => (self.platform().CPUInfo.Sockets > 1 ? `${self.platform().CPUInfo.Sockets}x ` : "") + self.platform().CPUInfo.ModelName.replaceAll(/\(R\)| CPU| (\d+|\w+)-Core Processor| @ \d\.\d+[MG]Hz/gm, "") + (self.platform().CPUInfo.Cores == self.platform().CPUInfo.Threads ? ` (${self.platform().CPUInfo.Cores} Cores)` : ` (${self.platform().CPUInfo.Cores}C\\${self.platform().CPUInfo.Threads}T)`));
    this.os = ko.computed(() => self.platform().OS);
    this.osName = ko.computed(() => self.platform().PlatformName.replaceAll(/GNU\/Linux| \(\w+\)|Microsoft | Standard| Enterprise| Evaluation/gm, ""));
    this.displayRAM = ko.computed(() => self.platform().InstalledRAMMB < 4096 ? `${self.platform().InstalledRAMMB}MB` : `${Math.round(self.platform().InstalledRAMMB / 1024)}GB`);
    this.instances = ko.observableArray(); //of AMPInstanceVM
    this.sortedInstances = ko.computed(() => {
        const sortCriteria = self.vm.sortMode(); //Member of sortModes
        const instances = self.instances().slice(0); //Clone the array so we don't modify the original
        instances.sort((a, b) => instanceSort(a, b, sortCriteria));
        return instances;
    });
    this.state = ko.observable(group.State);
    this.stateReason = ko.observable(group.StateReason);
    this.url = ko.observable(new URL(group.URL || document.location));
    this.availableIPs = ko.observableArray(group.AvailableIPs);
    this.isRemote = group.IsRemote;
    this.canCreate = ko.observable(userHasPermission("ADS.InstanceManagement.CreateInstance"));
    this.canEdit = ko.observable(group.IsRemote && userHasPermission("ADS.InstanceManagement.EditRemoteTargets"));
    this.canManage = ko.observable(group.IsRemote && userHasPermission(`Instances.${self.id}.Manage`));
    this.canStart = ko.observable(userHasPermission("ADS.InstanceManagement.StartInstances"));
    this.canStop = ko.observable(userHasPermission("ADS.InstanceManagement.StopInstances"));
    this.canUpgrade = ko.observable(userHasPermission("ADS.InstanceManagement.UpgradeInstances"));
    this.showMetrics = ko.observable(false);
    this.showGuide = ko.observable(localStorage.guide_firstTime == undefined);
    this.tags = ko.observableArray(group.Tags);
    this.datastores = ko.observableArray(ko.quickmap.to(DatastoreSummaryVM, group.Datastores));
    this.createsInContainers = ko.observable(group.CreatesInContainers);
    this.platformCompatibility = ko.computed(() => self.createsInContainers() ? supportedOS.Linux : self.os());
    this.selfInstance = null;

    this.OSNameString = ko.computed(function () {
        switch (self.os()) {
            case -1: return "Offline";
            case 1: return "Windows";
            default: return "Linux";
        }
    });
    this.manage = function () {
        ManageInstance(self.id, self.name(), self.description(), self.url(), self.selfInstance);
    };
    this.edit = function () {
        self.vm.selectedTarget(self);
        UI.ShowWizard("#tab_ADSModule_TargetInfoPopup");
    };
    this.closeTargetInfo = function () {
        UI.HideWizard();
        self.refresh();
    };

    this.addTag = async function () {
        const newTag = await UI.PromptAsync("New Tag", "Enter Tag");
        if (newTag == null || newTag == "") { return; }
        self.tags.push(newTag);
    };

    this.removeTag = function (toRemove) {
        self.tags.remove((v) => v == toRemove);
    }

    this.saveTargetInfo = async function () {
        const result = await API.ADSModule.UpdateTargetInfoAsync(self.id, self.name, self.url, self.description, self.tags());

        if (result.Status !== true) {
            UI.ShowModalAsync("Failed to update target info", result.Reason, UI.Icons.Info, UI.OKActionOnly);
            return;
        }

        UI.HideWizard();
        self.refresh();
    };

    this.DisplayName = `${self.name()} (${self.OSNameString()})`;

    this.update = function (data) {
        self.state(data.State);
        self.stateReason(data.StateReason);
        self.url(new URL(data.URL || document.location));
        self.name(data.FriendlyName);
        self.platform(data.Platform || emptyPlatformInfo);
        self.createsInContainers(data.CreatesInContainers);
        self.availableIPs.removeAll();
        ko.utils.arrayPushAll(self.availableIPs, data.AvailableIPs);
        self.datastores.removeAll();
        ko.utils.arrayPushAll(self.datastores, ko.quickmap.to(DatastoreSummaryVM, data.Datastores));

        if (data.AvailableInstances == undefined) { return; }

        for (const instance of data.AvailableInstances) {
            const existing = self.findInstance(instance.InstanceID);
            if (existing != null) {
                existing.refresh(instance);
                continue;
            }

            const newInstanceVM = new AMPInstanceVM(instance, self);

            if (instance.Module == "ADS" || instance.Module == "ADSModule") {
                self.selfInstance = newInstanceVM;
                continue;
            }

            self.instances.push(newInstanceVM);
        }

        for (const existingInstance of self.instances()) {
            const checkStillExists = ko.utils.arrayFirst(data.AvailableInstances, (inst) => inst.InstanceID == existingInstance.id);
            if (checkStillExists == null) {
                self.instances.remove(existingInstance);
            }
        }
    };

    this.findInstance = (id) => id == self.id ? self.selfInstance : ko.utils.arrayFirst(self.instances(), (instance) => instance.id == id) || null;
    this.findInstanceShort = (id) => id == self.id.substr(0, 8) ? self.selfInstance : ko.utils.arrayFirst(self.instances(), (instance) => instance.id.substr(0, 8) == id) || null;

    this.refresh = async function () {
        await API.ADSModule.UpdateTargetAsync(self.id);
        const group = await API.ADSModule.GetGroupAsync(self.id);

        if (group == null) {
            manageInstancesVM.refresh();
            return;
        }

        self.update(group);
    };

    this.remove = async function () {
        const promptResult = await UI.ShowModalAsync("Confirm target removal", `Are you sure you want to remove the ${self.name()} target? This will not delete the target or its instances, this controller will simply stop querying it. You will need to re-register this target to manage it again.`, UI.Icons.Exclamation, [
            new UI.ModalAction("Remove Target", true, "bgRed slideIcon icons_remove"),
            UI.CancelAction()
        ]);

        if (promptResult === false) { return; }
        const DetachResult = await API.ADSModule.DetachTargetAsync(self.id);

        if (DetachResult.Status === false) {
            UI.ShowModalAsync("Failed to remove target", DetachResult.Reason, UI.Icons.Info, UI.OKActionOnly);
            return;
        }

        UI.HideWizard("#tab_ADSModule_TargetInfoPopup");
        self.vm.refresh();
    };

    this.createInstance = function () {
        createInstanceVM.groupName("");
        createInstanceVM.selectedTarget(self);
        showCreateInstance();
    };

    this.startAllInstances = async function (data, event) {
        event.preventDefault();
        event.stopImmediatePropagation();
        const startResult = await API.ADSModule.StartAllInstancesAsync(self.id);

        if (!startResult.Status) {
            UI.ShowModalAsync("Unable to start instances", startResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
    };

    this.stopAllInstances = async function () {
        const result = await UI.ShowModalAsync("Confirm Stop", { text: "Are you sure you wish to stop all instances?", subtitle: self.name() }, UI.Icons.Exclamation, [
            new UI.ModalAction("Stop Servers", true, "bgRed slideIcon icons_stop"),
            UI.CancelAction()
        ]);

        if (!result) { return; }

        API.ADSModule.StopAllInstancesAsync(self.id);
    };

    this.upgradeAllInstances = async function () {
        const result = await UI.ShowModalAsync("Confirm Update", { text: "Are you sure you wish to stop all instances to perform an update?", subtitle: self.name() }, UI.Icons.Exclamation, [
            new UI.ModalAction("Stop Servers", true, "bgRed slideIcon icons_stop"),
            UI.CancelAction()
        ]);

        if (!result) { return; }

        API.ADSModule.UpgradeAllInstancesAsync(self.id, true);
    };
}

const groupModes = {
    ByTarget: 0,
    ByStatus: 1,
    ByGroupName: 2
};

const sortModes = {
    None: 0,
    NameAsc: 1,
    NameDesc: 2,
    AppAsc: 3,
    AppDesc: 4,
    PortAsc: 5,
    PortDesc: 6,
};

const sortModeIcons = {
    [sortModes.None]: "sortcheck_indeterminate_small",
    [sortModes.NameAsc]: "sort_by_alphaarrow_upward",
    [sortModes.NameDesc]: "sort_by_alphaarrow_downward",
    [sortModes.AppAsc]: "categoryarrow_upward",
    [sortModes.AppDesc]: "categoryarrow_downward",
    [sortModes.PortAsc]: "123arrow_upward",
    [sortModes.PortDesc]: "123arrow_downward",
}

function InstanceManagementVM() {
    const self = this;
    this.groups = ko.observableArray(); //of InstanceGroupVM
    this.displayGroups = ko.observableArray(); //of InstanceDisplayGroupVM
    this.createInstance = showCreateInstance;
    this.selectedInstance = ko.observable(null); //of AMPInstanceVM
    this.selectedTarget = ko.observable(null); //of InstanceGroupVM
    this.canCreate = userHasPermission("ADS.InstanceManagement.CreateInstance");
    this.ready = ko.observable(false);
    this.firstUpdate = true;
    this.listView = ko.observable(false);
    this.allDisplayGroupNames = ko.computed(() =>
        [
            ...[...new Set(self.groups().flatMap(x => x.instances()).filter(x => x.displayGroup() != null).map(x => x.displayGroup()))].map(name => ({ key: name, value: name })),
            { key: "-1", value: "Add new group..." }
        ]
    );
    //When the list view state changes - store it in local storage. On load, set the view state to the stored value.
    this.listView.subscribe((value) => localStorage.setItem("ADSModule.Instances.ListView", value));
    this.listView(localStorage.getItem("ADSModule.Instances.ListView") == "true");
    this.groupMode = ko.observable(localStorage.getItem("ADSModule.Instances.GroupMode") ?? groupModes.ByTarget);
    this.sortMode = ko.observable(localStorage.getItem("ADSModule.Instances.SortMode") ?? sortModes.None);
    this.cycleSortMode = function () {
        self.sortMode((self.sortMode() + 1) % Object.keys(sortModes).length);
        localStorage.setItem("ADSModule.Instances.SortMode", self.sortMode());
    };
    this.sortModeIcon = ko.pureComputed(() => sortModeIcons[self.sortMode()] || "sort");

    this.filterText = ko.observable("");
    this.filterStates = ko.observableArray([]);
    this.showFiltered = ko.pureComputed(() => self.filterText().length > 3 || self.filterStates().length > 0);
    let _pairTimeoutId = null;

    this.isController = ko.observable(false);
    this.canManageInstances = ko.observable(false);
    this.showPairingUI = async function () {
        const getPaircodeResult = await API.ADSModule.GetTargetPairingCodeAsync();

        if (!getPaircodeResult.Status) {
            await UI.ShowModalAsync("Failed to get pairing code", getPaircodeResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
            return;
        }

        $("#targetPairCode").text(getPaircodeResult.Result);
        UI.ShowWizard("#tab_ADSModule_PairTargetPopup");
        _pairTimeoutId = setTimeout(self.expirePairingUI, 60000);
    };

    this.expirePairingUI = function () {
        _pairTimeoutId = null;
        UI.HideWizard();
    };

    this.cancelPairingUI = async function () {
        clearTimeout(_pairTimeoutId);
        _pairTimeoutId = null;
        await API.ADSModule.CancelPairingAsync();
    };
    this.notifyPairComplete = async function () {
        UI.HideWizard();
        await self.refresh();
        await UI.ShowModalAsync("Pairing Complete", "The target has been successfully paired with this controller. You can now manage it.", UI.Icons.Info, UI.OKActionOnly);
        await self.refresh();
    }

    this.updateGroupMode = function (d, e, groupMode) {
        const attrValue = groupMode ?? e?.target.dataset.groupmode ?? e?.target.parentElement.dataset.groupmode ?? 0;
        if (attrValue == null) { return; }
        e?.preventDefault();
        e?.stopImmediatePropagation();
        const newValue = Number.parseInt(attrValue);
        self.groupMode(newValue);
        localStorage.setItem("ADSModule.Instances.GroupMode", newValue)
    };

    function filterMatch(instance, filter) {
        if (instance == null || filter == null) { return false; }
        filter = filter.toLocaleLowerCase();

        const prefixMatch = filter.match(/^(\w):\s*(.*)$/);
        if (prefixMatch) {
            const [, prefix, term] = prefixMatch;
            if (term.length === 0) { return false; }
            switch (prefix) {
                case 'g':
                    return instance.displayGroup()?.toLocaleLowerCase().includes(term);
                case 't':
                    return instance.group.name()?.toLocaleLowerCase().includes(term);
                case 'd':
                    return instance.description()?.toLocaleLowerCase().includes(term);
                case 'n':
                    return instance.name?.toLocaleLowerCase().includes(term) || instance.friendlyname()?.toLocaleLowerCase().includes(term);
                case 'm':
                    return instance.displayModule?.toLocaleLowerCase().includes(term) || instance.module?.toLocaleLowerCase().includes(term);
                case 'p':
                    return instance.endpoint?.endsWith(term);
                default:
                    return false;
            }
        }

        return instance.displayGroup()?.toLocaleLowerCase().includes(filter) ||
            instance.group.name()?.toLocaleLowerCase().includes(filter) ||
            instance.name?.toLocaleLowerCase().includes(filter) ||
            instance.description()?.toLocaleLowerCase().includes(filter) ||
            instance.friendlyname()?.toLocaleLowerCase().includes(filter) ||
            instance.displayModule?.toLocaleLowerCase().includes(filter) ||
            instance.module?.toLocaleLowerCase().includes(filter) ||
            instance.endpoint?.endsWith(filter);
    }

    this.filteredInstances = ko.computed(() => {
        if (self.filterText() == "") { return []; }
        //Use FlatMap to look at all the groups find all the instances within them matching the filter by either name, description, or friendlyname. Break the 'filter' out into a separate method that takes the filter text as a parameter.
        let result = self.groups().flatMap(x => x.instances().filter(y => filterMatch(y, self.filterText())));
        result.sort((a, b) => instanceSort(a, b, self.sortMode()));
        return result;
    });

    this.findInstance = function (instanceId) {
        for (const g of self.groups()) {
            const check = g.findInstance(instanceId);
            if (check != null) { return check; }
        }
        return null;
    }

    this.findInstanceShort = function (instanceId) {
        for (const g of self.groups()) {
            const check = g.findInstanceShort(instanceId);
            if (check != null) { return check; }
        }
        return null;
    }

    this.refreshDisplayGroups = function () {
        let allGroups = {};
        ko.utils.arrayForEach(self.groups(), function (group) {
            ko.utils.arrayForEach(group.instances(), function (instance) {
                let modGroup = allGroups[instance.displayGroup()] ||= []
                modGroup.push(instance);
            });
        });
        const instanceGroups = Object.keys(allGroups);
        //Rearrange instanceGroups such that "No Group" is always at the end of the list.
        instanceGroups.sort((a, b) => a == "No Group" ? 1 : b == "No Group" ? -1 : a.localeCompare(b));

        //Find displayGroups that don't exist in instanceGroups by their name() and remove them:
        const toRemove = self.displayGroups().filter(x => instanceGroups.filter(y => y == x.name()).length == 0);
        self.displayGroups.removeAll(toRemove);

        for (const group of instanceGroups) {
            const existing = ko.utils.arrayFirst(self.displayGroups(), (g) => g.name() == group);
            if (existing == null) {
                const newGroup = new InstanceDisplayGroupVM(group, allGroups[group]);
                self.displayGroups.push(newGroup);
            }
        }
    };

    this.refresh = async function () {
        const result = await API.ADSModule.GetInstancesAsync();

        const currentMode = Number.parseInt(GetSetting('ADSModule.ADS.Mode'));
        self.isController(currentMode == 10 || currentMode == 20);
        self.canManageInstances(currentMode == 10 || currentMode == 20 || currentMode == 100);

        if (result == null || result.length === 0) {
            self.groups.removeAll();
            self.ready(true);
            return;
        }

        for (const group of result) {
            const existingGroup = self.findGroup(group.InstanceId);
            if (existingGroup != null) {
                existingGroup.update(group);
                continue;
            }

            const groupVM = new InstanceGroupVM(group, self);
            groupVM.update(group);

            self.groups.push(groupVM);
        }

        for (const instanceGroup of self.groups()) {
            const checkStillExists = ko.utils.arrayFirst(result, (grp) => grp.InstanceId == instanceGroup.id);
            if (checkStillExists == null) {
                self.groups.remove(instanceGroup);
            }
        }

        if (self.groups().length == 1) {
            createInstanceVM.selectedTarget(self.groups()[0]);
        }
        self.refreshDisplayGroups();
        self.ready(true);

        if (self.firstUpdate) {
            UI.InitialViewchange();
            self.firstUpdate = false;
        }
    };
    this.findGroup = (id) => ko.utils.arrayFirst(self.groups(), (g) => g.id == id) || null;
    this.refreshGroup = function (id) {
        const group = self.findGroup(id);
        if (group != null) {
            group.refresh();
        }
    };
    this.bulkExtract = async function () {
        const fileName = await window.pickFile("Select an archive to extract", "Extract");
        if (fileName != null) {
            await API.ADSModule.ExtractEverywhereAsync(fileName);
        }
    };
    this.stopAllFiltered = async function () {
        const result = await UI.ShowModalAsync("Confirm Stop", { text: "Are you sure you wish to stop all visible instances?", subtitle: "" }, UI.Icons.Exclamation, [
            new UI.ModalAction("Stop Servers", true, "bgRed slideIcon icons_stop"),
            UI.CancelAction()
        ]);

        if (!result) { return; }

        const instances = self.filteredInstances();
        for (const instance of instances) {
            instance.stop(true);
        }
    };
    this.startAllFiltered = async function () {
        const instances = self.filteredInstances();
        for (const instance of instances) {
            instance.start();
        }
    };
    this.updateAllFiltered = async function () {
        const anyInstanceIsRunning = self.filteredInstances().some(x => x.running());

        if (anyInstanceIsRunning) {
            const result = await UI.ShowModalAsync("Confirm Shutdown", { text: "Some of the selected instances are running. Updating an instance requires that it is shut down, along with the application within it. Do you want to proceed with the upgrade?", subtitle: "" }, UI.Icons.Exclamation, [
                new UI.ModalAction("Shutdown and Update", true, "bgRed slideIcon icons_stop"),
                UI.CancelAction()
            ]);

            if (result === false) {
                return;
            }
        }

        const instances = self.filteredInstances();
        for (const instance of instances) {
            instance.update(true);
        }
    };
    this.canStart = ko.computed(() => {
        const instances = self.filteredInstances();
        return instances.some(x => x.group.canStart());
    });
    this.canStop = ko.computed(() => {
        const instances = self.filteredInstances();
        return instances.some(x => x.group.canStop());
    });
    this.canUpgrade = ko.computed(() => {
        const instances = self.filteredInstances();
        return instances.some(x => x.group.canUpgrade());
    });
}

async function updateInstanceList() {
    await manageInstancesVM.refresh();
}

async function updateNewAppInfo() {
    const apps = await API.ADSModule.GetSupportedAppSummariesAsync();
    createInstanceVM.availableApps.removeAll();
    for (const app of apps) {
        let appVM = new SupportedAppVM();
        ko.quickmap.map(appVM, app);
        createInstanceVM.availableApps.push(appVM);
    }
}

const supportedOS = {
    Windows: 1,
    Linux: 2,
};

function SupportedAppVM() {
    const self = this;
    this.Id = ko.observable("");
    this.FriendlyName = ko.observable("");
    this.Description = ko.observable("");
    this.Author = ko.observable("");
    this.SupportedPlatforms = ko.observable(0);
    this.ContainerSupport = ko.observable(0);
    this.ContainerReason = ko.observable("");
    this.ExtraSetupStepsURI = ko.observable("");
    this.ResourceUsageInfo = ko.observable("");
    this.DisplayName = self.FriendlyName;
    this.DisplayImageSource = ko.observable("");
    this.DisplayImageURI = ko.computed(function () {
        return GetDisplayImageURI(self.DisplayImageSource());
    });
    this.DeprecatedReason = ko.observable("");
    this.runsOnWindows = ko.computed(() => (self.SupportedPlatforms() & supportedOS.Windows) > 0);
    this.runsOnLinux = ko.computed(() => (self.SupportedPlatforms() & supportedOS.Linux) > 0);
    this.Visible = ko.computed(() => self.DeprecatedReason() == null || self.DeprecatedReason() == "" || GetSetting("ADSModule.ADS.ShowDeprecated"));
}

function NewInstanceVM() {
    const self = this;
    this.availableApps = ko.observableArray(); //of supportedAppVM
    this.filteredApps = ko.computed(() => ko.utils.arrayFilter(self.availableApps(), (app) => app.Visible()));
    this.availableTargets = manageInstancesVM.groups; //observableArray of InstanceGroupVM
    this.selectedApplication = ko.observable(new SupportedAppVM()); //of SupportedAppVM
    this.filteredTargets = ko.computed(() => self.selectedApplication() == null ? [] :
        ko.utils.arrayFilter(self.availableTargets(),
            (target) => (target.platformCompatibility() & self.selectedApplication().SupportedPlatforms()) > 0)
    ); //of InstanceGroupVM
    this.selectedTarget = ko.observable(); //of InstanceGroupVM
    this.friendlyName = ko.observable("");
    this.availableIPs = ko.computed(() => self.selectedTarget() == null ? [] : self.selectedTarget().availableIPs());
    this.selectedAMPIP = ko.observable("0.0.0.0");
    this.afterCreation = ko.observable(0);
    this.startOnBoot = ko.observable(true);
    this.runInContainer = ko.observable(false);
    this.datastore = ko.observable(); //of DatastoreSummaryVM
    this.OpenExtraSetupSteps = function () {
        plausible("SetupHelp", { props: { "app": self.selectedApplication().FriendlyName() } });
        window.open(self.selectedApplication().ExtraSetupStepsURI(), "_blank");
    };
    this.groupName = ko.observable("");

    const ContainerSupport = {
        NoPreference: 0,
        NotSupported: 1,
        SupportedOnLinux: 2,
        SupportedOnWindows: 4,
        Supported: 2 | 4, // SupportedOnLinux | SupportedOnWindows
        RecommendedOnLinux: 8,
        RecommendedOnWindows: 16,
        Recommended: 8 | 16, // RecommendedOnLinux | RecommendedOnWindows
        RequiredOnLinux: 32,
        RequiredOnWindows: 64,
        Required: 32 | 64, // RequiredOnLinux | RequiredOnWindows
        RecommendedOrRequiredOnWindows: 16 | 64, // RecommendedOnWindows | RequiredOnWindows
        RecommendedOrRequiredOnLinux: 8 | 32, // RecommendedOnLinux | RequiredOnLinux
        RecommendedOrRequired: (8 | 32) | (16 | 64), // RecommendedOrRequiredOnLinux | RecommendedOrRequiredOnWindows
    };

    //!self.selectedTarget().createsInContainers()
    this.selectionsMade = ko.computed(() =>
        self.selectedTarget() != null &&
        self.selectedApplication() != null
    );

    this.showNoContainerNotice = ko.computed(() =>
        self.selectionsMade() &&
        !self.containerPlatformMismatch() &&
        self.selectedTarget().createsInContainers() &&
        self.selectedApplication().ContainerSupport() == ContainerSupport.NotSupported
    );

    this.containerIsRequired = ko.computed(() =>
        self.selectionsMade() &&
        (
            (self.selectedTarget().os() == supportedOS.Linux ?
                ContainerSupport.RequiredOnLinux :
                ContainerSupport.RequiredOnWindows
            ) &
            self.selectedApplication().ContainerSupport()
        ) === (self.selectedTarget().os() == supportedOS.Linux ?
            ContainerSupport.RequiredOnLinux :
            ContainerSupport.RequiredOnWindows
        ) &&
        !self.selectedTarget().createsInContainers()
    );

    this.containerIsRecommended = ko.computed(() =>
        self.selectionsMade() &&
        (
            (self.selectedTarget().os() == supportedOS.Linux ?
                ContainerSupport.RecommendedOnLinux :
                ContainerSupport.RecommendedOnWindows
            ) &
            self.selectedApplication().ContainerSupport()
        ) === (self.selectedTarget().os() == supportedOS.Linux ?
            ContainerSupport.RecommendedOnLinux :
            ContainerSupport.RecommendedOnWindows
        ) &&
        !self.selectedTarget().createsInContainers()
    );


    this.appIsSupportedByTarget = ko.computed(() =>
        self.selectionsMade() &&
        (self.selectedTarget().platformCompatibility() &
            self.selectedApplication().SupportedPlatforms()
        ) > 0
    );

    this.containerPlatformMismatch = ko.computed(() =>
        self.selectionsMade() &&
        (
            (self.selectedApplication().SupportedPlatforms() &
                self.selectedTarget().os()
            ) > 0
        ) &&
        (
            (self.selectedApplication().SupportedPlatforms() &
                self.selectedTarget().platformCompatibility()
            ) == 0
        )
    );
    this.incompatibleReason = ko.computed(() => {
        if (self.containerPlatformMismatch()) {
            return `While this application is compatible with the target platform (${self.selectedTarget().OSNameString()}), it is not compatible with the Linux containers currently in use by the selected target.`;
        } else if (self.selectedTarget() != null) {
            return `The developers of this application have not made a server available for ${self.selectedTarget().OSNameString()} systems.`;
        } else {
            return "";
        }
    });

    this.canCreate = ko.computed(() =>
        self.appIsSupportedByTarget() &&
        !self.showNoContainerNotice() &&
        !self.containerPlatformMismatch() &&
        (self.containerIsRequired() ?
            self.selectedTarget().createsInContainers() :
            true
        )
    );

    this.availableIPs.subscribe(() => createInstanceVM.selectedAMPIP(createInstanceVM.availableIPs()[0]));

    this.selectedTarget.subscribe(() => self.datastore(null));

    this.create = async function () {
        if (self.selectedApplication() == null || self.selectedTarget() == null) { return; }
        if (!self.canCreate()) { return }
        if ((self.selectedApplication().SupportedPlatforms() & self.selectedTarget().platformCompatibility()) === 0) {
            return UI.ShowModalAsync("Unsupported Configuration", `The selected target (${self.selectedTarget().name()}) is running ${self.selectedTarget().os() == supportedOS.Windows ? "Windows" : "Linux"}, but ${self.selectedApplication().FriendlyName()} only supports ${self.selectedApplication().SupportedPlatforms() == 1 ? "Windows" : "Linux"}.`, UI.Icons.Exclamation, UI.OKActionOnly);
        }

        const datastoreId = self.datastore() == null ? -1 : self.datastore().Id();

        plausible("Create", {
            props: {
                "app": self.selectedApplication().DisplayName(),
                "os": self.selectedTarget().osName(),
                "container": self.selectedTarget().createsInContainers(),
                "AMPVersion": viewModels.support.installedVersion(),
            }
        });

        const createInstanceTask = await API.ADSModule.CreateInstanceFromSpecTaskAsync(self.selectedApplication().Id(), self.selectedTarget().id, self.friendlyName, self.afterCreation, self.startOnBoot, datastoreId, self.groupName);

        if (createInstanceTask.Status) {
            createInstanceTask.onComplete(updateInstanceList);
        }
        else {
            UI.ShowModalAsync("Failed to create instance", createInstanceTask.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
    };
}

const templateSettingNodeOptions = ko.observable(null);

function deploymentTemplateVM() {
    const self = this;
    this.Id = 0;
    this.Name = "";
    this.Description = "";
    this.Module = "";
    this.TemplateInstance = null;
    this.TemplateRole = null;
    this.TemplateBaseApp = ko.observable(null);
    this.CloneRoleIntoUser = false;
    this.ZipOverlayPath = "";
    this.StartOnBoot = false;
    this.MatchDatastoreTags = false;
    this.SettingMappings = ko.observable({});
    this.Tags = ko.observableArray([]);
    this.selected = ko.observable(false);
    this.DeployTag = ko.observable("");
    this.vm = null;

    this.SettingMapValues = ko.observableArray();
    this.SettingMappings.subscribe(() => self.UpdateMappings());

    this.CloneTemplate = async function () {
        const newName = await UI.PromptAsync("New template name", "Please enter a new name to clone this template into");
        if (newName === null) { return; }
        const cloneResult = await API.ADSModule.CloneTemplateAsync(self.Id, newName);
        if (cloneResult.Status === true) {
            this.vm.refresh();
        }
        else {
            UI.ShowModalAsync("Failed to clone template", cloneResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
    };

    this.toJS = function () {
        return {
            Id: self.Id,
            Name: self.Name,
            Description: self.Description,
            Module: self.Module,
            TemplateInstance: self.TemplateInstance,
            TemplateRole: self.TemplateRole,
            TemplateBaseApp: self.TemplateBaseApp(),
            CloneRoleIntoUser: self.CloneRoleIntoUser,
            ZipOverlayPath: self.ZipOverlayPath,
            StartOnBoot: self.StartOnBoot,
            MatchDatastoreTags: self.MatchDatastoreTags,
            SettingMappings: self.SettingMappings(),
            Tags: self.Tags(),
        };
    };

    this.UpdateMappings = () => {
        const newValue = self.SettingMappings();
        if (newValue == null) { return; }
        self.SettingMapValues.removeAll();

        for (const key of Object.keys(newValue)) {
            const value = newValue[key];
            self.SettingMapValues.push({
                Key: key,
                Value: value
            });
        }
    };

    this.addKVP = () => {
        let existing = self.SettingMappings() || {};
        existing[self.newKVPkey()] = self.newKVPvalue() ?? "";
        self.SettingMappings(existing);
        self.newKVPkey("");
        self.newKVPvalue("");
        self.ApplyChanges();
    };
    this.removeKVP = (e) => {
        let existing = self.SettingMappings() || {};
        delete existing[e.Key];
        self.SettingMappings(existing);
        self.ApplyChanges();
    };
    this.newKVPkey = ko.observable();
    this.newKVPvalue = ko.observable();

    this.nodePickerVisible = ko.observable(false);
    this.nodePickerFilter = ko.observable("");
    this.nodePickerHighlight = ko.observable(-1);
    this.nodePickerFiltered = ko.computed(() => {
        const options = templateSettingNodeOptions();
        if (!options) { return []; }
        const filter = (self.nodePickerFilter() ?? "").toLocaleLowerCase();
        if (filter.length === 0) { return options; }
        return options.filter(o =>
            o.name.toLocaleLowerCase().includes(filter) ||
            o.value.toLocaleLowerCase().includes(filter)
        );
    });
    this.nodePickerSelectItem = function (item) {
        self.newKVPkey(item.value);
        self.nodePickerVisible(false);
        self.nodePickerFilter("");
        self.nodePickerHighlight(-1);
    };
    this.nodePickerInputEvent = function (_, e) {
        const val = self.newKVPkey() ?? "";
        self.nodePickerFilter(val);
        self.nodePickerHighlight(-1);
        if (templateSettingNodeOptions()) { self.nodePickerVisible(true); }
        return true;
    };
    this.nodePickerFocusEvent = function (_, e) {
        if (templateSettingNodeOptions()) {
            self.nodePickerFilter(self.newKVPkey() ?? "");
            self.nodePickerVisible(true);
        }
        return true;
    };
    this.nodePickerKeydown = function (_, e) {
        if (!self.nodePickerVisible()) { return true; }
        const items = self.nodePickerFiltered();
        let idx = self.nodePickerHighlight();
        if (e.key === "ArrowDown") {
            self.nodePickerHighlight(Math.min(idx + 1, items.length - 1));
            return false;
        } else if (e.key === "ArrowUp") {
            self.nodePickerHighlight(Math.max(idx - 1, 0));
            return false;
        } else if (e.key === "Enter" && idx >= 0 && items[idx]) {
            self.nodePickerSelectItem(items[idx]);
            return false;
        } else if (e.key === "Escape") {
            self.nodePickerVisible(false);
            self.nodePickerHighlight(-1);
            return false;
        }
        return true;
    };

    this.addTag = async function () {
        const newTag = await UI.PromptAsync("New Tag", "Enter Tag");
        if (newTag == null || newTag == "") { return; }
        if (self.Tags() == null) {
            self.Tags([newTag]);
        } else {
            self.Tags.push(newTag);
        }
        self.ApplyChanges();
    };

    this.removeTag = function (toRemove) {
        self.Tags.remove((v) => v == toRemove);
        self.ApplyChanges();
    }

    this.ApplyChanges = async (_, e) => {
        UI.wait2sec(e);
        await API.ADSModule.UpdateDeploymentTemplate(self.toJS());
    };

    this.Click = (d, e) => {
        const existing = self.vm.selectedTemplate();
        if (existing != null) { existing.selected(false); }
        self.selected(true);
        self.vm.selectedTemplate(self);

        if (e != undefined) {
            UI.NavigateTo(`/templates/${self.Id}`);
        }
    };

    this.DeleteTemplate = async () => {
        const result = await UI.ShowModalAsync("Delete Template", { text: "Are you sure you want to delete this template?", subtitle: self.Name }, UI.Icons.Exclamation, [
            new UI.ModalAction("Delete Template", true, "bgRed slideIcon icons_remove", true),
            new UI.ModalAction("Cancel", false, "", true)
        ]);

        if (result === true) {
            await API.ADSModule.DeleteDeploymentTemplateAsync(self.Id);
            await self.vm.refresh();
        }
    };

    this.DeployTemplate = async function () {
        deployTemplatesVM.selectedTemplate(self);
        UI.ShowWizard("#tab_ADSModule_DeployTemplateWizard");
    };
}

function TemplateManagementVM(availableAppsObservable) {
    const self = this;
    this.selectedTemplate = ko.observable(); //of deploymentTemplateVM
    this.availableTemplates = ko.observableArray(); //of deploymentTemplateVM
    this.availableRoles = ko.observableArray(); //of Key Value
    this.availableApps = availableAppsObservable; //of supportedAppVM
    this.firstLoad = true;
    this.createTemplate = async () => {
        const newTemplateName = await UI.PromptAsync("Create new Template", "Please enter a name for your new template.");
        if (newTemplateName == null) { return; }
        await API.ADSModule.CreateDeploymentTemplateAsync(newTemplateName);
        await self.refresh();
    };
    this.refresh = async (skipReload = false) => {
        if (!self.firstLoad && skipReload) { return; }
        await viewModels.ampUserList.refresh();
        let result = await API.ADSModule.GetDeploymentTemplatesAsync();
        self.availableTemplates.removeAll();
        let data = ko.quickmap.to(deploymentTemplateVM, result, false, { vm: self });
        ko.utils.arrayPushAll(self.availableTemplates, data);
        self.selectedTemplate(null);
        self.availableRoles.removeAll();
        result = await API.Core.GetRoleIdsAsync();
        for (const k of Object.keys(result)) {
            self.availableRoles.push({ Id: k, Name: result[k] });
        }
        if (templateSettingNodeOptions() === null) {
            const spec = await API.Core.GetSettingsSpecAsync();
            const nodeOptions = [];
            for (const settings of Object.values(spec)) {
                for (const setting of settings) {
                    if (setting.Attributes?.KeyEnumValues) {
                        for (const [value, name] of Object.entries(setting.Attributes.KeyEnumValues)) {
                            if (!nodeOptions.find(o => o.value === value)) {
                                nodeOptions.push({ value, name });
                            }
                        }
                    }
                }
            }
            templateSettingNodeOptions(nodeOptions.length > 0 ? nodeOptions.sort((a, b) => a.name.localeCompare(b.name)) : null);
        }
        self.firstLoad = false;
    };
    this.handlePop = async function (_, parts) {
        if (parts.length < 2) { return; }
        if (self.firstLoad) { await self.refresh(); }
        self.availableTemplates().find(t => t.Id == parts[1])?.Click();
    };
}

function DeployTemplateVM(mgmtVM) {
    const self = this;
    this.mgmtVM = mgmtVM;
    this.deployType = ko.observable("10");
    this.deployType.subscribe(() => self.reset());
    this.NewUsername = ko.observable(null);
    this.NewPassword = ko.observable(null);
    this.ConfirmNewPassword = ko.observable(null);
    this.NewEmail = ko.observable(null);
    this.FriendlyName = ko.observable(null);
    this.selectedTemplate = ko.observable(null);
    this.users = viewModels.ampUserList.users;
    this.selectedUser = ko.observable(null);
    this.Tags = ko.observableArray();
    this.Tag = ko.observable(null);
    this.Secret = ko.observable(null);
    this.PostCreate = ko.observable("0");
    this.addTag = async function () {
        const newTag = await UI.PromptAsync("New Tag", "Enter Tag");
        if (newTag == null || newTag == "") { return; }
        self.Tags.push(newTag);
    };

    this.removeTag = function (toRemove) {
        self.Tags.remove((v) => v == toRemove);
    }

    this.reset = function () {
        self.NewUsername(null);
        self.NewPassword(null);
        self.ConfirmNewPassword(null);
        self.NewEmail(null);
        self.selectedUser(null);
        self.Tag(null);
        self.Tags.removeAll();
    };

    this.deploy = async function () {
        let deployUser = self.NewUsername();
        if (self.deployType() == "20") {
            if (self.selectedUser() == null) {
                await UI.ShowModalAsync("Unable to deploy template", "Please select a user to deploy the template to, or change deployment type.", UI.Icons.Info, UI.OKActionOnly);
                return;
            }
            deployUser = self.selectedUser().username;
        }
        const deployResult = await API.ADSModule.DeployTemplateAsync(self.selectedTemplate().Id, deployUser, self.NewPassword, self.NewEmail, self.Tags(), self.Tag(), self.FriendlyName, self.Secret, self.PostCreate);
        self.reset();
        if (deployResult.Status !== true) {
            UI.ShowModalAsync("Unable to deploy template", deployResult.Reason, UI.Icons.Info, UI.OKActionOnly);
        }
    };
}

function showCreateInstance() {
    createInstanceVM.afterCreation(Number.parseInt(GetSetting("ADSModule.Defaults.DefaultPostCreate") || 0));
    UI.ShowWizard("#tab_ADSModule_CreateInstanceWizard");
}

function CloneInstanceVM() {
    const self = this;
    this.sourceInstance = ko.observable(null);
    this.availableTargets = manageInstancesVM.groups;
    this.selectedTarget = ko.observable();
    this.datastore = ko.observable();
    this.newFriendlyName = ko.observable("");
    this.afterCreation = ko.observable(0);
    this.startOnBoot = ko.observable(false);
    this.deleteSource = ko.observable(false);
    this.copyFiles = ko.observable(false);

    this.selectedTarget.subscribe(() => self.datastore(null));

    this.canClone = ko.computed(() => self.sourceInstance() != null && self.selectedTarget() != null);

    this.clone = async function () {
        if (!self.canClone()) { return; }

        if (self.deleteSource()) {
            const instanceName = self.sourceInstance().name;
            const confirmation = await UI.PromptAsync("Confirm Migration", `This will permanently delete the source instance after migration completes. Please enter the name of the instance to confirm: ${instanceName}`);
            if (confirmation == null) { return; }
            if (confirmation !== instanceName) {
                UI.ShowModalAsync("Confirmation Failed", "The instance name you entered did not match. The migration will not proceed.", UI.Icons.Exclamation, UI.OKActionOnly);
                return;
            }
        }

        const datastoreId = self.datastore() == null ? -1 : self.datastore().Id();
        const cloneResult = await API.ADSModule.CloneInstanceTaskAsync(
            self.sourceInstance().id,
            self.selectedTarget().id,
            self.newFriendlyName().trim(),
            Number.parseInt(self.afterCreation()),
            self.startOnBoot(),
            datastoreId,
            null,
            null,
            null,
            self.deleteSource(),
            self.copyFiles()
        );
        if (cloneResult.Status) {
            cloneResult.onComplete(updateInstanceList);
        } else {
            const action = self.deleteSource() ? 'migrate' : 'clone';
            UI.ShowModalAsync(`Failed to ${action} instance`, cloneResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
        }
    };
}

class DatastoreVM {
    constructor() {
        const self = this;
        this.Id = ko.observable(0);
        this.FriendlyName = ko.observable("");
        this.Description = ko.observable("");
        this.Directory = ko.observable("");
        this.SoftLimitMB = ko.observable(0);
        this.CurrentUsageMB = ko.observable(0);
        this.InstanceLimit = ko.observable(0);
        this.Priority = ko.observable(0);
        this.Active = ko.observable(true);
        this.Tags = ko.observableArray([]);
        this.IsInternal = ko.observable(false);
        this._vm = null;

        this._usagePercent = ko.computed(() => self.SoftLimitMB() > 0 ? Math.floor((self.CurrentUsageMB() / self.SoftLimitMB()) * 100) : 0);
        this._dashOffset = ko.computed(() => 400 - (Math.min(100, self._usagePercent()) * 2));

        this.edit = async function () {
            await sleepAsync(100);
            let editVM = new AddEditDatastoreVM(self._vm);
            ko.quickmap.map(editVM, self.asJS());
            editVM.edit(true);
            self._vm.addEditStore(editVM);

            UI.ShowWizard("#tab_ADSModule_DatastoreInfoPopup");
        };

        this.menu = function (_, e) {
            if (e) { e.stopImmediatePropagation(); }
            self._vm.selectedDatastore(this);
            UI.ShowPopupMenu("#tab_ADSModule_DatastoreContextMenu", e);
        };

        this.refreshUsage = async function () {
            const updateResult = await API.ADSModule.RequestDatastoreSizeCalculationTaskAsync(self.Id());

            if (updateResult.Status) {
                updateResult.onComplete(self.refreshSelf);
            }
        };

        this.repair = async function () {
            const updateResult = await API.ADSModule.RepairDatastoreTaskAsync(self.Id());

            if (updateResult.Status) {
                updateResult.onComplete(self.refreshSelf);
            }
        };

        this.refreshSelf = async function () {
            const newInfo = await API.ADSModule.GetDatastoreAsync(self.Id());
            ko.quickmap.map(self, newInfo);
        };

        this.asJS = function () {
            const result = {
                Id: self.Id(),
                FriendlyName: self.FriendlyName(),
                Description: self.Description(),
                Directory: self.Directory(),
                SoftLimitMB: self.SoftLimitMB(),
                InstanceLimit: self.InstanceLimit(),
                Priority: self.Priority(),
                Active: self.Active(),
                Tags: self.Tags(),
                IsInternal: self.IsInternal()
            };
            return result;
        };
    }
}

class AddEditDatastoreVM extends DatastoreVM {
    constructor(vm) {
        super();
        const self = this;
        this.edit = ko.observable(false);
        this._vm = vm;

        this.saveChanges = async function () {
            const addEditResult = await (self.edit() ? API.ADSModule.UpdateDatastoreAsync(self.asJS()) : API.ADSModule.AddDatastoreAsync(self.asJS()));
            if (!addEditResult.Status) {
                await UI.ShowModalAsync(`Unable to ${this.edit() ? "edit" : "add new"} datastore.`, addEditResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
                return;
            }
            UI.HideWizard("#tab_ADSModule_DatastoreInfoPopup");
            await self.refreshSelf();
        };

        this.cancel = function () {
            UI.HideWizard("#tab_ADSModule_DatastoreInfoPopup");
        };

        this.addTag = async function () {
            const newTag = await UI.PromptAsync("New Tag", "Enter Tag");
            if (newTag == null || newTag == "") { return; }
            self.Tags.push(newTag);
        };

        this.removeTag = function (toRemove) {
            self.Tags.remove((v) => v == toRemove);
        }

        this.deleteDatastore = async function () {
            const confirmation = await UI.PromptAsync("Confirm Deletion", "Are you sure you want to delete this datastore? This will not delete any instances associated with it, but you will not be able to provision new instances at this location. Make sure you have at least one valid datastore before trying to provision new instances.\n\nPlease enter the name of this datastore to confirm.");
            if (confirmation != this.FriendlyName()) { return; }
            const deleteResult = await API.ADSModule.DeleteDatastoreAsync(self.Id());
            self._vm.refresh();
            if (!deleteResult.Status) {
                await UI.ShowModalAsync("Unable to delete datastore", deleteResult.Reason, UI.Icons.Exclamation, UI.OKActionOnly);
                return;
            }
            UI.HideWizard("#tab_ADSModule_DatastoreInfoPopup");
        }
    }
}

class DatastoreManagementVM {
    constructor() {
        const self = this;
        this.selectedDatastore = ko.observable(); //of DatastoreVM
        this.addEditStore = ko.observable(); //of DatastoreVM / addEditDatastoreVM
        this.Datastores = ko.observableArray([]); //of DatastoreVM
        this.refresh = async function () {
            self.Datastores.removeAll();
            ko.utils.arrayPushAll(self.Datastores, ko.quickmap.to(DatastoreVM, await API.ADSModule.GetDatastoresAsync(), false, { _vm: self }));
        };
        this.addNew = function () {
            const newDatastore = new AddEditDatastoreVM(self);
            self.addEditStore(newDatastore);
            UI.ShowWizard("#tab_ADSModule_DatastoreInfoPopup");
        };
    }
}

const manageInstancesVM = new InstanceManagementVM();
const createInstanceVM = new NewInstanceVM();
const cloneInstanceVM = new CloneInstanceVM();
const manageTemplatesVM = new TemplateManagementVM(createInstanceVM.availableApps);
const manageDatastoresVM = new DatastoreManagementVM();
const deployTemplatesVM = new DeployTemplateVM(manageTemplatesVM);

function ADSinit() {
    updateInstanceList();
    updateNewAppInfo();

    RegisterViewmodel(manageInstancesVM);
    RegisterViewmodel(createInstanceVM);
    RegisterViewmodel(cloneInstanceVM);
    RegisterViewmodel(manageTemplatesVM);
    RegisterViewmodel(manageDatastoresVM);
    RegisterViewmodel(deployTemplatesVM);

    setWizardCallback("CreateInstance", createInstanceVM.create, null);
    setWizardCallback("CloneInstance", cloneInstanceVM.clone, null);
    setWizardCallback("DeployTemplate", deployTemplatesVM.deploy, null);
    setWizardCallback("PairTarget", null, null, null, manageInstancesVM.cancelPairingUI);
}