URL Visit Tracker (Improved)

Track visits per URL, show corner badge history & link hover info - Massive Capacity (10K URLs) - ES2020+ & Smooth Tooltips. Advanced URL normalization and performance optimizations.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         URL Visit Tracker (Improved)
// @namespace    https://github.com/hongmd/userscript-improved
// @version      2.7.0
// @description  Track visits per URL, show corner badge history & link hover info - Massive Capacity (10K URLs) - ES2020+ & Smooth Tooltips. Advanced URL normalization and performance optimizations.
// @author       hongmd
// @contributor  Original idea by Chewy
// @license      MIT
// @homepageURL  https://github.com/hongmd/userscript-improved
// @supportURL   https://github.com/hongmd/userscript-improved/issues
// @match        https://*/*
// @run-at       document-start
// @noframes
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  'use strict';

  // Configuration options
  const CONFIG = {
    MAX_VISITS_STORED: 20,
    MAX_URLS_STORED: 10000,         // Massive capacity for extensive tracking
    CLEANUP_THRESHOLD: 12000,       // Cleanup when exceeding this (20% buffer)
    HOVER_DELAY: 1000,              // Delay before showing tooltip (ms)
    POLL_INTERVAL: 5000,            // Reduced polling frequency for better performance
    BADGE_POSITION: { right: '14px', bottom: '14px' },
    BADGE_VISIBLE: true,
    DEBUG: false,                   // Set to true to enable debug logging
    // Performance optimizations
    POLLING: {
      PAUSE_WHEN_HIDDEN: true,      // Pause polling timer when tab is hidden
      SKIP_WHEN_HIDDEN: true,       // Skip polling execution when tab is hidden (lighter)
      ADAPTIVE: true                // Enable adaptive polling based on activity
    },
    // Multi-tab coordination
    MULTI_TAB: {
      ENABLED: false,               // Set to true to enable multi-tab cache coordination
      SYNC_INTERVAL: 10000          // How often to sync cache across tabs (ms)
    },
    // Debounce settings for database writes
    DEBOUNCE: {
      ENABLED: true,                // Enable debounced writes for better performance
      DELAY: 1000                   // Delay in ms before writing to storage
    },
    // Web Worker for heavy operations
    WEB_WORKER: {
      ENABLED: true,                // Use Web Worker for cleanup operations
      TIMEOUT: 15000                // Timeout for worker operations (ms)
    },
    // URL normalization options
    NORMALIZE_URL: {
      REMOVE_QUERY: false,          // Set to true to ignore query params (?key=value)
      // false: tracks "site.com?q=A" and "site.com?q=B" separately
      // true:  groups them as "site.com" (same page)
      REMOVE_HASH: true,            // Set to true to ignore hash fragments (#section)
      // true:  treats "page.html#top" and "page.html#bottom" as same
      // false: tracks different sections separately
      REMOVE_WWW: true,             // Set to true to remove www. prefix
      REMOVE_PROTOCOL: true,        // Set to true to remove http/https
      REMOVE_TRAILING_SLASH: true,  // Set to true to remove trailing /
      CLEAN_SEARCH_URLS: true       // Clean search engine URLs (keep only main query)
    },
    // URL filtering - Skip tracking certain types of URLs
    URL_FILTERS: {
      SKIP_UTILITY_PAGES: true,     // Skip tracking utility/internal pages (cookies, auth, etc.)
      SKIP_PATTERNS: [              // URL patterns to skip (case-insensitive)
        '/RotateCookiesPage',       // YouTube cookie rotation
        '/ServiceLogin',            // Google login pages
        '/CheckCookie',             // Cookie check pages
        '/robots.txt',              // Robot files
        '/favicon.ico',             // Favicon requests
        'ogs.google.com',           // Google widgets/apps
        '/widget/app',              // Google widget apps
        '/persist_identity',        // YouTube identity persistence
        'studio.youtube.com/persist_identity' // YouTube Studio identity
      ]
    }
  };

  // Badge visibility state
  let badgeVisible = CONFIG.BADGE_VISIBLE;
  let menuRegistered = false; // Flag to prevent duplicate menu registration

  // Polling state
  let pollTimer = null;
  let lastHref = location.href;
  let lastCheck = Date.now();
  let activityCount = 0; // Track recent activity for adaptive polling

  // In-memory cache for hot path performance
  let dbCache = null;
  let cacheValid = false;

  function normalizeUrl(url) {
    // Validate input URL first
    if (!url || typeof url !== 'string') {
      console.warn('Invalid URL provided to normalizeUrl:', url);
      return location.href;
    }

    // Configurable URL normalization for flexible tracking granularity
    let normalized = url.trim();

    // Handle malformed URLs
    try {
      // Test if URL is valid by creating URL object
      new URL(normalized.startsWith('http') ? normalized : 'http://' + normalized);
    } catch (error) {
      if (CONFIG.DEBUG) {
        console.warn('Malformed URL detected, using current location:', url);
      }
      return normalizeUrl(location.href);
    }

    // Clean search URLs before other normalizations (must be done first)
    if (CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS) {
      normalized = cleanSearchUrl(normalized);
    }

    // Remove protocol if configured
    if (CONFIG.NORMALIZE_URL.REMOVE_PROTOCOL) {
      normalized = normalized.replace(/^https?:\/\//, '');
    }

    // Remove www prefix if configured
    if (CONFIG.NORMALIZE_URL.REMOVE_WWW) {
      normalized = normalized.replace(/^www\./, '');
    }

    // Remove trailing slash if configured
    if (CONFIG.NORMALIZE_URL.REMOVE_TRAILING_SLASH) {
      normalized = normalized.replace(/\/$/, '');
    }

    // Remove hash fragments if configured
    if (CONFIG.NORMALIZE_URL.REMOVE_HASH) {
      normalized = normalized.split('#')[0];
    }

    // Remove query parameters if configured (after search cleaning)
    if (CONFIG.NORMALIZE_URL.REMOVE_QUERY) {
      normalized = normalized.split('?')[0];
    }

    if (CONFIG.DEBUG) {
      console.log(`🔗 URL normalized: "${url}" → "${normalized}"`);
    }

    return normalized;
  }

  // Check if URL should be skipped from tracking
  function shouldSkipUrl(url) {
    if (!CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES) return false;

    const urlLower = url.toLowerCase();

    // Check against skip patterns
    for (const pattern of CONFIG.URL_FILTERS.SKIP_PATTERNS) {
      if (urlLower.includes(pattern.toLowerCase())) {
        if (CONFIG.DEBUG) {
          console.log(`🚫 Skipping URL (matches pattern "${pattern}"): ${url}`);
        }
        return true;
      }
    }

    return false;
  }

  // Clean search engine URLs to group similar searches
  function cleanSearchUrl(url) {
    if (!CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS) return url;

    try {
      const urlObj = new URL(url.startsWith('http') ? url : 'https://' + url);
      const hostname = urlObj.hostname.toLowerCase();
      const pathname = urlObj.pathname;
      const searchParams = new URLSearchParams(urlObj.search);

      // Google Search
      if ((hostname.includes('google.') || hostname === 'google.com') && pathname.includes('/search')) {
        const query = searchParams.get('q');
        if (query) {
          // Keep only the main query, remove tracking params
          const cleanUrl = `${urlObj.protocol}//${urlObj.hostname}${pathname}?q=${encodeURIComponent(query)}`;
          if (CONFIG.DEBUG) {
            console.log(`🔍 Cleaned Google search: "${url}" → "${cleanUrl}"`);
          }
          return cleanUrl;
        }
      }

      // Bing Search
      else if (hostname.includes('bing.com') && pathname.includes('/search')) {
        const query = searchParams.get('q');
        if (query) {
          const cleanUrl = `${urlObj.protocol}//${urlObj.hostname}${pathname}?q=${encodeURIComponent(query)}`;
          if (CONFIG.DEBUG) {
            console.log(`🔍 Cleaned Bing search: "${url}" → "${cleanUrl}"`);
          }
          return cleanUrl;
        }
      }

      // DuckDuckGo Search
      else if (hostname.includes('duckduckgo.com')) {
        const query = searchParams.get('q');
        if (query) {
          const cleanUrl = `${urlObj.protocol}//${urlObj.hostname}/?q=${encodeURIComponent(query)}`;
          if (CONFIG.DEBUG) {
            console.log(`🔍 Cleaned DuckDuckGo search: "${url}" → "${cleanUrl}"`);
          }
          return cleanUrl;
        }
      }

      // YouTube Search
      else if (hostname.includes('youtube.com') && pathname.includes('/results')) {
        const query = searchParams.get('search_query');
        if (query) {
          const cleanUrl = `${urlObj.protocol}//${urlObj.hostname}${pathname}?search_query=${encodeURIComponent(query)}`;
          if (CONFIG.DEBUG) {
            console.log(`🔍 Cleaned YouTube search: "${url}" → "${cleanUrl}"`);
          }
          return cleanUrl;
        }
      }
    } catch (error) {
      if (CONFIG.DEBUG) {
        console.warn('Failed to clean search URL:', error);
      }
    }

    return url; // Return original if not a search URL or parsing failed
  }

  // Safe closest() that handles Text nodes and elements without closest method
  function safeClosest(target, selector) {
    // Handle null/undefined
    if (!target) return null;

    // If target is a Text node, use its parent element
    if (target.nodeType === Node.TEXT_NODE) {
      target = target.parentElement;
    }

    // If target doesn't have closest method (SVG elements in old browsers), fallback
    if (!target || typeof target.closest !== 'function') {
      // Traverse up manually
      let element = target;
      while (element && element.nodeType === Node.ELEMENT_NODE) {
        if (element.matches && element.matches(selector)) {
          return element;
        }
        element = element.parentElement;
      }
      return null;
    }

    // Use native closest if available
    return target.closest(selector);
  }

  // Polling control functions
  function directPoll() {
    // Skip polling when tab is hidden for performance
    if (CONFIG.POLLING.SKIP_WHEN_HIDDEN && document.hidden) {
      if (CONFIG.DEBUG) {
        console.log('⏸️ Skipping poll - tab is hidden');
      }
      return;
    }

    const currentHref = location.href;
    const now = Date.now();

    // Check if we should process pending URL change
    if (pendingUrlChange && !pendingTimeout && (now - lastUrlChangeTime) >= URL_CHANGE_MIN_INTERVAL) {
      if (CONFIG.DEBUG) {
        console.log(`🔄 Polling processing pending URL change: ${currentUrl} → ${pendingUrlChange}`);
      }
      const savedPendingUrl = pendingUrlChange;
      pendingUrlChange = null;
      // Validate URL before processing
      if (savedPendingUrl && savedPendingUrl !== currentUrl) {
        currentUrl = savedPendingUrl;
        lastUrlChangeTime = now;
        updateVisit();
      }
    }

    // Only process if URL actually changed and enough time has passed
    if (currentHref !== lastHref && (now - lastCheck) >= 1000) {
      if (CONFIG.DEBUG) {
        console.log(`🔄 Polling detected URL change: ${lastHref} → ${currentHref}`);
      }
      lastHref = currentHref;
      lastCheck = now;
      onUrlChange();
    } else if (currentHref !== lastHref) {
      // URL changed but too soon - just update lastHref to prevent spam
      lastHref = currentHref;
    }
  }

  function startPolling() {
    if (pollTimer) clearInterval(pollTimer);

    // Adaptive polling interval based on activity
    let interval = CONFIG.POLL_INTERVAL;
    if (CONFIG.POLLING.ADAPTIVE) {
      // More frequent polling if recent activity, less if idle
      interval = activityCount > 0 ? Math.max(2000, CONFIG.POLL_INTERVAL / 2) : CONFIG.POLL_INTERVAL * 2;
      // Decay activity count over time
      activityCount = Math.max(0, activityCount - 1);
    }

    pollTimer = setInterval(directPoll, interval);
  }

  function stopPolling() {
    if (pollTimer) {
      clearInterval(pollTimer);
      pollTimer = null;
    }
  }

  // Optimized functions for timestamp storage
  function createTimestamp(date = new Date()) {
    return date.getTime();
  }

  function formatTimestamp(timestamp) {
    const date = new Date(timestamp);
    const pad = n => n.toString().padStart(2, '0');
    return `${pad(date.getHours())}:${pad(date.getMinutes())} ${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()}`;
  }

  // Calculate accurate UTF-8 byte size using Blob
  function getActualDataSize(data) {
    try {
      const jsonString = JSON.stringify(data);
      // Create a Blob to get the actual UTF-8 byte size
      const blob = new Blob([jsonString], { type: 'application/json' });
      return blob.size;
    } catch (error) {
      // Fallback to character count if Blob fails
      console.warn('Failed to calculate Blob size, using character count:', error);
      return JSON.stringify(data).length;
    }
  }

  // Smart cleanup to maintain database size with performance optimization
  // Uses Web Worker for heavy computation to avoid blocking UI
  async function cleanupOldUrls(db) {
    const urls = Object.keys(db);
    if (urls.length <= CONFIG.MAX_URLS_STORED) return db;

    if (CONFIG.DEBUG) {
      console.log(`🧹 Large database cleanup: ${urls.length} → ${CONFIG.MAX_URLS_STORED} URLs`);
    }

    // Cleanup logic - can run in main thread or Web Worker
    const performCleanup = (dbData, maxUrls) => {
      const urlKeys = Object.keys(dbData);
      // Calculate score for each URL (visits * recency)
      const scored = urlKeys.map(url => {
        const data = dbData[url];
        const recentVisit = data.visits?.[0] ?? 0;
        const daysSinceVisit = (Date.now() - recentVisit) / (1000 * 60 * 60 * 24);
        const recencyScore = Math.max(0, 30 - daysSinceVisit) / 30;
        const score = data.count * (1 + recencyScore);
        return { url, score };
      });

      // Keep top URLs by score
      scored.sort((a, b) => b.score - a.score);
      const keepUrls = scored.slice(0, maxUrls);

      // Build clean database
      const cleanDb = {};
      for (const { url } of keepUrls) {
        cleanDb[url] = dbData[url];
      }
      return cleanDb;
    };

    // Try Web Worker for large databases
    if (CONFIG.WEB_WORKER.ENABLED && urls.length > 5000 && typeof Worker !== 'undefined') {
      try {
        return await runCleanupInWorker(db, CONFIG.MAX_URLS_STORED);
      } catch (error) {
        if (CONFIG.DEBUG) {
          console.warn('🔧 Web Worker cleanup failed, falling back to main thread:', error);
        }
        // Fallback to main thread
      }
    }

    // Use requestIdleCallback for medium databases, or run directly for small ones
    if (window.requestIdleCallback && urls.length > 3000) {
      return new Promise(resolve => {
        requestIdleCallback(() => {
          resolve(performCleanup(db, CONFIG.MAX_URLS_STORED));
        }, { timeout: 10000 });
      });
    }

    return performCleanup(db, CONFIG.MAX_URLS_STORED);
  }

  // Web Worker implementation for cleanup (runs in separate thread)
  function runCleanupInWorker(db, maxUrls) {
    return new Promise((resolve, reject) => {
      // Create worker code as a Blob (works in userscript context)
      const workerCode = `
        self.onmessage = function(e) {
          const { db, maxUrls } = e.data;
          const urls = Object.keys(db);
          
          // Calculate scores
          const scored = urls.map(url => {
            const data = db[url];
            const recentVisit = data.visits?.[0] ?? 0;
            const daysSinceVisit = (Date.now() - recentVisit) / (1000 * 60 * 60 * 24);
            const recencyScore = Math.max(0, 30 - daysSinceVisit) / 30;
            const score = data.count * (1 + recencyScore);
            return { url, score };
          });
          
          // Sort and keep top URLs
          scored.sort((a, b) => b.score - a.score);
          const keepUrls = scored.slice(0, maxUrls);
          
          // Build clean database
          const cleanDb = {};
          for (const { url } of keepUrls) {
            cleanDb[url] = db[url];
          }
          
          self.postMessage(cleanDb);
        };
      `;

      const blob = new Blob([workerCode], { type: 'application/javascript' });
      const workerUrl = URL.createObjectURL(blob);
      const worker = new Worker(workerUrl);

      // Timeout handler
      const timeoutId = setTimeout(() => {
        worker.terminate();
        URL.revokeObjectURL(workerUrl);
        reject(new Error('Worker timeout'));
      }, CONFIG.WEB_WORKER.TIMEOUT);

      worker.onmessage = (e) => {
        clearTimeout(timeoutId);
        worker.terminate();
        URL.revokeObjectURL(workerUrl);

        if (CONFIG.DEBUG) {
          console.log('🔧 Web Worker cleanup completed successfully');
        }
        resolve(e.data);
      };

      worker.onerror = (error) => {
        clearTimeout(timeoutId);
        worker.terminate();
        URL.revokeObjectURL(workerUrl);
        reject(error);
      };

      // Send data to worker
      worker.postMessage({ db, maxUrls });
    });
  }

  function shortenNumber(num) {
    // Handle edge cases first
    if (!Number.isFinite(num)) return '0'; // Handle NaN, Infinity, -Infinity
    if (num < 0) return '0'; // Visits can't be negative
    if (num === 0) return '0';

    // Convert to absolute value and round to avoid floating point issues
    const absNum = Math.abs(Math.floor(num));

    // Handle very large numbers with appropriate suffixes
    if (absNum >= 1_000_000_000) {
      return (Math.round(absNum / 100_000_000) / 10) + 'B'; // Billions
    }
    if (absNum >= 1_000_000) {
      return (Math.round(absNum / 100_000) / 10) + 'M'; // Millions
    }
    if (absNum >= 1_000) {
      return (Math.round(absNum / 100) / 10) + 'K'; // Thousands
    }

    return String(absNum);
  }

  function getDB() {
    // Return cached version if available and valid
    if (cacheValid && dbCache !== null) {
      return dbCache;
    }

    try {
      dbCache = GM_getValue('visitDB', {});
      cacheValid = true;
      return dbCache;
    } catch (error) {
      console.warn('Failed to read visit database:', error);
      dbCache = {};
      cacheValid = true;
      return dbCache;
    }
  }

  // Fast read-only access for hot paths (tooltips, etc)
  function getDBCached() {
    if (cacheValid && dbCache !== null) {
      return dbCache;
    }
    return getDB(); // Fallback to full load
  }

  // Debounce state for setDB
  let pendingDbWrite = null;
  let debounceTimer = null;

  // Internal function to actually write to storage
  async function _persistDB(db) {
    try {
      // Auto cleanup if database is getting too large
      if (Object.keys(db).length > CONFIG.CLEANUP_THRESHOLD) {
        db = await cleanupOldUrls(db);
        // Update cache with cleaned data
        dbCache = db;
      }

      // Persist to storage
      GM_setValue('visitDB', db);

      if (CONFIG.DEBUG) {
        console.log('💾 Database persisted to storage');
      }
    } catch (error) {
      console.warn('Failed to save visit database:', error);
      // Invalidate cache on save failure
      cacheValid = false;
    }
  }

  // Debounced setDB - batches rapid writes
  async function setDB(db) {
    // Always update cache immediately for fast reads
    dbCache = db;
    cacheValid = true;

    if (CONFIG.DEBOUNCE.ENABLED) {
      // Store pending write
      pendingDbWrite = db;

      // Clear existing timer
      if (debounceTimer) {
        clearTimeout(debounceTimer);
      }

      // Schedule debounced write
      debounceTimer = setTimeout(async () => {
        if (pendingDbWrite) {
          const dataToWrite = pendingDbWrite;
          pendingDbWrite = null;
          debounceTimer = null;
          await _persistDB(dataToWrite);
        }
      }, CONFIG.DEBOUNCE.DELAY);
    } else {
      // No debounce - write immediately
      await _persistDB(db);
    }
  }

  // Force flush pending writes (call before page unload)
  function flushPendingWrites() {
    if (pendingDbWrite) {
      if (CONFIG.DEBUG) {
        console.log('💾 Flushing pending database writes');
      }
      // Clear timer and write synchronously
      if (debounceTimer) {
        clearTimeout(debounceTimer);
        debounceTimer = null;
      }
      try {
        GM_setValue('visitDB', pendingDbWrite);
      } catch (error) {
        console.warn('Failed to flush pending writes:', error);
      }
      pendingDbWrite = null;
    }
  }

  // Invalidate cache when external changes might occur (multi-tab coordination)
  // This function is used when CONFIG.MULTI_TAB.ENABLED is true to ensure
  // cache consistency across multiple tabs
  function invalidateCache() {
    cacheValid = false;
    // Use setTimeout to prevent race conditions
    setTimeout(() => {
      dbCache = null;
    }, 0);
  }

  let currentUrl = normalizeUrl(location.href);

  function updateVisit() {
    // Skip tracking if current URL matches filter patterns
    if (shouldSkipUrl(location.href)) {
      if (CONFIG.DEBUG) {
        console.log(`🚫 Skipping visit tracking: ${location.href}`);
      }
      return;
    }

    const db = getDB();
    const now = new Date();
    const timestamp = createTimestamp(now);

    // Use logical assignment and modern destructuring
    db[currentUrl] ??= { count: 0, visits: [] };

    const urlData = db[currentUrl];
    urlData.count += 1;
    urlData.visits.unshift(timestamp);

    // Trim visits array if needed
    if (urlData.visits.length > CONFIG.MAX_VISITS_STORED) {
      urlData.visits.length = CONFIG.MAX_VISITS_STORED;
    }

    if (CONFIG.DEBUG) {
      const isNew = urlData.count === 1;
      console.log(isNew
        ? `🆕 New URL tracked: ${currentUrl}`
        : `🔄 URL revisited: ${currentUrl} (${urlData.count} times)`
      );
    }

    setDB(db);
    renderBadge(urlData);

    // Only register menu once to prevent duplicates
    if (!menuRegistered) {
      registerMenu();
      menuRegistered = true;
    }
  }

  function registerMenu() {
    // Register static menu items once to prevent duplicates
    GM_registerMenuCommand('⚙️ Settings', openSettingsPanel);
    GM_registerMenuCommand('👁️ Toggle Badge', toggleBadgeVisibility);
    GM_registerMenuCommand('📊 Export Data', exportData);
    GM_registerMenuCommand('📈 Show Statistics', showStatistics);
    GM_registerMenuCommand('🗑️ Clear Current Page', clearCurrentPage);
    GM_registerMenuCommand('💥 Clear All Data', clearAllData);
    GM_registerMenuCommand('🐛 Toggle Debug Mode', toggleDebugMode);
  }

  function exportData() {
    try {
      // Use cached DB for export - same data, no extra I/O
      const db = getDBCached();
      const dataStr = JSON.stringify(db, null, 2);
      const blob = new Blob([dataStr], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `visit-tracker-${new Date().toISOString().split('T')[0]}.json`;

      // Safely append to DOM
      if (document.body) {
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      } else {
        // Fallback for early DOM state
        a.click();
      }

      URL.revokeObjectURL(url);
    } catch (error) {
      console.error('Export failed:', error);
      alert('Failed to export data: ' + error.message);
    }
  }

  function showStatistics() {
    // Use cached DB for statistics - read-only operation
    const db = getDBCached();
    const urls = Object.keys(db);
    const totalUrls = urls.length;

    // Handle empty database
    if (totalUrls === 0) {
      alert('📈 Visit Tracker Statistics\n\n🌐 No websites tracked yet!\n\nStart browsing to collect visit data.');
      return;
    }

    const totalVisits = urls.reduce((sum, url) => sum + db[url].count, 0);

    // Find most visited site using optional chaining
    const mostVisited = urls.reduce((max, url) =>
      db[url].count > (db[max]?.count ?? 0) ? url : max, '');

    // Find oldest entry using optional chaining
    const oldestEntry = urls.reduce((oldest, url) => {
      const visits = db[url].visits;
      if (!visits?.length) return oldest;

      const lastVisit = visits[visits.length - 1];
      const oldestVisits = db[oldest]?.visits;
      if (!oldestVisits?.length) return url;

      const oldestLastVisit = oldestVisits[oldestVisits.length - 1];
      return lastVisit < oldestLastVisit ? url : oldest;
    }, '');

    const stats = `
📈 Visit Tracker Statistics

🌐 Total websites tracked: ${totalUrls}
👆 Total visits recorded: ${totalVisits}
🏆 Most visited: ${mostVisited} (${db[mostVisited]?.count ?? 0} visits)
⏰ Oldest tracked site: ${oldestEntry}
📅 Current page visits: ${db[currentUrl]?.count ?? 0}

Database size: ${Math.round(getActualDataSize(db) / 1024)} KB (UTF-8)
    `.trim();

    alert(stats);
  }

  function clearCurrentPage() {
    if (confirm(`Clear visit data for current page?\n\nURL: ${currentUrl}\nThis will only affect this page.`)) {
      const db = getDB();

      // Clear old data and immediately set new entry in single operation
      const now = new Date();
      const timestamp = createTimestamp(now);
      db[currentUrl] = { count: 1, visits: [timestamp] };
      setDB(db);

      // Update UI immediately with new data
      renderBadge(db[currentUrl]);

      alert('Current page data cleared! Counter reset to 1.');
    }
  }

  function clearAllData() {
    if (confirm('⚠️ WARNING: This will clear ALL visit data from ALL websites!\n\nAre you absolutely sure?')) {
      // Clear all data and immediately create new entry for current page in single operation
      const now = new Date();
      const timestamp = createTimestamp(now);
      const newDb = {};
      newDb[currentUrl] = { count: 1, visits: [timestamp] };
      setDB(newDb);

      // Update UI immediately with new data
      renderBadge(newDb[currentUrl]);

      alert('All visit data cleared! Current page counter reset to 1.');
    }
  }

  function ensureBadgeStyles() {
    if (document.getElementById('vt-hover-styles')) return;
    const css = `
      .vt-badge {
        position: fixed;
        right: ${CONFIG.BADGE_POSITION.right};
        bottom: ${CONFIG.BADGE_POSITION.bottom};
        z-index: 2147483647;
        font-family: system-ui, sans-serif;
        cursor: pointer;
        transition: all 0.3s ease;
      }
      .vt-badge.hidden {
        opacity: 0;
        pointer-events: none;
        transform: scale(0.8);
      }
      .vt-link {
        display: inline-block;
        padding: 6px 10px;
        border-radius: 9999px;
        background: rgba(20,20,20,0.9);
        color: #fff !important;
        font-size: 12px;
        box-shadow: 0 4px 14px rgba(0,0,0,0.2);
        opacity: 0.85;
        transition: opacity 0.2s ease;
      }
      .vt-badge:hover .vt-link { opacity: 1; }
      .vt-tooltip {
        position: absolute;
        bottom: 120%;
        right: 0;
        background: #111;
        color: #fff;
        border-radius: 10px;
        padding: 8px 10px;
        font-size: 12px;
        white-space: nowrap;
        box-shadow: 0 10px 25px rgba(0,0,0,0.35);
        opacity: 0;
        transform: translateY(6px);
        transition: opacity 140ms ease, transform 140ms ease;
        pointer-events: none;
      }
      .vt-badge:hover .vt-tooltip {
        opacity: 1;
        transform: translateY(0);
      }
      .vt-tooltip .vt-line { display: block; }
    `;
    const style = document.createElement('style');
    style.id = 'vt-hover-styles';
    style.textContent = css;
    document.documentElement.appendChild(style);
  }

  function renderBadge(data) {
    ensureBadgeStyles();

    let badge = document.getElementById('vt-hover-badge');
    if (!badge) {
      badge = document.createElement('div');
      badge.id = 'vt-hover-badge';
      badge.className = 'vt-badge';
      badge.innerHTML = `
        <a class="vt-link" href="javascript:void(0)"></a>
        <div class="vt-tooltip"></div>
      `;

      // Add click handler for toggle visibility
      badge.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        toggleBadgeVisibility();
      });

      document.documentElement.appendChild(badge);
    }

    // Apply visibility state
    if (!badgeVisible) {
      badge.classList.add('hidden');
    } else {
      badge.classList.remove('hidden');
    }

    badge.querySelector('.vt-link').textContent = `Visit: ${shortenNumber(data.count)}`;

    const tooltip = badge.querySelector('.vt-tooltip');
    tooltip.innerHTML = `<span class="vt-line">Visit: ${data.count}</span>`;

    // Handle empty visits array - format timestamps for display
    if (data.visits && data.visits.length > 0) {
      data.visits.forEach((timestamp, i) => {
        const formattedTime = formatTimestamp(timestamp);
        tooltip.innerHTML += `<span class="vt-line">${i + 1}. ${formattedTime}</span>`;
      });
    } else {
      tooltip.innerHTML += `<span class="vt-line">No visit history</span>`;
    }
  }

  function toggleBadgeVisibility() {
    badgeVisible = !badgeVisible;
    const badge = document.getElementById('vt-hover-badge');
    if (badge) {
      if (badgeVisible) {
        badge.classList.remove('hidden');
      } else {
        badge.classList.add('hidden');
      }
    }

    // Save state to GM storage
    try {
      GM_setValue('badgeVisible', badgeVisible);
    } catch (error) {
      console.warn('Failed to save badge visibility state:', error);
    }
  }

  function toggleUrlFiltering() {
    CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES = !CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES;

    // Save state to GM storage
    try {
      GM_setValue('urlFiltering', CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES);
    } catch (error) {
      console.warn('Failed to save URL filtering state:', error);
    }

    const status = CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES ? 'enabled' : 'disabled';
    alert(`🚫 URL Filtering ${status}!\n\nUtility pages (cookies, auth, etc.) filtering is now ${status}.`);
  }

  function toggleSearchCleaning() {
    CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS = !CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS;

    // Save state to GM storage
    try {
      GM_setValue('searchCleaning', CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS);
    } catch (error) {
      console.warn('Failed to save search cleaning state:', error);
    }

    const status = CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS ? 'enabled' : 'disabled';
    alert(`🔍 Search URL Cleaning ${status}!\n\nSearch URLs will now ${CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS ? 'be cleaned (grouped by query)' : 'be tracked as-is (separate tracking)'}.`);
  }

  function toggleDebugMode() {
    CONFIG.DEBUG = !CONFIG.DEBUG;

    // Save state to GM storage
    try {
      GM_setValue('debugMode', CONFIG.DEBUG);
    } catch (error) {
      console.warn('Failed to save debug mode state:', error);
    }

    const status = CONFIG.DEBUG ? 'enabled' : 'disabled';
    alert(`🐛 Debug mode ${status}!\n\nDebug logging is now ${status}.`);

    if (CONFIG.DEBUG) {
      console.log('🐛 Visit Tracker Debug Mode: ENABLED');
    }
  }

  // ============================================
  // SETTINGS PANEL UI
  // ============================================

  let settingsPanel = null;
  let settingsPanelOpen = false;

  function getSettingsPanelStyles() {
    return `
      .vt-settings-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.6);
        backdrop-filter: blur(4px);
        z-index: 2147483646;
        opacity: 0;
        transition: opacity 0.2s ease;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .vt-settings-overlay.visible {
        opacity: 1;
      }
      .vt-settings-panel {
        background: linear-gradient(145deg, #1a1a2e 0%, #16213e 100%);
        border-radius: 16px;
        box-shadow: 0 25px 80px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.1);
        width: 480px;
        max-width: 95vw;
        max-height: 85vh;
        overflow: hidden;
        display: flex;
        flex-direction: column;
        transform: scale(0.9) translateY(20px);
        transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
        font-family: system-ui, -apple-system, sans-serif;
      }
      .vt-settings-overlay.visible .vt-settings-panel {
        transform: scale(1) translateY(0);
      }
      .vt-settings-header {
        padding: 20px 24px;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        display: flex;
        align-items: center;
        justify-content: space-between;
        background: rgba(255, 255, 255, 0.03);
      }
      .vt-settings-title {
        font-size: 18px;
        font-weight: 600;
        color: #fff;
        display: flex;
        align-items: center;
        gap: 10px;
      }
      .vt-settings-title::before {
        content: '⚙️';
        font-size: 20px;
      }
      .vt-settings-close {
        width: 32px;
        height: 32px;
        border: none;
        background: rgba(255, 255, 255, 0.1);
        border-radius: 8px;
        color: #fff;
        font-size: 18px;
        cursor: pointer;
        transition: all 0.15s ease;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .vt-settings-close:hover {
        background: rgba(239, 68, 68, 0.8);
        transform: scale(1.05);
      }
      .vt-settings-body {
        padding: 16px 24px;
        overflow-y: auto;
        flex: 1;
      }
      .vt-settings-section {
        margin-bottom: 20px;
      }
      .vt-settings-section:last-child {
        margin-bottom: 0;
      }
      .vt-section-title {
        font-size: 12px;
        font-weight: 600;
        color: #818cf8;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        margin-bottom: 12px;
        display: flex;
        align-items: center;
        gap: 6px;
      }
      .vt-setting-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 12px 14px;
        background: rgba(255, 255, 255, 0.03);
        border-radius: 10px;
        margin-bottom: 8px;
        transition: background 0.15s ease;
      }
      .vt-setting-row:hover {
        background: rgba(255, 255, 255, 0.06);
      }
      .vt-setting-label {
        display: flex;
        flex-direction: column;
        gap: 2px;
      }
      .vt-setting-name {
        font-size: 14px;
        color: #e2e8f0;
        font-weight: 500;
      }
      .vt-setting-desc {
        font-size: 11px;
        color: #64748b;
      }
      .vt-toggle {
        position: relative;
        width: 44px;
        height: 24px;
        background: rgba(255, 255, 255, 0.15);
        border-radius: 12px;
        cursor: pointer;
        transition: background 0.2s ease;
      }
      .vt-toggle.active {
        background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
      }
      .vt-toggle::after {
        content: '';
        position: absolute;
        top: 3px;
        left: 3px;
        width: 18px;
        height: 18px;
        background: #fff;
        border-radius: 50%;
        transition: transform 0.2s ease;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
      }
      .vt-toggle.active::after {
        transform: translateX(20px);
      }
      .vt-input-number {
        width: 80px;
        padding: 6px 10px;
        background: rgba(30, 30, 50, 0.9) !important;
        border: 1px solid rgba(255, 255, 255, 0.2);
        border-radius: 6px;
        color: #ffffff !important;
        font-size: 13px;
        font-weight: 500;
        text-align: center;
        transition: all 0.15s ease;
        -webkit-appearance: textfield;
        -moz-appearance: textfield;
        appearance: textfield;
      }
      .vt-input-number::-webkit-outer-spin-button,
      .vt-input-number::-webkit-inner-spin-button {
        -webkit-appearance: none;
        margin: 0;
      }
      .vt-input-number:focus {
        outline: none;
        border-color: #6366f1;
        background: rgba(99, 102, 241, 0.2) !important;
        box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
      }
      .vt-settings-footer {
        padding: 16px 24px;
        border-top: 1px solid rgba(255, 255, 255, 0.1);
        display: flex;
        gap: 12px;
        background: rgba(0, 0, 0, 0.2);
      }
      .vt-btn {
        flex: 1;
        padding: 10px 16px;
        border: none;
        border-radius: 8px;
        font-size: 13px;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.15s ease;
      }
      .vt-btn-primary {
        background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
        color: #fff;
      }
      .vt-btn-primary:hover {
        transform: translateY(-1px);
        box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
      }
      .vt-btn-secondary {
        background: rgba(255, 255, 255, 0.1);
        color: #e2e8f0;
      }
      .vt-btn-secondary:hover {
        background: rgba(255, 255, 255, 0.15);
      }
      .vt-toast {
        position: fixed;
        bottom: 24px;
        left: 50%;
        transform: translateX(-50%) translateY(100px);
        background: linear-gradient(135deg, #10b981 0%, #059669 100%);
        color: #fff;
        padding: 12px 24px;
        border-radius: 10px;
        font-size: 14px;
        font-weight: 500;
        box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
        z-index: 2147483647;
        opacity: 0;
        transition: all 0.3s ease;
      }
      .vt-toast.visible {
        opacity: 1;
        transform: translateX(-50%) translateY(0);
      }
    `;
  }

  function createSettingsPanel() {
    // Create overlay
    const overlay = document.createElement('div');
    overlay.className = 'vt-settings-overlay';
    overlay.id = 'vt-settings-overlay';

    // Create panel HTML
    overlay.innerHTML = `
      <div class="vt-settings-panel">
        <div class="vt-settings-header">
          <div class="vt-settings-title">URL Visit Tracker Settings</div>
          <button class="vt-settings-close" id="vt-close-settings">✕</button>
        </div>
        <div class="vt-settings-body">
          <!-- General Section -->
          <div class="vt-settings-section">
            <div class="vt-section-title">🎯 General</div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Show Badge</span>
                <span class="vt-setting-desc">Display visit counter badge on screen</span>
              </div>
              <div class="vt-toggle ${badgeVisible ? 'active' : ''}" data-setting="badgeVisible"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Debug Mode</span>
                <span class="vt-setting-desc">Enable console logging for debugging</span>
              </div>
              <div class="vt-toggle ${CONFIG.DEBUG ? 'active' : ''}" data-setting="debug"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Hover Delay</span>
                <span class="vt-setting-desc">Delay before showing tooltip (ms)</span>
              </div>
              <input type="number" class="vt-input-number" value="${CONFIG.HOVER_DELAY}" data-setting="hoverDelay" min="0" max="5000" step="100">
            </div>
          </div>

          <!-- URL Normalization Section -->
          <div class="vt-settings-section">
            <div class="vt-section-title">🔗 URL Normalization</div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Remove Query Params</span>
                <span class="vt-setting-desc">Ignore ?key=value in URLs</span>
              </div>
              <div class="vt-toggle ${CONFIG.NORMALIZE_URL.REMOVE_QUERY ? 'active' : ''}" data-setting="removeQuery"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Remove Hash</span>
                <span class="vt-setting-desc">Ignore #section in URLs</span>
              </div>
              <div class="vt-toggle ${CONFIG.NORMALIZE_URL.REMOVE_HASH ? 'active' : ''}" data-setting="removeHash"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Clean Search URLs</span>
                <span class="vt-setting-desc">Group search results by query</span>
              </div>
              <div class="vt-toggle ${CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS ? 'active' : ''}" data-setting="cleanSearchUrls"></div>
            </div>
          </div>

          <!-- URL Filtering Section -->
          <div class="vt-settings-section">
            <div class="vt-section-title">🚫 URL Filtering</div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Skip Utility Pages</span>
                <span class="vt-setting-desc">Don't track login, cookie pages, etc.</span>
              </div>
              <div class="vt-toggle ${CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES ? 'active' : ''}" data-setting="skipUtilityPages"></div>
            </div>
          </div>

          <!-- Performance Section -->
          <div class="vt-settings-section">
            <div class="vt-section-title">⚡ Performance</div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Debounce Writes</span>
                <span class="vt-setting-desc">Batch database writes for better performance</span>
              </div>
              <div class="vt-toggle ${CONFIG.DEBOUNCE.ENABLED ? 'active' : ''}" data-setting="debounceEnabled"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Debounce Delay</span>
                <span class="vt-setting-desc">Delay before writing to storage (ms)</span>
              </div>
              <input type="number" class="vt-input-number" value="${CONFIG.DEBOUNCE.DELAY}" data-setting="debounceDelay" min="100" max="5000" step="100">
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Use Web Worker</span>
                <span class="vt-setting-desc">Run cleanup in background thread</span>
              </div>
              <div class="vt-toggle ${CONFIG.WEB_WORKER.ENABLED ? 'active' : ''}" data-setting="webWorkerEnabled"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Pause When Hidden</span>
                <span class="vt-setting-desc">Stop polling when tab is not visible</span>
              </div>
              <div class="vt-toggle ${CONFIG.POLLING.PAUSE_WHEN_HIDDEN ? 'active' : ''}" data-setting="pauseWhenHidden"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Adaptive Polling</span>
                <span class="vt-setting-desc">Adjust polling frequency based on activity</span>
              </div>
              <div class="vt-toggle ${CONFIG.POLLING.ADAPTIVE ? 'active' : ''}" data-setting="adaptivePolling"></div>
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Poll Interval</span>
                <span class="vt-setting-desc">URL change check interval (ms)</span>
              </div>
              <input type="number" class="vt-input-number" value="${CONFIG.POLL_INTERVAL}" data-setting="pollInterval" min="1000" max="30000" step="1000">
            </div>
          </div>

          <!-- Storage Section -->
          <div class="vt-settings-section">
            <div class="vt-section-title">💾 Storage</div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Max URLs Stored</span>
                <span class="vt-setting-desc">Maximum number of URLs to track</span>
              </div>
              <input type="number" class="vt-input-number" value="${CONFIG.MAX_URLS_STORED}" data-setting="maxUrlsStored" min="1000" max="50000" step="1000">
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Max Visits per URL</span>
                <span class="vt-setting-desc">Visit timestamps to keep per URL</span>
              </div>
              <input type="number" class="vt-input-number" value="${CONFIG.MAX_VISITS_STORED}" data-setting="maxVisitsStored" min="5" max="100" step="5">
            </div>
            <div class="vt-setting-row">
              <div class="vt-setting-label">
                <span class="vt-setting-name">Multi-Tab Sync</span>
                <span class="vt-setting-desc">Sync data across browser tabs</span>
              </div>
              <div class="vt-toggle ${CONFIG.MULTI_TAB.ENABLED ? 'active' : ''}" data-setting="multiTabEnabled"></div>
            </div>
          </div>
        </div>
        <div class="vt-settings-footer">
          <button class="vt-btn vt-btn-secondary" id="vt-reset-settings">Reset to Defaults</button>
          <button class="vt-btn vt-btn-primary" id="vt-save-settings">Save Settings</button>
        </div>
      </div>
    `;

    return overlay;
  }

  function openSettingsPanel() {
    if (settingsPanelOpen) return;

    // Add styles if not already added
    if (!document.getElementById('vt-settings-styles')) {
      const style = document.createElement('style');
      style.id = 'vt-settings-styles';
      style.textContent = getSettingsPanelStyles();
      document.head.appendChild(style);
    }

    // Create and add panel
    settingsPanel = createSettingsPanel();
    document.body.appendChild(settingsPanel);
    settingsPanelOpen = true;

    // Trigger animation
    requestAnimationFrame(() => {
      settingsPanel.classList.add('visible');
    });

    // Add event listeners
    setupSettingsEventListeners();
  }

  // ESC key handler for settings panel (moved outside for proper cleanup)
  let settingsEscHandler = null;

  function closeSettingsPanel() {
    if (!settingsPanel || !settingsPanelOpen) return;

    // Remove ESC handler to prevent memory leak
    if (settingsEscHandler) {
      document.removeEventListener('keydown', settingsEscHandler);
      settingsEscHandler = null;
    }

    settingsPanel.classList.remove('visible');

    setTimeout(() => {
      if (settingsPanel && settingsPanel.parentNode) {
        settingsPanel.parentNode.removeChild(settingsPanel);
      }
      settingsPanel = null;
      settingsPanelOpen = false;
    }, 200);
  }

  function setupSettingsEventListeners() {
    // Close button
    document.getElementById('vt-close-settings').addEventListener('click', closeSettingsPanel);

    // Click outside to close
    settingsPanel.addEventListener('click', (e) => {
      if (e.target === settingsPanel) {
        closeSettingsPanel();
      }
    });

    // ESC key to close - use the outer variable for proper cleanup
    settingsEscHandler = (e) => {
      if (e.key === 'Escape' && settingsPanelOpen) {
        closeSettingsPanel();
      }
    };
    document.addEventListener('keydown', settingsEscHandler);

    // Toggle switches
    settingsPanel.querySelectorAll('.vt-toggle').forEach(toggle => {
      toggle.addEventListener('click', () => {
        toggle.classList.toggle('active');
      });
    });

    // Save button
    document.getElementById('vt-save-settings').addEventListener('click', saveSettings);

    // Reset button
    document.getElementById('vt-reset-settings').addEventListener('click', resetSettings);
  }

  function saveSettings() {
    try {
      // Read all settings from UI
      const getToggle = (name) => settingsPanel.querySelector(`[data-setting="${name}"]`).classList.contains('active');
      const getNumber = (name) => parseInt(settingsPanel.querySelector(`[data-setting="${name}"]`).value, 10);

      // Update CONFIG
      badgeVisible = getToggle('badgeVisible');
      CONFIG.DEBUG = getToggle('debug');
      CONFIG.HOVER_DELAY = getNumber('hoverDelay');
      CONFIG.NORMALIZE_URL.REMOVE_QUERY = getToggle('removeQuery');
      CONFIG.NORMALIZE_URL.REMOVE_HASH = getToggle('removeHash');
      CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS = getToggle('cleanSearchUrls');
      CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES = getToggle('skipUtilityPages');
      CONFIG.DEBOUNCE.ENABLED = getToggle('debounceEnabled');
      CONFIG.DEBOUNCE.DELAY = getNumber('debounceDelay');
      CONFIG.WEB_WORKER.ENABLED = getToggle('webWorkerEnabled');
      CONFIG.POLLING.PAUSE_WHEN_HIDDEN = getToggle('pauseWhenHidden');
      CONFIG.POLLING.ADAPTIVE = getToggle('adaptivePolling');
      CONFIG.POLL_INTERVAL = getNumber('pollInterval');
      CONFIG.MAX_URLS_STORED = getNumber('maxUrlsStored');
      CONFIG.MAX_VISITS_STORED = getNumber('maxVisitsStored');
      CONFIG.MULTI_TAB.ENABLED = getToggle('multiTabEnabled');

      // Persist to GM storage
      GM_setValue('badgeVisible', badgeVisible);
      GM_setValue('debugMode', CONFIG.DEBUG);
      GM_setValue('hoverDelay', CONFIG.HOVER_DELAY);
      GM_setValue('removeQuery', CONFIG.NORMALIZE_URL.REMOVE_QUERY);
      GM_setValue('removeHash', CONFIG.NORMALIZE_URL.REMOVE_HASH);
      GM_setValue('searchCleaning', CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS);
      GM_setValue('urlFiltering', CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES);
      GM_setValue('debounceEnabled', CONFIG.DEBOUNCE.ENABLED);
      GM_setValue('debounceDelay', CONFIG.DEBOUNCE.DELAY);
      GM_setValue('webWorkerEnabled', CONFIG.WEB_WORKER.ENABLED);
      GM_setValue('pauseWhenHidden', CONFIG.POLLING.PAUSE_WHEN_HIDDEN);
      GM_setValue('adaptivePolling', CONFIG.POLLING.ADAPTIVE);
      GM_setValue('pollInterval', CONFIG.POLL_INTERVAL);
      GM_setValue('maxUrlsStored', CONFIG.MAX_URLS_STORED);
      GM_setValue('maxVisitsStored', CONFIG.MAX_VISITS_STORED);
      GM_setValue('multiTabEnabled', CONFIG.MULTI_TAB.ENABLED);

      // Update badge visibility
      const badge = document.getElementById('vt-hover-badge');
      if (badge) {
        badge.classList.toggle('hidden', !badgeVisible);
      }

      // Restart polling with new settings
      stopPolling();
      startPolling();

      // Show success toast
      showToast('✅ Settings saved successfully!');

      // Close panel
      closeSettingsPanel();

    } catch (error) {
      console.error('Failed to save settings:', error);
      showToast('❌ Failed to save settings');
    }
  }

  function resetSettings() {
    if (!confirm('Reset all settings to default values?')) return;

    // Default values
    const defaults = {
      badgeVisible: true,
      debug: false,
      hoverDelay: 1000,
      removeQuery: false,
      removeHash: true,
      cleanSearchUrls: true,
      skipUtilityPages: true,
      debounceEnabled: true,
      debounceDelay: 1000,
      webWorkerEnabled: true,
      pauseWhenHidden: true,
      adaptivePolling: true,
      pollInterval: 5000,
      maxUrlsStored: 10000,
      maxVisitsStored: 20,
      multiTabEnabled: false
    };

    // Update UI
    Object.entries(defaults).forEach(([key, value]) => {
      const element = settingsPanel.querySelector(`[data-setting="${key}"]`);
      if (element) {
        if (element.classList.contains('vt-toggle')) {
          element.classList.toggle('active', value);
        } else {
          element.value = value;
        }
      }
    });

    showToast('🔄 Settings reset to defaults');
  }

  function showToast(message) {
    // Remove existing toast
    const existingToast = document.querySelector('.vt-toast');
    if (existingToast) {
      existingToast.remove();
    }

    const toast = document.createElement('div');
    toast.className = 'vt-toast';
    toast.textContent = message;
    document.body.appendChild(toast);

    requestAnimationFrame(() => {
      toast.classList.add('visible');
    });

    setTimeout(() => {
      toast.classList.remove('visible');
      setTimeout(() => toast.remove(), 300);
    }, 2500);
  }

  // Load all saved settings on initialization
  function loadSavedSettings() {
    try {
      badgeVisible = GM_getValue('badgeVisible', CONFIG.BADGE_VISIBLE);
      CONFIG.DEBUG = GM_getValue('debugMode', CONFIG.DEBUG);
      CONFIG.HOVER_DELAY = GM_getValue('hoverDelay', CONFIG.HOVER_DELAY);
      CONFIG.NORMALIZE_URL.REMOVE_QUERY = GM_getValue('removeQuery', CONFIG.NORMALIZE_URL.REMOVE_QUERY);
      CONFIG.NORMALIZE_URL.REMOVE_HASH = GM_getValue('removeHash', CONFIG.NORMALIZE_URL.REMOVE_HASH);
      CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS = GM_getValue('searchCleaning', CONFIG.NORMALIZE_URL.CLEAN_SEARCH_URLS);
      CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES = GM_getValue('urlFiltering', CONFIG.URL_FILTERS.SKIP_UTILITY_PAGES);
      CONFIG.DEBOUNCE.ENABLED = GM_getValue('debounceEnabled', CONFIG.DEBOUNCE.ENABLED);
      CONFIG.DEBOUNCE.DELAY = GM_getValue('debounceDelay', CONFIG.DEBOUNCE.DELAY);
      CONFIG.WEB_WORKER.ENABLED = GM_getValue('webWorkerEnabled', CONFIG.WEB_WORKER.ENABLED);
      CONFIG.POLLING.PAUSE_WHEN_HIDDEN = GM_getValue('pauseWhenHidden', CONFIG.POLLING.PAUSE_WHEN_HIDDEN);
      CONFIG.POLLING.ADAPTIVE = GM_getValue('adaptivePolling', CONFIG.POLLING.ADAPTIVE);
      CONFIG.POLL_INTERVAL = GM_getValue('pollInterval', CONFIG.POLL_INTERVAL);
      CONFIG.MAX_URLS_STORED = GM_getValue('maxUrlsStored', CONFIG.MAX_URLS_STORED);
      CONFIG.MAX_VISITS_STORED = GM_getValue('maxVisitsStored', CONFIG.MAX_VISITS_STORED);
      CONFIG.MULTI_TAB.ENABLED = GM_getValue('multiTabEnabled', CONFIG.MULTI_TAB.ENABLED);
    } catch (error) {
      console.warn('Failed to load saved settings:', error);
    }
  }

  // Rate limiting for URL changes with pending mechanism
  let lastUrlChangeTime = 0;
  let pendingUrlChange = null;
  let pendingTimeout = null;
  const URL_CHANGE_MIN_INTERVAL = 500; // Minimum 500ms between URL changes

  function onUrlChange() {
    const newUrl = normalizeUrl(location.href);
    if (newUrl === currentUrl) return;

    // Skip tracking if URL matches filter patterns
    if (shouldSkipUrl(location.href)) {
      if (CONFIG.DEBUG) {
        console.log(`🚫 Skipping URL tracking: ${location.href}`);
      }
      return;
    }

    // Increment activity counter for adaptive polling
    if (CONFIG.POLLING.ADAPTIVE) {
      activityCount = Math.min(10, activityCount + 2); // Cap at 10, add 2 for URL change
    }

    const now = Date.now();
    const timeSinceLastChange = now - lastUrlChangeTime;

    if (timeSinceLastChange < URL_CHANGE_MIN_INTERVAL) {
      if (CONFIG.DEBUG) {
        console.log(`⏰ URL change rate limited, scheduling: ${currentUrl} → ${newUrl}`);
      }

      // Store the pending change without updating currentUrl yet
      pendingUrlChange = newUrl;

      // Clear any existing pending timeout
      if (pendingTimeout) {
        clearTimeout(pendingTimeout);
      }

      // Schedule the change for when rate limit expires
      const remainingTime = URL_CHANGE_MIN_INTERVAL - timeSinceLastChange;
      pendingTimeout = setTimeout(() => {
        if (pendingUrlChange && pendingUrlChange !== currentUrl) {
          if (CONFIG.DEBUG) {
            console.log(`⏰ Processing pending URL change: ${currentUrl} → ${pendingUrlChange}`);
          }
          const savedPendingUrl = pendingUrlChange;
          pendingUrlChange = null;
          pendingTimeout = null;

          // Process the pending change
          currentUrl = savedPendingUrl;
          lastUrlChangeTime = Date.now();
          updateVisit();
        }
      }, remainingTime + 10); // +10ms buffer

      return;
    }

    // Clear any pending changes since we're processing immediately
    if (pendingTimeout) {
      clearTimeout(pendingTimeout);
      pendingTimeout = null;
      pendingUrlChange = null;
    }

    if (CONFIG.DEBUG) {
      console.log(`🌐 URL changed: ${currentUrl} → ${newUrl}`);
    }

    currentUrl = newUrl;
    lastUrlChangeTime = now;
    updateVisit();
  }

  function installUrlObservers() {
    // Enhanced history hooks with rate limiting
    const _pushState = history.pushState;
    const _replaceState = history.replaceState;

    history.pushState = function (...args) {
      const result = _pushState.apply(this, args);
      // Use setTimeout to avoid immediate execution conflicts
      setTimeout(onUrlChange, 50);
      return result;
    };

    history.replaceState = function (...args) {
      const result = _replaceState.apply(this, args);
      setTimeout(onUrlChange, 50);
      return result;
    };

    // Standard event listeners
    window.addEventListener('popstate', onUrlChange);
    window.addEventListener('hashchange', onUrlChange);

    // Optimized MutationObserver with focused title tracking
    let mutationTimeout = null;
    const mo = new MutationObserver((mutations) => {
      // Throttle mutation processing to avoid spam
      if (mutationTimeout) return;

      mutationTimeout = setTimeout(() => {
        mutationTimeout = null;
        let titleChanged = false;

        for (const mutation of mutations) {
          // Only check mutations that could affect title
          if (mutation.type === 'childList') {
            // Case 1: Title element added/removed from head
            const titleInAdded = Array.from(mutation.addedNodes).some(node =>
              node.nodeName === 'TITLE'
            );
            const titleInRemoved = Array.from(mutation.removedNodes).some(node =>
              node.nodeName === 'TITLE'
            );

            // Case 2: Direct title element changes
            if (mutation.target.nodeName === 'TITLE') {
              titleChanged = true;
              if (CONFIG.DEBUG) {
                console.log('📝 Title childList mutation detected:', mutation);
              }
              break;
            }

            if (titleInAdded || titleInRemoved) {
              titleChanged = true;
              if (CONFIG.DEBUG) {
                console.log('📝 Title element added/removed:', mutation);
              }
              break;
            }
          }

          // Case 3: Character data changed in title's text nodes (more targeted)
          else if (mutation.type === 'characterData' &&
            mutation.target.parentNode?.nodeName === 'TITLE') {
            titleChanged = true;
            if (CONFIG.DEBUG) {
              console.log('📝 Title characterData mutation detected:', mutation);
            }
            break;
          }
        }

        if (titleChanged) {
          if (CONFIG.DEBUG) {
            console.log('📝 Title change detected, triggering URL change check');
          }
          onUrlChange();
        }
      }, 150); // Slightly increased debounce for better performance
    });

    // Safely observe document.head with focused title tracking
    if (document.head) {
      mo.observe(document.head, {
        childList: true,      // Detect title element addition/removal
        subtree: false,       // Only direct children for better performance
        characterData: false  // Handle characterData separately for title only
      });

      // Separate observer for title content changes
      const titleEl = document.querySelector('title');
      if (titleEl) {
        mo.observe(titleEl, {
          childList: true,
          characterData: true,
          subtree: true
        });
      }
    } else {
      // Fallback: observe document for head creation (minimal scope)
      mo.observe(document, {
        childList: true,
        subtree: false
      });
    }

    // Initialize polling
    startPolling();
  }

  // Lazy tooltip initialization - only create when first needed
  let tooltip = null;
  let tooltipInitialized = false;

  // Create and initialize tooltip element (called lazily on first hover)
  function initializeTooltip() {
    if (tooltipInitialized) return tooltip;

    tooltip = document.createElement('div');
    // Apply styles using individual properties for better compatibility
    Object.assign(tooltip.style, {
      position: 'fixed',
      padding: '6px 8px',
      fontSize: '12px',
      fontFamily: 'system-ui, sans-serif',
      background: 'rgba(20, 20, 20, 0.9)',
      color: 'white',
      borderRadius: '6px',
      pointerEvents: 'none',
      whiteSpace: 'nowrap',
      zIndex: '999999',
      opacity: '0',
      transition: 'opacity 0.15s ease'
    });

    // Append to DOM
    if (document.body) {
      try {
        document.body.appendChild(tooltip);
        tooltipInitialized = true;
        if (CONFIG.DEBUG) {
          console.log('📋 Tooltip initialized lazily on first hover');
        }
      } catch (error) {
        console.warn('Failed to append tooltip to body:', error);
      }
    } else {
      // Fallback for early DOM state (shouldn't happen with lazy init)
      document.addEventListener('DOMContentLoaded', () => {
        try {
          if (document.body && !document.body.contains(tooltip)) {
            document.body.appendChild(tooltip);
            tooltipInitialized = true;
          }
        } catch (error) {
          console.warn('Failed to append tooltip on DOMContentLoaded:', error);
        }
      }, { passive: true, once: true });
    }

    return tooltip;
  }

  // Get tooltip element, initializing if needed
  function getTooltip() {
    if (!tooltipInitialized) {
      initializeTooltip();
    }
    return tooltip;
  }

  let hoverTimer;
  let currentHoveredLink = null;
  let rafId = null; // RequestAnimationFrame ID for smooth tooltip movement
  let pendingTooltipPosition = null; // Store pending position updates
  let tooltipAutoHideTimer = null; // Auto-hide timer to prevent stuck tooltips
  let tooltipValidationTimer = null; // Timer to validate tooltip state
  let lastMousePosition = { x: 0, y: 0 }; // Track last mouse position

  // Configuration for tooltip anti-stick measures
  const TOOLTIP_CONFIG = {
    AUTO_HIDE_DELAY: 10000,   // Auto-hide after 10 seconds
    VALIDATION_INTERVAL: 500, // Check every 500ms if tooltip should still be visible
    STALE_THRESHOLD: 2000     // Consider tooltip stale if no mouse movement for 2s
  };

  function showTooltip(e, linkUrl) {
    // Initialize tooltip lazily on first use
    const tip = getTooltip();
    if (!tip) return; // Safety check

    const key = normalizeUrl(linkUrl);
    // Use cached DB for hot path performance - no storage I/O!
    const db = getDBCached();
    const data = db[key];

    // Clear previous content safely
    tip.textContent = '';

    if (!data) {
      tip.textContent = 'No visits recorded';
    } else {
      // Create elements safely instead of using innerHTML
      const visitLine = document.createElement('div');
      visitLine.textContent = `Visit: ${shortenNumber(data.count)}`;

      const lastLine = document.createElement('div');
      // Format timestamp for display using optional chaining
      const lastVisit = data.visits?.[0] ? formatTimestamp(data.visits[0]) : 'Never';
      lastLine.textContent = `Last: ${lastVisit}`;

      tip.appendChild(visitLine);
      tip.appendChild(lastLine);
    }

    // Set initial position
    updateTooltipPosition(e.clientX, e.clientY);
    tip.style.opacity = 1;

    // Start auto-hide timer as safety net
    startAutoHideTimer();

    // Start validation timer to check if tooltip should still be visible
    startValidationTimer();
  }

  // Auto-hide timer - safety net to prevent stuck tooltips
  function startAutoHideTimer() {
    clearAutoHideTimer();
    tooltipAutoHideTimer = setTimeout(() => {
      if (CONFIG.DEBUG) {
        console.log('⏰ Tooltip auto-hide triggered after timeout');
      }
      hideTooltip();
    }, TOOLTIP_CONFIG.AUTO_HIDE_DELAY);
  }

  function clearAutoHideTimer() {
    if (tooltipAutoHideTimer) {
      clearTimeout(tooltipAutoHideTimer);
      tooltipAutoHideTimer = null;
    }
  }

  // Validation timer - periodically check if tooltip should still be visible
  function startValidationTimer() {
    clearValidationTimer();
    tooltipValidationTimer = setInterval(() => {
      if (!validateTooltipState()) {
        if (CONFIG.DEBUG) {
          console.log('🔍 Tooltip validation failed, hiding tooltip');
        }
        hideTooltip();
      }
    }, TOOLTIP_CONFIG.VALIDATION_INTERVAL);
  }

  function clearValidationTimer() {
    if (tooltipValidationTimer) {
      clearInterval(tooltipValidationTimer);
      tooltipValidationTimer = null;
    }
  }

  // Validate if tooltip should still be visible
  function validateTooltipState() {
    // No link being tracked - tooltip shouldn't be visible
    if (!currentHoveredLink) {
      return false;
    }

    // Link was removed from DOM
    if (!document.body.contains(currentHoveredLink)) {
      if (CONFIG.DEBUG) {
        console.log('🔗 Link removed from DOM, invalidating tooltip');
      }
      return false;
    }

    // Check if mouse is still over the link using elementFromPoint
    const elementAtMouse = document.elementFromPoint(lastMousePosition.x, lastMousePosition.y);
    if (elementAtMouse) {
      const linkAtMouse = safeClosest(elementAtMouse, 'a[href]');
      if (linkAtMouse !== currentHoveredLink) {
        if (CONFIG.DEBUG) {
          console.log('🔗 Mouse no longer over tracked link');
        }
        return false;
      }
    }

    return true;
  }

  function updateTooltipPosition(x, y) {
    // Store the position to be updated in the next frame
    pendingTooltipPosition = { x: x + 12, y: y + 12 };

    // Cancel previous frame if it exists
    if (rafId) {
      cancelAnimationFrame(rafId);
    }

    // Schedule position update for next frame
    rafId = requestAnimationFrame(() => {
      if (pendingTooltipPosition) {
        const tip = getTooltip();
        if (tip) {
          tip.style.left = pendingTooltipPosition.x + 'px';
          tip.style.top = pendingTooltipPosition.y + 'px';
        }
        pendingTooltipPosition = null;
      }
      rafId = null;
    });
  }

  function hideTooltip() {
    const tip = getTooltip();
    if (tip) {
      tip.style.opacity = 0;
    }
    currentHoveredLink = null;

    // Cancel any pending animation frame
    if (rafId) {
      cancelAnimationFrame(rafId);
      rafId = null;
    }
    pendingTooltipPosition = null;

    // Clear all timers
    clearAutoHideTimer();
    clearValidationTimer();

    // Ensure mousemove listener is properly removed
    document.removeEventListener('mousemove', moveTooltip);
  }

  function moveTooltip(e) {
    // Track mouse position for validation
    lastMousePosition.x = e.clientX;
    lastMousePosition.y = e.clientY;

    // Reset auto-hide timer on mouse movement (user is still active)
    startAutoHideTimer();

    // Use requestAnimationFrame for smooth movement
    updateTooltipPosition(e.clientX, e.clientY);
  }

  // Improved mouse event handling to prevent tooltip flicker
  // Using passive listeners for better performance on heavy pages
  document.addEventListener('mouseover', e => {
    const a = safeClosest(e.target, 'a[href]');
    if (!a) return;
    const href = a.href;
    if (!/^https?:\/\//.test(href)) return;

    // Prevent duplicate listeners for same link
    if (currentHoveredLink === a) return;

    // Clean up previous link if any
    if (currentHoveredLink) {
      clearTimeout(hoverTimer);
      hideTooltip();
    }

    currentHoveredLink = a;

    // Track initial mouse position
    lastMousePosition.x = e.clientX;
    lastMousePosition.y = e.clientY;

    clearTimeout(hoverTimer);
    hoverTimer = setTimeout(() => showTooltip(e, href), CONFIG.HOVER_DELAY);
    document.addEventListener('mousemove', moveTooltip, { passive: true });
  }, { passive: true });

  // Use mouseout with relatedTarget check to prevent flicker from child elements
  document.addEventListener('mouseout', e => {
    const a = safeClosest(e.target, 'a[href]');
    if (!a || a !== currentHoveredLink) return;

    // Check if we're moving to a child element of the same link
    const relatedTarget = e.relatedTarget;
    if (relatedTarget && a.contains(relatedTarget)) {
      if (CONFIG.DEBUG) {
        console.log('🔗 Mouse moved to child element, keeping tooltip visible');
      }
      return; // Still within the same link, don't hide tooltip
    }

    // Also check if we're moving from child to parent within same link
    const relatedLink = safeClosest(relatedTarget, 'a[href]');
    if (relatedLink === a) {
      if (CONFIG.DEBUG) {
        console.log('🔗 Mouse moved within same link structure, keeping tooltip visible');
      }
      return; // Still within the same link structure
    }

    if (CONFIG.DEBUG) {
      console.log('🔗 Mouse left link, hiding tooltip');
    }

    clearTimeout(hoverTimer);
    hideTooltip();
  }, { passive: true });

  // Additional safety: hide tooltip when clicking anywhere
  document.addEventListener('click', () => {
    if (currentHoveredLink) {
      clearTimeout(hoverTimer);
      hideTooltip();
    }
  }, { passive: true });

  // Additional safety: hide tooltip when scrolling
  document.addEventListener('scroll', () => {
    if (currentHoveredLink) {
      clearTimeout(hoverTimer);
      hideTooltip();
    }
  }, { passive: true, capture: true });

  // Initialize the tracker
  function initializeTracker() {
    // Load all saved settings
    loadSavedSettings();

    if (CONFIG.DEBUG) {
      console.log('🐛 Visit Tracker Debug Mode: ENABLED');
    }

    // Don't register menu for initial empty state - let updateVisit() handle it
    updateVisit();
    installUrlObservers();

    // Handle polling optimization and cache invalidation for multi-tab scenarios
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        // Tab became hidden - pause polling if configured
        if (CONFIG.POLLING.PAUSE_WHEN_HIDDEN) {
          stopPolling();
        }
        // Hide tooltip when tab is hidden to prevent stuck tooltips
        if (currentHoveredLink) {
          clearTimeout(hoverTimer);
          hideTooltip();
        }
      } else {
        // Tab became visible - resume polling if it was paused
        if (CONFIG.POLLING.PAUSE_WHEN_HIDDEN && !pollTimer) {
          // Boost activity for immediate responsiveness when tab becomes visible
          if (CONFIG.POLLING.ADAPTIVE) {
            activityCount = Math.min(10, activityCount + 3);
          }
          startPolling();
        }
        // Multi-tab cache coordination if enabled
        if (CONFIG.MULTI_TAB.ENABLED) {
          invalidateCache();
        }
      }
    }, { passive: true });

    // Multi-tab cache synchronization if enabled
    if (CONFIG.MULTI_TAB.ENABLED) {
      setInterval(() => {
        if (!document.hidden) {
          invalidateCache();
        }
      }, CONFIG.MULTI_TAB.SYNC_INTERVAL);
    }
  }

  // Cleanup pending operations on page unload
  window.addEventListener('beforeunload', () => {
    // Flush any pending debounced database writes
    flushPendingWrites();

    if (pendingTimeout) {
      clearTimeout(pendingTimeout);
      // Process any pending URL change immediately before unload
      if (pendingUrlChange && pendingUrlChange !== currentUrl) {
        currentUrl = pendingUrlChange;
        updateVisit();
        // Flush the new write as well
        flushPendingWrites();
      }
    }
  });

  initializeTracker();

})();