FicTrail - AO3 History Viewer

Advanced search and filtering for your reading history. Find fics by title, author, fandom, tags, or summary.

// ==UserScript==
// @name         FicTrail - AO3 History Viewer
// @namespace    https://github.com/serpentineegg/fictrail
// @version      0.1.6
// @description  Advanced search and filtering for your reading history. Find fics by title, author, fandom, tags, or summary.
// @author       serpentineegg
// @match        https://archiveofourown.org/users/*/readings*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==


(function() {
'use strict';

// Constants Module - Configuration and constants
const AO3_BASE_URL = 'https://archiveofourown.org';
const MAX_PAGES_FETCH = 100;
const ITEMS_PER_PAGE = 20;

// Error Messages
const ERROR_MESSAGES = {
  FETCH_FAILED: 'Uh oh! Something went wrong while fetching your reading adventures. Let\'s try again?',
  NO_DATA: 'Hmm, we didn\'t get any fic data back. Want to try that again?',
  LOGIN_REQUIRED: 'Please sign into your AO3 account first. We need access to your history!',
  LOGGED_OUT: 'It looks like you\'ve been logged out of AO3. Please sign in again!'
};


// Utils Module - Helper functions
function escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Check if we're on AO3 and user is logged in
function getUsername() {
  const greetingLink = document.querySelector('#greeting .user a[href*="/users/"]');
  if (greetingLink) {
    const href = greetingLink.getAttribute('href');
    const match = href.match(/\/users\/([^/]+)/);
    return match ? match[1] : null;
  }
  return null;
}

// Detect logged-out state based on AO3 login indicators within a fetched Document
function isLoggedOutDoc(doc) {
  const loginLink = doc.querySelector('a[href*="/users/login"]');
  const loggedOutMessage = doc.querySelector('.flash.notice');
  return Boolean(loginLink || (loggedOutMessage && loggedOutMessage.textContent.includes('log in')));
}


// Styles Module - All CSS styling for FicTrail

function addStyles() {
  const css = `
        /* FicTrail Styles */
        #fictrail-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            z-index: 10000;
            display: none;
        }
        
        #fictrail-container {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 95%;
            max-width: 1200px;
            min-width: 800px;
            height: 90%;
            background: #f8fafc;
            border-radius: 12px;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }
        
        #fictrail-close {
            background: rgba(0, 0, 0, 0.1);
            color: #4a5568;
            border: none;
            width: 32px;
            height: 32px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 16px;
            font-weight: 600;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s ease;
        }
        
        #fictrail-close:hover {
            background: rgba(239, 68, 68, 0.1);
            color: #dc2626;
        }
        
        #fictrail-header-main {
            flex: 1;
        }
        
        #fictrail-content {
            padding: 0;
            overflow-y: auto;
            height: 100%;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            display: flex;
            flex-direction: column;
        }
        
        .fictrail-header {
            padding: 24px 24px 20px 24px;
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            gap: 32px;
        }
        
        
        .fictrail-header h1 {
            font-size: 2em;
            font-weight: 300;
            color: #2d3748;
            margin: 0 0 3px 0;
        }
        
        .fictrail-header p {
            font-size: 1em;
            color: #718096;
            margin: 0;
        }
        
        .fictrail-main {
            flex: 1;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
        }
        
        .fictrail-btn {
            background: #3b82f6;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: background-color 0.2s ease;
        }
        
        .fictrail-btn:hover {
            background: #2563eb;
        }
        
        .fictrail-btn:disabled {
            background: #9ca3af;
            cursor: not-allowed;
            opacity: 0.6;
        }
        
        .fictrail-btn:disabled:hover {
            background: #9ca3af;
        }
        
        .fictrail-btn-secondary {
            background: white;
            color: #4a5568;
            border: 2px solid #e2e8f0;
            padding: 10px 20px;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
        }
        
        .fictrail-loading {
            padding: 80px 40px;
            text-align: center;
        }
        
        .fictrail-spinner {
            width: 40px;
            height: 40px;
            border: 3px solid #e2e8f0;
            border-top: 3px solid #3b82f6;
            border-radius: 50%;
            animation: fictrail-spin 1s linear infinite;
            margin: 0 auto 32px;
        }
        
        @keyframes fictrail-spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        
        .fictrail-error {
            padding: 80px 40px;
            text-align: center;
        }
        
        .fictrail-error h2 {
            font-size: 2.2em;
            font-weight: 300;
            color: #e53e3e;
            margin-bottom: 24px;
        }
        
        .fictrail-history {
            padding: 20px;
            padding-top: 20px;
            padding-bottom: 0px;
        }
        
        .fictrail-controls {
            display: flex;
            gap: 20px;
            align-items: flex-start;
            flex-wrap: wrap;
            margin-bottom: 20px;
        }
        
        .fictrail-search {
            flex: 1;
            min-width: 300px;
        }
        
        .fictrail-search input {
            width: 100%;
            padding: 10px 15px;
            border: 2px solid #e2e8f0;
            border-radius: 8px;
            font-size: 14px;
            outline: none;
            box-shadow: none !important;
            -webkit-box-shadow: none !important;
            -moz-box-shadow: none !important;
            height: 42px;
            line-height: 1.2;
            box-sizing: border-box;
        }
        
        .fictrail-results-count {
            margin-top: 8px;
            font-size: 14px;
            color: #718096;
            font-weight: 500;
        }
        
        .fictrail-filter select {
            padding: 10px 15px;
            border: 2px solid #e2e8f0;
            border-radius: 8px;
            background: white;
            font-size: 14px;
            cursor: pointer;
            outline: none;
            width: 200px;
            height: 42px;
            line-height: 1.2;
        }
        
        .fictrail-works {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
            gap: 20px;
        }
        
        .fictrail-work {
            background: white;
            border-radius: 12px;
            padding: 20px;
            border: 1px solid #e2e8f0;
            transition: all 0.2s ease;
        }
        
        .fictrail-work:hover {
            border-color: #cbd5e0;
        }
        
        .fictrail-work-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 8px;
        }
        
        .fictrail-work h3 {
            margin: 0;
            font-size: 1.1em;
            line-height: 1.4;
            flex: 1;
            margin-right: 15px;
        }
        
        .fictrail-work h3 a {
            color: #3b82f6;
            text-decoration: none;
            font-weight: 600;
            border-bottom: 1px solid transparent;
            transition: all 0.2s ease;
        }
        
        .fictrail-work h3 a:hover {
            color: #2563eb;
            border-bottom-color: #3b82f6;
            text-decoration: none !important;
        }
        
        .fictrail-work-number {
            background: #3b82f6;
            color: white;
            padding: 4px 10px;
            border-radius: 20px;
            font-size: 12px;
            font-weight: 600;
        }
        
        .fictrail-author {
            color: #4a5568;
            font-style: italic;
            margin-bottom: 6px;
            font-weight: 500;
        }
        
        .fictrail-author a {
            color: #3b82f6;
            text-decoration: none;
            font-weight: 600;
        }
        
        .fictrail-fandoms {
            color: #38a169;
            font-weight: 600;
            margin-bottom: 8px;
            font-size: 0.9em;
        }
        
        .fictrail-metadata {
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
            margin-top: 12px;
        }
        
        .fictrail-metadata span {
            background: #edf2f7;
            color: #4a5568;
            padding: 4px 8px;
            border-radius: 6px;
            font-size: 12px;
            font-weight: 500;
        }
        
        .fictrail-last-visited {
            color: #718096;
            font-size: 12px;
            margin: 12px 0;
            font-weight: 500;
        }
        
        .fictrail-summary {
            color: #718096;
            font-size: 14px;
            line-height: 1.4;
            margin: 10px 0 0 0;
            max-height: 120px;
            overflow-y: auto;
            border: 1px solid #e2e8f0;
            border-radius: 6px;
            padding: 8px;
            background: #fafafa;
        }
        
        .fictrail-matching-section {
            margin: 12px 0;
            padding: 12px;
            background: #f0f9ff;
            border-radius: 6px;
            border: 1px solid #bae6fd;
            font-size: 14px;
            line-height: 1.4;
        }
        
        .fictrail-tag-match {
            position: relative;
            display: inline-block;
            padding: 2px 6px;
            margin: 2px;
            border-radius: 4px;
            font-size: 12px;
            font-weight: 500;
            cursor: pointer;
        }
        
        /* Tooltip text hidden by default */
        .fictrail-tag-match::after {
            content: attr(data-tooltip);
            position: absolute;
            bottom: 125%;
            left: 50%;
            transform: translateX(-50%);
            background-color: #1f2937;
            color: white;
            padding: 6px 10px;
            border-radius: 6px;
            font-size: 12px;
            font-weight: 500;
            white-space: nowrap;
            z-index: 1000;
            
            /* Initially hidden */
            visibility: hidden;
            opacity: 0;
            transition: opacity 0.2s ease, visibility 0.2s ease;
            
            /* Pointer events to prevent interference */
            pointer-events: none;
        }
        
        /* Tooltip arrow */
        .fictrail-tag-match::before {
            content: "";
            position: absolute;
            bottom: 115%;
            left: 50%;
            transform: translateX(-50%);
            border: 5px solid transparent;
            border-top-color: #1f2937;
            z-index: 1000;
            
            /* Initially hidden */
            visibility: hidden;
            opacity: 0;
            transition: opacity 0.2s ease, visibility 0.2s ease;
            
            /* Pointer events to prevent interference */
            pointer-events: none;
        }
        
        /* Show tooltip on hover and focus */
        .fictrail-tag-match:hover::after,
        .fictrail-tag-match:hover::before,
        .fictrail-tag-match:focus::after,
        .fictrail-tag-match:focus::before {
            visibility: visible;
            opacity: 1;
        }
        
        /* Make tags focusable for keyboard accessibility */
        .fictrail-tag-match {
            outline: none;
            tabindex: 0;
        }
        
        /* Darker colors on focus for better visibility */
        .fictrail-tag-relationship:focus {
            background: #fde047;
            color: #b45309;
        }
        
        .fictrail-tag-character:focus {
            background: #bbf7d0;
            color: #15803d;
        }
        
        .fictrail-tag-freeform:focus {
            background: #c7d2fe;
            color: #3730a3;
        }
        
        .fictrail-tag-relationship {
            background: #fef3c7;
            color: #d97706;
            border: 1px solid #fed7aa;
        }
        
        .fictrail-tag-character {
            background: #dcfce7;
            color: #16a34a;
            border: 1px solid #bbf7d0;
        }
        
        .fictrail-tag-freeform {
            background: #e0e7ff;
            color: #4338ca;
            border: 1px solid #c7d2fe;
        }
        
        .fictrail-no-results {
            padding: 80px 40px;
            text-align: center;
        }
        
        .fictrail-no-results h3 {
            font-size: 1.8em;
            font-weight: 300;
            margin-bottom: 16px;
            color: #4a5568;
        }
        
        .fictrail-narrow-search-message {
            margin-top: 20px;
            background: #fef3c7;
            border: 1px solid #f59e0b;
            border-radius: 8px;
            padding: 15px;
            text-align: center;
        }
        
        .fictrail-narrow-search-message p {
            margin: 0;
            color: #92400e;
            font-size: 0.9em;
        }
        
        .fictrail-load-more-message {
            margin-top: 20px;
            text-align: center;
        }
        
        .fictrail-load-more-message p {
            margin: 0;
            color: #718096;
            font-size: 0.9em;
            font-weight: 500;
        }
        
        .fictrail-load-more-btn {
            margin-top: 15px;
            text-align: center;
            padding-bottom: 20px;
        }
        
        .fictrail-load-more-btn button {
            background: white;
            color: #3b82f6;
            border: 2px solid #3b82f6;
            padding: 12px 24px;
            border-radius: 8px;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s ease;
        }
        
        .fictrail-load-more-btn button:hover {
            background: #3b82f6;
            color: white;
        }
        
        .fictrail-favorite-tags-summary {
            margin: 10px 0 5px 0;
        }
        
        .fictrail-summary-text {
            color: #718096;
            font-size: 10px;
            font-style: italic;
            margin: 0;
            line-height: 1.5;
        }
        
        .fictrail-slider-container {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        
        .fictrail-slider-track {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 12px;
        }
        
        .fictrail-slider-min,
        .fictrail-slider-max {
            font-size: 12px;
            color: #718096;
            font-weight: 500;
            min-width: 20px;
            text-align: center;
        }
        
        .fictrail-slider {
            width: 200px;
            height: 6px;
            border-radius: 3px;
            background: #e2e8f0;
            outline: none;
            -webkit-appearance: none;
            appearance: none;
        }
        
        .fictrail-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            appearance: none;
            width: 18px;
            height: 18px;
            border-radius: 50%;
            background: #3b82f6;
            cursor: pointer;
        }
        
        .fictrail-slider::-moz-range-thumb {
            width: 18px;
            height: 18px;
            border-radius: 50%;
            background: #3b82f6;
            cursor: pointer;
            border: none;
        }
        
        /* Footer Styles */
        .fictrail-footer {
            padding: 30px 20px;
            text-align: center;
        }
        
        .fictrail-footer-content {
            display: flex;
            flex-direction: column;
            gap: 20px;
            align-items: center;
        }
        
        .fictrail-footer .fictrail-page-selector {
            display: flex;
            flex-direction: column;
            gap: 15px;
            align-items: center;
        }
        
        .fictrail-footer .fictrail-page-selector-header {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        
        .fictrail-footer .fictrail-page-selector-header label {
            font-size: 16px;
            font-weight: 600;
            color: #374151;
        }
        
        .fictrail-info-message p {
            margin: 10px 0;
            font-size: 14px;
            color: #6b7280;
        }
        
        .fictrail-footer .fictrail-slider-container {
            width: 300px;
        }
    `;

  // Use GM_addStyle if available, otherwise fallback to creating style element
  if (typeof GM_addStyle !== 'undefined') {
    GM_addStyle(css);
  } else {
    const style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);
  }
}


// Scraper Module - AO3 history fetching and parsing

// Safely extract text content while preserving line breaks from HTML elements
function extractTextWithLineBreaks(element) {
  // Clone the element to avoid modifying the original
  const clone = element.cloneNode(true);

  // Replace block elements with newlines
  const blockElements = clone.querySelectorAll('p, div, br');
  blockElements.forEach(el => {
    if (el.tagName === 'BR') {
      el.replaceWith('\n');
    } else {
      // Add newline after block elements
      el.insertAdjacentText('afterend', '\n');
    }
  });

  // Get text content and clean up extra whitespace
  return clone.textContent.replace(/\n\s*\n/g, '\n').trim();
}

function scrapeHistoryFromPage(doc) {
  const works = [];
  const workItems = doc.querySelectorAll('ol.reading li.work');

  workItems.forEach((item) => {
    const titleLink = item.querySelector('h4.heading a[href*="/works/"]');
    const authorLink = item.querySelector('h4.heading a[rel="author"]');
    const fandomLinks = item.querySelectorAll('h5.fandoms a.tag');
    const lastVisitedEl = item.querySelector('h4.viewed.heading');
    const summaryEl = item.querySelector('.userstuff.summary');
    const wordsEl = item.querySelector('.stats dd.words');
    const chaptersEl = item.querySelector('.stats dd.chapters');
    const dateEl = item.querySelector('.datetime');
    const tagsEl = item.querySelector('.tags.commas');

    if (titleLink) {
      // Extract last visited date from the h4.viewed.heading element
      let lastVisited = '';
      if (lastVisitedEl) {
        // Get the full text content and extract the date
        const fullText = lastVisitedEl.textContent;
        // Match pattern: "Last visited: DD MMM YYYY" or similar
        const dateMatch = fullText.match(/Last visited:\s*([^(]+)/);
        if (dateMatch) {
          lastVisited = dateMatch[1].trim();
        }
      }

      const work = {
        title: titleLink.textContent.trim(),
        url: AO3_BASE_URL + titleLink.getAttribute('href'),
        author: authorLink ? authorLink.textContent.trim() : 'Anonymous',
        authorUrl: authorLink ? AO3_BASE_URL + authorLink.getAttribute('href') : null,
        fandoms: Array.from(fandomLinks).map(link => link.textContent.trim()),
        lastVisited: lastVisited,
        summary: summaryEl ? extractTextWithLineBreaks(summaryEl) : '',
        words: wordsEl ? wordsEl.textContent.trim() : '',
        chapters: chaptersEl ? chaptersEl.textContent.trim() : '',
        publishDate: dateEl ? dateEl.textContent.trim() : '',
        tags: tagsEl ? Array.from(tagsEl.querySelectorAll('a.tag')).map(tag => tag.textContent.trim()) : [],
        relationships: tagsEl ? Array.from(tagsEl.querySelectorAll('.relationships a.tag')).map(rel => rel.textContent.trim()) : [],
        characters: tagsEl ? Array.from(tagsEl.querySelectorAll('.characters a.tag')).map(char => char.textContent.trim()) : [],
        freeforms: tagsEl ? Array.from(tagsEl.querySelectorAll('.freeforms a.tag')).map(tag => tag.textContent.trim()) : []
      };
      works.push(work);
    }
  });

  return works;
}

async function fetchHistoryPage(username, page = 1) {
  const url = `${AO3_BASE_URL}/users/${username}/readings?page=${page}`;

  try {
    const response = await fetch(url);
    const html = await response.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');

    // Check logged-out state using shared helper
    if (isLoggedOutDoc(doc)) {
      throw new Error('NOT_LOGGED_IN');
    }

    return scrapeHistoryFromPage(doc);
  } catch (error) {
    console.error(`Error fetching page ${page}:`, error);
    if (error.message === 'NOT_LOGGED_IN') {
      throw error;
    }
    return [];
  }
}

function getTotalPages(doc = document) {
  const pagination = doc.querySelector('.pagination');
  if (!pagination) return 1;

  const pageLinks = pagination.querySelectorAll('a');
  let maxPage = 1;

  pageLinks.forEach(link => {
    const pageNum = parseInt(link.textContent.trim());
    if (!isNaN(pageNum) && pageNum > maxPage) {
      maxPage = pageNum;
    }
  });

  const nextLink = pagination.querySelector('a[rel="next"]');
  if (nextLink && maxPage === 1) {
    maxPage = 2;
  }

  return maxPage;
}

async function fetchMultiplePages(username, maxPagesToFetch = MAX_PAGES_FETCH) {
  let totalPages;
  let firstPageWorks = [];

  try {
    const firstPageUrl = `${AO3_BASE_URL}/users/${username}/readings?page=1`;
    const response = await fetch(firstPageUrl);
    const html = await response.text();
    const parser = new DOMParser();
    const doc = parser.parseFromString(html, 'text/html');
    // Detect logged-out state on the first page fetch
    if (isLoggedOutDoc(doc)) {
      throw new Error('NOT_LOGGED_IN');
    }
    totalPages = getTotalPages(doc);
    firstPageWorks = scrapeHistoryFromPage(doc);
  } catch (error) {
    console.error('Error fetching first page:', error);
    if (error.message === 'NOT_LOGGED_IN') {
      // Propagate to caller so UI can show proper login prompt
      throw error;
    }
    return { works: [], totalPages: 1 };
  }

  const pagesToFetch = Math.min(maxPagesToFetch, totalPages);
  const works = [...firstPageWorks];

  // Start from page 2 since we already have page 1
  for (let page = 2; page <= pagesToFetch; page++) {
    showLoading(`Loading page ${page} of ${pagesToFetch}...`);
    const pageWorks = await fetchHistoryPage(username, page);
    works.push(...pageWorks);
    await new Promise(resolve => setTimeout(resolve, 500));
  }

  return { works: works, totalPages: totalPages };
}


// Search Module - Search and filtering functionality
function performSearch() {
  const query = document.getElementById('fictrail-search-input').value.toLowerCase().trim();

  if (query === '') {
    filteredWorks = [...allWorks];
    filteredWorks.forEach(work => {
      work.matchingTags = [];
      work.matchingSummary = null;
    });
    // Apply filter which will show the count
    applyFilter();
    return;
  } else {
    filteredWorks = allWorks.filter(work => {
      const matchingTags = [];
      let matchingSummary = null;

      if (work.relationships) {
        work.relationships.forEach(rel => {
          if (rel.toLowerCase().includes(query)) {
            matchingTags.push({ type: 'relationship', value: rel });
          }
        });
      }
      if (work.characters) {
        work.characters.forEach(char => {
          if (char.toLowerCase().includes(query)) {
            matchingTags.push({ type: 'character', value: char });
          }
        });
      }
      if (work.freeforms) {
        work.freeforms.forEach(tag => {
          if (tag.toLowerCase().includes(query)) {
            matchingTags.push({ type: 'freeform', value: tag });
          }
        });
      }

      // Check for summary match and extract fragment
      if (work.summary && work.summary.toLowerCase().includes(query)) {
        const summaryLower = work.summary.toLowerCase();
        const queryIndex = summaryLower.indexOf(query);
        const start = Math.max(0, queryIndex - 50);
        const end = Math.min(work.summary.length, queryIndex + query.length + 50);
        let fragment = work.summary.substring(start, end);

        if (start > 0) fragment = '...' + fragment;
        if (end < work.summary.length) fragment = fragment + '...';

        matchingSummary = fragment;
      }

      work.matchingTags = matchingTags;
      work.matchingSummary = matchingSummary;

      return work.title.toLowerCase().includes(query) ||
                     work.author.toLowerCase().includes(query) ||
                     work.fandoms.some(fandom => fandom.toLowerCase().includes(query)) ||
                     work.summary.toLowerCase().includes(query) ||
                     matchingTags.length > 0 ||
                     (work.tags && work.tags.some(tag => tag.toLowerCase().includes(query)));
    });
  }

  applyFilter();
}

function applyFilter() {
  const selectedFandom = document.getElementById('fictrail-fandom-filter').value;

  let worksToDisplay = [...filteredWorks];
  if (selectedFandom) {
    worksToDisplay = worksToDisplay.filter(work =>
      work.fandoms.includes(selectedFandom)
    );
  }

  worksToDisplay.sort((a, b) => {
    if (a.lastVisited && b.lastVisited) {
      return new Date(b.lastVisited) - new Date(a.lastVisited);
    }
    return 0;
  });

  // Reset pagination for new search/filter
  currentDisplayCount = ITEMS_PER_PAGE;

  // Show results count
  updateResultsCount(worksToDisplay.length);
  displayWorks(worksToDisplay);
}

function updateResultsCount(count) {
  const resultsCountElement = document.getElementById('fictrail-results-count');
  if (resultsCountElement) {
    if (count > 0) {
      resultsCountElement.textContent = `${count} result${count === 1 ? '' : 's'}`;
      resultsCountElement.style.display = 'block';
    } else {
      resultsCountElement.style.display = 'none';
    }
  }
}

function populateFandomFilter(works) {
  const fandomFilter = document.getElementById('fictrail-fandom-filter');
  const allFandoms = new Set();
  works.forEach(work => {
    work.fandoms.forEach(fandom => allFandoms.add(fandom));
  });

  const sortedFandoms = Array.from(allFandoms).sort((a, b) => a.localeCompare(b));
  fandomFilter.innerHTML = '<option value="">All Fandoms</option>';

  sortedFandoms.forEach(fandom => {
    const option = document.createElement('option');
    option.value = fandom;
    option.textContent = fandom;
    fandomFilter.appendChild(option);
  });
}


// UI Module - DOM creation and event handling

// Create FicTrail button - try history page placement first
function createFicTrailButton() {
  // Only add to history page if we're on the readings page
  if (window.location.pathname.includes('/readings')) {
    addToHistoryPage()
  }
}

// Add FicTrail button in front of "Full History" in subnav
function addToHistoryPage() {
  const subNav = document.querySelector('ul.navigation.actions[role="navigation"]');

  if (!subNav) {
    return false; // Subnav not found
  }

  // Create list item for the button
  const listItem = document.createElement('li');

  // Create the button using AO3's button styles
  const button = document.createElement('a');
  button.id = 'fictrail-history-btn';
  button.textContent = '📚 FicTrail';
  button.style.cursor = 'pointer';
  button.tabIndex = 0;

  button.addEventListener('click', openFicTrail);

  // Add keyboard support
  button.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      openFicTrail();
    }
  });

  listItem.appendChild(button);

  // Insert at the beginning of the subnav
  subNav.insertBefore(listItem, subNav.firstChild);

  return true;
}

