// Sequence counter used to discard stale loadProfileData() AJAX responses.
// Incremented both when a NEW loadProfileData() request is sent (so only the
// latest response is accepted) and when the user manually changes the driver
// source (so an in-flight loadProfileData() can no longer overwrite the user's
// selection).
var _loadProfileDataSeq = 0;

// Flag that suppresses onDriverSourceChange() during programmatic val()+refresh()
// calls on #driver_source.  Some versions of Bootstrap Select fire a deferred
// (setTimeout) 'change' event after selectpicker('refresh'), meaning the event
// arrives AFTER we re-attach the handler — making the off/on guard ineffective.
// Using this flag (cleared after a short timeout) is the only reliable defence.
var _suppressDriverSourceChange = false;

// Timestamped log helper — prepends HH:MM:SS.mmm so the log ordering is clear
// in the browser console even when messages are collapsed or reordered.
function _log(msg)
{
    var now = new Date();
    var ts = now.getHours().toString().padStart(2, '0') + ':' +
        now.getMinutes().toString().padStart(2, '0') + ':' +
        now.getSeconds().toString().padStart(2, '0') + '.' +
        now.getMilliseconds().toString().padStart(3, '0');
    console.log('[' + ts + '] ' + msg);
}

// Startup function
$(function ()
{
    $.fn.popover.Constructor.Default.whiteList.table = [];
    $.fn.popover.Constructor.Default.whiteList.tr = [];
    $.fn.popover.Constructor.Default.whiteList.td = [];
    $.fn.popover.Constructor.Default.whiteList.th = [];
    $.fn.popover.Constructor.Default.whiteList.div = [];
    $.fn.popover.Constructor.Default.whiteList.tbody = [];
    $.fn.popover.Constructor.Default.whiteList.thead = [];

    $('[data-toggle="tooltip"]').tooltip();

    loadCurrentProfileDrivers();
    getStatus();

    $("#drivers_list").change(function ()
    {
        var name = $("#profiles option:selected").text();
        saveProfileDrivers(name, true);
        // Re-sort the list so selected drivers always appear at the top
        bubbleSelectedDriversToTop();
        $("#drivers_list").selectpicker('refresh');
    });

    $("#remote_drivers").change(function ()
    {
        var name = $("#profiles option:selected").text();
        saveProfileDrivers(name, true);
    });

    // Handle driver source change.
    // The handler checks _suppressDriverSourceChange before acting so that
    // programmatic val()+selectpicker('refresh') calls in detectFlatpaks() and
    // loadProfileData() cannot accidentally trigger a source switch even when
    // Bootstrap Select fires a deferred 'change' event after the refresh.
    $("#driver_source").change(function ()
    {
        if (_suppressDriverSourceChange)
        {
            _log('[driver_source change] suppressed (programmatic update in progress)');
            return;
        }
        onDriverSourceChange();
    });

    // Restart
    $('#restart_sm').confirmation({
        onConfirm: function ()
        {
            $.ajax({ type: 'POST', url: '/api/system/reboot' });
            $("#server_notify").html("<p class='alert alert-success'>StellarMate is restarting...</p>");
        }
    });

    // Shutdown
    $('#shutdown_sm').confirmation({
        onConfirm: function ()
        {
            $.ajax({ type: 'POST', url: '/api/system/shutdown' });
            $("#server_notify").html("<p class='alert alert-success'>StellarMate is shutting down...</p>");
        }
    });

    getSMInfo();

    // Detect available KStars flatpaks for driver sources
    detectFlatpaks();

    $("#vnc_sm").attr("onClick", "location.href='http://" + window.location.hostname + ":6080/vnc.html'");
});

