Steam Infinite Wishlister

Advanced Steam Discovery Queue wishlisting: Trading Card/DLC/Owned options, Age Skip, Pause/Resume, Counters, Robustness++

// ==UserScript==
// @name         Steam Infinite Wishlister
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Advanced Steam Discovery Queue wishlisting: Trading Card/DLC/Owned options, Age Skip, Pause/Resume, Counters, Robustness++
// @icon         https://store.steampowered.com/favicon.ico
// @author       bernardopg
// @match        *://store.steampowered.com/app/*
// @match        *://store.steampowered.com/explore*
// @match        *://store.steampowered.com/explore/
// @match        *://store.steampowered.com/curator/*
// @match        *://steamcommunity.com/*
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // ====================================
  // Module: Configuration
  // ====================================
  const CONFIG = {
    // Timing configuration (all values in milliseconds)
    TIMING: {
      CHECK_INTERVAL: 3500, // How often to check the current page when running the loop
      ACTION_DELAY: 1800, // Delay after performing a major action (like adding to wishlist)
      ADVANCE_DELAY: 600, // Delay before advancing to next item (1/3 of ACTION_DELAY)
      PROCESSING_RELEASE_DELAY: 900, // Delay before releasing processing lock (1/2 of ACTION_DELAY)
      QUEUE_GENERATION_DELAY: 1500, // Delay after attempting to generate a new queue
      QUEUE_LOCK_RELEASE_DELAY: 2000, // Delay before releasing queue generation lock (unused currently)
      INITIAL_START_DELAY: 1500, // Delay before starting the loop on page load
      WISHLIST_CONFIRM_TIMEOUT: 1500, // Timeout for confirming wishlist action success
      MINI_DELAY: 100, // Very small delay for minor operations
      VERSION_CHECK_INTERVAL: 86400000, // Check for updates once per day (24h)
    },

    // DOM Selectors - organized by functional area
    SELECTORS: {
      // Wishlist related selectors
      wishlist: {
        area: "#add_to_wishlist_area, .queue_wishlist_ctn", // Added queue_wishlist_ctn for explore page
        addButton:
          ".add_to_wishlist .btn_addtocart .btnv6_blue_hoverfade, .queue_wishlist_button .btnv6_blue_hoverfade", // More specific button selectors + explore page
        successIndicator: ".add_to_wishlist_area_success, .queue_btn_active", // Added queue_btn_active for explore
      },

      // Game information selectors
      gameInfo: {
        tradingCardsIndicator:
          '.game_area_details_specs a[href*="/tradingcards/"], a.trading_card_details_link[href*="/tradingcards/"]',
        title: ".apphub_AppName",
        queueRemainingText: ".queue_sub_text",
        inLibraryIndicator: ".game_area_already_owned",
        dlcIndicator: ".game_area_dlc_bubble",
        appTypeElement: ".game_details .details_block",
      },

      // Queue navigation selectors
      queueNav: {
        nextButton:
          ".btn_next_in_queue_trigger, .btn_next_in_queue .btnv6_lightblue_blue", // Added second selector for explore page
        nextForm: "#next_in_queue_form",
        ignoreButtonContainer: "#ignoreBtn", // Used mainly for the button within
        ignoreButtonInContainer: ".queue_btn_ignore",
      },

      // Queue status and management selectors
      queueStatus: {
        container: "#discovery_queue_ctn, #discovery_queue", // Added #discovery_queue for explore page
        finishedIndicator: ".discover_queue_empty", // Should be sufficient
        emptyContainer: ".discover_queue_empty",
        // Selectors for starting a queue
        startLink:
          ".discovery_queue_start_link, #discovery_queue_start_link, .discovery_queue_winter_sale_cards_header a[href*='discovery_queue'], .discovery_queue_global_header a[href*='discoveryqueue']",
        // Selectors for starting *another* queue when one finished
        startAnotherButton:
          "#refresh_queue_btn, .discover_queue_empty_refresh_btn .btnv6_lightblue_blue, .discover_queue_empty a[href*='discoveryqueue'], .begin_exploring",
      },

      // Age gate selectors
      ageGate: {
        storeContainer: "#app_agegate",
        communityTextContainer: ".agegate_text_container",
      },

      // UI selectors
      ui: {
        container: "#wishlist-looper-controls",
        statusElement: "#wl-status",
        minimizeButton: "#wl-minimize",
        processOnceButton: "#wl-process-once",
        skipButton: "#wl-skip",
        pauseButton: "#wl-pause",
        wishlistCountElement: "#wl-wishlist-count",
        requireCardsCheckbox: "#wl-require-cards",
        skipNonGamesCheckbox: "#wl-skip-non-games",
        skipOwnedCheckbox: "#wl-skip-owned",
        startButton: "#wl-start",
        stopButton: "#wl-stop",
        autoStartCheckbox: "#wl-autostart",
        autoRestartCheckbox: "#wl-autorestart",
        versionInfo: "#wl-version-info",
      },
    },

    // Storage keys
    STORAGE_KEYS: {
      AUTO_START: "wishlistLooperAutoStartV2", // Renamed to avoid conflict with old versions
      AUTO_RESTART_QUEUE: "wishlistLooperAutoRestartQueueV2",
      UI_MINIMIZED: "wishlistLooperUiMinimizedV2",
      REQUIRE_CARDS: "wishlistLooperRequireCardsV2",
      SKIP_NON_GAMES: "wishlistLooperSkipNonGamesV2",
      SKIP_OWNED: "wishlistLooperSkipOwnedV2",
      LOG_LEVEL: "wishlistLooperLogLevel", // Keep log level key generic
      SESSION_WISHLIST_COUNT: "wishlistLooperSessionCountV2",
      LAST_VERSION_CHECK: "wishlistLooperLastVersionCheck",
      // Example version check URL (replace with your actual source if hosting)
      VERSION_CHECK_URL:
        "https://raw.githubusercontent.com/bernardopg/steam-wishlist-looper/main/version.json",
    },

    // App constants
    MAX_QUEUE_RESTART_FAILURES: 5,
    CURRENT_VERSION: "2.0",
    // URL for version checking, defined in STORAGE_KEYS now for consistency
    get VERSION_CHECK_URL() {
      return GM_getValue(
        CONFIG.STORAGE_KEYS.VERSION_CHECK_URL,
        "https://raw.githubusercontent.com/bernardopg/steam-wishlist-looper/main/version.json"
      );
    },
  };

  // ====================================
  // Module: State Management
  // ====================================
  const State = {
    loop: {
      state: "Stopped", // 'Stopped', 'Running', 'Paused'
      timeoutId: null, // Holds the timeout ID for the main loop
      isProcessing: false, // Whether we're currently processing an item
      manualActionInProgress: false, // Whether a manual action is in progress
      failedQueueRestarts: 0, // Counter for failed queue restart attempts
    },

    settings: {
      autoStartEnabled: GM_getValue(CONFIG.STORAGE_KEYS.AUTO_START, false),
      autoRestartQueueEnabled: GM_getValue(
        CONFIG.STORAGE_KEYS.AUTO_RESTART_QUEUE,
        true
      ),
      uiMinimized: GM_getValue(CONFIG.STORAGE_KEYS.UI_MINIMIZED, false),
      requireTradingCards: GM_getValue(CONFIG.STORAGE_KEYS.REQUIRE_CARDS, true),
      skipNonGames: GM_getValue(CONFIG.STORAGE_KEYS.SKIP_NON_GAMES, true),
      skipOwnedGames: GM_getValue(CONFIG.STORAGE_KEYS.SKIP_OWNED, true),
      logLevel: GM_getValue(CONFIG.STORAGE_KEYS.LOG_LEVEL, 0), // 0=Info, 1=Debug, 2=Verbose
    },

    stats: {
      wishlistedThisSession: parseInt(
        sessionStorage.getItem(CONFIG.STORAGE_KEYS.SESSION_WISHLIST_COUNT) ||
          "0"
      ),
      lastVersionCheck: GM_getValue(CONFIG.STORAGE_KEYS.LAST_VERSION_CHECK, 0),
      latestVersion: null, // Stores fetched latest version
      updateUrl: null, // Stores fetched update URL
    },

    ui: {
      elements: {}, // Will hold references to UI DOM elements
    },
  };

  // ====================================
  // Module: Logging
  // ====================================
  const Logger = {
    /**
     * Log a message with a specified level
     * @param {string} message - The message to log
     * @param {number} level - The log level (0=info, 1=debug, 2=verbose)
     */
    log: function (message, level = 0) {
      if (level <= State.settings.logLevel) {
        const prefix = level === 1 ? "[DEBUG]" : level === 2 ? "[VERBOSE]" : "";
        // Avoid double prefixing if message already has it
        if (!message.startsWith("[Steam Wishlist Looper]")) {
          console.log(`[Steam Wishlist Looper]${prefix}`, message);
        } else {
          console.log(`${prefix} ${message}`); // Assume message already has script name
        }
      }
    },
  };

  // ====================================
  // Module: UI Management
  // ====================================
  const UI = {
    /**
     * Update the status text in the UI
     * @param {string} message - The status message to display
     * @param {string} statusType - The type of status (info, action, success, skipped, error, paused)
     */
    updateStatusText: function (message, statusType = "info") {
      if (!State.ui.elements.status) return;

      State.ui.elements.status.textContent = `Status: ${message}`;
      // Clear previous status classes before adding new one
      State.ui.elements.status.className =
        CONFIG.SELECTORS.ui.statusElement.substring(1); // Reset to base class

      switch (statusType) {
        case "action":
          State.ui.elements.status.classList.add("wl-status-action");
          break;
        case "success":
          State.ui.elements.status.classList.add("wl-status-success");
          break;
        case "skipped":
          State.ui.elements.status.classList.add("wl-status-skipped");
          break;
        case "error":
          State.ui.elements.status.classList.add("wl-status-error");
          break;
        case "paused":
          State.ui.elements.status.classList.add("wl-status-paused");
          break;
        case "info":
        default:
          // Keep default color (no class added)
          break;
      }

      // Reset status highlight after a delay for transient types
      if (
        statusType === "action" ||
        statusType === "success" ||
        statusType === "skipped"
      ) {
        setTimeout(() => {
          // Only remove the class if the status hasn't changed to something else critical (like error/paused)
          if (
            State.ui.elements.status &&
            State.ui.elements.status.classList.contains(
              `wl-status-${statusType}`
            )
          ) {
            State.ui.elements.status.classList.remove(
              `wl-status-${statusType}`
            );
          }
        }, 1500);
      }
    },

    /**
     * Increment the wishlist counter and update UI
     */
    incrementWishlistCounter: function () {
      State.stats.wishlistedThisSession++;
      sessionStorage.setItem(
        CONFIG.STORAGE_KEYS.SESSION_WISHLIST_COUNT,
        State.stats.wishlistedThisSession.toString()
      );

      if (State.ui.elements.wishlistCount) {
        State.ui.elements.wishlistCount.textContent =
          State.stats.wishlistedThisSession;
      }
    },

    /**
     * Toggle enabled state of manual action buttons based on current state
     */
    updateManualButtonStates: function () {
      const disableManual =
        State.loop.state === "Running" ||
        State.loop.isProcessing ||
        State.loop.manualActionInProgress;

      if (State.ui.elements.processOnce) {
        State.ui.elements.processOnce.disabled = disableManual;
      }
      if (State.ui.elements.skip) {
        State.ui.elements.skip.disabled = disableManual;
      }
    },

    /**
     * Create and add the UI controls to the page
     */
    addControls: function () {
      // Don't add controls if they already exist
      if (document.querySelector(CONFIG.SELECTORS.ui.container)) return;

      const controlDiv = document.createElement("div");
      controlDiv.id = CONFIG.SELECTORS.ui.container.substring(1);
      controlDiv.classList.toggle("wl-minimized", State.settings.uiMinimized);

      // HTML template for the controls
      controlDiv.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid rgba(199, 213, 224, 0.1);">
               <strong style="color: #66c0f4; text-shadow: 1px 1px 1px #000; margin-right: auto;">Wishlist Looper</strong>
               <span title="Wishlisted this session" style="font-size: 10px; margin: 0 10px; color: #a1dd4a;">(<span id="${CONFIG.SELECTORS.ui.wishlistCountElement.substring(
                 1
               )}">${State.stats.wishlistedThisSession}</span> Added)</span>
               <button id="${CONFIG.SELECTORS.ui.minimizeButton.substring(
                 1
               )}" title="${
        State.settings.uiMinimized ? "Restore" : "Minimize"
      }" style="background: none; border: none; color: #66c0f4; font-size: 14px; cursor: pointer; padding: 0 5px; line-height: 1;">${
        State.settings.uiMinimized ? "□" : "▬"
      }</button>
            </div>
            <div class="wl-controls-body">
               <div style="margin-bottom: 5px; display: flex; align-items: center;">
                   <button id="${CONFIG.SELECTORS.ui.startButton.substring(
                     1
                   )}" title="Start/Resume automatic processing">Start</button>
                   <button id="${CONFIG.SELECTORS.ui.pauseButton.substring(
                     1
                   )}" title="Pause automatic processing" disabled>Pause</button>
                   <button id="${CONFIG.SELECTORS.ui.stopButton.substring(
                     1
                   )}" title="Stop processing and disable Auto features">Stop</button>
               </div>
               <div style="margin-bottom: 5px;">
                   <button id="${CONFIG.SELECTORS.ui.processOnceButton.substring(
                     1
                   )}" title="Process only the current item">Process Once</button>
                   <button id="${CONFIG.SELECTORS.ui.skipButton.substring(
                     1
                   )}" title="Skip the current item and advance">Skip Item</button>
               </div>
               <div id="${CONFIG.SELECTORS.ui.statusElement.substring(
                 1
               )}" class="${CONFIG.SELECTORS.ui.statusElement.substring(
        1
      )}">Status: Initializing...</div>
               <div style="margin-top: 8px; border-top: 1px solid rgba(199, 213, 224, 0.2); padding-top: 8px; font-size: 11px;">
                   <span style="display: block; margin-bottom: 4px; font-weight: bold; color: #66c0f4;">Options:</span>
                   <label title="Automatically start loop on compatible pages"><input type="checkbox" id="${CONFIG.SELECTORS.ui.autoStartCheckbox.substring(
                     1
                   )}">Auto-Start</label>
                   <label title="Automatically restart queue when finished (requires Auto-Start)" style="margin-left: 10px;"><input type="checkbox" id="${CONFIG.SELECTORS.ui.autoRestartCheckbox.substring(
                     1
                   )}">Auto-Restart</label>
                   <br>
                   <label title="Only wishlist items that have Steam Trading Cards"><input type="checkbox" id="${CONFIG.SELECTORS.ui.requireCardsCheckbox.substring(
                     1
                   )}">Require Cards</label>
                   <label title="Skip items already in your Steam library" style="margin-left: 10px;"><input type="checkbox" id="${CONFIG.SELECTORS.ui.skipOwnedCheckbox.substring(
                     1
                   )}">Skip Owned</label>
                   <br>
                   <label title="Skip items identified as DLC, Soundtracks, Demos, etc."><input type="checkbox" id="${CONFIG.SELECTORS.ui.skipNonGamesCheckbox.substring(
                     1
                   )}">Skip Non-Games</label>
               </div>
               <div id="${CONFIG.SELECTORS.ui.versionInfo.substring(
                 1
               )}" style="font-size: 9px; color: #8f98a0; margin-top: 8px; text-align: right;">v${
        CONFIG.CURRENT_VERSION
      }</div>
            </div>
        `;

      // Apply styles via GM_addStyle
      GM_addStyle(`
          #${CONFIG.SELECTORS.ui.container.substring(1)} {
            position: fixed; bottom: 10px; right: 10px; z-index: 9999;
            background: rgba(27, 40, 56, 0.9); color: #c7d5e0; padding: 10px;
            border-radius: 5px; font-family: 'Motiva Sans', sans-serif; font-size: 12px;
            border: 1px solid #000; box-shadow: 0 0 10px rgba(0,0,0,0.7);
            backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
            transition: all 0.3s ease-in-out; width: 250px;
          }
          #${CONFIG.SELECTORS.ui.container.substring(1)}.wl-minimized {
            padding: 5px 10px; height: auto; width: auto; min-width: 150px;
          }
          #${CONFIG.SELECTORS.ui.container.substring(
            1
          )}.wl-minimized .wl-controls-body {
            display: none;
          }
          #${CONFIG.SELECTORS.ui.container.substring(1)} button {
            padding: 4px 8px; border-radius: 2px; cursor: pointer; font-size: 11px;
            margin-right: 5px; border: 1px solid; transition: filter 0.15s ease;
          }
          #${CONFIG.SELECTORS.ui.container.substring(
            1
          )} button:last-child { margin-right: 0; }
          #${CONFIG.SELECTORS.ui.container.substring(1)} button:disabled {
            background-color: #555 !important; color: #999 !important; cursor: not-allowed !important;
            border-color: #333 !important; opacity: 0.7; filter: none !important;
          }
          #${CONFIG.SELECTORS.ui.container.substring(
            1
          )} button:hover:not(:disabled) { filter: brightness(1.15); }

          #${CONFIG.SELECTORS.ui.startButton.substring(
            1
          )} { background-color: #68932f; color: white; border-color: #3a511b; }
          #${CONFIG.SELECTORS.ui.pauseButton.substring(
            1
          )} { background-color: #4a6b9d; color: white; border-color: #2a3d5e; }
          #${CONFIG.SELECTORS.ui.stopButton.substring(
            1
          )} { background-color: #a33e29; color: white; border-color: #5c2416; }
          #${CONFIG.SELECTORS.ui.processOnceButton.substring(1)},
          #${CONFIG.SELECTORS.ui.skipButton.substring(
            1
          )} { background-color: #777; color: white; border-color: #444; }

          .${CONFIG.SELECTORS.ui.statusElement.substring(
            1
          )} { /* Target by class for easier style application */
            font-size: 11px; min-height: 1.2em; padding: 4px 0; text-align: left;
            transition: color 0.3s ease, font-weight 0.3s ease; color: #c7d5e0;
          }
          .${CONFIG.SELECTORS.ui.statusElement.substring(
            1
          )}.wl-status-action { color: #66c0f4 !important; }
          .${CONFIG.SELECTORS.ui.statusElement.substring(
            1
          )}.wl-status-success { color: #a1dd4a !important; }
          .${CONFIG.SELECTORS.ui.statusElement.substring(
            1
          )}.wl-status-skipped { color: #aaa !important; }
          .${CONFIG.SELECTORS.ui.statusElement.substring(
            1
          )}.wl-status-error { color: #ff7a7a !important; font-weight: bold; }
          .${CONFIG.SELECTORS.ui.statusElement.substring(
            1
          )}.wl-status-paused { color: #e4d00a !important; font-style: italic; }

          #${CONFIG.SELECTORS.ui.container.substring(1)} label {
            display: inline-flex; align-items: center; cursor: pointer;
            font-size: 11px; vertical-align: middle; margin-bottom: 3px;
          }
          #${CONFIG.SELECTORS.ui.container.substring(
            1
          )} input[type="checkbox"] {
            margin-right: 4px; vertical-align: middle; cursor: pointer; accent-color: #66c0f4;
          }
          #${CONFIG.SELECTORS.ui.versionInfo.substring(1)}.wl-update-available {
             color: #ffa500 !important; text-decoration: underline; cursor: pointer; font-weight: bold;
          }
        `);

      // Add to document
      document.body.appendChild(controlDiv);

      // Store references to UI elements
      State.ui.elements = {
        container: controlDiv,
        startBtn: controlDiv.querySelector(CONFIG.SELECTORS.ui.startButton),
        pauseBtn: controlDiv.querySelector(CONFIG.SELECTORS.ui.pauseButton),
        stopBtn: controlDiv.querySelector(CONFIG.SELECTORS.ui.stopButton),
        processOnce: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.processOnceButton
        ),
        skip: controlDiv.querySelector(CONFIG.SELECTORS.ui.skipButton),
        status: controlDiv.querySelector(CONFIG.SELECTORS.ui.statusElement),
        minimizeBtn: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.minimizeButton
        ),
        wishlistCount: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.wishlistCountElement
        ),
        autoStartCheckbox: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.autoStartCheckbox
        ),
        autoRestartCheckbox: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.autoRestartCheckbox
        ),
        requireCardsCheckbox: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.requireCardsCheckbox
        ),
        skipOwnedCheckbox: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.skipOwnedCheckbox
        ),
        skipNonGamesCheckbox: controlDiv.querySelector(
          CONFIG.SELECTORS.ui.skipNonGamesCheckbox
        ),
        versionInfo: controlDiv.querySelector(CONFIG.SELECTORS.ui.versionInfo),
      };

      // Add event listeners
      State.ui.elements.startBtn.addEventListener(
        "click",
        LoopController.startLoop
      );
      State.ui.elements.pauseBtn.addEventListener(
        "click",
        LoopController.pauseLoop
      );
      State.ui.elements.stopBtn.addEventListener(
        "click",
        () => LoopController.stopLoop(false) // Stop and disable auto features
      );
      State.ui.elements.processOnce.addEventListener(
        "click",
        QueueProcessor.processOnce
      );
      State.ui.elements.skip.addEventListener("click", QueueProcessor.skipItem);
      State.ui.elements.minimizeBtn.addEventListener(
        "click",
        this.toggleMinimizeUI
      );

      // Settings listeners using SettingsManager
      State.ui.elements.autoStartCheckbox.addEventListener("change", (e) =>
        SettingsManager.updateSetting(
          CONFIG.STORAGE_KEYS.AUTO_START,
          e.target.checked
        )
      );
      State.ui.elements.autoRestartCheckbox.addEventListener("change", (e) =>
        SettingsManager.updateSetting(
          CONFIG.STORAGE_KEYS.AUTO_RESTART_QUEUE,
          e.target.checked
        )
      );
      State.ui.elements.requireCardsCheckbox.addEventListener("change", (e) =>
        SettingsManager.updateSetting(
          CONFIG.STORAGE_KEYS.REQUIRE_CARDS,
          e.target.checked
        )
      );
      State.ui.elements.skipOwnedCheckbox.addEventListener("change", (e) =>
        SettingsManager.updateSetting(
          CONFIG.STORAGE_KEYS.SKIP_OWNED,
          e.target.checked
        )
      );
      State.ui.elements.skipNonGamesCheckbox.addEventListener("change", (e) =>
        SettingsManager.updateSetting(
          CONFIG.STORAGE_KEYS.SKIP_NON_GAMES,
          e.target.checked
        )
      );

      // Update UI to match current state
      this.updateUI();
    },

    /**
     * Update all UI elements to match current state
     */
    updateUI: function () {
      if (!State.ui.elements.container) return;

      const isRunning = State.loop.state === "Running";
      const isPaused = State.loop.state === "Paused";

      // Update button states
      State.ui.elements.startBtn.disabled = isRunning;
      State.ui.elements.startBtn.textContent = isPaused ? "Resume" : "Start";
      State.ui.elements.startBtn.title = isPaused
        ? "Resume automatic processing"
        : "Start automatic processing";
      State.ui.elements.pauseBtn.disabled = !isRunning;
      State.ui.elements.stopBtn.disabled = !(isRunning || isPaused);

      // Update manual action buttons based on state
      this.updateManualButtonStates();

      // Update checkboxes
      State.ui.elements.autoStartCheckbox.checked =
        State.settings.autoStartEnabled;
      State.ui.elements.autoRestartCheckbox.checked =
        State.settings.autoRestartQueueEnabled;
      State.ui.elements.requireCardsCheckbox.checked =
        State.settings.requireTradingCards;
      State.ui.elements.skipOwnedCheckbox.checked =
        State.settings.skipOwnedGames;
      State.ui.elements.skipNonGamesCheckbox.checked =
        State.settings.skipNonGames;

      // Update UI minimization state
      State.ui.elements.container.classList.toggle(
        "wl-minimized",
        State.settings.uiMinimized
      );
      State.ui.elements.minimizeBtn.innerHTML = State.settings.uiMinimized
        ? "□"
        : "▬";
      State.ui.elements.minimizeBtn.title = State.settings.uiMinimized
        ? "Restore"
        : "Minimize";

      // Update wishlist count
      if (State.ui.elements.wishlistCount) {
        State.ui.elements.wishlistCount.textContent =
          State.stats.wishlistedThisSession;
      }

      // Initial status text update if needed (avoid overwriting transient messages)
      // Check if the current status is just the base "Status: Initializing..." or empty
      const currentStatusText = State.ui.elements.status
        ? State.ui.elements.status.textContent
        : "";
      if (
        !currentStatusText ||
        currentStatusText === "Status: Initializing..."
      ) {
        if (isPaused) UI.updateStatusText("Paused", "paused");
        else if (isRunning) UI.updateStatusText("Running - Idle...");
        else UI.updateStatusText("Stopped.");
      }
    },

    /**
     * Toggle UI minimized state
     */
    toggleMinimizeUI: function () {
      State.settings.uiMinimized = !State.settings.uiMinimized;
      GM_setValue(CONFIG.STORAGE_KEYS.UI_MINIMIZED, State.settings.uiMinimized);
      UI.updateUI(); // Just call updateUI which handles the class and button text
    },

    /**
     * Update the version info element if a new version is available
     * @param {string} latestVersion - The latest version available
     * @param {string} updateUrl - The URL to the update page/script
     */
    updateVersionInfo: function (latestVersion, updateUrl) {
      if (!State.ui.elements.versionInfo) return;

      // Simple version comparison (assumes semantic versioning or similar numeric comparison)
      const isNewer =
        latestVersion &&
        latestVersion.localeCompare(CONFIG.CURRENT_VERSION, undefined, {
          numeric: true,
          sensitivity: "base",
        }) === 1;

      if (isNewer) {
        State.ui.elements.versionInfo.textContent = `v${CONFIG.CURRENT_VERSION} (Update: v${latestVersion})`;
        State.ui.elements.versionInfo.classList.add("wl-update-available");
        State.ui.elements.versionInfo.title = `New version ${latestVersion} available! Click to view.`;
        // Make clickable only if update URL is provided and valid
        if (updateUrl && updateUrl !== "#") {
          State.ui.elements.versionInfo.style.cursor = "pointer";
          // Remove previous listener before adding new one
          State.ui.elements.versionInfo.onclick = null;
          State.ui.elements.versionInfo.onclick = () => {
            window.open(updateUrl, "_blank");
          };
        } else {
          State.ui.elements.versionInfo.style.cursor = "default";
          State.ui.elements.versionInfo.onclick = null;
        }
      } else {
        State.ui.elements.versionInfo.textContent = `v${CONFIG.CURRENT_VERSION}`;
        State.ui.elements.versionInfo.classList.remove("wl-update-available");
        State.ui.elements.versionInfo.title = "";
        State.ui.elements.versionInfo.style.cursor = "default";
        State.ui.elements.versionInfo.onclick = null;
      }
    },
  };

  // ====================================
  // Module: Settings Manager
  // ====================================
  const SettingsManager = {
    /**
     * Update a setting value in state and GM storage
     * @param {string} key - The storage key from CONFIG.STORAGE_KEYS
     * @param {any} newValue - The new value for the setting
     */
    updateSetting: function (key, newValue) {
      GM_setValue(key, newValue);

      // Find the corresponding key in State.settings based on the GM key
      const stateKeyEntry = Object.entries(CONFIG.STORAGE_KEYS).find(
        ([stateName, gmKey]) => gmKey === key
      );

      if (stateKeyEntry) {
        // Convert state key from uppercase_snake_case (like AUTO_START) to camelCase (like autoStartEnabled)
        const camelCaseKey = stateKeyEntry[0]
          .toLowerCase()
          .replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
        if (camelCaseKey in State.settings) {
          State.settings[camelCaseKey] = newValue;
          Logger.log(`${camelCaseKey} updated to: ${newValue}`, 1);
        } else {
          Logger.log(
            `Warning: No matching key found in State.settings for ${camelCaseKey} (derived from ${key})`,
            0
          );
        }
      } else {
        Logger.log(
          `Warning: No CONFIG.STORAGE_KEYS entry found matching GM key ${key}`,
          0
        );
      }

      // Refresh UI to reflect changes (checkboxes, potentially behavior)
      // Avoid calling updateUI directly if this might cause rapid updates; maybe defer or be selective.
      // For checkbox changes, UI.updateUI() is generally fine.
      UI.updateUI();
    },

    /**
     * Toggles a boolean setting and saves it. Used primarily by menu commands.
     * @param {string} key - The storage key from CONFIG.STORAGE_KEYS
     * @param {boolean} currentValue - The current value to toggle
     * @returns {boolean} The new value after toggling
     */
    toggleSetting: function (key, currentValue) {
      const newValue = !currentValue;
      this.updateSetting(key, newValue); // updateSetting handles state update and logging

      // Find the state key again to return the accurate new value from the state object
      const stateKeyEntry = Object.entries(CONFIG.STORAGE_KEYS).find(
        ([stateName, gmKey]) => gmKey === key
      );
      if (stateKeyEntry) {
        const camelCaseKey = stateKeyEntry[0]
          .toLowerCase()
          .replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
        if (camelCaseKey in State.settings) {
          return State.settings[camelCaseKey];
        }
      }
      // Fallback return if state mapping fails (shouldn't happen)
      return newValue;
    },
  };

  // ====================================
  // Module: Age Verification Bypass
  // ====================================
  const AgeVerificationBypass = {
    /**
     * Initialize age verification bypass functionality
     */
    init: function () {
      // Only run on matching domains
      if (
        !window.location.hostname.includes("steampowered.com") &&
        !window.location.hostname.includes("steamcommunity.com")
      ) {
        return;
      }

      Logger.log("[Steam Age Skip] Initializing...", 1);

      try {
        // Set cookies for age verification immediately
        this.setCookies();

        // Handle based on current site using event listeners for robustness
        if (location.hostname.includes("store.steampowered.com")) {
          this.handleStoreSite();
        } else if (location.hostname.includes("steamcommunity.com")) {
          this.handleCommunitySite();
        }
      } catch (e) {
        Logger.log(`[Steam Age Skip] Error during init: ${e.message}`, 0);
      }
    },

    /**
     * Set cookies for age verification on both domains
     */
    setCookies: function () {
      const birthTimeKey = "birthtime";
      const matureContentKey = "wants_mature_content";
      const sessionMatureContentKey = "session_mature_content"; // Sometimes needed

      // Calculate a plausible birth date (e.g., >= 21 years ago for safety)
      const twentyOneYearsInSeconds = 21 * 365.25 * 24 * 60 * 60;
      const birthTimestamp = Math.floor(
        Date.now() / 1000 - twentyOneYearsInSeconds
      );

      // Use Lax for better compatibility, Secure is important
      const baseCookieOptions = `; max-age=315360000; secure; samesite=Lax`; // 10 years expiration

      // Construct cookie strings for each domain
      const storeDomain = ".store.steampowered.com";
      const communityDomain = ".steamcommunity.com";
      const genericDomain = ".steampowered.com"; // Some cookies might be set here

      const cookiesToSet = [
        { key: birthTimeKey, value: birthTimestamp },
        { key: matureContentKey, value: 1 },
        { key: sessionMatureContentKey, value: 1 }, // Often set without Max-Age
      ];

      cookiesToSet.forEach((cookie) => {
        document.cookie = `${cookie.key}=${cookie.value}; path=/; domain=${storeDomain}${baseCookieOptions}`;
        document.cookie = `${cookie.key}=${cookie.value}; path=/; domain=${communityDomain}${baseCookieOptions}`;
        document.cookie = `${cookie.key}=${cookie.value}; path=/; domain=${genericDomain}${baseCookieOptions}`;
        // Set session cookie without max-age too, just in case
        if (cookie.key === sessionMatureContentKey) {
          document.cookie = `${cookie.key}=${cookie.value}; path=/; domain=${storeDomain}; secure; samesite=Lax`;
          document.cookie = `${cookie.key}=${cookie.value}; path=/; domain=${communityDomain}; secure; samesite=Lax`;
          document.cookie = `${cookie.key}=${cookie.value}; path=/; domain=${genericDomain}; secure; samesite=Lax`;
        }
      });

      Logger.log(
        `[Steam Age Skip] Age cookies set for domains (Store, Community, Generic).`,
        1
      );
    },

    /**
     * Handle age verification on Steam store page load and dynamically
     */
    handleStoreSite: function () {
      const checkAndReload = () => {
        const ageGate = document.querySelector(
          CONFIG.SELECTORS.ageGate.storeContainer
        );
        // Added more selectors for robustness
        const ageGateOverlay = document.querySelector(
          ".agegate_birthday_desc, #agegate_box .agegate_text_container"
        );
        if (ageGate || ageGateOverlay) {
          Logger.log(
            "[Steam Age Skip] Age gate detected on store. Attempting bypass/reload...",
            0
          );

          // Attempt to click the view button first if available
          const viewButton = document.querySelector(
            "#view_product_page_btn, .btn_medium.btnv6_lightblue_blue > span"
          ); // Try common view buttons
          if (viewButton && viewButton.offsetParent) {
            // Check visibility
            Logger.log(
              "[Steam Age Skip] Found visible view button, attempting click...",
              1
            );
            viewButton.click();
            // Don't reload immediately, give click time to work and check again
            setTimeout(() => {
              const ageGateAfterClick = document.querySelector(
                CONFIG.SELECTORS.ageGate.storeContainer
              );
              const ageGateOverlayAfterClick = document.querySelector(
                ".agegate_birthday_desc, #agegate_box .agegate_text_container"
              );
              if (ageGateAfterClick || ageGateOverlayAfterClick) {
                Logger.log(
                  "[Steam Age Skip] Age gate still present after click, reloading.",
                  1
                );
                location.reload();
              } else {
                Logger.log(
                  "[Steam Age Skip] Age gate seems dismissed by click.",
                  1
                );
              }
            }, 500); // Wait 500ms
          } else {
            // If no visible button found, just reload - cookies should handle it.
            Logger.log(
              "[Steam Age Skip] No view button found or visible, relying on reload.",
              1
            );
            location.reload();
          }
          return true; // Gate found
        }
        return false; // No gate found
      };

      // Run check immediately and on DOMContentLoaded/load
      if (!checkAndReload()) {
        // If no gate initially
        window.addEventListener("DOMContentLoaded", checkAndReload, {
          once: true,
        });
        window.addEventListener("load", checkAndReload, { once: true }); // Backup check on full load
      }
    },

    /**
     * Handle age verification on Steam community page load and dynamically
     */
    handleCommunitySite: function () {
      const checkAndProceed = () => {
        const ageCheck = document.querySelector(
          CONFIG.SELECTORS.ageGate.communityTextContainer
        );
        if (ageCheck && ageCheck.offsetParent) {
          // Check visibility
          Logger.log(
            "[Steam Age Skip] Age gate detected on community. Attempting bypass...",
            0
          );
          // Try multiple strategies to bypass age gate
          if (!this.tryProceedFunction()) {
            Logger.log(
              "[Steam Age Skip] Proceed functions failed or not found. Relying on cookies/reload.",
              1
            );
            // Cookies should have been set, maybe a reload is needed if JS fails?
            // Avoid reload loops. If the function call didn't work, manual interaction might be needed.
          } else {
            Logger.log(
              "[Steam Age Skip] Proceed function called successfully (or attempted via injection).",
              1
            );
            // Function call might trigger navigation or content loading.
          }
          return true; // Gate found
        }
        return false; // No gate found
      };

      // Run check immediately and on DOMContentLoaded/load
      if (!checkAndProceed()) {
        // If no gate initially
        window.addEventListener("DOMContentLoaded", checkAndProceed, {
          once: true,
        });
        window.addEventListener("load", checkAndProceed, { once: true });
      }
    },

    /**
     * Try different methods to call the Proceed/Accept function (more robust)
     * @returns {boolean} Whether any attempt was potentially successful
     */
    tryProceedFunction: function () {
      let executed = false;
      const functionsToTry = ["Proceed", "AcceptAppHub", "ViewProductPage"]; // Add more potential function names if needed

      // Helper to log execution attempt
      const attemptExecution = (source, funcName, func) => {
        Logger.log(`[Steam Age Skip] Attempting ${source}.${funcName}()...`, 1);
        try {
          func();
          executed = true; // Mark as executed if call doesn't throw immediately
          Logger.log(` -> Call successful (no immediate error).`, 1);
          return true; // Stop trying other methods
        } catch (e) {
          Logger.log(
            ` -> Error calling ${source}.${funcName}: ${e.message}`,
            1
          );
          return false; // Continue trying other methods
        }
      };

      // 1. Try direct unsafeWindow call (GreaseMonkey/Tampermonkey standard)
      if (typeof unsafeWindow !== "undefined") {
        for (const funcName of functionsToTry) {
          if (typeof unsafeWindow[funcName] === "function") {
            if (
              attemptExecution("unsafeWindow", funcName, unsafeWindow[funcName])
            )
              return true;
          }
        }
      }

      // 2. Try direct window call (less likely due to sandboxing, but check anyway)
      if (!executed) {
        for (const funcName of functionsToTry) {
          if (typeof window[funcName] === "function") {
            if (attemptExecution("window", funcName, window[funcName]))
              return true;
          }
        }
      }

      // 3. Try wrappedJSObject (Firefox-specific)
      if (
        !executed &&
        typeof XPCNativeWrapper !== "undefined" &&
        typeof XPCNativeWrapper.unwrap === "function"
      ) {
        try {
          const unwrappedWindow = XPCNativeWrapper.unwrap(window);
          for (const funcName of functionsToTry) {
            if (typeof unwrappedWindow[funcName] === "function") {
              if (
                attemptExecution(
                  "wrappedJSObject",
                  funcName,
                  unwrappedWindow[funcName]
                )
              )
                return true;
            }
          }
        } catch (e) {
          Logger.log(` -> Error accessing wrappedJSObject: ${e.message}`, 1);
        }
      }

      // 4. Script Injection (Last resort if other methods fail)
      if (!executed) {
        Logger.log(
          "[Steam Age Skip] Direct calls failed, injecting script tag...",
          1
        );
        try {
          const script = document.createElement("script");
          let scriptContent = `"use strict"; (function() { console.log("[Steam Age Skip - Injected] Trying functions..."); var executed = false;`;
          functionsToTry.forEach((funcName) => {
            // Check if function exists before calling, prevent errors in injected script
            scriptContent += `if (!executed && typeof window.${funcName} === 'function') { console.log('[Steam Age Skip - Injected] Calling ${funcName}()'); try { window.${funcName}(); executed = true; } catch(e) { console.error('Error in injected ${funcName}:', e); } } `;
          });
          scriptContent += `if (!executed) console.log("[Steam Age Skip - Injected] No known function found or executed successfully."); })();`;
          script.textContent = scriptContent;

          const target = document.head || document.documentElement;
          if (target) {
            target.appendChild(script); // Append might be safer than prepend sometimes
            executed = true; // Assume injection itself worked, even if function inside fails silently
            Logger.log(" -> Script injected.", 1);
            // Remove script after a short delay to allow execution
            setTimeout(() => script.remove(), 100);
          } else {
            Logger.log(
              " -> Script injection failed: No target element (head/documentElement).",
              0
            );
          }
        } catch (e) {
          Logger.log(
            `[Steam Age Skip] Script injection creation failed: ${e.message}`,
            0
          );
        }
      }

      return executed; // Return true if any method was attempted (direct call) or if injection was done
    },
  };

  // ====================================
  // Module: Game Info Utilities
  // ====================================
  const GameInfoUtils = {
    /**
     * Get the app type from various indicators on the page.
     * @returns {string} The determined app type (Game, DLC, Soundtrack, Demo, Application, Video, Mod, Unknown)
     */
    getAppType: function () {
      // 1. Check DLC bubble first (most reliable for DLC)
      const dlcIndicator = document.querySelector(
        CONFIG.SELECTORS.gameInfo.dlcIndicator
      );
      if (dlcIndicator?.offsetParent) return "DLC";

      // 2. Check details block text content
      const appTypeBlock = document.querySelector(
        CONFIG.SELECTORS.gameInfo.appTypeElement
      );
      if (appTypeBlock) {
        // Use textContent for broader matching, trim and uppercase
        const detailText = appTypeBlock.textContent?.trim().toUpperCase() || "";
        if (detailText.includes("DOWNLOADABLE CONTENT")) return "DLC";
        if (detailText.includes("SOUNDTRACK")) return "Soundtrack";
        if (detailText.includes("DEMO")) return "Demo";
        if (detailText.includes("APPLICATION")) return "Application";
        if (detailText.includes("VIDEO") || detailText.includes("MOVIE"))
          return "Video"; // Added Movie
        if (detailText.includes("MOD")) return "Mod";
      }

      // 3. Check breadcrumbs for clues (e.g., "Software", "Videos")
      // Ensure robust selector for breadcrumbs
      const breadcrumbs = document.querySelectorAll(
        ".breadcrumbs .breadcrumb a, .game_title_area .blockbg a"
      );
      if (breadcrumbs.length > 0) {
        // Check all breadcrumbs, not just second-to-last
        for (const crumb of breadcrumbs) {
          const crumbText = crumb.textContent?.trim().toUpperCase();
          if (crumbText === "SOFTWARE") return "Application";
          if (crumbText === "VIDEOS" || crumbText === "VIDEO") return "Video"; // Check plural too
          if (crumbText === "SOUNDTRACKS" || crumbText === "SOUNDTRACK")
            return "Soundtrack";
          if (crumbText === "DEMOS" || crumbText === "DEMO") return "Demo";
          if (crumbText === "MODS") return "Mod";
          // Add more checks if needed (e.g., "HARDWARE"?)
        }
      }

      // 4. Check for specific demo notice elements
      const demoNotice = document.querySelector(
        ".demo_notice, .game_area_purchase_game.demo_above_purchase"
      );
      if (demoNotice?.offsetParent) return "Demo";

      // 5. Check common tags often associated with non-games (less reliable)
      // Example: document.querySelector('.app_tag[data-tagid="1774"]') // Utilities tag

      // If none of the above match, assume it's a Game
      return "Game"; // Default assumption
    },

    /**
     * Checks if the item is considered a "Non-Game" based on settings and type detection.
     * @returns {string | null} Reason string if it should be skipped as non-game, or null otherwise.
     */
    checkIfNonGame: function () {
      if (!State.settings.skipNonGames) {
        return null; // Skip check if setting is off
      }

      const appType = this.getAppType();
      // Define the list of types to skip when the setting is enabled
      const nonGameTypesToSkip = [
        "DLC",
        "Soundtrack",
        "Demo",
        "Application",
        "Video",
        "Mod",
      ];

      if (nonGameTypesToSkip.includes(appType)) {
        return `Type: ${appType}`; // Return the reason for skipping
      }

      // Additional check: Sometimes items are technically "Games" but act like DLC (e.g., Chapter Packs)
      // This requires more complex logic, perhaps checking tags or descriptions, omitted for now.

      return null; // Considered a game according to current checks
    },
  };

  // ====================================
  // Module: Queue Navigation
  // ====================================
  const QueueNavigation = {
    /**
     * Advance to the next item in the queue using the best available method.
     * Returns the method used ('Next', 'Ignore', 'FormSubmit', 'Failed')
     * @returns {Promise<string>} The method used or 'Failed'.
     */
    advanceQueue: async function () {
      let advanceMethod = "Failed"; // Default status

      // Prioritize visible Next button (check both app page and explore page selectors)
      const nextButton = document.querySelector(
        CONFIG.SELECTORS.queueNav.nextButton
      );
      if (nextButton?.offsetParent) {
        // offsetParent checks visibility
        Logger.log(" -> Found visible 'Next in Queue' button. Clicking...", 1);
        UI.updateStatusText("Navigating Next...", "action");
        nextButton.click();
        advanceMethod = "Next";
      } else {
        // Try Ignore button if Next isn't visible
        const ignoreContainer = document.getElementById(
          CONFIG.SELECTORS.queueNav.ignoreButtonContainer.substring(1)
        );
        const ignoreButton = ignoreContainer?.querySelector(
          CONFIG.SELECTORS.queueNav.ignoreButtonInContainer
        );
        if (ignoreButton?.offsetParent) {
          Logger.log(
            " -> 'Next' button not visible, found visible 'Ignore' button. Clicking...",
            1
          );
          UI.updateStatusText("Ignoring...", "action");
          ignoreButton.click();
          advanceMethod = "Ignore";
        } else {
          // Fallback to form submission if no visible buttons
          const nextForm = document.querySelector(
            CONFIG.SELECTORS.queueNav.nextForm
          );
          if (nextForm) {
            Logger.log(
              " -> No visible buttons, submitting next_in_queue_form...",
              1
            );
            UI.updateStatusText("Submitting form...", "action");
            // Ensure form submission actually navigates
            nextForm.submit();
            // Since form submission navigates away, the rest of the script execution stops here for this page load.
            advanceMethod = "FormSubmit";
            // Add a small delay to *potentially* allow navigation to start visually before script terminates
            await new Promise((resolve) =>
              setTimeout(resolve, CONFIG.TIMING.MINI_DELAY)
            );
            // NOTE: Code after submit() might not execute reliably.
          } else {
            Logger.log(
              " -> Failed to find any method to advance queue (Next/Ignore/Form).",
              0
            );
            UI.updateStatusText("Error: Cannot advance queue.", "error");
            // No change needed, advanceMethod remains 'Failed'
          }
        }
      }

      if (advanceMethod !== "Failed" && advanceMethod !== "FormSubmit") {
        Logger.log(
          ` -> Successfully advanced queue using: ${advanceMethod}`,
          1
        );
        // Add a short delay after successful click actions before the next check might happen
        await new Promise((resolve) =>
          setTimeout(resolve, CONFIG.TIMING.ADVANCE_DELAY)
        );
      } else if (advanceMethod === "FormSubmit") {
        Logger.log(
          ` -> Advanced queue using: FormSubmit (Page will reload).`,
          1
        );
        // No further delay needed as page navigation occurs.
      }

      return advanceMethod;
    },

    /**
     * Ensure queue container is visible if it seems hidden incorrectly.
     * This is less critical now with visibility checks on buttons, but kept as a safeguard.
     */
    ensureQueueVisible: function () {
      const queueContainer = document.querySelector(
        CONFIG.SELECTORS.queueStatus.container
      );
      const emptyContainer = document.querySelector(
        CONFIG.SELECTORS.queueStatus.emptyContainer
      );

      if (queueContainer) {
        // Check if the queue container is present but not visible, AND the empty message is NOT visible
        if (!queueContainer.offsetParent && !emptyContainer?.offsetParent) {
          Logger.log(
            " -> Queue container exists but seems hidden, ensuring visibility.",
            1
          );
          queueContainer.style.display = ""; // Reset potential display:none set by Steam scripts
        }
      }
    },

    /**
     * Generate a new discovery queue by finding and clicking the appropriate button/link.
     * Handles failure counting and potential loop stopping.
     * @returns {Promise<boolean>} Whether queue generation was successfully initiated.
     */
    generateNewQueue: async function () {
      Logger.log("Attempting to generate a new queue...", 1);
      UI.updateStatusText("Generating new queue...", "action");
      let generated = false;

      // Combine selectors for various start/refresh buttons/links
      const startRefreshSelectors = `${CONFIG.SELECTORS.queueStatus.startAnotherButton}, ${CONFIG.SELECTORS.queueStatus.startLink}`;
      const buttons = document.querySelectorAll(startRefreshSelectors);

      // Find the first visible and clickable button/link
      let targetButton = null;
      for (const btn of buttons) {
        // Check visibility (offsetParent) and also check if it's not disabled (common for buttons)
        if (btn.offsetParent && !btn.disabled) {
          targetButton = btn;
          break;
        }
      }

      if (targetButton) {
        Logger.log(
          ` -> Found visible & enabled button/link: '${
            targetButton.innerText?.trim() || targetButton.id || "Start Link"
          }'. Clicking...`,
          1
        );
        targetButton.click();
        generated = true;
      } else {
        // Try Steam's JS object as a fallback if no suitable button found
        Logger.log(
          " -> No visible/enabled button found. Trying DiscoveryQueue.GenerateNewQueue()...",
          1
        );
        try {
          // Check existence carefully
          if (
            typeof window.DiscoveryQueue === "object" &&
            window.DiscoveryQueue !== null &&
            typeof window.DiscoveryQueue.GenerateNewQueue === "function"
          ) {
            window.DiscoveryQueue.GenerateNewQueue();
            generated = true;
            Logger.log(
              " -> Called DiscoveryQueue.GenerateNewQueue() successfully.",
              1
            );
          } else {
            Logger.log(
              " -> DiscoveryQueue.GenerateNewQueue() not available or not a function.",
              1
            );
          }
        } catch (e) {
          Logger.log(` -> Error calling DiscoveryQueue: ${e.message}`, 0);
        }
      }

      if (!generated) {
        Logger.log(" -> Failed to find any method to generate a new queue.", 0);
        UI.updateStatusText("Queue generation failed.", "error");
        State.loop.failedQueueRestarts++; // Increment failure count immediately

        // Check failure count and stop if exceeded
        if (
          State.loop.failedQueueRestarts >= CONFIG.MAX_QUEUE_RESTART_FAILURES
        ) {
          Logger.log(
            `Queue generation failed ${State.loop.failedQueueRestarts} times. Stopping loop.`,
            0
          );
          UI.updateStatusText(
            `Restart Failed ${CONFIG.MAX_QUEUE_RESTART_FAILURES}x. Stopping.`,
            "error"
          );
          // Stop the loop but keep settings enabled, allowing manual restart later
          LoopController.stopLoop(true);
          return false; // Indicate definitive failure
        }
      } else {
        // Reset failure count on success
        State.loop.failedQueueRestarts = 0;
        Logger.log(" -> Queue generation initiated.", 1);
        // Wait after initiating generation for page to potentially update
        await new Promise((resolve) =>
          setTimeout(resolve, CONFIG.TIMING.QUEUE_GENERATION_DELAY)
        );
        // Optionally ensure queue elements are visible after delay (might help if Steam UI is slow)
        this.ensureQueueVisible();
      }

      return generated; // True if initiated, False if definitively failed after retries
    },
  };

  // ====================================
  // Module: Queue Processor
  // ====================================
  const QueueProcessor = {
    /**
     * Checks the overall queue status (finished, needs starting, error state) and handles auto-start/restart.
     * @returns {Promise<boolean>} True if processing should continue on the current item, False otherwise.
     */
    checkQueueStatusAndHandle: async function () {
      const queueEmptyContainer = document.querySelector(
        CONFIG.SELECTORS.queueStatus.emptyContainer
      );
      const isOnExplorePage = window.location.pathname.includes("/explore");
      const queueContainer = document.querySelector(
        CONFIG.SELECTORS.queueStatus.container
      );
      const isQueueVisible = queueContainer?.offsetParent; // Check if queue area is visible and in layout
      const isEmptyMessageVisible =
        queueEmptyContainer?.offsetParent &&
        queueEmptyContainer.style.display !== "none";

      // --- Case 1: Queue finished message is visible ---
      if (isEmptyMessageVisible) {
        Logger.log("Discovery Queue finished/empty message visible.");

        if (
          State.settings.autoStartEnabled &&
          State.settings.autoRestartQueueEnabled
        ) {
          Logger.log(
            "Auto-restart enabled. Attempting new queue generation..."
          );
          // generateNewQueue handles failure counting and potential loop stopping
          await QueueNavigation.generateNewQueue();
        } else {
          Logger.log(
            "Queue finished and Auto-restart disabled. Stopping loop."
          );
          UI.updateStatusText("Queue finished. Stopped.");
          LoopController.stopLoop(true); // Stop but keep settings enabled
        }
        return false; // Don't process current (non-existent) item
      }

      // --- Case 2: On explore page, but queue is not visible (needs starting) ---
      // This implies we are on /explore/ but haven't clicked "Start Queue" or it hasn't loaded yet.
      if (isOnExplorePage && !isQueueVisible) {
        Logger.log(
          "On explore page, queue container not visible or not found."
        );

        if (State.settings.autoStartEnabled) {
          Logger.log(
            "Auto-start enabled. Attempting to start/generate queue from explore page..."
          );
          // Use generateNewQueue which finds the start/refresh button
          await QueueNavigation.generateNewQueue();
        } else {
          Logger.log(
            "On explore page, queue inactive, Auto-start disabled. Stopping loop."
          );
          UI.updateStatusText("Stopped (Needs Queue Start).");
          LoopController.stopLoop(true); // Keep settings
        }
        return false; // Don't process yet, wait for queue to load after generation attempt
      }

      // --- Case 3: On an app page, check for essential navigation elements ---
      // If we're on an app page (/app/...), we expect queue navigation buttons. If they're missing, something is wrong.
      if (window.location.pathname.includes("/app/")) {
        const nextButton = document.querySelector(
          CONFIG.SELECTORS.queueNav.nextButton
        );
        const ignoreContainer = document.getElementById(
          CONFIG.SELECTORS.queueNav.ignoreButtonContainer.substring(1)
        );
        const ignoreButton = ignoreContainer?.querySelector(
          CONFIG.SELECTORS.queueNav.ignoreButtonInContainer
        );
        const nextForm = document.querySelector(
          CONFIG.SELECTORS.queueNav.nextForm
        );

        // Check if *none* of the advancement methods seem available and visible
        if (
          !nextButton?.offsetParent &&
          !ignoreButton?.offsetParent &&
          !nextForm
        ) {
          Logger.log(
            "On app page but missing visible queue navigation elements. Potential error or not a queue item?",
            0
          );
          // This could happen if navigating directly to an app page not via the queue.
          // If the loop is running, treat this as an error state for the queue.
          if (State.loop.state === "Running") {
            UI.updateStatusText("Error: Invalid queue state?", "error");
            Logger.log(
              " -> Stopping loop due to invalid state on app page.",
              0
            );
            LoopController.stopLoop(true); // Stop but keep settings
          } else {
            // If stopped/paused, just indicate the state but don't force stop
            UI.updateStatusText("Stopped (Invalid state?)");
          }
          return false; // Cannot proceed on this page
        }
      }

      // --- Case 4: On explore page WITH visible queue ---
      // Need to ensure wishlist/ignore buttons are present on the explore page itself
      if (isOnExplorePage && isQueueVisible) {
        const exploreWishlistButton = document.querySelector(
          CONFIG.SELECTORS.wishlist.addButton
        ); // Check specific explore wishlist button
        const exploreIgnoreButton = document.querySelector(
          CONFIG.SELECTORS.queueNav.ignoreButtonInContainer
        );
        const exploreNextButton = document.querySelector(
          CONFIG.SELECTORS.queueNav.nextButton
        );

        // If the core interaction buttons are missing on the explore page queue, something is wrong
        if (
          !exploreWishlistButton &&
          !exploreIgnoreButton &&
          !exploreNextButton?.offsetParent
        ) {
          Logger.log(
            "On explore page queue, but missing interaction buttons (Wishlist/Ignore/Next). Potential error.",
            0
          );
          if (State.loop.state === "Running") {
            UI.updateStatusText("Error: Invalid queue state?", "error");
            Logger.log(
              " -> Stopping loop due to invalid state on explore page.",
              0
            );
            LoopController.stopLoop(true);
          } else {
            UI.updateStatusText("Stopped (Invalid state?)");
          }
          return false;
        }
      }

      // If none of the above problematic conditions are met, assume queue is active and ready.
      State.loop.failedQueueRestarts = 0; // Reset failure counter as we seem to have a valid item/state
      return true; // Okay to proceed with processing the current item
    },

    /**
     * Process the current game/item in the queue based on settings.
     * Handles checking criteria, wishlisting or skipping, and triggers advancement if needed.
     * @param {boolean} isManualTrigger - True if triggered by "Process Once" button.
     */
    processCurrentGameItem: async function (isManualTrigger = false) {
      UI.updateStatusText("Checking page...");

      // Get game title (best effort, works on app page, fallback for explore)
      const gameTitleElement = document.querySelector(
        CONFIG.SELECTORS.gameInfo.title
      );
      // On explore page, title might be inside the queue item itself
      const exploreTitleElement = document.querySelector(
        "#discovery_queue .queue_item_title, #discovery_queue .title"
      ); // Adjust selectors if needed
      const gameTitle =
        gameTitleElement?.textContent?.trim() ||
        exploreTitleElement?.textContent?.trim() ||
        "Current Item";

      // Get queue remaining text (if available)
      const queueRemainingElement = document.querySelector(
        CONFIG.SELECTORS.gameInfo.queueRemainingText
      );
      const queueRemaining = queueRemainingElement
        ? queueRemainingElement.textContent.trim()
        : "";

      UI.updateStatusText(`Checking ${gameTitle}... ${queueRemaining}`);
      Logger.log(
        `Processing: ${gameTitle} ${
          queueRemaining ? "- " + queueRemaining : ""
        }`,
        1
      );

      // --- Check Skip Conditions ---
      let skipReason = null;

      // 1. Owned Game Check (selector works on app page, might need adjustment for explore page if structure differs)
      // Steam usually hides the wishlist button on explore if owned, relying on that might be better. See wishlist check below.
      const ownedIndicator = document.querySelector(
        CONFIG.SELECTORS.gameInfo.inLibraryIndicator
      );
      if (State.settings.skipOwnedGames && ownedIndicator?.offsetParent) {
        skipReason = "Already in Library";
        Logger.log(` -> Skipping: ${skipReason} (Indicator found).`, 1);
      }

      // 2. Non-Game Check (if not already skipped)
      if (!skipReason) {
        skipReason = GameInfoUtils.checkIfNonGame(); // Returns reason string or null
        if (skipReason)
          Logger.log(` -> Skipping: ${skipReason} (Type detected).`, 1);
      }

      // 3. Trading Card Check (if not already skipped)
      // Note: Trading card info might not be readily available on the explore page view.
      // This check primarily works on the app page.
      if (
        !skipReason &&
        State.settings.requireTradingCards &&
        window.location.pathname.includes("/app/")
      ) {
        const hasTradingCards = document.querySelector(
          CONFIG.SELECTORS.gameInfo.tradingCardsIndicator
        );
        if (!hasTradingCards) {
          skipReason = "No Trading Cards";
          Logger.log(
            ` -> Skipping: ${skipReason} (Indicator not found on app page).`,
            1
          );
        } else {
          Logger.log(` -> Has Trading Cards (App page indicator found).`, 2); // Verbose log
        }
      } else if (
        !skipReason &&
        State.settings.requireTradingCards &&
        !window.location.pathname.includes("/app/")
      ) {
        // Cannot reliably check cards on explore page, proceed cautiously or skip?
        // Current behavior: Proceed, card check only enforced on app pages.
        Logger.log(` -> Trading card check skipped (not on app page).`, 2);
      }

      // --- Perform Action (Wishlist or Skip) ---
      let actionTaken = false; // Did we actively wishlist?

      if (skipReason) {
        // Already logged skip reason above
        UI.updateStatusText(`Skipped (${skipReason})`, "skipped");
        // No wishlist action needed
      } else {
        // Eligible for wishlisting according to checks. Now check UI for wishlist button/status.
        const wishlistArea = document.querySelector(
          CONFIG.SELECTORS.wishlist.area
        );
        if (!wishlistArea) {
          // This is unexpected if queue status check passed. Log as error.
          Logger.log(
            " -> ERROR: Wishlist area not found after status check passed.",
            0
          );
          UI.updateStatusText("Error: Wishlist area missing", "error");
          skipReason = "Wishlist Area Missing"; // Treat as skipped due to error
        } else {
          const wishlistedIndicator = wishlistArea.querySelector(
            CONFIG.SELECTORS.wishlist.successIndicator
          );
          // Check visibility of success text OR if the area/button has the 'active' class (common on explore page)
          const isWishlisted =
            (wishlistedIndicator?.offsetParent &&
              wishlistedIndicator.style.display !== "none") ||
            wishlistArea.classList.contains("queue_btn_active") || // Check area class
            wishlistArea.querySelector(".queue_btn_active") !== null; // Check for child with class

          const addButton = wishlistArea.querySelector(
            CONFIG.SELECTORS.wishlist.addButton
          );
          const isAddButtonVisible =
            addButton?.offsetParent && !addButton.disabled;

          // Check if owned based on add button visibility (Steam often hides/disables it if owned)
          if (
            State.settings.skipOwnedGames &&
            !isAddButtonVisible &&
            !isWishlisted
          ) {
            // If the add button isn't visible/enabled, and it's not already wishlisted,
            // it's highly likely the item is owned or otherwise ineligible.
            skipReason = "Owned/Ineligible";
            Logger.log(
              ` -> Skipping: ${skipReason} (Wishlist button absent/disabled).`,
              1
            );
            UI.updateStatusText(`Skipped (${skipReason})`, "skipped");
          } else if (isWishlisted) {
            Logger.log(` -> Already on wishlist.`);
            UI.updateStatusText(`On Wishlist`, "info"); // Informative status
            // No action needed, not technically skipped based on criteria
          } else if (isAddButtonVisible) {
            // Okay to add!
            Logger.log(` -> Adding to wishlist...`);
            UI.updateStatusText(`Adding ${gameTitle}...`, "action");
            addButton.click(); // Perform the click
            actionTaken = true;

            // Wait for action and confirmation using combined delay/check approach
            const confirmed = await this.checkWishlistSuccessAfterAction(
              wishlistArea
            );

            if (confirmed) {
              UI.updateStatusText(`Added ${gameTitle}!`, "success");
              UI.incrementWishlistCounter();
            } else {
              // Even if confirmation failed, Steam might have processed it. Log uncertainty.
              Logger.log(
                " -> Wishlist add confirmation failed/timed out (UI didn't update). May have worked.",
                0
              );
              UI.updateStatusText(`Add Confirm Failed? ${gameTitle}`, "error");
              actionTaken = false; // Treat as failed for state purposes if UI doesn't confirm
            }
            // Add remaining delay regardless of confirmation to ensure pace
            await new Promise((resolve) =>
              setTimeout(resolve, CONFIG.TIMING.ACTION_DELAY * 0.7)
            );
          } else {
            // Should have been caught by the owned/ineligible check above, but log as fallback
            Logger.log(
              ` -> Cannot add: Wishlist button not found or not visible/enabled.`
            );
            UI.updateStatusText("Wishlist button missing?", "error");
            skipReason = "Add Button Missing"; // Treat as skipped due to error
          }
        }
      }

      // --- Advance Queue (if not manual trigger and no critical error occurred) ---
      if (!isManualTrigger) {
        Logger.log(" -> Triggering advance to next item...", 1);
        const advanceResult = await QueueNavigation.advanceQueue();
        if (advanceResult === "Failed") {
          // Stop the loop if advancing failed critically
          Logger.log(" -> Advancing failed, stopping loop.", 0);
          LoopController.stopLoop(true);
        }
        // Add a small delay after advancing completes (if not form submit) before next cycle check
        if (advanceResult !== "FormSubmit") {
          await new Promise((resolve) =>
            setTimeout(resolve, CONFIG.TIMING.MINI_DELAY)
          );
        }
      } else {
        Logger.log(
          " -> Manual trigger ('Process Once'), automatic advance skipped.",
          1
        );
        // Manual lock is released in processQueueCycle finally block
      }
    },

    /**
     * Waits for a short period then checks if the wishlist success indicator becomes visible.
     * Combines waiting and checking.
     * @param {HTMLElement} wishlistAreaElement - The wishlist area DOM element.
     * @returns {Promise<boolean>} True if success indicator found within time, false otherwise.
     */
    checkWishlistSuccessAfterAction: async function (wishlistAreaElement) {
      // Initial delay to allow Steam's backend/frontend to react
      await new Promise((resolve) =>
        setTimeout(resolve, CONFIG.TIMING.ACTION_DELAY * 0.3)
      );

      let attempts = 0;
      const maxAttempts = 8; // Check multiple times within the remaining action delay window
      const intervalTime =
        (CONFIG.TIMING.WISHLIST_CONFIRM_TIMEOUT * 0.7) / maxAttempts; // Check interval

      return new Promise((resolve) => {
        const intervalId = setInterval(() => {
          const successIndicator = wishlistAreaElement.querySelector(
            CONFIG.SELECTORS.wishlist.successIndicator
          );
          const isActive =
            wishlistAreaElement.classList.contains("queue_btn_active") ||
            wishlistAreaElement.querySelector(".queue_btn_active") !== null;

          if (
            (successIndicator?.offsetParent &&
              successIndicator.style.display !== "none") ||
            isActive
          ) {
            Logger.log(" -> Wishlist success confirmed by UI.", 1);
            clearInterval(intervalId);
            resolve(true);
          } else {
            attempts++;
            if (attempts >= maxAttempts) {
              Logger.log(" -> Wishlist success confirmation timed out.", 1);
              clearInterval(intervalId);
              resolve(false);
            }
          }
        }, intervalTime);
      });
    },

    /**
     * Main processing cycle called by the loop or manual triggers.
     * Manages locking, calls status checks, item processing, and handles errors.
     * @param {boolean} isManualTrigger - If true, skips the automatic advance step.
     */
    processQueueCycle: async function (isManualTrigger = false) {
      // Prevent overlapping automatic executions. Allow manual trigger if paused.
      if (State.loop.isProcessing && !isManualTrigger) {
        Logger.log("Cycle skipped, already processing.", 2); // Verbose log
        return;
      }
      if (State.loop.state === "Paused" && !isManualTrigger) {
        Logger.log("Cycle skipped, loop paused.", 2); // Verbose log
        return;
      }
      // Prevent multiple concurrent manual actions
      if (State.loop.manualActionInProgress && isManualTrigger) {
        Logger.log("Manual action already in progress.", 1);
        return;
      }

      // Set locks
      State.loop.isProcessing = true;
      if (isManualTrigger) {
        State.loop.manualActionInProgress = true;
        UI.updateManualButtonStates(); // Disable buttons during manual action
      }

      try {
        // 1. Check overall queue status (finished, needs starting, error?)
        const shouldProcessItem = await this.checkQueueStatusAndHandle();

        // 2. If queue status is okay, proceed to process the item if loop is running or it's manual
        if (
          shouldProcessItem &&
          (State.loop.state === "Running" || isManualTrigger)
        ) {
          // Double check state hasn't changed during checkQueueStatus async operations
          if (State.loop.state === "Running" || isManualTrigger) {
            await this.processCurrentGameItem(isManualTrigger);
          } else {
            Logger.log(
              ` -> Loop state changed to '${State.loop.state}' during status check, skipping item processing.`,
              1
            );
          }
        } else if (!shouldProcessItem) {
          Logger.log(
            " -> Queue status check indicated no item to process or action was taken (like restart).",
            1
          );
          // If status check initiated restart/stop, the loop state might already be changed.
        } else {
          // This case means shouldProcessItem was true, but loop state is neither Running nor is it a manual trigger.
          // Should only happen if paused.
          Logger.log(
            ` -> Loop state is '${State.loop.state}', skipping item processing.`,
            1
          );
        }
      } catch (error) {
        Logger.log(`ERROR during processQueueCycle: ${error.message}`, 0);
        console.error(
          "[Steam Wishlist Looper] Error details:",
          error.stack || error
        );
        UI.updateStatusText("Runtime Error!", "error");
        // Consider stopping the loop on unhandled errors to prevent repeated issues
        // LoopController.stopLoop(true);
      } finally {
        // Use a delay before releasing locks to allow UI updates and prevent overly rapid cycles
        setTimeout(() => {
          State.loop.isProcessing = false;
          if (isManualTrigger) {
            State.loop.manualActionInProgress = false;
          }
          // Update button states after action potentially completes
          UI.updateManualButtonStates();

          // Set appropriate status text based on the final loop state after processing
          if (State.loop.state === "Running") {
            // Avoid overwriting success/skipped messages immediately with Idle
            const currentStatus = State.ui.elements.status?.textContent || "";
            if (
              !currentStatus.includes("Added") &&
              !currentStatus.includes("Skipped") &&
              !currentStatus.includes("Error")
            ) {
              UI.updateStatusText("Idle...");
            }
          } else if (State.loop.state === "Paused") {
            UI.updateStatusText("Paused", "paused");
          } else {
            // Stopped
            UI.updateStatusText("Stopped.");
          }
        }, CONFIG.TIMING.PROCESSING_RELEASE_DELAY);
      }
    },

    /**
     * Manually trigger processing for the current item once. Requires loop to be Paused or Stopped.
     */
    processOnce: function () {
      if (State.loop.state === "Running") {
        Logger.log(
          "Cannot 'Process Once' while loop is running. Pause or Stop first.",
          0
        );
        UI.updateStatusText("Pause/Stop to Process Once", "info");
        return;
      }
      if (State.loop.isProcessing || State.loop.manualActionInProgress) {
        Logger.log("Cannot 'Process Once', action already in progress.", 1);
        return;
      }

      Logger.log("Manual trigger: Processing current item once...");
      UI.updateStatusText("Processing (Manual)...", "action");
      // Call processQueueCycle with manual flag true
      QueueProcessor.processQueueCycle(true);
    },

    /**
     * Manually trigger skipping the current item. Requires loop to be Paused or Stopped.
     */
    skipItem: async function () {
      if (State.loop.state === "Running") {
        Logger.log(
          "Cannot 'Skip Item' while loop is running. Pause or Stop first.",
          0
        );
        UI.updateStatusText("Pause/Stop to Skip Item", "info");
        return;
      }
      if (State.loop.isProcessing || State.loop.manualActionInProgress) {
        Logger.log("Cannot 'Skip Item', action already in progress.", 1);
        return;
      }

      Logger.log("Manual trigger: Skipping current item...");
      UI.updateStatusText("Skipping (Manual)...", "action");
      State.loop.isProcessing = true; // Lock processing during manual skip
      State.loop.manualActionInProgress = true;
      UI.updateManualButtonStates(); // Disable buttons

      try {
        // Directly call advanceQueue to move to the next item
        const advanceResult = await QueueNavigation.advanceQueue();
        if (advanceResult === "Failed") {
          UI.updateStatusText("Skip failed: Cannot advance.", "error");
        } else {
          UI.updateStatusText("Skipped (Manual)", "skipped");
          // No need to wait long after skip, just release lock below
        }
      } catch (error) {
        Logger.log(`Error during manual skip: ${error.message}`, 0);
        UI.updateStatusText("Error during skip!", "error");
      } finally {
        // Release lock after a shorter delay for skip
        setTimeout(() => {
          State.loop.isProcessing = false;
          State.loop.manualActionInProgress = false;
          UI.updateManualButtonStates(); // Re-enable buttons
          // Restore appropriate status text based on whether paused or stopped
          if (State.loop.state === "Paused") {
            UI.updateStatusText("Paused", "paused");
          } else {
            UI.updateStatusText("Stopped.");
          }
        }, CONFIG.TIMING.ADVANCE_DELAY); // Use shorter delay matching advance
      }
    },
  };

  // ====================================
  // Module: Loop Controller
  // ====================================
  const LoopController = {
    /**
     * The main loop function called repeatedly by setTimeout. Manages the cycle execution.
     */
    mainLoop: function () {
      // Strict check: Only proceed if state is 'Running' AND the timeoutId matches the current one.
      if (State.loop.state !== "Running" || !State.loop.timeoutId) {
        Logger.log(
          `Main loop called but state is '${State.loop.state}' or timeoutId is invalid. Exiting loop.`,
          1
        );
        // Ensure timeout is cleared if it somehow exists but state isn't Running
        if (State.loop.timeoutId) {
          clearTimeout(State.loop.timeoutId);
          State.loop.timeoutId = null;
        }
        return;
      }

      // Store the current timeout ID associated with this execution instance
      const currentTimeoutId = State.loop.timeoutId;

      // Call the processing cycle
      QueueProcessor.processQueueCycle(false) // false indicates automatic cycle
        .then(() => {
          // AFTER the async processQueueCycle completes or errors, check state *again*
          // Only reschedule if the state is still 'Running' AND the timeout ID hasn't been changed
          // (e.g., by a quick stop/pause action during the processing cycle)
          if (
            State.loop.state === "Running" &&
            State.loop.timeoutId === currentTimeoutId
          ) {
            // Clear previous timeout just in case (should be redundant but safe)
            clearTimeout(State.loop.timeoutId);
            // Schedule the next run using CHECK_INTERVAL
            State.loop.timeoutId = setTimeout(
              LoopController.mainLoop,
              CONFIG.TIMING.CHECK_INTERVAL
            );
            Logger.log(
              `Next check scheduled in ${
                CONFIG.TIMING.CHECK_INTERVAL / 1000
              }s.`,
              2
            ); // Verbose
          } else {
            // If state changed or timeoutId is different, don't reschedule.
            Logger.log(
              `Loop state changed to '${State.loop.state}' or timeoutId mismatch (current: ${State.loop.timeoutId}, expected: ${currentTimeoutId}) during processing. Next check cancelled.`,
              1
            );
            // If a different timeoutId exists (e.g., rapid stop/start), clear it.
            if (
              State.loop.timeoutId &&
              State.loop.timeoutId !== currentTimeoutId
            ) {
              clearTimeout(State.loop.timeoutId);
            }
            // Ensure timeoutId is null if we are not rescheduling
            State.loop.timeoutId = null;
          }
        })
        .catch((error) => {
          // Catch unexpected errors from the processQueueCycle promise chain itself
          Logger.log(
            `Unhandled error in mainLoop promise chain: ${error.message}`,
            0
          );
          console.error(
            "[Steam Wishlist Looper] mainLoop promise error:",
            error.stack || error
          );
          UI.updateStatusText("Critical Error in Loop!", "error");

          // Decide recovery: Stop the loop? Or try to reschedule?
          // Stopping might be safer on unhandled errors.
          if (
            State.loop.state === "Running" &&
            State.loop.timeoutId === currentTimeoutId
          ) {
            Logger.log(" -> Stopping loop due to critical error.", 0);
            LoopController.stopLoop(true); // Stop but keep settings
          } else {
            // Ensure timeout is cleared if state already changed
            if (State.loop.timeoutId) clearTimeout(State.loop.timeoutId);
            State.loop.timeoutId = null;
          }
        });
    },

    /**
     * Start the processing loop (or resume if paused).
     */
    startLoop: function () {
      if (State.loop.state === "Running") {
        Logger.log("Loop already running.", 1);
        return;
      }

      if (State.loop.state === "Paused") {
        LoopController.resumeLoop(); // Delegate to resume function
        return;
      }

      // --- Starting from Stopped state ---
      Logger.log("Starting loop...");
      UI.updateStatusText("Starting...");
      State.loop.state = "Running";
      State.loop.isProcessing = false; // Ensure processing lock is clear initially
      State.loop.manualActionInProgress = false; // Ensure manual lock is clear
      State.loop.failedQueueRestarts = 0; // Reset failure count on fresh start
      UI.updateUI(); // Update button states immediately

      // Clear any lingering timeout from previous states just in case
      if (State.loop.timeoutId) clearTimeout(State.loop.timeoutId);

      // Schedule the *first* cycle with a minimal delay
      State.loop.timeoutId = setTimeout(
        LoopController.mainLoop,
        CONFIG.TIMING.MINI_DELAY
      );
      // Update status after scheduling the first check
      // Set a slightly more informative initial running status
      setTimeout(() => {
        if (State.loop.state === "Running")
          UI.updateStatusText("Running - Initializing cycle...");
      }, CONFIG.TIMING.MINI_DELAY + 10);
    },

    /**
     * Pause the processing loop if it is currently running.
     */
    pauseLoop: function () {
      if (State.loop.state !== "Running") {
        Logger.log(`Loop is '${State.loop.state}', cannot pause.`, 1);
        return;
      }

      Logger.log("Pausing loop...");
      State.loop.state = "Paused";

      // Clear the *scheduled* next timeout. This stops new cycles from starting.
      if (State.loop.timeoutId) {
        clearTimeout(State.loop.timeoutId);
        State.loop.timeoutId = null;
        Logger.log(" -> Next cycle cancelled.", 1);
      }

      // Note: An ongoing 'processQueueCycle' might still be running. We don't interrupt it.
      // The 'isProcessing' flag will remain true until that cycle finishes.
      // The 'finally' block in processQueueCycle will eventually set the correct Paused status text.

      UI.updateUI(); // Update button states immediately
      UI.updateStatusText("Paused", "paused"); // Set status text explicitly
    },

    /**
     * Resume the processing loop from a paused state.
     */
    resumeLoop: function () {
      if (State.loop.state !== "Paused") {
        Logger.log(`Loop is '${State.loop.state}', cannot resume.`, 1);
        return;
      }

      Logger.log("Resuming loop...");
      State.loop.state = "Running";

      // Explicitly reset locks when resuming, assuming any previous action completed while paused.
      State.loop.isProcessing = false;
      State.loop.manualActionInProgress = false;

      UI.updateUI(); // Update button states
      UI.updateStatusText("Resuming...");

      // Clear any lingering timeout (should be null, but safety first)
      if (State.loop.timeoutId) clearTimeout(State.loop.timeoutId);

      // Schedule the next cycle almost immediately to get things going again
      State.loop.timeoutId = setTimeout(
        LoopController.mainLoop,
        CONFIG.TIMING.MINI_DELAY
      );
      setTimeout(() => {
        if (State.loop.state === "Running")
          UI.updateStatusText("Running - Resuming cycle...");
      }, CONFIG.TIMING.MINI_DELAY + 10);
    },

    /**
     * Stop the processing loop completely.
     * @param {boolean} keepSettings - If true, Auto-Start/Restart settings are NOT disabled.
     */
    stopLoop: function (keepSettings = false) {
      if (State.loop.state === "Stopped") {
        Logger.log("Loop already stopped.", 1);
        // Still ensure UI is correct for stopped state
        UI.updateUI();
        UI.updateStatusText("Stopped.");
        return;
      }

      Logger.log("Stopping loop...");
      const wasRunning = State.loop.state === "Running";
      State.loop.state = "Stopped"; // Set state immediately

      // Clear any scheduled timeout
      if (State.loop.timeoutId) {
        clearTimeout(State.loop.timeoutId);
        State.loop.timeoutId = null;
        Logger.log(" -> Next cycle cancelled.", 1);
      }

      // Reset flags - Note: isProcessing might briefly stay true if stopped mid-action,
      // but the finally block of that action will see state is 'Stopped' and won't reschedule.
      // Resetting here ensures clean state if stopped while idle.
      State.loop.isProcessing = false;
      State.loop.manualActionInProgress = false;

      // Handle Auto settings based on parameter
      if (!keepSettings) {
        Logger.log("-> Disabling Auto-Start & Auto-Restart Queue settings.", 1);
        // Use SettingsManager to update state and GM storage
        SettingsManager.updateSetting(CONFIG.STORAGE_KEYS.AUTO_START, false);
        SettingsManager.updateSetting(
          CONFIG.STORAGE_KEYS.AUTO_RESTART_QUEUE,
          false
        );
      } else {
        Logger.log("-> Keeping Auto-Start/Restart settings enabled.", 1);
      }

      // Update UI after a very brief moment to allow state change to reflect correctly
      // and potentially allow any final status message from an interrupted cycle to show briefly.
      setTimeout(() => {
        UI.updateUI();
        UI.updateStatusText("Stopped.");
      }, CONFIG.TIMING.MINI_DELAY);
    },
  };

  // ====================================
  // Module: Version Checker
  // ====================================
  const VersionChecker = {
    /**
     * Check for script updates periodically using GM_xmlhttpRequest.
     */
    checkForUpdates: function () {
      const currentTime = Date.now();
      const lastCheck = State.stats.lastVersionCheck;
      const checkInterval = CONFIG.TIMING.VERSION_CHECK_INTERVAL;

      // Only check if interval has passed
      if (currentTime - lastCheck < checkInterval) {
        Logger.log(
          `Skipping version check, last checked ${Math.round(
            (currentTime - lastCheck) / 3600000
          )} hours ago.`,
          2
        ); // Verbose
        // Still update UI in case previous check found an update and stored it in State.stats
        this.updateUIAfterCheck();
        return;
      }

      Logger.log("Checking for updates...", 1);
      const checkUrl = CONFIG.VERSION_CHECK_URL; // Get URL from config getter

      if (!checkUrl || !checkUrl.startsWith("http")) {
        Logger.log(
          "Version check URL is invalid or not configured. Skipping check.",
          0
        );
        // Update last check time anyway to prevent constant checks with bad URL
        GM_setValue(CONFIG.STORAGE_KEYS.LAST_VERSION_CHECK, currentTime);
        State.stats.lastVersionCheck = currentTime;
        return;
      }

      GM_xmlhttpRequest({
        method: "GET",
        url: checkUrl + `?ts=${currentTime}`, // Add cache-busting timestamp
        timeout: 10000, // 10 second timeout
        headers: {
          // Add headers to potentially help with caching issues
          "Cache-Control": "no-cache, no-store, must-revalidate",
          Pragma: "no-cache",
          Expires: "0",
        },
        onload: (response) => {
          // Update last check time on success or expected failure (like 404)
          GM_setValue(CONFIG.STORAGE_KEYS.LAST_VERSION_CHECK, currentTime);
          State.stats.lastVersionCheck = currentTime;

          if (response.status === 200) {
            try {
              const data = JSON.parse(response.responseText);
              // Validate response structure
              if (data && typeof data.version === "string") {
                Logger.log(
                  `Latest version fetched: ${data.version}, Current: ${CONFIG.CURRENT_VERSION}`,
                  1
                );
                // Store latest version info in State for UI update
                State.stats.latestVersion = data.version;
                // Store update URL if provided, ensure it's a string
                State.stats.updateUrl =
                  typeof data.updateUrl === "string" ? data.updateUrl : null;
              } else {
                Logger.log(
                  "Version check response missing 'version' field or invalid format.",
                  0
                );
                State.stats.latestVersion = null; // Clear old data
                State.stats.updateUrl = null;
              }
            } catch (e) {
              Logger.log(`Error parsing version data: ${e.message}`, 0);
              State.stats.latestVersion = null;
              State.stats.updateUrl = null;
            }
          } else {
            // Log non-200 responses as errors, but don't spam if it's a persistent 404 etc.
            Logger.log(
              `Version check failed: HTTP Status ${response.status}`,
              0
            );
            State.stats.latestVersion = null;
            State.stats.updateUrl = null;
          }
          // Update UI based on fetched data (or lack thereof)
          this.updateUIAfterCheck();
        },
        onerror: (error) => {
          // Update last check time even on network errors to prevent rapid retries
          GM_setValue(CONFIG.STORAGE_KEYS.LAST_VERSION_CHECK, currentTime);
          State.stats.lastVersionCheck = currentTime;
          Logger.log(
            `Error during version check request: ${
              error.statusText || "Network Error"
            }`,
            0
          );
          // Clear potentially stale version info on error
          State.stats.latestVersion = null;
          State.stats.updateUrl = null;
          this.updateUIAfterCheck(); // Update UI to show no update available
        },
        ontimeout: () => {
          // Update last check time on timeout
          GM_setValue(CONFIG.STORAGE_KEYS.LAST_VERSION_CHECK, currentTime);
          State.stats.lastVersionCheck = currentTime;
          Logger.log("Version check timed out.", 0);
          // Clear potentially stale version info on timeout
          State.stats.latestVersion = null;
          State.stats.updateUrl = null;
          this.updateUIAfterCheck();
        },
      });
    },

    /**
     * Updates the UI version info element based on stored version check results in State.stats.
     */
    updateUIAfterCheck: function () {
      // Ensure UI elements have been created before attempting to update
      if (State.ui.elements.versionInfo) {
        UI.updateVersionInfo(State.stats.latestVersion, State.stats.updateUrl);
      } else {
        Logger.log("Version UI element not ready, skipping update display.", 2); // Verbose
      }
    },
  };

  // ====================================
  // Module: Initialization
  // ====================================
  const Initialization = {
    /**
     * Initialize the entire script: Age bypass, UI, Loop logic, Menu commands.
     */
    init: function () {
      // 1. Run Age Verification Bypass Early - runs on all matched pages
      // This needs to run before DOMContentLoaded sometimes for best effect
      AgeVerificationBypass.init();

      // 2. Skip main UI/Loop logic if running inside an iframe
      if (window.top !== window.self) {
        Logger.log(
          "Wishlist Looper running in iframe, main features skipped.",
          1
        );
        return;
      }

      // --- Top-level window initialization ---
      Logger.log(
        `Steam Infinite Wishlister v${CONFIG.CURRENT_VERSION} Initializing (Top Window)...`,
        0
      );

      // 3. Initialize main functionality once the DOM is ready
      const initializeMainComponents = () => {
        Logger.log("DOM ready, initializing main components.", 1);

        // Add UI controls (creates elements and updates based on current state)
        UI.addControls();

        // Perform initial check of page state and handle auto-start/restart logic
        this.handleInitialPageState();

        // Check for script updates (uses interval logic internally)
        VersionChecker.checkForUpdates();

        // Register userscript menu commands for easy access
        this.registerMenuCommands();

        Logger.log("Initialization complete.", 0);

        // Set initial status message if loop didn't auto-start
        if (State.loop.state === "Stopped" && State.ui.elements.status) {
          // Check if status is still 'Initializing' before setting to 'Stopped'
          if (State.ui.elements.status.textContent.includes("Initializing")) {
            UI.updateStatusText("Stopped.");
          }
        }
      };

      // Execute main initialization when the DOM is interactive or complete
      if (
        document.readyState === "complete" ||
        document.readyState === "interactive"
      ) {
        // Use setTimeout to ensure it runs after the current execution stack, allowing other scripts potentially
        setTimeout(initializeMainComponents, 0);
      } else {
        // Wait for DOMContentLoaded if the DOM isn't ready yet
        window.addEventListener("DOMContentLoaded", initializeMainComponents, {
          once: true,
        });
      }
    },

    /**
     * Checks the initial page state (URL, queue elements) and decides whether to
     * trigger auto-start or auto-restart based on user settings.
     */
    handleInitialPageState: function () {
      // Determine page context
      const isOnAppPage = window.location.pathname.includes("/app/");
      const isOnExplorePage = window.location.pathname.includes("/explore");

      // Check queue elements carefully
      const queueContainer = document.querySelector(
        CONFIG.SELECTORS.queueStatus.container
      );
      const queueEmptyContainer = document.querySelector(
        CONFIG.SELECTORS.queueStatus.emptyContainer
      );
      // Check visibility using offsetParent, which is more reliable than style checks
      const isQueueVisible = !!queueContainer?.offsetParent;
      const isEmptyMessageVisible = !!queueEmptyContainer?.offsetParent;

      Logger.log(
        `Initial Page State: App=${isOnAppPage}, Explore=${isOnExplorePage}, QueueVisible=${isQueueVisible}, EmptyMsgVisible=${isEmptyMessageVisible}, AutoStart=${State.settings.autoStartEnabled}, AutoRestart=${State.settings.autoRestartQueueEnabled}`,
        1
      );

      // Condition 1: Auto-Restart finished queue (Empty message is visible)
      if (
        State.settings.autoStartEnabled &&
        State.settings.autoRestartQueueEnabled &&
        isEmptyMessageVisible
      ) {
        Logger.log(
          "Initial state: Queue finished/empty. Auto-restarting queue...",
          0
        );
        UI.updateStatusText("Queue empty, auto-restarting...", "action");
        // Use a slight delay to ensure page scripts (like DiscoveryQueue) might be ready
        setTimeout(() => {
          QueueNavigation.generateNewQueue().then((success) => {
            if (success && State.loop.state === "Stopped") {
              // If generation was initiated and loop is stopped, maybe auto-start it now?
              // Check state again after delay in case generation fails quickly
              setTimeout(() => {
                if (State.loop.state === "Stopped") {
                  Logger.log("Queue generation initiated, starting loop.", 1);
                  LoopController.startLoop();
                }
              }, CONFIG.TIMING.QUEUE_GENERATION_DELAY + 500);
            } else if (!success) {
              Logger.log("Auto-restart failed to initiate generation.", 0);
              // generateNewQueue handles stopping after max failures
            }
          });
        }, CONFIG.TIMING.INITIAL_START_DELAY / 2); // Shorter delay for restart attempt
      }
      // Condition 2: Auto-Start on explore page where queue needs starting (Explore page, no visible queue, no empty message)
      else if (
        State.settings.autoStartEnabled &&
        isOnExplorePage &&
        !isQueueVisible &&
        !isEmptyMessageVisible
      ) {
        Logger.log(
          "Initial state: On explore page, queue needs starting. Auto-starting queue generation...",
          0
        );
        UI.updateStatusText("On explore, auto-starting queue...", "action");
        setTimeout(() => {
          QueueNavigation.generateNewQueue().then((success) => {
            if (success && State.loop.state === "Stopped") {
              // Similar to above, start loop after generation initiated
              setTimeout(() => {
                if (State.loop.state === "Stopped") {
                  Logger.log("Queue generation initiated, starting loop.", 1);
                  LoopController.startLoop();
                }
              }, CONFIG.TIMING.QUEUE_GENERATION_DELAY + 500);
            } else if (!success) {
              Logger.log(
                "Auto-start failed to initiate generation from explore.",
                0
              );
            }
          });
        }, CONFIG.TIMING.INITIAL_START_DELAY / 2);
      }
      // Condition 3: Auto-Start on a valid, active queue page (app page OR explore page with visible queue)
      else if (
        State.settings.autoStartEnabled &&
        (isOnAppPage || (isOnExplorePage && isQueueVisible))
      ) {
        // Check if essential interaction elements are present before auto-starting
        const canInteract =
          document.querySelector(CONFIG.SELECTORS.wishlist.addButton) ||
          document.querySelector(CONFIG.SELECTORS.queueNav.nextButton)
            ?.offsetParent ||
          document.querySelector(
            CONFIG.SELECTORS.queueNav.ignoreButtonInContainer
          );
        if (canInteract) {
          Logger.log(
            "Initial state: On valid & active queue page. Auto-starting loop...",
            0
          );
          // Delay start slightly to allow page scripts to fully load
          setTimeout(
            LoopController.startLoop,
            CONFIG.TIMING.INITIAL_START_DELAY
          );
        } else {
          Logger.log(
            "Initial state: On potential queue page, but interaction elements missing. Auto-start aborted.",
            1
          );
          UI.updateStatusText("Stopped (Invalid state?).");
        }
      }
      // Condition 4: No auto-start conditions met
      else {
        if (!State.settings.autoStartEnabled) {
          Logger.log("Initial state: Auto-start disabled.", 1);
        } else {
          // Log reason if auto-start is on but conditions aren't met
          if (!isOnAppPage && !isOnExplorePage) {
            Logger.log(
              `Initial state: Not on a recognised auto-start page (Path: ${window.location.pathname}).`,
              1
            );
          } else if (
            isOnExplorePage &&
            !isQueueVisible &&
            isEmptyMessageVisible
          ) {
            // Covered by case 1, but log here if somehow missed
            Logger.log(
              `Initial state: On explore page, queue empty, auto-restart disabled or failed.`,
              1
            );
          } else {
            // Other edge cases
            Logger.log(
              `Initial state: Conditions for auto-start not met (Explore=${isOnExplorePage}, QueueVisible=${isQueueVisible}, Empty=${isEmptyMessageVisible}).`,
              1
            );
          }
        }
        // Ensure UI reflects stopped state if not auto-starting
        if (
          State.loop.state === "Stopped" &&
          State.ui.elements.status &&
          State.ui.elements.status.textContent.includes("Initializing")
        ) {
          UI.updateStatusText("Stopped.");
        }
      }
    },

    /**
     * Register menu commands for userscript manager (e.g., Tampermonkey menu).
     * Dynamically updates labels based on current settings.
     */
    registerMenuCommands: function () {
      // Clear existing commands if necessary (Tampermonkey usually handles this, but good practice)
      // Note: GM_unregisterMenuCommand is not standard, so we rely on Tampermonkey's replacement behavior.

      GM_registerMenuCommand(
        "[Wishlister] Start / Resume Loop",
        LoopController.startLoop,
        "r" // Access key 'r' for Resume/Run
      );
      GM_registerMenuCommand(
        "[Wishlister] Pause Loop",
        LoopController.pauseLoop,
        "p" // Access key 'p' for Pause
      );
      GM_registerMenuCommand(
        "[Wishlister] Stop Loop (Keep Auto Settings)",
        () => LoopController.stopLoop(true), // Stop but keep settings
        "k" // Access key 'k' for Keep
      );
      GM_registerMenuCommand(
        "[Wishlister] Stop Loop & Disable Auto",
        () => LoopController.stopLoop(false), // Stop AND disable settings
        "s" // Access key 's' for Stop
      );
      GM_registerMenuCommand(
        "[Wishlister] Process Current Item Once",
        QueueProcessor.processOnce,
        "o" // Access key 'o' for Once
      );
      GM_registerMenuCommand(
        "[Wishlister] Skip Current Item",
        QueueProcessor.skipItem,
        "i" // Access key 'i' for Ignore/Item Skip
      );

      GM_registerMenuCommand("--- Wishlister Settings ---", () => {}); // Separator

      // Settings toggles with dynamic labels
      GM_registerMenuCommand(
        `[Wishlister] ${
          State.settings.autoStartEnabled ? "✅ Disable" : "⬜ Enable"
        } Auto-Start`,
        () => {
          SettingsManager.toggleSetting(
            CONFIG.STORAGE_KEYS.AUTO_START,
            State.settings.autoStartEnabled
          );
          this.registerMenuCommands(); // Re-register to update label
        }
      );
      GM_registerMenuCommand(
        `[Wishlister] ${
          State.settings.autoRestartQueueEnabled ? "✅ Disable" : "⬜ Enable"
        } Auto-Restart Queue`,
        () => {
          SettingsManager.toggleSetting(
            CONFIG.STORAGE_KEYS.AUTO_RESTART_QUEUE,
            State.settings.autoRestartQueueEnabled
          );
          this.registerMenuCommands(); // Re-register to update label
        }
      );
      GM_registerMenuCommand(
        `[Wishlister] ${
          State.settings.requireTradingCards ? "✅ Disable" : "⬜ Enable"
        } Require Trading Cards`,
        () => {
          SettingsManager.toggleSetting(
            CONFIG.STORAGE_KEYS.REQUIRE_CARDS,
            State.settings.requireTradingCards
          );
          this.registerMenuCommands(); // Re-register to update label
        }
      );
      GM_registerMenuCommand(
        `[Wishlister] ${
          State.settings.skipOwnedGames ? "✅ Disable" : "⬜ Enable"
        } Skip Owned Games`,
        () => {
          SettingsManager.toggleSetting(
            CONFIG.STORAGE_KEYS.SKIP_OWNED,
            State.settings.skipOwnedGames
          );
          this.registerMenuCommands(); // Re-register to update label
        }
      );
      GM_registerMenuCommand(
        `[Wishlister] ${
          State.settings.skipNonGames ? "✅ Disable" : "⬜ Enable"
        } Skip Non-Games (DLC, etc.)`,
        () => {
          SettingsManager.toggleSetting(
            CONFIG.STORAGE_KEYS.SKIP_NON_GAMES,
            State.settings.skipNonGames
          );
          this.registerMenuCommands(); // Re-register to update label
        }
      );
      GM_registerMenuCommand(
        `[Wishlister] ${
          State.settings.uiMinimized ? " R" : "➖ M"
        }estore/Minimize UI Panel`, // Use symbols for state
        () => {
          UI.toggleMinimizeUI(); // UI update handles button text, menu needs re-register
          this.registerMenuCommands(); // Re-register to update label
        },
        "m" // Access key 'm' for Minimize/Maximize
      );

      GM_registerMenuCommand("--- Wishlister Info ---", () => {}); // Separator

      GM_registerMenuCommand(
        "[Wishlister] Check for Updates Now",
        () => {
          // Reset last check time to force an update check immediately
          GM_setValue(CONFIG.STORAGE_KEYS.LAST_VERSION_CHECK, 0);
          State.stats.lastVersionCheck = 0; // Update state too
          VersionChecker.checkForUpdates(); // Trigger check
          if (State.ui.elements.status)
            UI.updateStatusText("Checking for updates...", "action");
        },
        "u" // Access key 'u' for Update
      );

      Logger.log("Menu commands registered/updated.", 1);
    },
  };

  // ====================================
  // Script Entry Point
  // ====================================
  Initialization.init();
})();