// Create overlay HTML
function createOverlay() {
  const overlay = document.createElement('div');
  overlay.id = 'fictrail-overlay';

  overlay.innerHTML = `
          <div id="fictrail-container">
              <div id="fictrail-content">
                  <header class="fictrail-header">
                      <div id="fictrail-header-main">
                          <h1>📚 FicTrail</h1>
                          <p id="fictrail-subtitle">Your AO3 History</p>
                      </div>
                      <div id="fictrail-controls" style="display: flex; flex-direction: column; align-items: center;">
                      </div>
                      <div id="fictrail-close-btn">
                          <button id="fictrail-close">×</button>
                      </div>
                  </header>
                  
                  <hr style="border: none; border-top: 1px solid #e2e8f0; margin: 0;">
                  
                  <main class="fictrail-main">
                      <div id="fictrail-loading-section" class="fictrail-loading" style="display: none;">
                          <div class="fictrail-spinner"></div>
                          <h2>Summoning your fic history...</h2>
                          <p id="fictrail-loading-status">Diving deep into your AO3 rabbit hole...</p>
                      </div>
                      
                      <div id="fictrail-error-section" class="fictrail-error" style="display: none;">
                          <h2>💀 Plot Twist!</h2>
                          <p id="fictrail-error-message"></p>
                          <button id="fictrail-retry-btn" class="fictrail-btn-secondary">Try Again (Please?)</button>
                      </div>
                      
                      <div id="fictrail-history-section" class="fictrail-history" style="display: none;">
                          <div class="fictrail-controls">
                              <div class="fictrail-search">
                                  <input type="text" id="fictrail-search-input" placeholder="Search by fandoms, titles, tags, authors, or summaries...">
                                  <div id="fictrail-results-count" class="fictrail-results-count" style="display: none;"></div>
                              </div>
                              <div class="fictrail-filter">
                                  <select id="fictrail-fandom-filter">
                                      <option value="">All Fandoms</option>
                                  </select>
                              </div>
                          </div>
                          
                          <div id="fictrail-works-list" class="fictrail-works"></div>
                          
                          <div id="fictrail-no-results" class="fictrail-no-results" style="display: none;">
                              <h3>No Results Found</h3>
                              <p>Your search came up empty! Try different keywords or maybe you haven't read that trope yet? 👀</p>
                          </div>
                          
                          <footer id="fictrail-footer" class="fictrail-footer" style="display: none;">
                              <div class="fictrail-footer-content">
                                  <div class="fictrail-page-selector" id="fictrail-page-selector">
                                      <div class="fictrail-page-selector-header">
                                          <label for="fictrail-pages-slider" id="fictrail-pages-label">You have ? pages of history. How deep should we search?</label>
                                      </div>
                                      <div class="fictrail-info-message">
                                          <p>Loading many pages can be slow. Start with fewer pages for better performance, then reload with more if needed.</p>
                                      </div>
                                      <div class="fictrail-slider-container">
                                          <div class="fictrail-slider-track">
                                              <span class="fictrail-slider-min">1</span>
                                              <input type="range" id="fictrail-pages-slider" min="1" max="${MAX_PAGES_FETCH}" value="1" class="fictrail-slider">
                                              <span class="fictrail-slider-max">${MAX_PAGES_FETCH}</span>
                                          </div>
                                      </div>
                                  </div>
                                  <button id="fictrail-load-btn" class="fictrail-btn">Reload History</button>
                              </div>
                          </footer>
                      </div>
                  </main>
              </div>
          </div>
      `;

  document.body.appendChild(overlay);

  // Add event listeners
  document.getElementById('fictrail-close').addEventListener('click', closeFicTrail);
  document.getElementById('fictrail-load-btn').addEventListener('click', reloadHistory);
  document.getElementById('fictrail-retry-btn').addEventListener('click', retryLastAction);
  document.getElementById('fictrail-search-input').addEventListener('input', debounce(performSearch, 300));
  document.getElementById('fictrail-fandom-filter').addEventListener('change', applyFilter);
  document.getElementById('fictrail-pages-slider').addEventListener('input', updatePagesValue);

  // Add click handler for tags
  document.addEventListener('click', function(e) {
    if (e.target.classList.contains('fictrail-tag-match')) {
      const tagValue = e.target.getAttribute('data-tag-value');
      const searchInput = document.getElementById('fictrail-search-input');
      searchInput.value = tagValue;
      performSearch();
    }
  });

  // Close on overlay click
  overlay.addEventListener('click', (e) => {
    if (e.target === overlay) closeFicTrail();
  });
}