function getSMInfo()
{
    var hostname = "Uknown";
    $.getJSON("/api/info/hostname", function (jsonResponse)
    {
        if (jsonResponse)
        {
            hostname = jsonResponse.hostname;
        }
    });

    var model = "Unknown";
    $.getJSON("/api/info/model", function (jsonResponse)
    {
        if (jsonResponse)
        {
            model = jsonResponse.model;
        }
    });

    var version = "Unknown";
    $.getJSON("/api/info/version", function (jsonResponse)
    {
        if (jsonResponse)
        {
            version = jsonResponse.version;
        }
    });

    let interfaces = [];

    $.getJSON("/api/info/interfaces", function (jsonResponse)
    {

        if (jsonResponse instanceof Array) 
        {
            interfaces = jsonResponse;
        }
        else
        {
            interfaces = jsonResponse.interfaces;
        }

    });

    $('#sm-info-button').popover(
        {
            trigger: 'click',
            placement: 'right',
            html: true,
            title: "StellarMate Info",
            content: function ()
            {
                let listHTML = "<table class='table table-striped'><tbody>";
                listHTML += "<tr><td><b>Name</b></td><td>" + hostname + "</td></tr>";
                listHTML += "<tr><td><b>Model</b></td><td>" + model + "</td></tr>";
                listHTML += "<tr><td><b>Version</b></td><td>" + version + "</td></tr>";
                listHTML += "</tbody></table>";

                listHTML += "<table class='table table-striped'><thead class='thead-light'><tr><th>Interface</th><th>IP Address</th></tr></thead>";
                listHTML += "<tbody>";
                for (const iface of interfaces)
                // //interfaces.forEach( function(iface)
                {
                    listHTML += "<tr><td>" + iface.interface + "</td><td>" + iface.ip + "</td></tr>";
                }
                listHTML += "</tbody></table>";

                return listHTML;
            }
        });
}


function saveProfile()
{
    var options = profiles.options;
    var name = options[options.selectedIndex].value;
    // Remove any extra spaces
    name = name.trim();

    var url = "/api/profiles/" + name;

    //console.log(url)

    $.ajax({
        type: 'POST',
        url: encodeURI(url),
        success: function ()
        {
            //console.log("add new a profile " + name);
            // Save profile info (port, driver_source, etc.) then save drivers
            saveProfileInfo(function ()
            {
                saveProfileDrivers(name);
            });
        },
        error: function ()
        {
            alert('error add new  profile failed');
        }
    });
}

function saveProfileInfo(callback)
{
    var options = profiles.options;
    var name = options[options.selectedIndex].value;
    console.log(name);
    var port = parseInt($("#profile_port").val());
    console.log(port);
    var autostart = ($('#profile_auto_start').is(':checked')) ? 1 : 0;
    var autoconnect = ($('#profile_auto_connect').is(':checked')) ? 1 : 0;
    var driver_source = $("#driver_source").val();
    //console.log(autostart);
    //var url     =  "/api/profiles/" + name + "/" + port + "/" + autostart;
    var url = "/api/profiles/" + name;

    // Build profile info object (omit scripts when empty, matching KStars behaviour)
    var profileInfo = {
        "port": port,
        "autostart": autostart,
        "autoconnect": autoconnect,
        "driver_source": driver_source
    };

    // Only include scripts in the payload when they are a non-empty array.
    // The DB stores scripts as a JSON-encoded string (e.g. '""' for empty, '[{...}]' for real
    // scripts). After parsing, we must verify the result is an actual non-empty array before
    // sending — sending an empty string or null causes a Pydantic list_type validation error.
    var scripts = window.currentProfileScripts || "";
    if (scripts && scripts !== "")
    {
        try
        {
            var parsedScripts = JSON.parse(scripts);
            if (Array.isArray(parsedScripts) && parsedScripts.length > 0)
            {
                profileInfo["scripts"] = parsedScripts;
            }
            // Otherwise (empty string, null, empty array) — omit scripts from payload entirely
        }
        catch (e)
        {
            // If parsing fails, skip — don't send a malformed value
            console.warn("Could not parse profile scripts, omitting from payload:", e);
        }
    }
    profileInfo = JSON.stringify(profileInfo);
    console.log("Profile info " + profileInfo);

    console.log(url);

    $.ajax({
        type: 'PUT',
        url: encodeURI(url),
        data: profileInfo,
        contentType: "application/json; charset=utf-8",
        success: function ()
        {
            console.log("Profile " + name + " info is updated");
            // Fire callback only after save is confirmed
            if (callback && typeof callback === 'function')
            {
                callback();
            }
        },
        error: function ()
        {
            alert('error update profile info failed');
        }
    });
}

