Neopets: A Better Book Highlighter

Integrates a per-pet read-list extractor with item highlighting---clearly marking books you've already read across all shops and the Safety Deposit Box.

// ==UserScript==
// @name         Neopets: A Better Book Highlighter
// @version      3.2
// @description  Integrates a per-pet read-list extractor with item highlighting---clearly marking books you've already read across all shops and the Safety Deposit Box.
// @namespace    https://git.gay/valkyrie1248/Neopets-Userscripts
// @author       valkryie1248 (Refactored with help of Gemini)
// @license      MIT
// @match        https://www.neopets.com/objects.phtml?type=shop&obj_type=7
// @match        https://www.neopets.com/objects.phtml?obj_type=7&type=shop
// @match        https://www.neopets.com/objects.phtml?*obj_type=38*
// @match        https://www.neopets.com/objects.phtml?*obj_type=51*
// @match        https://www.neopets.com/objects.phtml?*obj_type=70*
// @match        https://www.neopets.com/objects.phtml?*obj_type=77*
// @match        https://www.neopets.com/objects.phtml?*obj_type=92*
// @match        https://www.neopets.com/objects.phtml?*obj_type=106*
// @match        https://www.neopets.com/objects.phtml?*obj_type=112*
// @match        https://www.neopets.com/objects.phtml?*obj_type=114*
// @match        https://www.neopets.com/*books_read.phtml*
// @match        https://www.neopets.com/safetydeposit.phtml*
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @grant        GM.registerMenuCommand
// @run-at       document-end
// ==/UserScript==

/* eslint-disable no-useless-escape */