function openFicTrail() {
  document.getElementById('fictrail-overlay').style.display = 'block';
  document.body.style.overflow = 'hidden';

  // Only load data if we don't have any works yet
  if (allWorks.length === 0) {
    setTimeout(() => {
      loadFirstPage();
    }, 100);
  }
}

function closeFicTrail() {
  document.getElementById('fictrail-overlay').style.display = 'none';
  document.body.style.overflow = '';
}

function updatePagesValue() {
  updateReloadButtonText();
}

function updateReloadButtonText() {
  const currentPages = parseInt(document.getElementById('fictrail-pages-slider').value);
  const footer = document.getElementById('fictrail-footer');
  const loadBtn = document.getElementById('fictrail-load-btn');

  // Check if we're in reload mode (footer is visible)
  if (footer.style.display === 'block') {
    loadBtn.textContent = `Reload History (${currentPages} pages)`;
  }
}

function getPagesToLoad() {
  const slider = document.getElementById('fictrail-pages-slider');
  // If slider doesn't exist yet, default to 1 page
  if (!slider) return 1;
  return parseInt(slider.value);
}

function showSection(sectionId) {
  const sections = ['fictrail-loading-section', 'fictrail-error-section', 'fictrail-history-section'];
  sections.forEach(id => {
    document.getElementById(id).style.display = id === sectionId ? 'block' : 'none';
  });
}