function saveProfileDrivers(profile, silent)
{

    if (typeof (silent) === 'undefined') silent = false;

    var url = "/api/profiles/" + profile + "/drivers";
    var drivers = [];

    $("#drivers_list :selected").each(function (i, sel)
    {
        drivers.push({
            "label": $(sel).text()
        });
    });

    // Check for remote drivers
    var remote = $("#remote_drivers").val();
    if (remote)
    {
        drivers.push({
            "remote": remote
        });
        console.log({
            "remote": remote
        });
    }

    drivers = JSON.stringify(drivers);

    //console.log("my json string is " + drivers);

    $.ajax({
        type: 'POST',
        url: encodeURI(url),
        data: drivers,
        contentType: "application/json; charset=utf-8",
        // Identify this as a web UI call so the server knows the driver list is
        // authoritative (user-selected) and must NOT apply KStars-sync preservation
        // logic (re-injecting cross-source-only drivers that KStars cannot see).
        headers: { "X-SM-Source": "webui" },
        success: function ()
        {
            //console.log("Drivers added successfully to profile");
            if (silent === false)
                $("#notify_message").html('<br/><div class="alert alert-success"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>Profile ' + profile + ' saved.</div>');
        },
        error: function ()
        {
            alert('error failed to add drivers to profile');
        }
    });
}

function loadCurrentProfileDrivers()
{
    clearDriverSelection();

    var name = $("#profiles option:selected").text();

    // First load profile data to get the driver source.
    // We receive driverSource as a direct argument from the callback so we are
    // not affected by the race condition where detectFlatpaks() may not have
    // enabled the flatpak option in the <select> yet (causing jQuery.val() to
    // silently return the wrong value).
    loadProfileData(function (driverSource)
    {
        // After profile data is loaded, load drivers from the correct source
        driverSource = driverSource || 'system';
        loadDriversFromSource(driverSource, function ()
        {
            // Then select the profile's drivers
            var url = "/api/profiles/" + name + "/labels";
            $.getJSON(url, function (drivers)
            {
                $.each(drivers, function (i, driver)
                {
                    var label = driver.label;
                    // Use fuzzy matching: exact match first, then base-label match.
                    // This handles cases where a label was stored as "LibCamera" (plain)
                    // but now appears as "LibCamera [system]" in a flatpak-master merged list,
                    // or vice-versa.
                    var opt = findOptionByLabel(label);
                    if (opt.length > 0)
                    {
                        opt.prop('selected', true);
                        _log('[loadCurrentProfileDrivers] selected "' + opt.val() + '" for stored label "' + label + '"');
                    }
                    else
                    {
                        _log('[loadCurrentProfileDrivers] driver not found in list: "' + label + '"');
                    }
                });

                // Move selected drivers to the top of the list
                bubbleSelectedDriversToTop();
                $("#drivers_list").selectpicker('refresh');
            });
        });
    });

    var url = encodeURI("/api/profiles/" + name + "/remote");

    $.getJSON(url, function (data)
    {
        if (data && typeof data === 'string' && data !== '')
        {
            $("#remote_drivers").val(data);
        }
        else
        {
            $("#remote_drivers").val("");
        }
    });
}

function loadProfileData(callback)
{
    var name = $("#profiles option:selected").text();
    var url = encodeURI("/api/profiles/" + name);

    // Capture a sequence number for THIS request.  If _loadProfileDataSeq has
    // been bumped (by a newer request or by the user manually changing the
    // source) before our response arrives, we simply discard the stale result.
    var mySeq = ++_loadProfileDataSeq;

    $.getJSON(url, function (info)
    {
        // Stale response — a newer load or a user-initiated source change has
        // already superseded this one.  Do nothing.
        if (mySeq !== _loadProfileDataSeq)
        {
            return;
        }

        if (info.autostart == 1)
            $("#profile_auto_start").prop("checked", true);
        else
            $("#profile_auto_start").prop("checked", false);

        if (info.autoconnect == 1)
            $("#profile_auto_connect").prop("checked", true);
        else
            $("#profile_auto_connect").prop("checked", false);

        $("#profile_port").val(info.port);

        // Resolve the driver source from the profile data directly — do NOT
        // rely on reading the value back from the DOM, because the flatpak
        // option may still be disabled when this callback fires (detectFlatpaks
        // is running concurrently) and jQuery silently ignores val() calls that
        // target a disabled/missing option.
        var driverSource = info.driver_source || 'system';

        // Update the dropdown value AND refresh the Bootstrap Select button so
        // the displayed label matches the profile's driver_source.
        // Bootstrap Select does NOT auto-update its button on a plain val() call —
        // selectpicker('refresh') is required.
        //
        // Suppress onDriverSourceChange() while doing this.  Some versions of
        // Bootstrap Select fire a deferred (setTimeout) 'change' event after
        // selectpicker('refresh'), so simply doing off/on around the call is not
        // reliable — the deferred event can arrive after the handler is re-attached.
        // We use _suppressDriverSourceChange with a timeout reset instead.
        _log('[loadProfileData] setting driver_source to "' + driverSource + '" (seq=' + mySeq + ')');
        _suppressDriverSourceChange = true;
        $("#driver_source").val(driverSource);
        $("#driver_source").selectpicker('refresh');
        // Clear the suppression flag after a short delay to catch any deferred events
        setTimeout(function () { _suppressDriverSourceChange = false; }, 100);

        // Store scripts for later use (to preserve when updating profile)
        window.currentProfileScripts = info.scripts || "";

        // Call callback if provided, passing driverSource directly so callers
        // don't have to re-read it from the (possibly incorrect) DOM value.
        if (callback && typeof callback === 'function')
        {
            callback(driverSource);
        }
    });
}

