pragma Singleton

import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Modules.Panels.Settings
import qs.Services.Noctalia
import qs.Services.UI

Singleton {
  id: root

  signal pluginLoaded(string pluginId)
  signal pluginUnloaded(string pluginId)
  signal pluginEnabled(string pluginId)
  signal pluginDisabled(string pluginId)
  signal pluginReloaded(string pluginId)
  signal availablePluginsUpdated
  signal allPluginsLoaded

  // When available plugins are updated, check if we should perform update check
  onAvailablePluginsUpdated: {
    if (shouldCheckUpdatesAfterFetch && Object.keys(activeFetches).length === 0) {
      Logger.d("PluginService", "All registry fetches complete, performing update check");
      performUpdateCheck();
    }
  }

  // Loaded plugin instances
  property var loadedPlugins: ({}) // { pluginId: { component, instance, api } }

  // Available plugins from all sources (fetched from registries)
  property var availablePlugins: ([]) // Array of plugin metadata from all sources

  // Plugin updates available: { pluginId: { currentVersion, availableVersion } }
  property var pluginUpdates: ({})

  // Plugin updates that require a newer Noctalia version: { pluginId: { currentVersion, availableVersion, minNoctaliaVersion } }
  property var pluginUpdatesPending: ({})

  // Plugin load errors: { pluginId: { error: string, entryPoint: string, timestamp: date } }
  property var pluginErrors: ({})
  signal pluginLoadError(string pluginId, string entryPoint, string error)

  // Track currently installing plugins: { pluginId: true }
  property var installingPlugins: ({})

  // Hot reload: file watchers for plugin directories
  property var pluginFileWatchers: ({}) // { pluginId: FileView }
  property list<string> pluginHotReloadEnabled: [] // List of pluginIds that have hot reload enabled

  // Track active fetches
  property var activeFetches: ({})

  property bool initialized: false
  property bool pluginsFullyLoaded: false

  // Plugin container from shell.qml (for placing Main instances in graphics scene)
  property var pluginContainer: null

  // Screen detector from shell.qml (for withCurrentScreen in plugin API)
  property var screenDetector: null

  // Track if we need to initialize once container is ready
  property bool needsInit: false

  // Watch for pluginContainer to be set
  onPluginContainerChanged: {
    if (root.pluginContainer && root.needsInit) {
      Logger.d("PluginService", "Plugin container now available, initializing plugins");
      root.needsInit = false;
      root.init();
    }
  }

  // Listen for PluginRegistry to finish loading
  Connections {
    target: PluginRegistry

    function onPluginsChanged() {
      if (!root.initialized) {
        if (root.pluginContainer) {
          // Container already available, init now
          root.init();
        } else {
          // Container not ready, wait for it
          Logger.d("PluginService", "Deferring plugin init until container is ready");
          root.needsInit = true;
        }
      }
    }
  }

  // When debug mode is disabled, tear down all hot reload watchers
  Connections {
    target: Settings

    function onIsDebugChanged() {
      if (!Settings.isDebug && root.pluginHotReloadEnabled.length > 0) {
        Logger.i("PluginService", "Debug mode disabled, removing all hot reload watchers");
        // Remove watchers for all hot-reload-enabled plugins
        var plugins = root.pluginHotReloadEnabled.slice(); // copy since we mutate
        for (var i = 0; i < plugins.length; i++) {
          removePluginFileWatcher(plugins[i]);
        }
        root.pluginHotReloadEnabled = [];
      }
    }
  }

  // Listen for language changes to reload plugin translations
  Connections {
    target: I18n

    function onLanguageChanged() {
      Logger.d("PluginService", "Language changed to:", I18n.langCode, "- reloading plugin translations");

      // Reload translations for all loaded plugins
      for (var pluginId in root.loadedPlugins) {
        // Use IIFE to capture current loop values (avoid closure bug)
        (function (id, plugin) {
          if (plugin && plugin.api && plugin.manifest) {
            // Update current language
            plugin.api.currentLanguage = I18n.langCode;

            // Reload translations
            loadPluginTranslationsAsync(id, plugin.manifest, I18n.langCode, function (translations) {
              plugin.api.pluginTranslations = translations;

              // Reload English fallback for non-English languages
              if (I18n.langCode !== "en") {
                loadPluginTranslationsAsync(id, plugin.manifest, "en", function (fallbackTranslations) {
                  plugin.api.pluginFallbackTranslations = fallbackTranslations;
                  plugin.api.translationVersion++;
                  Logger.d("PluginService", "Reloaded translations for plugin:", id);
                });
              } else {
                plugin.api.pluginFallbackTranslations = {};
                plugin.api.translationVersion++;
                Logger.d("PluginService", "Reloaded translations for plugin:", id);
              }
            });
          }
        })(pluginId, root.loadedPlugins[pluginId]);
      }

      // Update translation file watchers to watch the new language's files
      if (root.pluginHotReloadEnabled.length > 0) {
        updateTranslationWatchers();
      }
    }
  }

  // Track pending plugin loads for init completion
  property int _pendingPluginLoads: 0

  function init() {
    if (root.initialized) {
      Logger.d("PluginService", "Already initialized, skipping");
      return;
    }

    Logger.i("PluginService", "Initializing plugin system");
    root.initialized = true;

    // Debug: Check what's in PluginRegistry
    var allInstalled = PluginRegistry.getAllInstalledPluginIds();
    Logger.d("PluginService", "All installed plugins:", JSON.stringify(allInstalled));
    Logger.d("PluginService", "Plugin states:", JSON.stringify(PluginRegistry.pluginStates));

    // Load all enabled plugins
    var enabledIds = PluginRegistry.getEnabledPluginIds();
    Logger.i("PluginService", "Found", enabledIds.length, "enabled plugins:", JSON.stringify(enabledIds));

    // Count plugins that need to be loaded
    var pluginsToLoad = [];
    for (var i = 0; i < enabledIds.length; i++) {
      var manifest = PluginRegistry.getPluginManifest(enabledIds[i]);
      if (manifest) {
        pluginsToLoad.push(enabledIds[i]);
      } else {
        Logger.w("PluginService", "Plugin", enabledIds[i], "is enabled but not found on disk - install");
        var sourceUrl = PluginRegistry.getPluginSourceUrl(enabledIds[i]);
        root.installPlugin({
                             id: enabledIds[i],
                             source: {
                               url: sourceUrl
                             }
                           }, false, function (success, error, registeredKey) {
                             if (success) {
                               ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-success", {
                                                                                                  "plugin": registeredKey
                                                                                                }));
                               // Load the plugin since it was already enabled (state persisted but files were missing)
                               loadPlugin(registeredKey);

                               // Add plugin widget to bar if it provides one
                               var manifest = PluginRegistry.getPluginManifest(registeredKey);
                               if (manifest && manifest.entryPoints && manifest.entryPoints.barWidget) {
                                 var widgetId = "plugin:" + registeredKey;
                                 addWidgetToBar(widgetId, "right");
                               }
                             } else {
                               ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-error", {
                                                                                                 "error": error || "Unknown error"
                                                                                               }));
                             }
                           });
      }
    }

    // If no plugins to load, mark as complete immediately
    if (pluginsToLoad.length === 0) {
      root.pluginsFullyLoaded = true;
      Logger.i("PluginService", "No plugins to load");
      root.allPluginsLoaded();
      root._isStartupCheck = true;
      refreshAvailablePlugins();
      return;
    }

    // Track pending loads
    root._pendingPluginLoads = pluginsToLoad.length;

    // Load all plugins (async - they will call _onPluginLoadComplete when done)
    for (var j = 0; j < pluginsToLoad.length; j++) {
      Logger.d("PluginService", "Attempting to load plugin:", pluginsToLoad[j]);
      loadPlugin(pluginsToLoad[j]);
    }
  }

  // Called when a plugin finishes loading (success or failure)
  function _onPluginLoadComplete() {
    root._pendingPluginLoads--;

    if (root._pendingPluginLoads <= 0) {
      // All plugins finished loading
      root.pluginsFullyLoaded = true;
      Logger.i("PluginService", "All plugins loaded");
      root.allPluginsLoaded();

      // Fetch available plugins from all sources
      root._isStartupCheck = true;
      refreshAvailablePlugins();
    }
  }

  // Refresh available plugins from all sources
  function refreshAvailablePlugins() {
    // If fetches are already in progress, don't start new ones
    if (Object.keys(activeFetches).length > 0) {
      Logger.d("PluginService", "Refresh already in progress, skipping duplicate refresh");
      return;
    }

    Logger.i("PluginService", "Refreshing available plugins");
    root.availablePlugins = [];

    // Signal that we want to check for updates after refresh completes
    shouldCheckUpdatesAfterFetch = true;

    var enabledSources = PluginRegistry.getEnabledSources();
    Logger.d("PluginService", "Fetching from", enabledSources.length, "enabled sources");
    for (var i = 0; i < enabledSources.length; i++) {
      fetchPluginRegistry(enabledSources[i]);
    }
  }

  // Fetch plugin registry from a source using git sparse-checkout
  function fetchPluginRegistry(source) {
    var repoUrl = source.url;

    Logger.d("PluginService", "Fetching registry from:", repoUrl);

    // Use git sparse-checkout to fetch only registry.json (--no-cone for single file)
    // GIT_TERMINAL_PROMPT=0 prevents hanging on private repos that need auth
    var fetchCmd = "temp_dir=$(mktemp -d) && GIT_TERMINAL_PROMPT=0 git clone --filter=blob:none --sparse --depth=1 --quiet '" + repoUrl + "' \"$temp_dir\" 2>/dev/null && cd \"$temp_dir\" && git sparse-checkout set --no-cone /registry.json 2>/dev/null && cat \"$temp_dir/registry.json\"; rm -rf \"$temp_dir\"";

    var fetchProcess = Qt.createQmlObject('import QtQuick; import Quickshell.Io; Process { command: ["sh", "-c", "' + fetchCmd.replace(/"/g, '\\"') + '"]; stdout: StdioCollector {} }', root, "FetchRegistry_" + Date.now());

    activeFetches[source.url] = fetchProcess;

    fetchProcess.stdout.onStreamFinished.connect(function () {
      var response = fetchProcess.stdout.text;

      // Debug: log the raw response
      Logger.d("PluginService", "Registry response length:", response ? response.length : 0);

      if (!response || response.trim() === "") {
        Logger.e("PluginService", "Empty response from", source.name);
        delete activeFetches[source.url];
        fetchProcess.destroy();
        return;
      }

      try {
        var registry = JSON.parse(response);

        if (registry && registry.plugins && Array.isArray(registry.plugins)) {
          // Add source info to each plugin
          for (var i = 0; i < registry.plugins.length; i++) {
            var plugin = registry.plugins[i];
            plugin.source = source;

            // Check if already downloaded - use composite key
            var compositeKey = PluginRegistry.generateCompositeKey(plugin.id, source.url);
            plugin.downloaded = PluginRegistry.isPluginDownloaded(compositeKey);
            plugin.enabled = PluginRegistry.isPluginEnabled(compositeKey);

            root.availablePlugins.push(plugin);
          }

          Logger.i("PluginService", `Parsed ${registry.plugins.length} plugins manifest from '${source.name}'`);

          // Remove from active fetches BEFORE emitting signal so handler sees correct count
          delete activeFetches[source.url];
          fetchProcess.destroy();

          root.availablePluginsUpdated();
          return;
        }
      } catch (e) {
        Logger.e("PluginService", "Failed to parse registry from", source.name, ":", e);
        Logger.e("PluginService", "Response was:", response ? response.substring(0, 200) : "null");
      }

      // Clean up on error or empty response
      delete activeFetches[source.url];
      fetchProcess.destroy();
    });

    fetchProcess.exited.connect(function (exitCode) {
      if (exitCode !== 0) {
        Logger.e("PluginService", "Failed to fetch registry from", source.name, "- exit code:", exitCode);
        delete activeFetches[source.url];
        fetchProcess.destroy();
      }
    });

    fetchProcess.running = true;
  }

  // Check for plugin ID collision before installation
  function checkPluginCollision(pluginMetadata) {
    var sourceUrl = pluginMetadata.source.url;
    var compositeKey = PluginRegistry.generateCompositeKey(pluginMetadata.id, sourceUrl);

    // Check if this exact composite key already exists
    if (PluginRegistry.isPluginDownloaded(compositeKey)) {
      return {
        collision: true,
        reason: "already_installed",
        existingKey: compositeKey,
        message: I18n.tr("panels.plugins.collision-already-installed")
      };
    }

    // For official plugins, also check if any custom version with same base ID exists
    if (PluginRegistry.isMainSource(sourceUrl)) {
      var allInstalled = PluginRegistry.getAllInstalledPluginIds();
      for (var i = 0; i < allInstalled.length; i++) {
        var parsed = PluginRegistry.parseCompositeKey(allInstalled[i]);
        if (parsed.pluginId === pluginMetadata.id && !parsed.isOfficial) {
          var sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash) || I18n.tr("panels.plugins.source-custom");
          return {
            collision: true,
            reason: "custom_version_exists",
            existingKey: allInstalled[i],
            message: I18n.tr("panels.plugins.collision-custom-version-exists", {
                               source: sourceName
                             })
          };
        }
      }
    }

    // For custom plugins, check if official version exists
    if (!PluginRegistry.isMainSource(sourceUrl)) {
      if (PluginRegistry.isPluginDownloaded(pluginMetadata.id)) {
        return {
          collision: true,
          reason: "official_version_exists",
          existingKey: pluginMetadata.id,
          message: I18n.tr("panels.plugins.collision-official-version-exists")
        };
      }
    }

    return {
      collision: false
    };
  }

  // Download and install a plugin using git sparse-checkout
  // skipCollisionCheck: set to true when updating an existing plugin
  function installPlugin(pluginMetadata, skipCollisionCheck, callback) {
    var pluginId = pluginMetadata.id;
    // Do not include hash for 3rd party plugins
    var pluginIdRegex = /^[a-f0-9]{6}:/;
    if (pluginIdRegex.test(pluginId)) {
      pluginId = pluginId.substring(7);
    }

    var source = pluginMetadata.source;

    // Check for collision first (skip when updating)
    if (!skipCollisionCheck) {
      var collision = checkPluginCollision(pluginMetadata);
      if (collision.collision) {
        Logger.w("PluginService", "Plugin collision detected:", collision.message);
        ToastService.showError(I18n.tr("panels.plugins.title"), collision.message);
        if (callback)
          callback(false, collision.message);
        return;
      }

      // Check Noctalia version compatibility (skip when updating - that's handled in performUpdateCheck)
      if (pluginMetadata.minNoctaliaVersion) {
        var noctaliaVersion = UpdateService.baseVersion;
        if (compareVersions(pluginMetadata.minNoctaliaVersion, noctaliaVersion) > 0) {
          var incompatibleMsg = I18n.tr("panels.plugins.install-incompatible", {
                                          "plugin": pluginMetadata.name,
                                          "version": pluginMetadata.minNoctaliaVersion
                                        });
          Logger.w("PluginService", "Plugin incompatible:", incompatibleMsg);
          if (callback)
            callback(false, incompatibleMsg);
          return;
        }
      }
    }

    // Generate composite key for the plugin folder
    var compositeKey = PluginRegistry.generateCompositeKey(pluginId, source.url);
    Logger.i("PluginService", "Installing plugin:", compositeKey, "from", source.name);

    var pluginDir = PluginRegistry.getPluginDir(compositeKey);
    var repoUrl = source.url;

    // Use git sparse-checkout to clone only the plugin subfolder
    // GIT_TERMINAL_PROMPT=0 prevents hanging on private repos that need auth
    // Note: We download from the original pluginId folder in the repo, but save to compositeKey folder
    var downloadCmd = "temp_dir=$(mktemp -d) && GIT_TERMINAL_PROMPT=0 git clone --filter=blob:none --sparse --depth=1 --quiet '" + repoUrl + "' \"$temp_dir\" 2>/dev/null && cd \"$temp_dir\" && git sparse-checkout set '" + pluginId + "' 2>/dev/null && mkdir -p '" + pluginDir + "' && rm -f \"$temp_dir/" + pluginId + "/settings.json\" && cp -r \"$temp_dir/" + pluginId
        + "/.\" '" + pluginDir + "/'; exit_code=$?; rm -rf \"$temp_dir\"; exit $exit_code";

    // Mark as installing
    var newInstalling = Object.assign({}, root.installingPlugins);
    newInstalling[pluginId] = true;
    root.installingPlugins = newInstalling;

    var downloadProcess = Qt.createQmlObject('import QtQuick; import Quickshell.Io; Process { command: ["sh", "-c", "' + downloadCmd.replace(/"/g, '\\"') + '"] }', root, "DownloadPlugin_" + pluginId);

    downloadProcess.exited.connect(function (exitCode) {
      // Mark as finished (remove from installing)
      var currentInstalling = Object.assign({}, root.installingPlugins);
      delete currentInstalling[pluginId];
      root.installingPlugins = currentInstalling;

      if (exitCode === 0) {
        Logger.i("PluginService", "Downloaded plugin:", compositeKey);

        // Load and validate manifest
        var manifestPath = pluginDir + "/manifest.json";
        loadManifest(manifestPath, function (success, manifest) {
          if (success) {
            var validation = PluginRegistry.validateManifest(manifest);
            if (validation.valid) {
              // Register plugin with source URL
              var registeredKey = PluginRegistry.registerPlugin(manifest, source.url);
              Logger.i("PluginService", "Installed plugin:", registeredKey);

              // Update available plugins list
              updatePluginInAvailable(pluginId, {
                                        downloaded: true
                                      });

              if (callback)
                callback(true, null, registeredKey);
            } else {
              Logger.e("PluginService", "Invalid manifest:", validation.error);
              if (callback)
                callback(false, "Invalid manifest: " + validation.error);
            }
          } else {
            Logger.e("PluginService", "Failed to load manifest for:", compositeKey);
            if (callback)
              callback(false, "Failed to load manifest");
          }
        });
      } else {
        Logger.e("PluginService", "Failed to download plugin:", compositeKey);
        if (callback)
          callback(false, "Download failed");
      }

      downloadProcess.destroy();
    });

    downloadProcess.running = true;
  }

  // Uninstall a plugin (compositeKey is the full key like "abc123:my-plugin" or plain "my-plugin")
  function uninstallPlugin(compositeKey, callback) {
    Logger.i("PluginService", "Uninstalling plugin:", compositeKey);

    // Disable and unload first
    if (PluginRegistry.isPluginEnabled(compositeKey)) {
      disablePlugin(compositeKey);
    }

    var pluginDir = PluginRegistry.getPluginDir(compositeKey);

    var removeProcess = Qt.createQmlObject(`
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["rm", "-rf", "${pluginDir}"]
      }
    `, root, "RemovePlugin_" + compositeKey);

    removeProcess.exited.connect(function (exitCode) {
      if (exitCode === 0) {
        PluginRegistry.unregisterPlugin(compositeKey);
        Logger.i("PluginService", "Uninstalled plugin:", compositeKey);

        // Update available plugins list (use plain ID to match against availablePlugins)
        var parsed = PluginRegistry.parseCompositeKey(compositeKey);
        updatePluginInAvailable(parsed.pluginId, {
                                  downloaded: false,
                                  enabled: false
                                });

        if (callback)
          callback(true, null);
      } else {
        Logger.e("PluginService", "Failed to uninstall plugin:", pluginId);
        if (callback)
          callback(false, "Failed to remove plugin files");
      }

      removeProcess.destroy();
    });

    removeProcess.running = true;
  }

  // Enable a plugin (compositeKey is the full key like "abc123:my-plugin" or plain "my-plugin")
  function enablePlugin(compositeKey, skipAddToBar) {
    if (PluginRegistry.isPluginEnabled(compositeKey)) {
      Logger.w("PluginService", "Plugin already enabled:", compositeKey);
      return true;
    }

    if (!PluginRegistry.isPluginDownloaded(compositeKey)) {
      Logger.e("PluginService", "Cannot enable: plugin not downloaded:", compositeKey);
      return false;
    }

    PluginRegistry.setPluginEnabled(compositeKey, true);
    loadPlugin(compositeKey);

    // Add plugin widget to bar if it provides one (unless we're restoring from backup)
    if (!skipAddToBar) {
      var manifest = PluginRegistry.getPluginManifest(compositeKey);
      if (manifest && manifest.entryPoints && manifest.entryPoints.barWidget) {
        var widgetId = "plugin:" + compositeKey;
        addWidgetToBar(widgetId, "right");
      }
    }

    // Update available plugins list (use plain ID to match against availablePlugins)
    var parsed = PluginRegistry.parseCompositeKey(compositeKey);
    updatePluginInAvailable(parsed.pluginId, {
                              enabled: true
                            });
    root.pluginEnabled(compositeKey);

    return true;
  }

  // Helper function to add a widget to the bar (global + all screen overrides)
  function addWidgetToBar(widgetId, section) {
    section = section || "right"; // Default to right section

    // Check if widget already exists in any section (global)
    var sections = ["left", "center", "right"];
    for (var s = 0; s < sections.length; s++) {
      var widgets = Settings.data.bar.widgets[sections[s]] || [];
      for (var i = 0; i < widgets.length; i++) {
        if (widgets[i].id === widgetId) {
          Logger.d("PluginService", "Widget already in bar:", widgetId);
          return false;
        }
      }
    }

    // Add to global
    var globalWidgets = Settings.data.bar.widgets[section] || [];
    globalWidgets.push({
                         id: widgetId
                       });
    Settings.data.bar.widgets[section] = globalWidgets;

    // Also add to any screen overrides that have widget configurations
    var overrides = Settings.data.bar.screenOverrides || [];
    for (var o = 0; o < overrides.length; o++) {
      if (overrides[o] && overrides[o].widgets) {
        var overrideWidgets = overrides[o].widgets;
        var sectionWidgets = overrideWidgets[section] || [];
        // Check if widget already exists in this override
        var alreadyExists = false;
        for (var j = 0; j < sections.length; j++) {
          var owSec = overrideWidgets[sections[j]] || [];
          for (var k = 0; k < owSec.length; k++) {
            if (owSec[k].id === widgetId) {
              alreadyExists = true;
              break;
            }
          }
          if (alreadyExists)
            break;
        }
        if (!alreadyExists) {
          sectionWidgets.push({
                                id: widgetId
                              });
          overrideWidgets[section] = sectionWidgets;
          Settings.setScreenOverride(overrides[o].name, "widgets", overrideWidgets);
        }
      }
    }

    Logger.i("PluginService", "Added widget", widgetId, "to bar section:", section);
    return true;
  }

  // Disable a plugin (compositeKey is the full key like "abc123:my-plugin" or plain "my-plugin")
  function disablePlugin(compositeKey) {
    if (!PluginRegistry.isPluginEnabled(compositeKey)) {
      Logger.w("PluginService", "Plugin already disabled:", compositeKey);
      return true;
    }

    // Remove plugin widget from bar before unloading
    var widgetId = "plugin:" + compositeKey;
    removeWidgetFromBar(widgetId);

    PluginRegistry.setPluginEnabled(compositeKey, false);
    unloadPlugin(compositeKey);

    // Update available plugins list (use plain ID to match against availablePlugins)
    var parsed = PluginRegistry.parseCompositeKey(compositeKey);
    updatePluginInAvailable(parsed.pluginId, {
                              enabled: false
                            });
    root.pluginDisabled(compositeKey);
    return true;
  }

  // Helper function to remove a widget from all bar sections (global + screen overrides)
  function removeWidgetFromBar(widgetId) {
    var sections = ["left", "center", "right"];
    var changed = false;

    // Remove from global
    for (var s = 0; s < sections.length; s++) {
      var section = sections[s];
      var widgets = Settings.data.bar.widgets[section] || [];
      var newWidgets = [];

      for (var i = 0; i < widgets.length; i++) {
        if (widgets[i].id !== widgetId) {
          newWidgets.push(widgets[i]);
        } else {
          changed = true;
          Logger.i("PluginService", "Removed widget", widgetId, "from bar section:", section);
        }
      }

      if (changed) {
        Settings.data.bar.widgets[section] = newWidgets;
      }
    }

    // Also remove from any screen overrides that have widget configurations
    var overrides = Settings.data.bar.screenOverrides || [];
    for (var o = 0; o < overrides.length; o++) {
      if (overrides[o] && overrides[o].widgets) {
        var overrideWidgets = overrides[o].widgets;
        var overrideChanged = false;
        for (var s2 = 0; s2 < sections.length; s2++) {
          var sec = sections[s2];
          var owWidgets = overrideWidgets[sec] || [];
          var owNew = [];
          for (var j = 0; j < owWidgets.length; j++) {
            if (owWidgets[j].id !== widgetId) {
              owNew.push(owWidgets[j]);
            } else {
              overrideChanged = true;
              changed = true;
              Logger.i("PluginService", "Removed widget", widgetId, "from screen override:", overrides[o].name, "section:", sec);
            }
          }
          if (overrideChanged) {
            overrideWidgets[sec] = owNew;
          }
        }
        if (overrideChanged) {
          Settings.setScreenOverride(overrides[o].name, "widgets", overrideWidgets);
        }
      }
    }

    // Signal the bar to refresh if widgets were removed
    if (changed) {
      BarService.widgetsRevision++;
    }

    return changed;
  }

  // Remove plugin desktop widgets from all monitors' saved settings
  function removePluginDesktopWidgetsFromSettings(pluginId) {
    var widgetId = "plugin:" + pluginId;
    var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || [];
    var changed = false;

    for (var m = 0; m < monitorWidgets.length; m++) {
      var monitor = monitorWidgets[m];
      var widgets = monitor.widgets || [];
      var newWidgets = [];

      for (var i = 0; i < widgets.length; i++) {
        if (widgets[i].id !== widgetId) {
          newWidgets.push(widgets[i]);
        } else {
          changed = true;
          Logger.i("PluginService", "Removed desktop widget", widgetId, "from monitor:", monitor.name);
        }
      }

      if (newWidgets.length !== widgets.length) {
        monitorWidgets[m].widgets = newWidgets;
      }
    }

    if (changed) {
      Settings.data.desktopWidgets.monitorWidgets = monitorWidgets;
    }

    return changed;
  }

  // Load plugin settings and translations before instantiating components
  // This ensures pluginApi is fully populated before being passed to createObject()
  function loadPluginData(pluginId, manifest, callback) {
    // Load settings first
    loadPluginSettings(pluginId, function (settings) {
      // Then load translations
      loadPluginTranslationsAsync(pluginId, manifest, I18n.langCode, function (translations) {
        // Load English fallback for non-English languages
        if (I18n.langCode !== "en") {
          loadPluginTranslationsAsync(pluginId, manifest, "en", function (fallbackTranslations) {
            callback(settings, translations, fallbackTranslations);
          });
        } else {
          callback(settings, translations, {});
        }
      });
    });
  }

  // Load a plugin
  function loadPlugin(pluginId) {
    if (root.loadedPlugins[pluginId]) {
      Logger.w("PluginService", "Plugin already loaded:", pluginId);
      return;
    }

    var manifest = PluginRegistry.getPluginManifest(pluginId);
    if (!manifest) {
      Logger.e("PluginService", "Cannot load: manifest not found for:", pluginId);
      return;
    }

    var pluginDir = PluginRegistry.getPluginDir(pluginId);

    Logger.i("PluginService", "Loading plugin:", pluginId);

    // Load settings and translations FIRST, then create API and instantiate components
    loadPluginData(pluginId, manifest, function (settings, translations, fallbackTranslations) {
      // Create plugin API object with pre-loaded data
      var pluginApi = createPluginAPI(pluginId, manifest, settings, translations, fallbackTranslations);

      // Initialize plugin entry with API and manifest
      root.loadedPlugins[pluginId] = {
        barWidget: null,
        desktopWidget: null,
        launcherProvider: null,
        mainInstance: null,
        api: pluginApi,
        manifest: manifest
      };

      // Clear any previous errors for this plugin
      root.clearPluginError(pluginId);

      // Load Main.qml entry point if it exists
      if (manifest.entryPoints && manifest.entryPoints.main) {
        var mainPath = pluginDir + "/" + manifest.entryPoints.main;
        var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
        var mainComponent = Qt.createComponent("file://" + mainPath + "?v=" + loadVersion);

        if (mainComponent.status === Component.Ready) {
          // Get the plugin container from shell.qml (must be in graphics scene)
          if (!root.pluginContainer) {
            Logger.e("PluginService", "Plugin container not set. Shell must set PluginService.pluginContainer.");
            return;
          }

          // Instantiate Main.qml with pluginApi passed directly in createObject
          var mainInstance = mainComponent.createObject(root.pluginContainer, {
                                                          pluginApi: pluginApi
                                                        });

          if (mainInstance) {
            root.loadedPlugins[pluginId].mainInstance = mainInstance;
            pluginApi.mainInstance = mainInstance;
            Logger.i("PluginService", "Loaded Main.qml for plugin:", pluginId);
          } else {
            root.recordPluginError(pluginId, "main", "Failed to instantiate Main.qml");
          }
        } else if (mainComponent.status === Component.Error) {
          root.recordPluginError(pluginId, "main", mainComponent.errorString());
        }
      }

      // Load bar widget component if provided (don't instantiate - BarWidgetRegistry will do that)
      if (manifest.entryPoints && manifest.entryPoints.barWidget) {
        var widgetPath = pluginDir + "/" + manifest.entryPoints.barWidget;
        var widgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
        var widgetComponent = Qt.createComponent("file://" + widgetPath + "?v=" + widgetLoadVersion);

        if (widgetComponent.status === Component.Ready) {
          root.loadedPlugins[pluginId].barWidget = widgetComponent;
          pluginApi.barWidget = widgetComponent;

          // Register with BarWidgetRegistry
          BarWidgetRegistry.registerPluginWidget(pluginId, widgetComponent, manifest.metadata);
          Logger.i("PluginService", "Loaded bar widget for plugin:", pluginId);

          // Now that the widget is registered, bump widgetsRevision so the bar can render it
          BarService.widgetsRevision++;
        } else if (widgetComponent.status === Component.Error) {
          root.recordPluginError(pluginId, "barWidget", widgetComponent.errorString());
        }
      }

      // Load desktop widget component if provided (don't instantiate - DesktopWidgetRegistry will do that)
      if (manifest.entryPoints && manifest.entryPoints.desktopWidget) {
        var desktopWidgetPath = pluginDir + "/" + manifest.entryPoints.desktopWidget;
        var desktopWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
        var desktopWidgetComponent = Qt.createComponent("file://" + desktopWidgetPath + "?v=" + desktopWidgetLoadVersion);

        if (desktopWidgetComponent.status === Component.Ready) {
          root.loadedPlugins[pluginId].desktopWidget = desktopWidgetComponent;
          pluginApi.desktopWidget = desktopWidgetComponent;

          // Register with DesktopWidgetRegistry
          DesktopWidgetRegistry.registerPluginWidget(pluginId, desktopWidgetComponent, manifest.metadata);
          Logger.i("PluginService", "Loaded desktop widget for plugin:", pluginId);
        } else if (desktopWidgetComponent.status === Component.Error) {
          root.recordPluginError(pluginId, "desktopWidget", desktopWidgetComponent.errorString());
        }
      }

      // Load launcher provider component if provided (don't instantiate - Launcher will do that)
      if (manifest.entryPoints && manifest.entryPoints.launcherProvider) {
        var launcherProviderPath = pluginDir + "/" + manifest.entryPoints.launcherProvider;
        var launcherProviderLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
        var launcherProviderComponent = Qt.createComponent("file://" + launcherProviderPath + "?v=" + launcherProviderLoadVersion);

        if (launcherProviderComponent.status === Component.Ready) {
          root.loadedPlugins[pluginId].launcherProvider = launcherProviderComponent;
          pluginApi.launcherProvider = launcherProviderComponent;

          // Register with LauncherProviderRegistry
          LauncherProviderRegistry.registerPluginProvider(pluginId, launcherProviderComponent, manifest.metadata);
          Logger.i("PluginService", "Loaded launcher provider for plugin:", pluginId);
        } else if (launcherProviderComponent.status === Component.Error) {
          root.recordPluginError(pluginId, "launcherProvider", launcherProviderComponent.errorString());
        }
      }

      // Load control center widget component if provided
      if (manifest.entryPoints && manifest.entryPoints.controlCenterWidget) {
        var ccWidgetPath = pluginDir + "/" + manifest.entryPoints.controlCenterWidget;
        var ccWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
        var ccWidgetComponent = Qt.createComponent("file://" + ccWidgetPath + "?v=" + ccWidgetLoadVersion);

        if (ccWidgetComponent.status === Component.Ready) {
          root.loadedPlugins[pluginId].controlCenterWidget = ccWidgetComponent;
          pluginApi.controlCenterWidget = ccWidgetComponent;

          // Register with ControlCenterWidgetRegistry
          ControlCenterWidgetRegistry.registerPluginWidget(pluginId, ccWidgetComponent, manifest.metadata);
          Logger.i("PluginService", "Loaded control center widget for plugin:", pluginId);
        } else if (ccWidgetComponent.status === Component.Error) {
          root.recordPluginError(pluginId, "controlCenterWidget", ccWidgetComponent.errorString());
        }
      }

      Logger.i("PluginService", "Plugin loaded:", pluginId);
      root.pluginLoaded(pluginId);

      // Set up hot reload watcher if enabled
      setupPluginFileWatcher(pluginId);

      // Notify that this plugin finished loading (for init tracking)
      root._onPluginLoadComplete();
    });
  }

  // Unload a plugin
  // preserveSettings: if true, don't remove desktop widget settings (used for hot reload)
  function unloadPlugin(pluginId, preserveSettings) {
    var plugin = root.loadedPlugins[pluginId];
    if (!plugin) {
      Logger.w("PluginService", "Plugin not loaded:", pluginId);
      return;
    }

    Logger.i("PluginService", "Unloading plugin:", pluginId);

    // Remove hot reload watcher
    removePluginFileWatcher(pluginId);

    // Unregister from BarWidgetRegistry
    if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.barWidget) {
      BarWidgetRegistry.unregisterPluginWidget(pluginId);
    }

    // Unregister from DesktopWidgetRegistry
    if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.desktopWidget) {
      // Only remove settings when uninstalling, not during hot reload
      if (!preserveSettings) {
        removePluginDesktopWidgetsFromSettings(pluginId);
      }
      DesktopWidgetRegistry.unregisterPluginWidget(pluginId);
    }

    // Unregister from LauncherProviderRegistry
    if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.launcherProvider) {
      LauncherProviderRegistry.unregisterPluginProvider(pluginId);
    }

    // Unregister from ControlCenterWidgetRegistry
    if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.controlCenterWidget) {
      ControlCenterWidgetRegistry.unregisterPluginWidget(pluginId);
    }

    // Destroy Main instance if any
    if (plugin.mainInstance) {
      plugin.mainInstance.destroy();
    }

    delete root.loadedPlugins[pluginId];
    root.pluginUnloaded(pluginId);
    Logger.i("PluginService", "Unloaded plugin:", pluginId);
  }

  // Create plugin API object with pre-loaded settings and translations
  function createPluginAPI(pluginId, manifest, settings, translations, fallbackTranslations) {
    var pluginDir = PluginRegistry.getPluginDir(pluginId);

    var api = Qt.createQmlObject(`
      import QtQuick

      QtObject {
        // Plugin-specific
        readonly property string pluginId: "${pluginId}"
        readonly property string pluginDir: "${pluginDir}"
        property var pluginSettings: ({})
        property var manifest: ({})

        // Instance references (set after loading)
        property var mainInstance: null
        property var barWidget: null
        property var desktopWidget: null
        property var launcherProvider: null
        property var controlCenterWidget: null

        // Panel state: which screen the plugin's panel is currently open on (null if closed)
        property var panelOpenScreen: null

        // IPC handlers storage
        property var ipcHandlers: ({})

        // Translation storage
        property var pluginTranslations: ({})
        property var pluginFallbackTranslations: ({})  // English fallback for missing keys
        property string currentLanguage: ""
        property int translationVersion: 0  // Increments when translations change - plugins should depend on this

        // Functions will be bound below
        property var saveSettings: null
        property var openPanel: null
        property var closePanel: null
        property var togglePanel: null
        property var openLauncher: null
        property var closeLauncher: null
        property var toggleLauncher: null
        property var withCurrentScreen: null
        property var tr: null
        property var trp: null
        property var hasTranslation: null
      }
    `, root, "PluginAPI_" + pluginId);

    // Set manifest
    api.manifest = manifest;

    // Set current language (can't use binding in Qt.createQmlObject string)
    api.currentLanguage = I18n.langCode;

    // Merge manifest defaults with loaded settings (user settings take priority)
    var defaults = (manifest.metadata && manifest.metadata.defaultSettings) || {};
    api.pluginSettings = Object.assign({}, defaults, settings || {});
    api.pluginTranslations = translations || {};
    api.pluginFallbackTranslations = fallbackTranslations || {};

    // ----------------------------------------
    // Helper function to get nested property by dot notation
    var getNestedProperty = function (obj, path) {
      var keys = path.split('.');
      var current = obj;
      for (var i = 0; i < keys.length; i++) {
        if (current === undefined || current === null) {
          return undefined;
        }
        current = current[keys[i]];
      }
      return current;
    };

    // ----------------------------------------
    // Bind functions
    // ----------------------------------------
    api.saveSettings = function () {
      savePluginSettings(pluginId, api.pluginSettings);

      // Replace the entire pluginSettings object to trigger QML property bindings
      // Make a shallow copy so bindings detect the change
      api.pluginSettings = Object.assign({}, api.pluginSettings);
    };

    // ----------------------------------------
    api.togglePanel = function (screen, buttonItem) {
      // Toggle this plugin's panel on the specified screen
      // buttonItem: optional, if provided the panel will position near this button
      if (!screen) {
        Logger.w("PluginAPI", "No screen available for toggling panel");
        return false;
      }
      return togglePluginPanel(pluginId, screen, buttonItem);
    };

    // ----------------------------------------
    api.openPanel = function (screen, buttonItem) {
      // Open this plugin's panel on the specified screen
      // buttonItem: optional, if provided the panel will position near this button
      if (!screen) {
        Logger.w("PluginAPI", "No screen available for opening panel");
        return false;
      }
      return openPluginPanel(pluginId, screen, buttonItem);
    };

    // ----------------------------------------
    api.closePanel = function (screen) {
      // Close this plugin's panel (find which slot it's in and close it)
      for (var slotNum = 1; slotNum <= 2; slotNum++) {
        var panelName = "pluginPanel" + slotNum;
        var panel = PanelService.getPanel(panelName, screen);
        if (panel && panel.currentPluginId === pluginId) {
          panel.close();
          return true;
        }
      }
      return false;
    };

    // ----------------------------------------
    // Launcher provider methods
    // ----------------------------------------

    // Get the search prefix for this plugin's launcher provider
    var getSearchPrefix = function () {
      var metadata = LauncherProviderRegistry.getProviderMetadata("plugin:" + pluginId);
      var prefix = (metadata && metadata.commandPrefix) ? metadata.commandPrefix : pluginId;
      return ">" + prefix + " ";
    };

    api.openLauncher = function (screen) {
      // Open the launcher with this plugin's provider active
      if (!screen) {
        Logger.w("PluginAPI", "No screen available for opening launcher");
        return;
      }
      PanelService.openLauncherWithSearch(screen, getSearchPrefix());
    };

    api.closeLauncher = function (screen) {
      // Close the launcher
      if (!screen) {
        Logger.w("PluginAPI", "No screen available for closing launcher");
        return;
      }
      PanelService.closeLauncher(screen);
    };

    api.toggleLauncher = function (screen) {
      // Toggle the launcher with this plugin's provider active
      if (!screen) {
        Logger.w("PluginAPI", "No screen available for toggling launcher");
        return;
      }
      var searchPrefix = getSearchPrefix();
      var searchText = PanelService.getLauncherSearchText(screen);
      var isInThisMode = searchText.startsWith(searchPrefix);
      if (!PanelService.isLauncherOpen(screen)) {
        PanelService.openLauncherWithSearch(screen, searchPrefix);
      } else if (isInThisMode) {
        PanelService.closeLauncher(screen);
      } else {
        PanelService.setLauncherSearchText(screen, searchPrefix);
      }
    };

    // ----------------------------------------
    api.withCurrentScreen = function (callback) {
      // Detect which screen the cursor is on and call callback with that screen
      if (!root.screenDetector) {
        Logger.w("PluginAPI", "Screen detector not available, using primary screen");
        callback(Quickshell.screens[0]);
        return;
      }
      root.screenDetector.withCurrentScreen(callback);
    };

    // ----------------------------------------
    // Translation function
    api.tr = function (key, interpolations) {
      if (typeof interpolations === 'undefined') {
        interpolations = {};
      }

      var translation = getNestedProperty(api.pluginTranslations, key);

      // Fallback to English if not found in current language
      if (translation === undefined || translation === null || typeof translation !== 'string') {
        translation = getNestedProperty(api.pluginFallbackTranslations, key);
      }

      // Return formatted key if translation not found in any language
      if (translation === undefined || translation === null) {
        return `!!${key}!!`;
      }

      // Ensure translation is a string
      if (typeof translation !== 'string') {
        return `!!${key}!!`;
      }

      // Handle interpolations (e.g., "Hello {name}!")
      var result = translation;
      for (var placeholder in interpolations) {
        var regex = new RegExp('\\{' + placeholder + '\\}', 'g');
        result = result.replace(regex, interpolations[placeholder]);
      }

      return result;
    };

    // ----------------------------------------
    // Plural translation function
    api.trp = function (key, count, interpolations) {
      if (typeof interpolations === 'undefined') {
        interpolations = {};
      }

      // Use key for singular, key-plural for plural
      const realKey = count === 1 ? key : `${key}-plural`;

      // Merge interpolations with count
      var finalInterpolations = {
        'count': count
      };
      for (var prop in interpolations) {
        finalInterpolations[prop] = interpolations[prop];
      }

      // Use tr() to look up the translation
      return api.tr(realKey, finalInterpolations);
    };

    // ----------------------------------------
    // Check if translation exists
    api.hasTranslation = function (key) {
      return getNestedProperty(api.pluginTranslations, key) !== undefined || getNestedProperty(api.pluginFallbackTranslations, key) !== undefined;
    };

    return api;
  }

  // Load plugin translations asynchronously
  function loadPluginTranslationsAsync(pluginId, manifest, language, callback) {
    var pluginDir = PluginRegistry.getPluginDir(pluginId);
    var translationFile = pluginDir + "/i18n/" + language + ".json";

    var readProcess = Qt.createQmlObject(`
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["cat", "${translationFile}"]
        stdout: StdioCollector {}
      }
    `, root, "ReadTranslation_" + pluginId + "_" + language);

    readProcess.exited.connect(function (exitCode) {
      var translations = {};

      if (exitCode === 0) {
        try {
          translations = JSON.parse(readProcess.stdout.text);
          Logger.d("PluginService", "Loaded translations for", pluginId, "language:", language);
        } catch (e) {
          Logger.w("PluginService", "Failed to parse translations for", pluginId, "language:", language);
        }
      } else {
        Logger.d("PluginService", "No translation file for", pluginId, "language:", language);
      }

      if (callback) {
        callback(translations);
      }

      readProcess.destroy();
    });

    readProcess.running = true;
  }

  // Load plugin settings
  function loadPluginSettings(pluginId, callback) {
    var settingsFile = PluginRegistry.getPluginSettingsFile(pluginId);

    var readProcess = Qt.createQmlObject(`
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["cat", "${settingsFile}"]
        stdout: StdioCollector {}
      }
    `, root, "ReadSettings_" + pluginId);

    readProcess.exited.connect(function (exitCode) {
      if (exitCode === 0) {
        try {
          var settings = JSON.parse(readProcess.stdout.text);
          callback(settings);
        } catch (e) {
          Logger.w("PluginService", "Failed to parse settings for", pluginId, "- using defaults");
          callback({});
        }
      } else {
        // File doesn't exist - use defaults
        callback({});
      }

      readProcess.destroy();
    });

    readProcess.running = true;
  }

  // Save plugin settings
  function savePluginSettings(pluginId, settings) {
    var settingsFile = PluginRegistry.getPluginSettingsFile(pluginId);
    var settingsJson = JSON.stringify(settings, null, 2);

    // Use heredoc delimiter pattern to avoid all escaping issues
    var delimiter = "PLUGIN_SETTINGS_EOF_" + Math.random().toString(36).substr(2, 9);
    var fileEsc = settingsFile.replace(/'/g, "'\\''");

    // Get parent directory and ensure it exists
    var settingsDir = settingsFile.substring(0, settingsFile.lastIndexOf('/'));
    var dirEsc = settingsDir.replace(/'/g, "'\\''");

    // Build the shell command with heredoc (create dir first)
    var writeCmd = "mkdir -p '" + dirEsc + "' && cat > '" + fileEsc + "' << '" + delimiter + "'\n" + settingsJson + "\n" + delimiter + "\n";

    Logger.d("PluginService", "Saving settings to:", settingsFile);

    // Use Quickshell.execDetached to execute the command (use array syntax)
    var pid = Quickshell.execDetached(["sh", "-c", writeCmd]);
    Logger.d("PluginService", "Write process started, PID:", pid);
  }

  // Load manifest from file
  function loadManifest(manifestPath, callback) {
    var readProcess = Qt.createQmlObject(`
      import QtQuick
      import Quickshell.Io
      Process {
        command: ["cat", "${manifestPath}"]
        stdout: StdioCollector {}
      }
    `, root, "ReadManifest_" + Date.now());

    readProcess.exited.connect(function (exitCode) {
      if (exitCode === 0) {
        try {
          var manifest = JSON.parse(readProcess.stdout.text);
          callback(true, manifest);
        } catch (e) {
          Logger.e("PluginService", "Failed to parse manifest:", e);
          callback(false, null);
        }
      } else {
        Logger.e("PluginService", "Failed to read manifest at:", manifestPath);
        callback(false, null);
      }

      readProcess.destroy();
    });

    readProcess.running = true;
  }

  // Update plugin metadata in available plugins list
  function updatePluginInAvailable(pluginId, updates) {
    for (var i = 0; i < root.availablePlugins.length; i++) {
      if (root.availablePlugins[i].id === pluginId) {
        for (var key in updates) {
          root.availablePlugins[i][key] = updates[key];
        }
        root.availablePluginsUpdated();
        break;
      }
    }
  }

  // Find available plugin by ID
  function findAvailablePlugin(compositeKeyOrId) {
    var parsed = PluginRegistry.parseCompositeKey(compositeKeyOrId);
    var pluginId = parsed.pluginId;
    var sourceUrl = PluginRegistry.getPluginSourceUrl(compositeKeyOrId);

    for (var i = 0; i < root.availablePlugins.length; i++) {
      if (root.availablePlugins[i].id === pluginId && root.availablePlugins[i].source.url === sourceUrl) {
        return root.availablePlugins[i];
      }
    }
    return null;
  }

  // Internal flag to track if we should check for updates after registry fetch
  property bool shouldCheckUpdatesAfterFetch: false

  // Flag to track if this is the initial startup update check (for auto-update)
  property bool _isStartupCheck: false

  // Check for plugin updates (call this after availablePlugins are loaded)
  function checkForUpdates() {
    Logger.i("PluginService", "Checking for plugin updates");

    // If we have available plugins, check immediately regardless of active fetches
    if (root.availablePlugins.length > 0) {
      Logger.d("PluginService", "Available plugins already loaded, checking now");
      performUpdateCheck();
      return;
    }

    // No plugins yet - check if fetch is in progress
    if (Object.keys(activeFetches).length > 0) {
      Logger.d("PluginService", "Registry fetch in progress, will check after fetch completes");
      shouldCheckUpdatesAfterFetch = true;
      return;
    }

    // No plugins and no fetches - trigger refresh
    Logger.d("PluginService", "No available plugins yet, triggering refresh");
    shouldCheckUpdatesAfterFetch = true;
    refreshAvailablePlugins();
  }

  // Perform the actual update check
  function performUpdateCheck() {
    var updates = {};
    var pendingUpdates = {};
    var installedIds = PluginRegistry.getAllInstalledPluginIds();

    Logger.d("PluginService", "Checking", installedIds.length, "installed plugins against", root.availablePlugins.length, "available plugins");

    for (var i = 0; i < installedIds.length; i++) {
      var pluginId = installedIds[i];
      var installedManifest = PluginRegistry.getPluginManifest(pluginId);
      var availablePlugin = findAvailablePlugin(pluginId);

      if (installedManifest && availablePlugin) {
        var currentVersion = installedManifest.version;
        var availableVersion = availablePlugin.version;

        Logger.d("PluginService", "Comparing", pluginId + ":", currentVersion, "vs", availableVersion);

        // Compare versions
        if (compareVersions(availableVersion, currentVersion) > 0) {
          // Check if the available version requires a higher Noctalia version
          if (availablePlugin.minNoctaliaVersion) {
            var noctaliaVersion = UpdateService.baseVersion;
            if (compareVersions(availablePlugin.minNoctaliaVersion, noctaliaVersion) > 0) {
              Logger.d("PluginService", "Pending update for", pluginId + ": requires Noctalia v" + availablePlugin.minNoctaliaVersion + " (current: v" + noctaliaVersion + ")");
              pendingUpdates[pluginId] = {
                currentVersion: currentVersion,
                availableVersion: availableVersion,
                minNoctaliaVersion: availablePlugin.minNoctaliaVersion
              };
              continue;
            }
          }

          updates[pluginId] = {
            currentVersion: currentVersion,
            availableVersion: availableVersion
          };
          Logger.i("PluginService", "Update available for", pluginId + ":", currentVersion, "→", availableVersion);
        }
      } else if (installedManifest && !availablePlugin) {
        Logger.d("PluginService", "Plugin", pluginId, "not found in available plugins (might be from disabled source)");
      }
    }

    root.pluginUpdates = updates;
    root.pluginUpdatesPending = pendingUpdates;
    var updateCount = Object.keys(updates).length;
    var pendingCount = Object.keys(pendingUpdates).length;
    var updatesDescription = Object.keys(updates).map(function (pluginId) {
      return pluginId + ": " + updates[pluginId].currentVersion + " → " + updates[pluginId].availableVersion;
    }).join("\n");

    if (updateCount > 0) {
      Logger.i("PluginService", updateCount, "plugin update(s) available");

      if (Settings.data.plugins.notifyUpdates) {
        ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.trp("panels.plugins.update-available", updateCount) + "\n\n" + updatesDescription, "plugin", 5000, I18n.tr("panels.plugins.open-plugins-tab"), function () {
          // Open settings panel to Plugins tab on the screen where the cursor is
          if (root.screenDetector) {
            root.screenDetector.withCurrentScreen(function (screen) {
              var panel = PanelService.getPanel("settingsPanel", screen);
              if (panel) {
                panel.requestedTab = SettingsPanel.Tab.Plugins;
                panel.open();
              }
            });
          } else {
            // Fallback to primary screen if screen detector is not available
            var panel = PanelService.getPanel("settingsPanel", Quickshell.screens[0]);
            if (panel) {
              panel.requestedTab = SettingsPanel.Tab.Plugins;
              panel.open();
            }
          }
        });
      }
    } else if (pendingCount > 0) {
      Logger.i("PluginService", pendingCount, "plugin update(s) pending (require newer Noctalia)");
    } else {
      Logger.i("PluginService", "All installed plugins are up to date");
    }

    // Auto-update on startup if enabled
    if (root._isStartupCheck && Settings.data.plugins.autoUpdate && updateCount > 0) {
      Logger.i("PluginService", "Auto-updating", updateCount, "plugin(s)");
      updateAllPlugins();
    }

    root._isStartupCheck = false;
    shouldCheckUpdatesAfterFetch = false;
  }

  // Update all plugins sequentially
  function updateAllPlugins(callback) {
    var pluginIds = Object.keys(root.pluginUpdates);
    var currentIndex = 0;

    function updateNext() {
      if (currentIndex >= pluginIds.length) {
        ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-all-success"));
        if (callback)
          callback();
        return;
      }

      var pluginId = pluginIds[currentIndex];
      currentIndex++;

      root.updatePlugin(pluginId, function (success, error) {
        if (!success) {
          Logger.w("PluginService", "Failed to auto-update", pluginId + ":", error);
        }
        Qt.callLater(updateNext);
      });
    }

    updateNext();
  }

  // Simple version comparison (semantic versioning x.y.z)
  function compareVersions(a, b) {
    var aParts = a.split('.').map(function (x) {
      return parseInt(x) || 0;
    });
    var bParts = b.split('.').map(function (x) {
      return parseInt(x) || 0;
    });

    for (var i = 0; i < 3; i++) {
      var aNum = aParts[i] || 0;
      var bNum = bParts[i] || 0;
      if (aNum > bNum)
        return 1;
      if (aNum < bNum)
        return -1;
    }
    return 0;
  }

  // Update a plugin to the latest version
  function updatePlugin(pluginId, callback) {
    Logger.i("PluginService", "Updating plugin:", pluginId);

    // Find available plugin metadata
    var availablePlugin = findAvailablePlugin(pluginId);
    if (!availablePlugin) {
      Logger.e("PluginService", "Plugin not found in available plugins:", pluginId);
      if (callback)
        callback(false, "Plugin not found");
      return;
    }

    // Check Noctalia compatibility
    if (availablePlugin.minNoctaliaVersion) {
      // Simple check: just warn, don't block (UpdateService would have more sophisticated logic)
      Logger.d("PluginService", "Plugin requires Noctalia v" + availablePlugin.minNoctaliaVersion);
    }

    // Backup entire bar layout (global + screen overrides)
    var barBackup = {
      left: JSON.parse(JSON.stringify(Settings.data.bar.widgets.left || [])),
      center: JSON.parse(JSON.stringify(Settings.data.bar.widgets.center || [])),
      right: JSON.parse(JSON.stringify(Settings.data.bar.widgets.right || []))
    };
    var screenOverridesBackup = JSON.parse(JSON.stringify(Settings.data.bar.screenOverrides || []));
    Logger.d("PluginService", "Backed up bar layout (global + screen overrides)");

    // Backup desktop widget settings (includes this plugin's widgets)
    var desktopWidgetsBackup = JSON.parse(JSON.stringify(Settings.data.desktopWidgets.monitorWidgets || []));
    Logger.d("PluginService", "Backed up desktop widget settings");

    // Close any open panels for this plugin before update
    for (var slotNum = 1; slotNum <= 2; slotNum++) {
      var panelName = "pluginPanel" + slotNum;
      for (var s = 0; s < Quickshell.screens.length; s++) {
        var panel = PanelService.getPanel(panelName, Quickshell.screens[s]);
        if (panel && panel.currentPluginId === pluginId) {
          Logger.d("PluginService", "Closing plugin panel before update");
          panel.close();
          panel.unloadPluginPanel();
        }
      }
    }

    // Disable plugin (this removes widgets and unloads code)
    if (PluginRegistry.isPluginEnabled(pluginId)) {
      disablePlugin(pluginId);
    }

    // Now install the new version (reuse installPlugin logic, skip collision check since we're updating)
    installPlugin(availablePlugin, true, function (success, error) {
      if (success) {
        Logger.i("PluginService", "Plugin updated successfully:", pluginId);

        // Increment load version to invalidate Qt component cache
        PluginRegistry.incrementPluginLoadVersion(pluginId);

        // Re-enable the plugin first, so the new component is registered
        // Skip adding to bar since we'll restore the layout from backup
        enablePlugin(pluginId, true);

        // Then restore bar layout (so BarWidgetLoaders can find the new component)
        Settings.data.bar.widgets.left = barBackup.left;
        Settings.data.bar.widgets.center = barBackup.center;
        Settings.data.bar.widgets.right = barBackup.right;
        Settings.data.bar.screenOverrides = screenOverridesBackup;
        Logger.d("PluginService", "Restored bar layout (global + screen overrides)");

        // Restore desktop widget settings
        Settings.data.desktopWidgets.monitorWidgets = desktopWidgetsBackup;
        Logger.d("PluginService", "Restored desktop widget settings");

        // Persist restored layout immediately to prevent race with file watcher reload
        // (the earlier disablePlugin write triggers a reload that can overwrite the restore)
        Settings.saveImmediate();

        // Remove from updates list
        var updates = Object.assign({}, root.pluginUpdates);
        delete updates[pluginId];
        root.pluginUpdates = updates;

        if (callback)
          callback(true, null);
      } else {
        Logger.e("PluginService", "Failed to update plugin:", pluginId, error);

        // Restore bar layout even on failure (global + screen overrides)
        Settings.data.bar.widgets.left = barBackup.left;
        Settings.data.bar.widgets.center = barBackup.center;
        Settings.data.bar.widgets.right = barBackup.right;
        Settings.data.bar.screenOverrides = screenOverridesBackup;

        // Restore desktop widget settings even on failure
        Settings.data.desktopWidgets.monitorWidgets = desktopWidgetsBackup;
        Settings.saveImmediate();

        if (callback)
          callback(false, error);
      }
    });
  }

  // Get plugin API for a loaded plugin
  function getPluginAPI(pluginId) {
    return root.loadedPlugins[pluginId]?.api || null;
  }

  // Check if plugin is loaded
  function isPluginLoaded(pluginId) {
    return !!root.loadedPlugins[pluginId];
  }

  // Open a plugin's panel (finds a free slot and loads the panel)
  function openPluginPanel(pluginId, screen, buttonItem) {
    if (!isPluginLoaded(pluginId)) {
      Logger.w("PluginService", "Cannot open panel: plugin not loaded:", pluginId);
      return false;
    }

    var plugin = root.loadedPlugins[pluginId];
    if (!plugin || !plugin.manifest || !plugin.manifest.entryPoints || !plugin.manifest.entryPoints.panel) {
      Logger.w("PluginService", "Plugin does not provide a panel:", pluginId);
      return false;
    }

    // Try to find the plugin panel slot (pluginPanel1 or pluginPanel2)
    // Priority: 1) toggle same plugin, 2) empty slot, 3) closed slot, 4) replace open slot
    var closedSlot = null;

    for (var slotNum = 1; slotNum <= 2; slotNum++) {
      var panelName = "pluginPanel" + slotNum;
      var panel = PanelService.getPanel(panelName, screen);

      if (panel) {
        // If this slot is already showing this plugin's panel, toggle it
        if (panel.currentPluginId === pluginId) {
          panel.toggle(buttonItem);
          return true;
        }

        // If this slot is empty, use it
        if (panel.currentPluginId === "") {
          panel.currentPluginId = pluginId;
          panel.open(buttonItem);
          return true;
        }

        // Track first closed slot (panel assigned but not showing)
        if (!closedSlot && !panel.isPanelOpen) {
          closedSlot = panel;
        }
      }
    }

    // Prefer reusing a closed slot over replacing an open one
    if (closedSlot) {
      closedSlot.currentPluginId = pluginId;
      closedSlot.open(buttonItem);
      return true;
    }

    // If both slots are occupied and open, use slot 1 (replace existing)
    var panel1 = PanelService.getPanel("pluginPanel1", screen);
    if (panel1) {
      var wasAlreadyOpen = panel1.isPanelOpen;
      panel1.unloadPluginPanel();
      panel1.currentPluginId = pluginId;

      // If panel was already open, Component.onCompleted won't fire again
      // since panelContent is already loaded. We need to load the plugin manually.
      if (wasAlreadyOpen && panel1.contentLoader) {
        panel1.loadPluginPanel(pluginId);
      }

      panel1.open(buttonItem);
      return true;
    }

    Logger.e("PluginService", "Failed to find plugin panel slot");
    return false;
  }

  // Toggle a plugin's panel - close if open, open if closed
  // buttonItem: optional, if provided the panel will position near this button
  function togglePluginPanel(pluginId, screen, buttonItem) {
    if (!isPluginLoaded(pluginId)) {
      Logger.w("PluginService", "Cannot toggle panel: plugin not loaded:", pluginId);
      return false;
    }

    var plugin = root.loadedPlugins[pluginId];
    if (!plugin || !plugin.manifest || !plugin.manifest.entryPoints || !plugin.manifest.entryPoints.panel) {
      Logger.w("PluginService", "Plugin does not provide a panel:", pluginId);
      return false;
    }

    // Check if this plugin's panel is already open in any slot
    for (var slotNum = 1; slotNum <= 2; slotNum++) {
      var panelName = "pluginPanel" + slotNum;
      var panel = PanelService.getPanel(panelName, screen);

      if (panel && panel.currentPluginId === pluginId) {
        // Panel is open for this plugin - toggle it (close)
        panel.toggle(buttonItem);
        return true;
      }
    }

    // Panel is not open - open it using the existing logic
    return openPluginPanel(pluginId, screen, buttonItem);
  }

  // ----- Error tracking functions -----

  function recordPluginError(pluginId, entryPoint, errorMessage) {
    var errors = Object.assign({}, root.pluginErrors);
    errors[pluginId] = {
      error: errorMessage,
      entryPoint: entryPoint,
      timestamp: new Date()
    };
    root.pluginErrors = errors;
    root.pluginLoadError(pluginId, entryPoint, errorMessage);
    Logger.e("PluginService", "Plugin load error [" + pluginId + "/" + entryPoint + "]:", errorMessage);
  }

  function clearPluginError(pluginId) {
    if (pluginId in root.pluginErrors) {
      var errors = Object.assign({}, root.pluginErrors);
      delete errors[pluginId];
      root.pluginErrors = errors;
    }
  }

  function getPluginError(pluginId) {
    return root.pluginErrors[pluginId] || null;
  }

  function hasPluginError(pluginId) {
    return pluginId in root.pluginErrors;
  }

  // ----- Hot reload functions -----

  // Set up file watcher for a plugin directory
  function setupPluginFileWatcher(pluginId) {
    if (!isPluginHotReloadEnabled(pluginId)) {
      return;
    }

    // Don't create duplicate watchers
    if (root.pluginFileWatchers[pluginId]) {
      return;
    }

    var manifest = PluginRegistry.getPluginManifest(pluginId);
    if (!manifest) {
      return;
    }

    var pluginDir = PluginRegistry.getPluginDir(pluginId);

    // Create a debounce timer for this plugin
    var debounceTimer = Qt.createQmlObject(`
      import QtQuick
      Timer {
        property string targetPluginId: ""
        property var reloadCallback: null
        interval: 500
        repeat: false
        onTriggered: {
          if (reloadCallback) reloadCallback(targetPluginId);
        }
      }
    `, root, "HotReloadDebounce_" + pluginId);

    // Set properties after creation to pass the callback
    debounceTimer.targetPluginId = pluginId;
    debounceTimer.reloadCallback = root.reloadPlugin;

    // Watch the manifest file - changes here indicate plugin updates
    var manifestWatcher = Qt.createQmlObject(`
      import Quickshell.Io
      FileView {
        path: "${pluginDir}/manifest.json"
        watchChanges: true
      }
    `, root, "ManifestWatcher_" + pluginId);

    var watchers = [manifestWatcher];

    // Only watch .qml and .js files, also follow symlinks since some of the plugins might have been symlinked in.
    var qmlWatcher = Qt.createQmlObject(`
        import QtQuick
        import Quickshell.Io

        import qs.Commons

        Item {
            id: root
            signal fileChanged();

            Process {
                command: [ "sh", "-c", "find -L ${pluginDir} -name '*.qml' -o -name '*.js'" ]
                running: true
                stdout: SplitParser {
                    splitMarker: "\n"
                    onRead: line => {
                        fileWatcher.createObject(root, { path: Qt.resolvedUrl(line) });
                    }
                }
            }

            Component {
                id: fileWatcher
                FileView {
                    watchChanges: true

                    onFileChanged: {
                        root.fileChanged();
                    }
                }
            }

        }
    `, root, "QmlWatcher_" + pluginId);
    watchers.push(qmlWatcher);

    // Connect all watchers to the debounce timer
    for (var j = 0; j < watchers.length; j++) {
      watchers[j].fileChanged.connect(function () {
        debounceTimer.restart();
      });
    }

    // Create a separate debounce timer for translation reloads (lighter weight)
    var translationDebounceTimer = Qt.createQmlObject(`
      import QtQuick
      Timer {
        property string targetPluginId: ""
        property var reloadCallback: null
        interval: 300
        repeat: false
        onTriggered: {
          if (reloadCallback) reloadCallback(targetPluginId);
        }
      }
    `, root, "TranslationReloadDebounce_" + pluginId);

    translationDebounceTimer.targetPluginId = pluginId;
    translationDebounceTimer.reloadCallback = root.reloadPluginTranslations;

    // Watch the current language's translation file
    var translationWatcher = createTranslationWatcher(pluginId, pluginDir, I18n.langCode, translationDebounceTimer);

    root.pluginFileWatchers[pluginId] = {
      watchers: watchers,
      debounceTimer: debounceTimer,
      translationWatcher: translationWatcher,
      translationDebounceTimer: translationDebounceTimer,
      pluginDir: pluginDir
    };

    Logger.d("PluginService", "Set up hot reload watcher for plugin:", pluginId, "(including translations)");
  }

  // Create a translation file watcher for a specific language
  function createTranslationWatcher(pluginId, pluginDir, language, debounceTimer) {
    var translationFile = pluginDir + "/i18n/" + language + ".json";

    var watcher = Qt.createQmlObject(`
      import Quickshell.Io
      FileView {
        path: "${translationFile}"
        watchChanges: true
      }
    `, root, "TranslationWatcher_" + pluginId + "_" + language);

    watcher.fileChanged.connect(function () {
      debounceTimer.restart();
    });

    Logger.d("PluginService", "Watching translation file:", translationFile);
    return watcher;
  }

  // Update translation watchers when language changes
  function updateTranslationWatchers() {
    for (var pluginId in root.pluginFileWatchers) {
      var watcherData = root.pluginFileWatchers[pluginId];
      if (!watcherData || !watcherData.translationDebounceTimer)
        continue;

      // Destroy old translation watcher
      if (watcherData.translationWatcher) {
        watcherData.translationWatcher.destroy();
      }

      // Create new watcher for current language
      watcherData.translationWatcher = createTranslationWatcher(pluginId, watcherData.pluginDir, I18n.langCode, watcherData.translationDebounceTimer);
    }
    Logger.d("PluginService", "Updated translation watchers for language:", I18n.langCode);
  }

  // Remove file watcher for a plugin
  function removePluginFileWatcher(pluginId) {
    var watcherData = root.pluginFileWatchers[pluginId];
    if (!watcherData) {
      return;
    }

    // Destroy all watchers
    if (watcherData.watchers) {
      for (var i = 0; i < watcherData.watchers.length; i++) {
        if (watcherData.watchers[i]) {
          watcherData.watchers[i].destroy();
        }
      }
    }

    // Destroy debounce timer
    if (watcherData.debounceTimer) {
      watcherData.debounceTimer.destroy();
    }

    // Destroy translation watcher
    if (watcherData.translationWatcher) {
      watcherData.translationWatcher.destroy();
    }

    // Destroy translation debounce timer
    if (watcherData.translationDebounceTimer) {
      watcherData.translationDebounceTimer.destroy();
    }

    delete root.pluginFileWatchers[pluginId];
    Logger.d("PluginService", "Removed hot reload watcher for plugin:", pluginId);
  }

  // Reload a plugin (hot reload)
  function reloadPlugin(pluginId) {
    if (!root.loadedPlugins[pluginId]) {
      Logger.w("PluginService", "Cannot reload: plugin not loaded:", pluginId);
      return false;
    }

    Logger.i("PluginService", "Hot reloading plugin:", pluginId);

    var manifest = PluginRegistry.getPluginManifest(pluginId);
    if (!manifest) {
      Logger.e("PluginService", "Cannot reload: manifest not found for:", pluginId);
      return false;
    }

    // Unregister widget instances from the bar
    BarService.destroyPluginWidgetInstances(pluginId);

    // Unload the plugin (destroys components and instances)
    // Pass true to preserve desktop widget settings during hot reload
    unloadPlugin(pluginId, true);

    // Increment load version to invalidate Qt's component cache
    PluginRegistry.incrementPluginLoadVersion(pluginId);

    // Use Qt.callLater to ensure destruction is complete before reloading
    // This prevents IPC handler conflicts and other timing issues
    Qt.callLater(function () {
      // Reload the plugin
      loadPlugin(pluginId);

      // Re-setup file watcher (it was destroyed during unload)
      setupPluginFileWatcher(pluginId);

      // Emit signal
      root.pluginReloaded(pluginId);

      // Show toast notification
      var pluginName = manifest.name || pluginId;
      ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.hot-reloaded", {
                                                                         "name": pluginName
                                                                       }));

      Logger.i("PluginService", "Hot reload complete for plugin:", pluginId);
    });

    return true;
  }

  // Hot reload only translations for a plugin (lightweight, no component reload)
  function reloadPluginTranslations(pluginId) {
    var plugin = root.loadedPlugins[pluginId];
    if (!plugin || !plugin.api || !plugin.manifest) {
      Logger.w("PluginService", "Cannot reload translations: plugin not loaded:", pluginId);
      return false;
    }

    Logger.i("PluginService", "Hot reloading translations for plugin:", pluginId);

    loadPluginTranslationsAsync(pluginId, plugin.manifest, I18n.langCode, function (translations) {
      plugin.api.pluginTranslations = translations;

      // Also reload English fallback for non-English languages
      if (I18n.langCode !== "en") {
        loadPluginTranslationsAsync(pluginId, plugin.manifest, "en", function (fallbackTranslations) {
          plugin.api.pluginFallbackTranslations = fallbackTranslations;
          plugin.api.translationVersion++;

          var pluginName = plugin.manifest.name || pluginId;
          ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.translations-reloaded", {
                                                                             "name": pluginName
                                                                           }));
          Logger.i("PluginService", "Translation hot reload complete for plugin:", pluginId);
        });
      } else {
        plugin.api.pluginFallbackTranslations = {};
        plugin.api.translationVersion++;

        var pluginName = plugin.manifest.name || pluginId;
        ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.translations-reloaded", {
                                                                           "name": pluginName
                                                                         }));
        Logger.i("PluginService", "Translation hot reload complete for plugin:", pluginId);
      }
    });

    return true;
  }

  // Check if a certain plugin has hot reload enabled
  function isPluginHotReloadEnabled(pluginId) {
    return root.pluginHotReloadEnabled.indexOf(pluginId) !== -1;
  }

  // Toggle the hot reload state of a certain plugin
  function togglePluginHotReload(pluginId) {
    const index = root.pluginHotReloadEnabled.indexOf(pluginId);
    if (index === -1) {
      root.pluginHotReloadEnabled.push(pluginId);
      setupPluginFileWatcher(pluginId);
      Logger.i("PluginService", "Hot reload enabled for plugin:", pluginId);
    } else {
      root.pluginHotReloadEnabled.splice(index, 1);
      removePluginFileWatcher(pluginId);
      Logger.i("PluginService", "Hot reload disabled for plugin:", pluginId);
    }
  }
}