function showLoading(message = 'Summoning your fic history...') {
  showSection('fictrail-loading-section');
  document.getElementById('fictrail-loading-status').textContent = message;
}

function showError(message) {
  showSection('fictrail-error-section');
  document.getElementById('fictrail-error-message').innerHTML = message;
}

function displayWorks(works, append = false) {
  const worksList = document.getElementById('fictrail-works-list');
  const noResults = document.getElementById('fictrail-no-results');

  if (works.length === 0) {
    worksList.style.display = 'none';
    noResults.style.display = 'block';
    // Remove existing load more button and message when there are no results
    removeLoadMoreElements();
    return;
  }

  worksList.style.display = 'grid';
  noResults.style.display = 'none';

  // Reset display count if not appending (new search/filter)
  if (!append) {
    currentDisplayCount = ITEMS_PER_PAGE;
  }

  const worksToShow = works.slice(0, currentDisplayCount);
  const hasMoreResults = works.length > currentDisplayCount;

  // Calculate starting index for work numbers
  const startIndex = append ? worksList.children.length : 0;

  const worksHTML = worksToShow
    .slice(append ? currentDisplayCount - ITEMS_PER_PAGE : 0)
    .map((work, index) => `
        <div class="fictrail-work">
            <div class="fictrail-work-header">
                <h3>
                    <a href="${work.url}" target="_blank" rel="noopener">
                        ${escapeHtml(work.title)}
                    </a>
                </h3>
                <span class="fictrail-work-number">#${startIndex + index + 1}</span>
            </div>

            <p class="fictrail-author">
                by ${work.authorUrl ? `<a href="${work.authorUrl}" target="_blank" rel="noopener">${escapeHtml(work.author)}</a>` : escapeHtml(work.author)}
            </p>

            ${work.fandoms.length > 0 ? `<p class="fictrail-fandoms">
                    ${work.fandoms.map(f => escapeHtml(f)).join(', ')}
                   </p>` : ''}
            
            ${work.matchingTags && work.matchingTags.length > 0 ? `<div class="fictrail-matching-section">
                    <strong>Matching tags:</strong> 
                    ${work.matchingTags.map(tag => `<span class="fictrail-tag-match fictrail-tag-${tag.type}" 
                               data-tooltip="${tag.type.charAt(0).toUpperCase() + tag.type.slice(1)} tag" 
                               data-tag-value="${escapeHtml(tag.value)}" 
                               tabindex="0" 
                               role="button" 
                               aria-label="${escapeHtml(tag.value)} - ${tag.type.charAt(0).toUpperCase() + tag.type.slice(1)} tag">
                            ${escapeHtml(tag.value)}
                        </span>`).join('')}
                   </div>` : ''}
               
            ${work.matchingSummary ? `<div class="fictrail-matching-section">
                    <strong>Matching summary:</strong> 
                    ${escapeHtml(work.matchingSummary)}
                   </div>` : ''}

            <div class="fictrail-metadata">
                ${work.words ? `<span>${escapeHtml(work.words)} words</span>` : ''}
                ${work.chapters ? `<span>${escapeHtml(work.chapters)} chapters</span>` : ''}
                ${work.publishDate ? `<span>Published: ${escapeHtml(work.publishDate)}</span>` : ''}
            </div>

            ${work.lastVisited ? `<p class="fictrail-last-visited">
                    Last visited: ${escapeHtml(work.lastVisited)}
                   </p>` : ''}

            ${work.summary && !work.matchingSummary ? `<div class="fictrail-summary">
                    ${escapeHtml(work.summary).replace(/\n/g, '<br>')}
                   </div>` : ''}
        </div>
    `)
    .join('');

  if (append) {
    worksList.insertAdjacentHTML('beforeend', worksHTML);
  } else {
    worksList.innerHTML = worksHTML;
  }

  // Remove existing load more elements before adding new ones
  removeLoadMoreElements();

  // Add load more button if there are more results
  if (hasMoreResults) {
    createLoadMoreButton(works, currentDisplayCount);
  }
}