function clearDriverSelection()
{
    // Move any options from the ★ Selected Drivers group back to their family groups
    // so the list is in a clean natural state before loading a new profile
    flattenDriverList();
    $("#drivers_list option").prop('selected', false);
    $("#drivers_list").selectpicker('refresh');
    // Uncheck Auto Start
    $("#profile_auto").prop("checked", false);
    $("#profile_port").val(7624);
}

// Restore the ★ Selected Drivers optgroup back into natural family groups
function flattenDriverList()
{
    var selectedGroup = $("#drivers_list optgroup[label='\u2605 Selected Drivers']");
    if (selectedGroup.length === 0) return;

    // Move each option back into its original family group
    selectedGroup.find('option').each(function ()
    {
        var family = $(this).attr('data-family');
        var familyGroup = $("#drivers_list optgroup[label='" + family + "']");
        if (familyGroup.length > 0)
        {
            familyGroup.append($(this));
        }
    });

    // Remove the now-empty ★ group
    selectedGroup.remove();
}

function addNewProfile()
{
    var profile_name = $("#new_profile_name").val();
    if (profile_name)
    {
        //console.log("profile is " + profile_name);
        $("#profiles").append("<option id='" + profile_name + "'>" + profile_name + "</option>");

        // Explicitly select the new profile and refresh the selectpicker UI
        $("#profiles").val(profile_name);
        $("#profiles").selectpicker('refresh');

        clearDriverSelection();

        $("#notify_message").html('<br/><div class="alert alert-success"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>Profile ' + profile_name + ' created. Select the profile drivers and then save the profile.</div>');
    }
}

function removeProfile()
{
    //console.log("in delete profile");
    var name = $("#profiles option:selected").text();
    var url = "/api/profiles/" + name;

    console.log(url);

    if ($("#profiles option").size() == 1)
    {
        $("#notify_message").html('<br/><div class="alert alert-success"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>Cannot delete default profile.</div>');
        return;
    }

    $.ajax({
        type: 'DELETE',
        url: encodeURI(url),
        success: function ()
        {
            //console.log("delete profile " + name);
            $("#profiles option:selected").remove();
            $("#profiles").selectpicker('refresh');
            loadCurrentProfileDrivers();

            $("#notify_message").html('<br/><div class="alert alert-success"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>Profile ' + name + ' deleted.</div>');
        },
        error: function ()
        {
            alert('error delete profile failed');
        }
    });
}

function toggleServer()
{
    var status = $.trim($("#server_command").text());

    if (status == "Start")
    {
        var profile = $("#profiles option:selected").text();
        var url = "/api/server/start/" + profile;

        // Show loading state
        $("#server_notify").html("<p class='alert alert-info'><span class='fas fa-spinner fa-spin'></span> Starting INDI server...</p>");
        $("#server_command").prop('disabled', true);

        $.ajax({
            type: 'POST',
            url: encodeURI(url),
            success: function ()
            {
                //console.log("INDI Server started!");
                // Poll for server status with timeout
                pollServerStatus(0);
            },
            error: function ()
            {
                alert('Failed to start INDI server.');
                $("#server_command").prop('disabled', false);
            }
        });
    } else
    {
        $.ajax({
            type: 'POST',
            url: "/api/server/stop",
            success: function ()
            {
                //console.log("INDI Server stopped!");
                getStatus();
            },
            error: function ()
            {
                alert('Failed to stop INDI server.');
            }
        });
    }
}