(function () {
  ("use strict");
  console.log("[BM Debug Log] Starting application lifecycle.");

  // --- CONFIGURATION ---
  const READ_LIST_KEY_PREFIX = "ReadList_"; // Prefix for pet-specific read lists
  const TIERED_LISTS_KEY = "tieredLists"; // Key for global tiered lists

  // --- CORE STYLING CONSTANTS (Centralized) ---
  const CSS_CLASSES = {
    READ_HIGHLIGHT: "ddg-read-highlight",
    OVERLAY: "ddg-img-overlay",
    LIST1: "ddg-list1",
    LIST2: "ddg-list2",
    LIST3: "ddg-list3",
    LIST4: "ddg-list4",
  };

  // --- GLOBAL STATE ---
  // Sets are used for O(1) lookup speed for normalized item names.
  // Add in your own prioritized lists (ideally based on known np values from JellyNeo or ItemDB).
  let NORM_LISTREAD = new Set();
  let NORM_LISTBOOKTASTICREAD = new Set();
  let NORM_LIST1 = new Set();
  let NORM_LIST2 = new Set();
  let NORM_LIST3 = new Set();
  let NORM_LIST4 = new Set();

  // UI elements
  let keyDiv = null;

  // --- UTILITY FUNCTIONS ---

  /** Logs errors safely to the console. */
  function safeLogError(e) {
    console.error(`[Book Highlighter Error]`, e.message, e);
  }

  /** Extracts a URL query parameter. */
  function getQueryParam(key) {
    // console.log(`[BM Debug Log] Getting query param: ${key}`);
    const urlParams = new URLSearchParams(window.location.search);
    return urlParams.get(key);
  }

  /** Non-destructive normalization function for item names. */
  function normalize(name) {
    // console.log(`[BM Debug Log] Normalizing name: ${name}`);
    if (typeof name !== "string") return "";
    return name
      .toLowerCase() // 1. Standardize case
      .trim() // 2. Remove leading/trailing whitespace
      .replace(/\s+/g, " "); // 3. Replace multiple spaces/newlines/tabs with a single space
  }

  /**
   * @param {Element} imgDiv The item's image div ([data-name] element).
   * @param {string} keyType Specifies which attribute to use ('TITLE' or 'DESCRIPTION').
   * @returns {string|null} The normalized text key for list lookup.
   */
  function getItemMatchKey(imgDiv, keyType) {
    let matchText = null;

    if (keyType === "DESCRIPTION") {
      // Booktastic matching: Prioritize alt/title (description).
      matchText = imgDiv.getAttribute("alt") || imgDiv.getAttribute("title");

      // CRITICAL: Do not fall back to data-name (Title) if matching by Description,
      // as the Title is not in the extracted read list.
      if (!matchText) return null;
    } else {
      // Regular matching: Use data-name (Title)
      matchText = imgDiv.getAttribute("data-name");
    }

    if (!matchText) return null;
    return normalize(matchText);
  }
  // --- CORE REFACTORED FUNCTIONS (New) ---

  /** Registers the Tampermonkey/Greasemonkey menu commands. */
  function registerMenuCommands() {
    // console.log("[BM Debug Log] Registering GM Menu Commands.");
    GM.registerMenuCommand("Export Pet Read Lists", exportReadLists);
    GM.registerMenuCommand("Import Pet Read Lists", importReadLists);
    GM.registerMenuCommand("Clear ALL Pet Read Lists", clearReadLists);
  }

  /**
   * Searches the DOM and URL for the active pet's name.
   * @returns {string|null} The normalized pet name or null if not found.
   */
  function findActivePetName() {
    let petName = null;

    // 1. Prioritize URL parameter (used for 'books_read.phtml?view=PetName')
    const viewParam = getQueryParam("view");
    if (viewParam) {
      petName = viewParam;
      //   console.log(
      //     `[BM Debug Log] Pet Name found via 'view' query param: ${petName}.`
      //   );
      return petName;
    }

    // 2. Check DOM elements (Active Pet Status Bar)
    petName =
      document
        .querySelector(".active-pet-status .active-pet-name") // Original Selector 1
        ?.textContent?.trim() ||
      document
        .querySelector(".active-pet-status img[alt]") // Original Selector 2
        ?.alt?.split(/\s+/)?.[0] ||
      document
        .querySelector(".nav-profile-dropdown-text .profile-dropdown-link") // New Selector for Navbar
        ?.textContent?.trim() ||
      document
        .querySelector(".sidebarHeader a b") // NEW: Selector for SDB/QuickRef sidebar
        ?.textContent?.trim() ||
      null;

    if (petName) {
      console.log(`[BM Debug Log] Pet Name found via DOM element: ${petName}.`);
    } else {
      console.warn("[BM Debug Log] FAILED to detect active pet name on page.");
    }

    return petName;
  }

  /** Loads all pet-specific and global tiered lists from storage. */
  async function loadAllLists(petName) {
    console.log("[BM Debug Log] Starting asynchronous list loading...");
    await loadStoredListsToSetsForPet(petName);
    await loadTieredListsToSets();
    console.log(
      "[BM Debug Log] Asynchronous list loading complete. Lists are now in global Sets."
    );
  }

  /** Sets up the UI key and the mutation observer. */
  function setupUIKeyAndObserver() {
    console.log("[BM Debug Log] Setting up UI key and observer.");
    updateKeyUI(); // The existing function that creates and populates keyDiv
    initShopObserver(); // Observer setup is often considered part of UI initialization
    if (keyDiv) {
      document.body.appendChild(keyDiv);
    }
  }

  /**
   * Displays a non-blocking confirmation modal after successful data extraction and saving.
   * @param {string} petName The name of the pet whose lists were saved.
   * @param {number} finalRegularCount The final total count of regular books read.
   * @param {number} finalBooktasticCount The final total count of booktastic books read.
   */
  function displayExtractionSuccessModal(
    petName,
    finalRegularCount,
    finalBooktasticCount
  ) {
    console.log("[BM Debug Log] Entering displayExtractionSuccessModal.");

    // 1. Create Modal Overlay (Preserves the current page content)
    const modalOverlay = document.createElement("div");
    modalOverlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.6);
            display: flex;
            align-items: flex-start;
            justify-content: center;
            z-index: 10000;
            padding-top: 50px;
        `;

    const modalContent = document.createElement("div");
    modalContent.style.cssText = `
            background: #fff;
            padding: 30px 40px;
            border-radius: 10px;
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
            text-align: center;
            font-family: Arial, sans-serif;
            color: #333;
            max-width: 90%;
            min-width: 300px;
        `;

    // 2. Message showing both counts
    const successHeader = document.createElement("h2");
    successHeader.innerHTML = `Read Lists for <strong>${petName}</strong> Saved Successfully!`;
    successHeader.style.cssText = "margin-bottom: 20px; color: #006400;";
    modalContent.appendChild(successHeader);

    const countMessage = document.createElement("p");
    countMessage.innerHTML = `
          Regular Books Total: <strong>${finalRegularCount}</strong><br>
          Booktastic Books Total: <strong>${finalBooktasticCount}</strong>
        `;
    countMessage.style.cssText = "font-size: 1.1em; margin-bottom: 30px;";
    modalContent.appendChild(countMessage);

    // 3. Close Button
    const button = document.createElement("button");
    button.textContent = "Continue Viewing List";
    button.style.cssText = `
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
            background-color: #007BFF;
            color: white;
            border: none;
            border-radius: 5px;
            box-shadow: 0 4px #0056b3;
            transition: background-color 0.1s;
        `;
    button.onmouseover = () => (button.style.backgroundColor = "#0056b3");
    button.onmouseout = () => (button.style.backgroundColor = "#007BFF");
    button.onmousedown = () => (button.style.boxShadow = "0 2px #0056b3");
    button.onmouseup = () => (button.style.boxShadow = "0 4px #0056b3");

    // Action: Close the modal
    button.addEventListener("click", () => {
      modalOverlay.remove();
    });

    modalContent.appendChild(button);
    modalOverlay.appendChild(modalContent);
    document.body.appendChild(modalOverlay);

    console.log(
      "[BM Debug Log] Exiting displayExtractionSuccessModal. Modal appended."
    );
  }

  // --- STORAGE (GM API Abstraction) ---
  // Added logging for read/write operations.

  /**
   * Loads and deserializes data from GM storage.
   * @param {string} key The key under which the data is stored.
   * @param {object|array|null} defaultValue The default value to return.
   * @returns {Promise<any>} The deserialized data or the default value.
   */
  async function loadData(key, defaultValue = null) {
    console.log(
      `[BM Debug Log] [Storage] Attempting to load data for key: ${key}`
    );
    try {
      const storedValue = await GM.getValue(key);

      if (storedValue) {
        const data = JSON.parse(storedValue);
        console.log(
          `[BM Debug Log] [Storage] Successfully loaded and parsed data for key: ${key}.`
        );
        return data;
      } else {
        console.warn(
          `[BM Debug Log] [Storage] Key not found or empty: ${key}. Returning default value.`
        );
      }
    } catch (e) {
      safeLogError({
        message: `[Storage] Failed to load or parse data for key: ${key}. Data may be corrupted. Returning default.`,
        stack: e.stack,
      });
    }
    return defaultValue;
  }

  /**
   * Serializes and saves data to GM storage.
   * @param {string} key The key under which the data should be stored.
   * @param {any} data The data (object or array) to be stored.
   * @returns {Promise<void>}
   */
  async function saveData(key, data) {
    console.log(
      `[BM Debug Log] [Storage] Attempting to save data for key: ${key}`
    );
    try {
      await GM.setValue(key, JSON.stringify(data));
      console.log(
        `[BM Debug Log] [Storage] Data saved successfully for key: ${key}.`
      );
    } catch (e) {
      safeLogError({
        message: `[Storage] Failed to save data for key: ${key}`,
        stack: e.stack,
      });
    }
  }

  /**
   * Loads the pet-specific read lists from storage.
   * @param {string} petName The name of the pet (used for the storage key).
   */
  async function loadStoredListsToSetsForPet(petName) {
    console.log(
      `[BM Debug Log] Entering loadStoredListsToSetsForPet for pet: ${petName}.`
    );
    if (!petName) {
      console.warn(
        `[BM Debug Log] Cannot load pet lists: petName is null or undefined.`
      );
      return;
    }

    const key = `${READ_LIST_KEY_PREFIX}${petName}`;
    const defaultPayload = {
      petName: petName,
      readBooks: [],
      readBooktastic: [],
    };

    const data = await loadData(key, defaultPayload);

    if (data && data.readBooks && Array.isArray(data.readBooks)) {
      NORM_LISTREAD = new Set(data.readBooks);
      console.log(
        `[BM Debug Log] Loaded ${NORM_LISTREAD.size} regular books read by ${petName}.`
      );
    } else {
      NORM_LISTREAD = new Set();
      console.log(
        `[BM Debug Log] No regular read books found for ${petName}. Initializing empty Set.`
      );
    }

    if (data && data.readBooktastic && Array.isArray(data.readBooktastic)) {
      NORM_LISTBOOKTASTICREAD = new Set(data.readBooktastic);
      console.log(
        `[BM Debug Log] Loaded ${NORM_LISTBOOKTASTICREAD.size} booktastic books read by ${petName}.`
      );
    } else {
      NORM_LISTBOOKTASTICREAD = new Set();
      console.log(
        `[BM Debug Log] No booktastic books found for ${petName}. Initializing empty Set.`
      );
    }
    console.log(`[BM Debug Log] Exiting loadStoredListsToSetsForPet.`);
  }

  /** Loads the global tiered lists from GM storage. */
  async function loadTieredListsToSets() {
    console.log(
      `[BM Debug Log] Entering loadTieredListsToSets (Global Tiered Lists).`
    );
    const data = await loadData(TIERED_LISTS_KEY, {});

    if (data) {
      NORM_LIST1 = new Set(data.list1 || []);
      NORM_LIST2 = new Set(data.list2 || []);
      NORM_LIST3 = new Set(data.list3 || []);
      NORM_LIST4 = new Set(data.list4 || []);
      console.log(
        `[BM Debug Log] Loaded Tiered Lists summary: L1:${NORM_LIST1.size}, L2:${NORM_LIST2.size}, L3:${NORM_LIST3.size}, L4:${NORM_LIST4.size}.`
      );
    } else {
      NORM_LIST1 = new Set();
      NORM_LIST2 = new Set();
      NORM_LIST3 = new Set();
      NORM_LIST4 = new Set();
      console.log(
        `[BM Debug Log] No Tiered Lists found in storage. Initializing empty sets.`
      );
    }
    console.log(`[BM Debug Log] Exiting loadTieredListsToSets.`);
  }

  // --- STYLING LOGIC ---

  /** Core function to apply the "Read Status" style and overlay. */
  function applyReadStatus(element, nameNorm) {
    // Keeping this function quiet to prevent console spam during DOM iteration.
    try {
      const isRead =
        NORM_LISTREAD.has(nameNorm) || NORM_LISTBOOKTASTICREAD.has(nameNorm);

      const container = element.closest(
        ".shop-item, .item-container, tr[bgcolor]"
      );
      if (isRead) {
        element.classList.add(CSS_CLASSES.READ_HIGHLIGHT);
        if (container) {
          container.classList.add(CSS_CLASSES.OVERLAY);
          container.classList.remove("no-overlay");
        }
      } else {
        element.classList.remove(CSS_CLASSES.READ_HIGHLIGHT);
        if (container) {
          container.classList.remove(CSS_CLASSES.OVERLAY);
        }
      }
    } catch (e) {
      safeLogError(e);
    }
  }

  /** Core function to apply the "Tiered List" highlight style. */
  function applyTieredHighlight(element, nameNorm) {
    // Keeping this function quiet to prevent console spam during DOM iteration.
    try {
      element.classList.remove(
        CSS_CLASSES.LIST1,
        CSS_CLASSES.LIST2,
        CSS_CLASSES.LIST3,
        CSS_CLASSES.LIST4
      );

      if (NORM_LIST4.has(nameNorm)) {
        element.classList.add(CSS_CLASSES.LIST4);
      } else if (NORM_LIST3.has(nameNorm)) {
        element.classList.add(CSS_CLASSES.LIST3);
      } else if (NORM_LIST2.has(nameNorm)) {
        element.classList.add(CSS_CLASSES.LIST2);
      } else if (NORM_LIST1.has(nameNorm)) {
        element.classList.add(CSS_CLASSES.LIST1);
      }
    } catch (e) {
      safeLogError(e);
    }
  }

  /**
   * Renamed from applyStylesToItems.
   * Locates all shop/inventory items,
   * extracts normalized match keys (Title/Description) using getItemMatchKey,
   * and initiates the final styling calls for each item.
   * @param {Document} doc - The document to search (defaults to document).
   * @param {string} keyType - 'TITLE' or 'DESCRIPTION', passed from initializeAndRoute.
   */
  function processShopItems(doc = document, keyType) {
    console.log(
      `[BM Debug Log] Entering processShopItems with keyType: ${keyType}.`
    );
    try {
      // 1. Locate Items: Use the robust universal selector for the item image div
      const itemContainers = doc.querySelectorAll("[data-name]");

      if (itemContainers.length === 0) {
        console.warn(
          "[BM Debug Log] processShopItems: No items found with selector '[data-name]'."
        );
        return;
      }
      console.log(
        `[BM Debug Log] processShopItems: Found ${itemContainers.length} items to process.`
      );

      itemContainers.forEach((imgDiv) => {
        try {
          // 2. Extract Match Key: Call the helper to get the normalized Title or Description
          const nameNorm = getItemMatchKey(imgDiv, keyType);
          if (!nameNorm) return;

          // 3. Find the Styling Targets: Locate the <b> element for the text highlight.
          const parentItemContainer = imgDiv.closest(
            ".shop-item, .item-container"
          );
          const bElement = parentItemContainer?.querySelector("b");

          if (bElement) {
            // 4. Initiate Final Styling (applyReadStatus handles both the text highlight and the fade overlay)
            applyReadStatus(bElement, nameNorm);
            applyTieredHighlight(bElement, nameNorm);
          }
        } catch (inner) {
          safeLogError(inner);
        }
      });
      console.log(
        `[BM Debug Log] Exiting processShopItems. All items processed.`
      );
    } catch (err) {
      safeLogError(err);
    }
  }

  // --- CONTEXT HANDLERS ---
  /**
   * Executes logic for the Safety Deposit Box page.
   * Uses the SDB's unique table structure to extract item info.
   */
  function handleSDBPage(doc = document) {
    console.log("[BM Debug Log] Entering handleSDBPage context handler.");
    try {
      // 1. Find all SDB item rows (the old-school table rows)
      const itemRows = doc.querySelectorAll(
        'tr[bgcolor="#F6F6F6"], tr[bgcolor="#FFFFFF"]'
      );

      if (itemRows.length === 0) {
        console.warn("[BM Debug Log] handleSDBPage: No item rows found.");
        return;
      }

      itemRows.forEach((row) => {
        const cells = row.cells;
        if (cells.length < 5) return;

        const itemTypeCell = cells[3]; // The cell containing "Booktastic Book" or similar
        const titleCell = cells[1]; // The cell containing the item Title (with the <b> tag)
        const descriptionCell = cells[2]; // The cell containing the Description (for Booktastic)

        // The element we will apply the text highlight to (the title <b> tag)
        const bElement = titleCell.querySelector("b");
        if (!bElement) return;

        let rawMatchText = null;

        // 2. Determine the Item Type and Set the Match Key
        const itemTypeText = itemTypeCell.textContent;
        let isRegularBook =
          itemTypeText.includes("Book") ||
          itemTypeText.includes("Qasalan Tablets") ||
          itemTypeText.includes("Desert Scroll") ||
          itemTypeText.includes("Neovian Press");

        if (itemTypeText.includes("Booktastic Book")) {
          // Booktastic: The extracted list uses the Description (3rd column).
          rawMatchText = descriptionCell.textContent.trim();
        } else if (isRegularBook) {
          // Regular Book: The extracted list uses the Title (2nd column).

          // Use firstChild.textContent to extract ONLY the clean Title text
          // and ignore subsequent <br> and <span> elements inside the <b> tag.
          if (
            bElement.firstChild &&
            bElement.firstChild.nodeType === Node.TEXT_NODE
          ) {
            rawMatchText = bElement.firstChild.textContent.trim();
          } else {
            // Fallback to the whole text content, but this is less reliable
            rawMatchText = bElement.textContent.trim();
          }
        } else {
          return; // Not a book, skip.
        }

        if (!rawMatchText) return; // Skip if no text was found

        const nameNorm = normalize(rawMatchText);

        // 3. Apply Styles using the correct match key
        applyReadStatus(bElement, nameNorm);
        applyTieredHighlight(bElement, nameNorm);
      });
      console.log(
        "[BM Debug Log] Exiting handleSDBPage context handler (SDB styling complete)."
      );
    } catch (err) {
      safeLogError(err);
    }
  }

  /**
   * Extracts and normalizes book titles from the Regular Books Read page.
   * Handles the 'Title: Description' format and isolates the title.
   * @returns {string[]} An array of normalized Regular book titles.
   */
  function extractRegularBooks() {
    // Select all relevant table data elements that contain book information.
    const bookTdsNodeList = document.querySelectorAll(
      'td[align="center"][style*="border:1px solid black;"]'
    );

    // Skip the first two elements, which are known table headers.
    const bookTds = Array.from(bookTdsNodeList).slice(2);
    const books = [];

    bookTds.forEach((td) => {
      let rawText = td.textContent?.trim() || "";

      // Skip TDs containing only the pet's reading count (e.g., "(1007)").
      if (
        rawText.startsWith("(") &&
        rawText.endsWith(")") &&
        rawText.length < 10
      ) {
        return;
      }

      // Create a unique separator (':::') by replacing the Title/Description divider
      // (a colon followed by whitespace/non-breaking spaces). This handles titles
      // that contain colons.
      rawText = rawText.replace(/:\s*(\u00A0|&nbsp;)+/gi, ":::");

      const separator = ":::";
      const separatorIndex = rawText.indexOf(separator);

      let title;
      if (separatorIndex !== -1) {
        // Isolate the title by taking only the text before the unique separator.
        title = rawText.substring(0, separatorIndex).trim();
      } else {
        // Default to the entire text if the expected format is not found.
        title = rawText;
      }

      // Normalize the title for consistent storage and lookup.
      if (title) {
        books.push(normalize(title));
      }
    });

    return books;
  }

  /**
   * Extracts normalized descriptions (used as IDs) from the Booktastic Books Read page.
   * @returns {string[]} Array of normalized Booktastic descriptions.
   */
  function extractBooktasticBooks() {
    // Selector based on the original script's logic for Booktastic (e.g., has <i> tag, no style)
    const bookTds = document.querySelectorAll(
      'td[align="center"]:not([style]) i'
    );
    const books = [];

    bookTds.forEach((iTag) => {
      // We capture the text content of the parent <td> element
      // (or the i tag's content if that is the identifier)
      const rawText = iTag.closest("td")?.textContent?.trim() || "";

      if (rawText) {
        // The entire description text is the identifier, so we normalize the whole thing.
        books.push(normalize(rawText));
      }
    });

    return books;
  }

  /** Extracts read lists from the 'Books Read' page and saves them to storage. */
  async function handleBooksReadPage() {
    console.log("[BM Debug Log] Entering handleBooksReadPage (EXTRACTOR).");
    try {
      const petName = getQueryParam("pet_name");
      if (!petName) {
        console.error(
          "[BM Debug Log] Extractor Error: pet_name is missing from URL. Aborting extraction."
        );
        return;
      }

      // RESILIENT TYPE IDENTIFICATION (using the /moon/ path)
      const isMoonPage = location.href.includes("/moon/");
      const pageType = isMoonPage ? "Booktastic" : "Regular";

      let extractedList = [];

      // DELEGATION: Call the specialized extractor (Information Hiding)
      if (isMoonPage) {
        extractedList = extractBooktasticBooks();
      } else {
        extractedList = extractRegularBooks();
      }

      const booksFound = extractedList.length;

      if (booksFound === 0) {
        console.warn(
          `[BM Debug Log] Extractor found 0 ${pageType} books. Proceeding with save to ensure data is cleared.`
        );
      }

      console.log(
        `[BM Debug Log] Extractor Success: Extracted ${extractedList.length} books.`
      );

      const key = `${READ_LIST_KEY_PREFIX}${petName}`;
      const defaultPayload = {
        petName: petName,
        readBooks: [],
        readBooktastic: [],
      };

      // Step 1: Load existing data
      const existingData = await loadData(key, defaultPayload);

      // Step 2: Update the specific list
      if (isMoonPage) {
        existingData.readBooktastic = extractedList;
      } else {
        existingData.readBooks = extractedList;
      }

      // ... (omitted logging for brevity) ...

      // Step 3: Save the updated payload
      await saveData(key, existingData);
      console.log(
        `[BM Debug Log] Extractor: Read list for ${petName} successfully persisted.`
      );

      // --- Call Dedicated UI Function (SoC Principle) ---
      const finalRegularCount = existingData.readBooks.length;
      const finalBooktasticCount = existingData.readBooktastic.length;

      displayExtractionSuccessModal(
        petName,
        finalRegularCount,
        finalBooktasticCount
      );
    } catch (e) {
      safeLogError(e);
    }
    console.log("[BM Debug Log] Exiting handleBooksReadPage.");
  }

  // --- MENU COMMANDS ---

  /** Exports all stored pet read lists into a single JSON file. */
  async function exportReadLists() {
    console.log(
      "[BM Debug Log] [Menu Command] Starting exportReadLists process."
    );
    let allKeys = [];
    let allData = [];

    try {
      console.log(
        "[BM Debug Log] [Menu Command] Listing all GM storage keys..."
      );
      allKeys = await GM.listValues();
      const readListKeys = allKeys.filter((key) =>
        key.startsWith(READ_LIST_KEY_PREFIX)
      );
      console.log(
        `[BM Debug Log] [Menu Command] Found ${readListKeys.length} pet read list keys to export.`
      );

      for (const key of readListKeys) {
        const data = await loadData(key, null);
        if (data) {
          allData.push(data);
          console.log(
            `[BM Debug Log] [Menu Command] Loaded data for pet key: ${key}.`
          );
        }
      }

      const exportPayload = {
        version: 4,
        dataType: "NeopetsBookHighlighterReadLists",
        readLists: allData,
      };
      console.log(
        `[BM Debug Log] [Menu Command] Final export payload generated with ${allData.length} lists.`
      );

      const dataStr =
        "data:text/json;charset=utf-8," +
        encodeURIComponent(JSON.stringify(exportPayload, null, 2));
      const downloadAnchorNode = document.createElement("a");
      downloadAnchorNode.setAttribute("href", dataStr);
      downloadAnchorNode.setAttribute(
        "download",
        `book_highlighter_export_${Date.now()}.json`
      );
      document.body.appendChild(downloadAnchorNode);
      downloadAnchorNode.click();
      downloadAnchorNode.remove();

      console.log(
        `[BM Debug Log] [Menu Command] Successfully finished export.`
      );
      alert(
        `Successfully exported ${allData.length} pet read lists. Check your downloads folder.`
      );
    } catch (error) {
      safeLogError({
        message: "[Menu Command] Export failed",
        stack: error.stack,
      });
      alert("Export failed. Check the console for details.");
    }
    console.log("[BM Debug Log] [Menu Command] Exiting exportReadLists.");
  }

  /** Imports pet read lists from a JSON file. */
  function importReadLists() {
    console.log(
      "[BM Debug Log] [Menu Command] Starting importReadLists process (File dialogue opening)."
    );
    const input = document.createElement("input");
    input.type = "file";
    input.accept = ".json";
    input.onchange = async (event) => {
      console.log("[BM Debug Log] [Menu Command] File change event triggered.");
      const file = event.target.files[0];
      if (!file) return;
      console.log(`[BM Debug Log] [Menu Command] File selected: ${file.name}.`);

      try {
        const text = await file.text();
        const payload = JSON.parse(text);
        console.log("[BM Debug Log] [Menu Command] File parsed successfully.");

        if (
          payload.dataType !== "NeopetsBookHighlighterReadLists" ||
          !Array.isArray(payload.readLists)
        ) {
          throw new Error(
            "Invalid file format. Does not contain valid book highlighter read lists."
          );
        }

        const importedLists = payload.readLists;
        let successfulSaves = 0;
        console.log(
          `[BM Debug Log] [Menu Command] Found ${importedLists.length} lists to import.`
        );

        for (const list of importedLists) {
          if (list.petName) {
            const key = `${READ_LIST_KEY_PREFIX}${list.petName}`;
            console.log(
              `[BM Debug Log] [Menu Command] Saving list for pet: ${list.petName}.`
            );
            await saveData(key, list);
            successfulSaves++;
          }
        }

        alert(
          `Successfully imported ${successfulSaves} pet read lists. Reload the page to see changes.`
        );
        console.log(
          `[BM Debug Log] [Menu Command] Successfully imported ${successfulSaves} pet read lists.`
        );
      } catch (error) {
        safeLogError({
          message: "[Menu Command] Import failed",
          stack: error.stack,
        });
        alert("Import failed. Check the console for details.");
      }
    };
    input.click();
    console.log(
      "[BM Debug Log] [Menu Command] Exiting importReadLists (waiting for file selection)."
    );
  }

  /** Clears all stored pet read lists. */
  async function clearReadLists() {
    console.log(
      "[BM Debug Log] [Menu Command] Starting clearReadLists process."
    );
    if (
      !confirm(
        "Are you sure you want to CLEAR ALL stored pet read lists? This cannot be undone."
      )
    ) {
      console.log(
        "[BM Debug Log] [Menu Command] Clear operation cancelled by user."
      );
      return;
    }

    console.log(
      "[BM Debug Log] [Menu Command] Clear operation confirmed. Listing keys..."
    );
    let allKeys = [];
    let deletedCount = 0;

    try {
      allKeys = await GM.listValues();
      const readListKeys = allKeys.filter((key) =>
        key.startsWith(READ_LIST_KEY_PREFIX)
      );
      console.log(
        `[BM Debug Log] [Menu Command] Found ${readListKeys.length} pet read list keys to delete.`
      );

      for (const key of readListKeys) {
        await GM.deleteValue(key);
        deletedCount++;
        console.log(`[BM Debug Log] [Menu Command] Deleted key: ${key}.`);
      }

      alert(
        `Successfully cleared ${deletedCount} pet read lists. Reload the page to confirm.`
      );
      console.log(
        `[BM Debug Log] [Menu Command] Successfully cleared ${deletedCount} pet read lists. Operation complete.`
      );
    } catch (error) {
      safeLogError({
        message: "[Menu Command] Clear operation failed",
        stack: error.stack,
      });
      alert("Clear operation failed. Check the console for details.");
    }
    console.log("[BM Debug Log] [Menu Command] Exiting clearReadLists.");
  }

  // --- OBSERVER & UI ---

  function initShopObserver() {
    console.log("[BM Debug Log] Entering initShopObserver.");
    const targetNode = document.body;
    const config = { childList: true, subtree: true };

    const callback = function (mutationsList, observer) {
      // Optimization: Only run if we are on an objects.phtml page
      if (!location.href.includes("objects.phtml")) return;

      console.log(
        "[BM Debug Log] Observer Callback: Mutation detected. Checking for added nodes."
      );

      // Determine keyType based on current URL (replicates logic from initializeAndRoute)
      const url = new URL(location.href);
      const objType = url.searchParams.get("obj_type");
      let keyType = "TITLE";

      if (objType === "70") {
        keyType = "DESCRIPTION";
      }

      // Iterate mutations, but only process shop items once per cycle if new nodes were added.
      for (const mutation of mutationsList) {
        if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
          console.log(
            "[BM Debug Log] Observer: Found new element. Re-processing shop items with keyType:",
            keyType
          );

          // Call the correct processing function on the whole document to re-scan
          // all items, including the newly added ones.
          processShopItems(document, keyType);
          return; // Exit the callback after the first successful shop re-scan
        }
      }
    };

    const observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
    console.log(
      "[BM Debug Log] Shop MutationObserver initialized on document body (only runs on objects.phtml)."
    );
    console.log("[BM Debug Log] Exiting initShopObserver.");
  }

  function updateKeyUI() {
    console.log("[BM Debug Log] Entering updateKeyUI.");
    if (!keyDiv) {
      keyDiv = document.createElement("div");
      keyDiv.id = "book-highlighter-key";
      keyDiv.style.cssText = `
                    position: fixed;
                    bottom: 10px;
                    left: 10px;
                    background: rgba(255, 255, 255, 0.9);
                    border: 1px solid #ccc;
                    padding: 10px;
                    border-radius: 8px;
                    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
                    font-family: Arial, sans-serif;
                    font-size: 12px;
                    z-index: 9999;
                    max-width: 250px;
                `;
    }

    let content = "<strong>Book Highlighter Key</strong>";

    const totalRead = NORM_LISTREAD.size + NORM_LISTBOOKTASTICREAD.size;
    content += `<p style="margin-top: 5px;">Total Books Read: (${totalRead})<br>Read books marked.</p>`;
    console.log(`[BM Debug Log] Key UI: Total read books count: ${totalRead}`);

    content += `<p style="margin-top: 5px; margin-bottom: 0;"><strong>Tiered Lists:</strong></p>`;

    if (NORM_LIST1.size > 0) {
      content += `<p style="color: #4CAF50; margin: 0 0 2px 10px;">• List 1 (Top Tier): ${NORM_LIST1.size}</p>`;
    }
    if (NORM_LIST2.size > 0) {
      content += `<p style="color: #FFC107; margin: 0 0 2px 10px;">• List 2: ${NORM_LIST2.size}</p>`;
    }
    if (NORM_LIST3.size > 0) {
      content += `<p style="color: #2196F3; margin: 0 0 2px 10px;">• List 3: ${NORM_LIST3.size}</p>`;
    }
    if (NORM_LIST4.size > 0) {
      content += `<p style="color: #F44336; margin: 0 0 2px 10px;">• List 4 (Low Tier): ${NORM_LIST4.size}</p>`;
    }

    if (
      totalRead === 0 &&
      NORM_LIST1.size === 0 &&
      NORM_LIST2.size === 0 &&
      NORM_LIST3.size === 0 &&
      NORM_LIST4.size === 0
    ) {
      content += `<p style="margin: 0 0 2px 10px;">No lists loaded.</p>`;
    }

    keyDiv.innerHTML = content;
    console.log("[BM Debug Log] Exiting updateKeyUI. UI content updated.");
  }

  // --- DYNAMIC CSS STYLES ---

  function injectStyles() {
    console.log("[BM Debug Log] Entering injectStyles. Injecting core CSS.");
    const style = document.createElement("style");
    style.type = "text/css";
    const css = `
                /* Core Styles for READ Books (FADE) */
                .${CSS_CLASSES.READ_HIGHLIGHT} {
                    /* Text style for read items (optional, but good for visibility) */
                    text-decoration: line-through;
                }

                /* Container overlay for read items */
                .${CSS_CLASSES.OVERLAY} {
                    position: relative;
                    background: rgba(0,255,255,0.6);
                    opacity: 0.35 !important;
                    transition: opacity 0.3s ease;
                    padding-top:5px;
                    box-shadow:0 4px 8px rgba(0,255,255,0.6);
                }

                .${CSS_CLASSES.OVERLAY}:hover {
                    opacity: 0.8 !important; /* Slightly brighter on hover */
                }

                /* Tier 1 Highlight (Top Tier) */
                .${CSS_CLASSES.LIST1} {
                    color: #4CAF50 !important; /* Bright Green */
                    font-weight: bold;
                    text-shadow: 0 0 2px rgba(76, 175, 80, 0.5);
                }

                /* Tier 2 Highlight */
                .${CSS_CLASSES.LIST2} {
                    color: #FFC107 !important; /* Amber/Yellow */
                    font-weight: bold;
                    text-shadow: 0 0 2px rgba(255, 193, 7, 0.5);
                }

                /* Tier 3 Highlight */
                .${CSS_CLASSES.LIST3} {
                    color: #2196F3 !important; /* Blue */
                    font-weight: bold;
                    text-shadow: 0 0 2px rgba(33, 150, 243, 0.5);
                }

                /* Tier 4 Highlight (Lower Tier/Misc) */
                .${CSS_CLASSES.LIST4} {
                    color: #F44336 !important; /* Red */
                    font-weight: bold;
                    text-shadow: 0 0 2px rgba(244, 67, 54, 0.5);
                }
            `;
    style.appendChild(document.createTextNode(css));
    document.head.appendChild(style);
    console.log("[BM Debug Log] Exiting injectStyles. Styles added to <head>.");
  }

  // --- MAIN INITIALIZATION & ROUTING ---

  /**
   * The core function that runs the script.
   * Determines the current page context and executes the necessary logic.
   */
  async function initializeAndRoute() {
    console.log(
      "[BM Debug Log] Entering initializeAndRoute (Contextual Router)."
    );
    const currentURL = location.href;

    // 1. Check for the Read Book Extractor Page
    if (currentURL.includes("books_read.phtml")) {
      console.log(
        "[BM Debug Log] Routing: Found 'books_read.phtml'. Executing extractor."
      );
      await handleBooksReadPage();
      return; // Stop further execution on extractor page
    }

    // 2. Inject CSS and Register Menu Commands (Setup Phase)
    injectStyles();
    registerMenuCommands();

    // 3. Detect Pet and Load Lists (Data Phase)
    const petName = findActivePetName();

    if (petName) {
      await loadAllLists(petName);
    } else {
      console.warn(
        "[BM Debug Log] No pet name detected. Loading only Tiered Lists."
      );
      await loadAllLists(null); // Load tiered lists even without a pet list
    }

    // 4. Apply Initial Styles (Rendering Phase)
    if (currentURL.includes("safetydeposit.phtml")) {
      console.log(
        "[BM Debug Log] Routing: Found 'safetydeposit.phtml'. Executing SDB handler."
      );
      handleSDBPage();
    } else if (currentURL.includes("objects.phtml")) {
      console.log(
        "[BM Debug Log] Routing: Found 'objects.phtml'. Determining Match Key Type."
      );

      // Determine match key type based on url parameters
      const url = new URL(currentURL);
      const objType = url.searchParams.get("obj_type");
      let keyType = "TITLE"; // Default for almost all shops/inventory

      // Check for Booktastic Book Shops (obj_type=70)
      if (objType === "70") {
        keyType = "DESCRIPTION";
        console.log(
          "[BM Debug Log] Booktastic Shop detected. Using DESCRIPTION for match key."
        );
      } else {
        console.log(
          "[BM Debug Log] Regular Shop/Inventory detected. Using TITLE for match key."
        );
      }

      // Call the unified processing function with the determined key type
      processShopItems(document, keyType);
    }

    // 5. Setup UI and Observer (Finalization Phase)
    setupUIKeyAndObserver();

    // 5. Setup UI and Observer (Finalization Phase)
    setupUIKeyAndObserver();

    console.log(
      "[BM Debug Log] Exiting initializeAndRoute. Main script execution finished."
    );
  }

  // Start the application lifecycle
  initializeAndRoute();
})();