function removeLoadMoreElements() {
  const existingButton = document.getElementById('fictrail-load-more-btn');
  const existingMessage = document.getElementById('fictrail-load-more-message');
  if (existingButton) existingButton.remove();
  if (existingMessage) existingMessage.remove();
}

function createLoadMoreButton(works, currentCount) {
  const worksList = document.getElementById('fictrail-works-list');
  const remainingCount = works.length - currentCount;
  const nextBatchSize = Math.min(ITEMS_PER_PAGE, remainingCount);

  // Create load more message
  const messageDiv = document.createElement('div');
  messageDiv.id = 'fictrail-load-more-message';
  messageDiv.className = 'fictrail-load-more-message';
  messageDiv.innerHTML = `
    <p>Showing ${currentCount} of ${works.length} results</p>
  `;

  // Create load more button
  const buttonDiv = document.createElement('div');
  buttonDiv.id = 'fictrail-load-more-btn';
  buttonDiv.className = 'fictrail-load-more-btn';
  buttonDiv.innerHTML = `
    <button class="fictrail-btn-secondary" id="fictrail-load-more-button">
      Load ${nextBatchSize} More Results
    </button>
  `;

  // Insert after the works-list div
  worksList.parentNode.insertBefore(messageDiv, worksList.nextSibling);
  messageDiv.parentNode.insertBefore(buttonDiv, messageDiv.nextSibling);

  // Add event listener to the load more button
  const loadMoreButton = document.getElementById('fictrail-load-more-button');
  loadMoreButton.addEventListener('click', loadMoreWorks);
}