function pollServerStatus(attempts)
{
    if (attempts >= 10)
    {
        // Timeout after 10 attempts (5 seconds)
        $("#server_notify").html("<p class='alert alert-warning'>Server start timeout. Please check manually.</p>");
        $("#server_command").prop('disabled', false);
        getStatus();
        return;
    }

    $.getJSON("/api/server/status", function (data)
    {
        if (data[0].status == "True")
        {
            // Server is running, get active drivers
            getActiveDrivers();
            $("#server_command").prop('disabled', false);
        }
        else
        {
            // Server not yet running, poll again
            setTimeout(function ()
            {
                pollServerStatus(attempts + 1);
            }, 500); // Poll every 500ms
        }
    }).fail(function ()
    {
        // Error checking status, try again
        setTimeout(function ()
        {
            pollServerStatus(attempts + 1);
        }, 500);
    });
}

function getStatus()
{
    $.getJSON("/api/server/status", function (data)
    {
        if (data[0].status == "True")
            getActiveDrivers();
        else
        {
            $("#server_command").html("<span class='fas fa-play fa-fw' aria-hidden='true'></span> Start");
            $("#server_notify").html("<p class='alert alert-info'>Server is Offline.</p>");
        }

    });
}

function getActiveDrivers()
{
    $.getJSON("/api/server/drivers", function (data)
    {
        $("#server_command").html("<span class='fas fa-stop fa-fw' aria-hidden='true'></span> Stop");
        var msg = "<p class='alert alert-info'>Server is Online.<ul  class=\"list-unstyled\">";
        var counter = 0;
        $.each(data, function (i, field)
        {
            msg += "<li>" + "<button class=\"btn btn-xs\" " +
                "onCLick=\"restartDriver('" + field.label + "')\" data-toggle=\"tooltip\" " +
                "title=\"Restart Driver\">" +
                "<span class=\"fa fa-redo fa-fw\" aria-hidden=\"true\"></span></button> " +
                field.label + "</li>";
            counter++;
        });

        msg += "</ul></p>";

        $("#server_notify").html(msg);

        if (counter < $("#drivers_list :selected").size())
        {
            $("#notify_message").html('<br/><div class="alert alert-success"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>Not all profile drivers are running. Make sure all devices are powered and connected.</div>');
            return;
        }
    });

}

function restartDriver(label)
{
    $.ajax({
        type: 'POST',
        url: "/api/drivers/restart/" + label,
        success: function ()
        {
            getStatus();
            $("#notify_message").html('<br/><div class="alert alert-success"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>Restarting driver "' + label + '" succeeded.</div>');
        },
        error: function ()
        {
            alert('Restarting driver "' + label + '" failed!');
        }
    });
}

// ---------------------------------------------------------------------------
// Source-tag helpers (mirror the Python _parse_driver_label / _tagged_label)
// ---------------------------------------------------------------------------

/**
 * Strip the trailing "[source]" suffix from a driver label, if present.
 * E.g. "ZWO CCD [flatpak-stable]" → "ZWO CCD"
 * @param {string} label
 * @returns {string}
 */
function getBaseLabel(label)
{
    return label.replace(/\s+\[(system|flatpak-stable|flatpak-master)\]\s*$/, '');
}

/**
 * Find a <option> in #drivers_list by label, using two-pass matching:
 *   1. Exact value match.
 *   2. Base-label match (ignores any [source] suffix on either side).
 *
 * Returns the jQuery object for the option (may be empty if not found).
 * @param {string} label
 * @returns {jQuery}
 */