function loadMoreWorks() {
  currentDisplayCount += ITEMS_PER_PAGE;
  displayWorks(filteredWorks, true);
}

function addFavoriteTagsSummary(works) {
  // Only consider the most recent works (approximately first 2 pages worth)
  const recentWorksLimit = 40;
  const recentWorks = works.slice(0, recentWorksLimit);

  // Count all tags across recent works only
  const tagCounts = {};

  recentWorks.forEach(work => {
    // Count relationships
    if (work.relationships) {
      work.relationships.forEach(tag => {
        tagCounts[tag] = (tagCounts[tag] || 0) + 1;
      });
    }
    // Count characters
    if (work.characters) {
      work.characters.forEach(tag => {
        tagCounts[tag] = (tagCounts[tag] || 0) + 1;
      });
    }
    // Count freeforms
    if (work.freeforms) {
      work.freeforms.forEach(tag => {
        tagCounts[tag] = (tagCounts[tag] || 0) + 1;
      });
    }
  });

  // Sort tags by frequency and get the most popular one
  const sortedTags = Object.entries(tagCounts)
    .sort((a, b) => b[1] - a[1]);

  if (sortedTags.length > 0) {
    // Remove existing summary if it exists
    const existingSummary = document.getElementById('fictrail-favorite-tags-summary');
    if (existingSummary) {
      existingSummary.remove();
    }

    // Get the most popular tag
    const [mostPopularTag] = sortedTags[0];

    // Create summary element
    const summaryDiv = document.createElement('div');
    summaryDiv.id = 'fictrail-favorite-tags-summary';
    summaryDiv.className = 'fictrail-favorite-tags-summary';
    summaryDiv.innerHTML = `
              <p class="fictrail-summary-text">So you've been really into ${escapeHtml(mostPopularTag)} lately. Love it for you.</p>
          `;

    // Insert after header subtitle
    const headerSubtitle = document.getElementById('fictrail-subtitle');
    headerSubtitle.parentNode.insertBefore(summaryDiv, headerSubtitle.nextSibling);
  }
}