function findOptionByLabel(label)
{
    // Pass 1 — exact match
    var sel = "#drivers_list option[value='" + label.replace(/'/g, "\\'") + "']";
    if ($(sel).length > 0)
        return $(sel);

    // Pass 2 — base-label match (strip [source] from both query and candidate)
    var base = getBaseLabel(label);
    var found = $();
    $("#drivers_list option").each(function ()
    {
        if (getBaseLabel($(this).attr('data-base-label') || $(this).val()) === base)
        {
            found = $(this);
            return false; // break $.each
        }
    });
    return found;
}

// ---------------------------------------------------------------------------
// Load drivers from specific source (merged=true includes all sources)
// ---------------------------------------------------------------------------
/**
 * Populate #drivers_list from the API using merged mode.
 *
 * When merged=true the server returns:
 *   • Drivers that exist in *source* — plain label (no suffix).
 *   • Drivers exclusive to other installed sources — label suffixed with
 *     "[source]", e.g. "ZWO CCD [flatpak-stable]".
 *
 * Each <option> stores:
 *   • value        = full label (possibly tagged)
 *   • data-family  = driver family for group re-building
 *   • data-base-label = label with any [source] tag stripped (for fuzzy matching)
 *   • data-source  = driver_source value from the API
 *
 * @param {string}   source   Master driver source ('system', 'flatpak-stable', …)
 * @param {Function} callback Optional callback fired after the list is rebuilt.
 */
function loadDriversFromSource(source, callback)
{
    // Use merged=true so drivers from all installed sources are shown.
    // Drivers exclusive to non-master sources get a "[source]" label suffix.
    var url = "/api/drivers?source=" + encodeURIComponent(source) + "&merged=true";

    $.getJSON(url, function (drivers)
    {
        // Clear current driver list
        $("#drivers_list").empty();

        // Group drivers by family
        var families = {};
        $.each(drivers, function (i, driver)
        {
            if (!families[driver.family])
            {
                families[driver.family] = [];
            }
            families[driver.family].push(driver);
        });

        // Add drivers to select list grouped by family
        $.each(Object.keys(families).sort(), function (i, family)
        {
            var optgroup = $('<optgroup>').attr('label', family);
            $.each(families[family], function (j, driver)
            {
                var baseLabel = getBaseLabel(driver.label);
                optgroup.append(
                    $('<option>')
                        .attr('value', driver.label)
                        .attr('data-family', family)
                        .attr('data-base-label', baseLabel)
                        .attr('data-source', driver.driver_source || source)
                        .text(driver.label)
                );
            });
            $("#drivers_list").append(optgroup);
        });

        // Refresh the selectpicker
        $("#drivers_list").selectpicker('refresh');

        _log("Loaded " + drivers.length + " drivers (merged) from master source: " + source);

        // Call callback if provided
        if (callback && typeof callback === 'function')
        {
            callback();
        }
    }).fail(function ()
    {
        console.error('Failed to load drivers from source: ' + source);
        alert('Failed to load drivers from ' + source + '. Please check the logs.');
    });
}

// Detect available KStars flatpaks for driver sources
function detectFlatpaks()
{
    $.getJSON("/api/flatpak/app", function (response)
    {
        if (!response.result || !response.payload)
        {
            console.log('Flatpak not available on this system');
            return;
        }

        // Capture the current selection BEFORE touching any options so we can
        // restore it afterwards.  Disabling an option that is currently selected
        // causes the browser (and jQuery) to reset the <select> value to the
        // first available option, silently breaking whatever loadProfileData()
        // already set.
        var previousVal = $("#driver_source").val();

        // Reset options first
        $("#flatpak-stable-option").prop('disabled', true).text('KStars Flatpak - Stable (not installed)');
        $("#flatpak-master-option").prop('disabled', true).text('KStars Flatpak - Nightly (not installed)');

        // Process each flatpak in the payload array
        response.payload.forEach(function (flatpak)
        {
            if (flatpak.application_id === "org.kde.kstars")
            {
                // Check branch to determine stable vs master
                if (flatpak.branch === "stable")
                {
                    $("#flatpak-stable-option").prop('disabled', false).text('KStars Flatpak - Stable (v' + flatpak.version + ')');
                }
                else if (flatpak.branch === "master")
                {
                    $("#flatpak-master-option").prop('disabled', false).text('KStars Flatpak - Nightly (v' + flatpak.version + ')');
                }
            }
        });

        // Restore the previously-selected value now that the relevant option
        // may have been re-enabled.  This ensures that a concurrent
        // loadProfileData() call that set the value before our options were
        // ready is not silently overridden.
        //
        // Use the suppression flag (not off/on) to guard against deferred
        // Bootstrap Select change events fired after selectpicker('refresh').
        _log('[detectFlatpaks] restoring driver_source to "' + previousVal + '"');
        _suppressDriverSourceChange = true;

        if (previousVal)
        {
            $("#driver_source").val(previousVal);
        }
        $("#driver_source").selectpicker('refresh');

        // Clear the suppression flag after a short delay to catch deferred events
        setTimeout(function () { _suppressDriverSourceChange = false; }, 100);

    }).fail(function ()
    {
        console.error('Failed to detect flatpaks');
        $("#flatpak-stable-option").text('KStars Flatpak - Stable (detection failed)');
        $("#flatpak-master-option").text('KStars Flatpak - Nightly (detection failed)');
    });
}

// Bubble selected drivers to the top of the list in a dedicated group
function bubbleSelectedDriversToTop()
{
    // Collect all current options with their metadata
    var allOptions = [];
    $("#drivers_list option").each(function ()
    {
        allOptions.push({
            value: $(this).attr('value'),
            text: $(this).text(),
            selected: $(this).prop('selected'),
            family: $(this).attr('data-family')
        });
    });

    var selectedOpts = allOptions.filter(function (o) { return o.selected; });
    var unselectedOpts = allOptions.filter(function (o) { return !o.selected; });

    // Nothing to do if no drivers are selected
    if (selectedOpts.length === 0) return;

    // Rebuild the list from scratch
    $("#drivers_list").empty();

    // 1. "Selected Drivers" group at the top
    var selectedGroup = $('<optgroup>').attr('label', '\u2605 Selected Drivers');
    selectedOpts.forEach(function (opt)
    {
        selectedGroup.append(
            $('<option>')
                .attr('value', opt.value)
                .attr('data-family', opt.family)
                .prop('selected', true)
                .text(opt.text)
        );
    });
    $("#drivers_list").append(selectedGroup);

    // 2. Remaining family groups (without selected drivers)
    var families = {};
    unselectedOpts.forEach(function (opt)
    {
        if (!families[opt.family]) families[opt.family] = [];
        families[opt.family].push(opt);
    });

    Object.keys(families).sort().forEach(function (family)
    {
        var optgroup = $('<optgroup>').attr('label', family);
        families[family].forEach(function (opt)
        {
            optgroup.append(
                $('<option>')
                    .attr('value', opt.value)
                    .attr('data-family', opt.family)
                    .text(opt.text)
            );
        });
        $("#drivers_list").append(optgroup);
    });
}

// Handle driver source change
function onDriverSourceChange()
{
    // Invalidate any in-flight loadProfileData() AJAX response so it cannot
    // arrive late and silently overwrite the user's manual source selection.
    _loadProfileDataSeq++;

    var source = $("#driver_source").val();
    _log('[onDriverSourceChange] source="' + source + '" seq=' + _loadProfileDataSeq);

    // Step 1: Capture currently selected drivers before switching
    var selectedDrivers = [];
    $("#drivers_list :selected").each(function (i, sel)
    {
        selectedDrivers.push($(sel).text());
    });
    console.log("Currently selected drivers:", selectedDrivers);

    // Step 2: Save the profile info first, then load drivers ONLY after save succeeds
    saveProfileInfo(function ()
    {
        // Step 3: Restore selections for drivers that exist in new source.
        //
        // Because the merged driver list may rename drivers when the master
        // source changes (e.g. "LibCamera" → "LibCamera [system]" or vice-
        // versa), we use findOptionByLabel() which does a two-pass lookup:
        //   1. Exact value match.
        //   2. Base-label match (strips any [source] suffix before comparing).
        loadDriversFromSource(source, function ()
        {
            var restoredCount = 0;
            var notFoundDrivers = [];

            selectedDrivers.forEach(function (driverLabel)
            {
                var opt = findOptionByLabel(driverLabel);
                if (opt.length > 0)
                {
                    opt.prop('selected', true);
                    restoredCount++;
                    _log('[onDriverSourceChange] restored "' + opt.val() + '" (was "' + driverLabel + '")');
                }
                else
                {
                    notFoundDrivers.push(driverLabel);
                    _log('[onDriverSourceChange] driver not found in new source: "' + driverLabel + '"');
                }
            });

            // Move selected drivers to the top of the list
            bubbleSelectedDriversToTop();
            $("#drivers_list").selectpicker('refresh');

            var message = 'Driver source changed to ' + source + '. ';
            if (restoredCount > 0)
            {
                message += restoredCount + ' driver(s) preserved.';
                if (notFoundDrivers.length > 0)
                {
                    message += ' Note: ' + notFoundDrivers.length + ' driver(s) tagged from another source: ' + notFoundDrivers.join(', ') + '.';
                }

                // Auto-save the profile with preserved selections
                var profileName = $("#profiles option:selected").text();
                saveProfileDrivers(profileName, true);
            }
            else
            {
                message += 'Please select drivers for this profile.';
            }

            $("#notify_message").html('<br/><div class="alert alert-info"><a href="#" class="close" data-dismiss="alert" aria-label="close">&times;</a>' + message + '</div>');
        });
    });
}