// Core Module - Main functionality and history loading
let allWorks = [];
let filteredWorks = [];
let lastFailedAction = null;

// Pagination state
let currentDisplayCount = 20;

function showLoginError(message = ERROR_MESSAGES.LOGIN_REQUIRED) {
  showError(`
    <strong>Oops! You're not logged in</strong><br>
    ${message}<br><br>
    <button onclick="window.open('${AO3_BASE_URL}/users/login', '_blank')" class="fictrail-btn">
      Log In to AO3
    </button>
  `);
}

function retryLastAction() {
  if (lastFailedAction === 'reloadHistory') {
    reloadHistory();
  } else {
    loadFirstPage();
  }
}

async function loadFirstPage() {
  lastFailedAction = 'loadFirstPage';
  const username = getUsername();
  if (!username) {
    showLoginError();
    return;
  }

  try {
    // Check if we're on page 1 of readings - if so, parse current DOM instantly
    const urlParams = new URLSearchParams(window.location.search);
    const currentPage = parseInt(urlParams.get('page')) || 1;

    if (window.location.pathname.includes('/readings') && currentPage === 1 && !urlParams.has('show')) {
      const works = scrapeHistoryFromPage(document);
      const totalPages = getTotalPages(document);

      if (works && works.length > 0) {
        displayHistory(username, works, totalPages, 1);
        return;
      }
    }

    // Fallback: use reloadHistory to fetch first page
    await reloadHistory();
  } catch (error) {
    if (error.message === 'NOT_LOGGED_IN') {
      showLoginError(ERROR_MESSAGES.LOGGED_OUT);
      return;
    }
    console.error('Error loading first page:', error);
    showError(ERROR_MESSAGES.FETCH_FAILED);
  }
}

async function reloadHistory() {
  lastFailedAction = 'reloadHistory';
  const username = getUsername();
  if (!username) {
    showLoginError();
    return;
  }

  // Disable buttons while loading
  const loadBtn = document.getElementById('fictrail-load-btn');
  const retryBtn = document.getElementById('fictrail-retry-btn');
  loadBtn.disabled = true;
  if (retryBtn) retryBtn.disabled = true;

  // Clear search bar when reloading
  document.getElementById('fictrail-search-input').value = '';

  // Get pages to load from slider
  const pagesToLoad = getPagesToLoad();

  showLoading(`Loading ${pagesToLoad} pages of ${username}'s fic history...`);

  try {
    const result = await fetchMultiplePages(username, pagesToLoad);
    if (result.works && result.works.length > 0) {
      displayHistory(username, result.works, result.totalPages, pagesToLoad);
    } else {
      showError(ERROR_MESSAGES.NO_DATA);
    }
  } catch (error) {
    if (error.message === 'NOT_LOGGED_IN') {
      showLoginError(ERROR_MESSAGES.LOGGED_OUT);
      return;
    }
    console.error('Error loading history:', error);
    showError(ERROR_MESSAGES.FETCH_FAILED);
  } finally {
    // Re-enable buttons after loading completes
    const loadBtn = document.getElementById('fictrail-load-btn');
    const retryBtn = document.getElementById('fictrail-retry-btn');
    loadBtn.disabled = false;
    if (retryBtn) retryBtn.disabled = false;
  }
}

function displayHistory(username, works, totalPages, actualPagesLoaded) {
  showSection('fictrail-history-section');

  allWorks = works;
  filteredWorks = [...works];

  // Reset pagination when loading new history
  currentDisplayCount = ITEMS_PER_PAGE;

  const workCount = works.length;
  const uniqueAuthors = new Set(works.map(work => work.author)).size;
  const uniqueFandoms = new Set(works.flatMap(work => work.fandoms)).size;

  document.getElementById('fictrail-subtitle').textContent =
          `${username} • ${workCount} works • ${uniqueFandoms} fandoms • ${uniqueAuthors} authors`;

  // Update slider max value to match user's actual page count
  if (totalPages && totalPages > 0) {
    const slider = document.getElementById('fictrail-pages-slider');
    const sliderMax = document.querySelector('.fictrail-slider-max');
    const pagesLabel = document.getElementById('fictrail-pages-label');

    slider.max = totalPages;
    sliderMax.textContent = totalPages;

    // Update the label with actual page count and current loaded pages
    if (actualPagesLoaded === totalPages) {
      pagesLabel.textContent = `You have ${totalPages} pages of history. All pages loaded.`;
    } else {
      pagesLabel.textContent = `You have ${totalPages} pages of history. Now ${actualPagesLoaded} ${actualPagesLoaded === 1 ? 'page is' : 'pages are'} loaded. Shall we go deeper?`;
    }

    // Set slider value to the actual pages loaded (for initial load) or keep current value (for reload)
    if (actualPagesLoaded !== undefined) {
      slider.value = actualPagesLoaded;
    } else {
      const newValue = Math.min(parseInt(slider.value), totalPages);
      slider.value = newValue;
    }
  }

  // Show footer with page selector and update button for reload functionality
  const footer = document.getElementById('fictrail-footer');
  const loadBtn = document.getElementById('fictrail-load-btn');
  footer.style.display = 'block';
  loadBtn.textContent = 'Reload History';
  loadBtn.onclick = reloadHistory;
  updateReloadButtonText();

  // Add favorite tags summary
  addFavoriteTagsSummary(works);

  populateFandomFilter(works);
  updateResultsCount(works.length);
  displayWorks(works);
}

// Initialize when page loads
function init() {
  addStyles();
  createFicTrailButton();
  createOverlay();
}

// Auto-initialization
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  init();
}



})();