/**
 * LinkMLViewerPage.tsx - LinkML Schema Viewer Page
 * 
 * Displays LinkML schema files with:
 * - Sidebar listing schemas by category (main, classes, enums, slots)
 * - Visual display of selected schema showing classes, slots, and enums
 * - Raw YAML view toggle
 * - Schema metadata and documentation
 * - URL parameter support for deep linking to specific classes (?class=ClassName)
 */

import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import debounce from 'lodash/debounce';
import { useSearchParams } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import {
  loadSchema,
  loadSchemaRaw,
  type LinkMLSchema,
  type LinkMLClass,
  type LinkMLSlot,
  type LinkMLEnum,
  type SchemaFile,
  extractClasses,
  extractSlots,
  extractEnums,
} from '../lib/linkml/schema-loader';
import { linkmlSchemaService, type ClassExportInfo } from '../lib/linkml/linkml-schema-service';
import { useLanguage } from '../contexts/LanguageContext';
import { useSchemaLoadingProgress } from '../hooks/useSchemaLoadingProgress';
import { CustodianTypeBadge } from '../components/uml/CustodianTypeIndicator';
import { CustodianTypeIndicator3D } from '../components/uml/CustodianTypeIndicator3D';
import { CustodianTypeLegendBar } from '../components/uml/CustodianTypeLegend';
import { UMLVisualization, type UMLDiagram, type UMLNode, type UMLLink } from '../components/uml/UMLVisualization';
import { CUSTODIAN_TYPE_CODES, type CustodianTypeCode } from '../lib/custodian-types';
import { LoadingScreen } from '../components/LoadingScreen';
import {
  getCustodianTypesForClass,
  getCustodianTypesForSlot,
  getCustodianTypesForEnum,
  getCustodianTypesForClassAsync,
  getCustodianTypesForSlotAsync,
  getCustodianTypesForEnumAsync,
  isUniversalElement,
} from '../lib/schema-custodian-mapping';
import './LinkMLViewerPage.css';

/**
 * Converts snake_case or kebab-case identifiers to human-readable Title Case.
 * 
 * Examples:
 * - "custodian_appellation_class" → "Custodian Appellation Class"
 * - "feature_place_class" → "Feature Place Class"
 * - "heritage-custodian-observation" → "Heritage Custodian Observation"
 */
const formatDisplayName = (name: string): string => {
  if (!name) return name;
  
  return name
    // Replace underscores and hyphens with spaces
    .replace(/[_-]/g, ' ')
    // Capitalize first letter of each word
    .replace(/\b\w/g, char => char.toUpperCase());
};

/**
 * Admonition transformer for markdown content.
 * Converts patterns like "CRITICAL: message" into styled admonition elements.
 * 
 * Supported patterns:
 * - CRITICAL: Urgent, must-know information (red)
 * - WARNING: Potential issues or gotchas (orange)
 * - IMPORTANT: Significant but not urgent (amber)
 * - NOTE: Informational, helpful context (blue)
 * 
 * The pattern captures everything from the keyword until:
 * - End of line (for single-line admonitions)
 * - A blank line (paragraph break)
 */
const transformAdmonitions = (text: string): string => {
  if (!text) return text;
  
  // Pattern matches: KEYWORD: rest of the text until end of paragraph
  // [^\n]* captures everything on the current line
  // (?:\n(?!\n)[^\n]*)* captures continuation lines (lines that don't start a new paragraph)
  const admonitionPattern = /\b(CRITICAL|WARNING|IMPORTANT|NOTE):\s*([^\n]*(?:\n(?!\n)[^\n]*)*)/g;
  
  return text.replace(admonitionPattern, (_match, keyword, content) => {
    const type = keyword.toLowerCase();
    // Return HTML that ReactMarkdown will pass through
    return `<span class="linkml-admonition linkml-admonition--${type}"><span class="linkml-admonition__label">${keyword}:</span><span class="linkml-admonition__content">${content.trim()}</span></span>`;
  });
};

/**
 * CURIE (Compact URI) highlighter for markdown content.
 * Highlights patterns like "schema:name", "crm:E41_Appellation", "skos:altLabel".
 * 
 * CURIE format: prefix:localName
 * - prefix: lowercase letters (e.g., schema, crm, skos, foaf, dcterms)
 * - localName: letters, numbers, underscores, hyphens (e.g., name, E41_Appellation)
 * 
 * Also handles compound CURIEs like "schema:dateCreated/dateModified"
 */
const highlightCuries = (text: string): string => {
  if (!text) return text;
  
  // Common ontology prefixes used in heritage/linked data
  const knownPrefixes = [
    'schema', 'crm', 'skos', 'foaf', 'dcterms', 'dct', 'dc', 'rdfs', 'rdf', 'owl',
    'prov', 'org', 'locn', 'cpov', 'tooi', 'rico', 'bf', 'wikidata', 'wd', 'wdt',
    'xsd', 'linkml', 'hc', 'pico', 'cv'
  ].join('|');
  
  // Pattern matches: prefix:localName (with optional /additional parts)
  // Negative lookbehind (?<![:/]) prevents matching URLs like http://...
  // Negative lookahead (?![:/]) prevents partial URL matches
  const curiePattern = new RegExp(
    `(?<![:/])\\b((?:${knownPrefixes}):(?:[A-Za-z][A-Za-z0-9_-]*(?:/[A-Za-z][A-Za-z0-9_-]*)*))(?![:/])`,
    'g'
  );
  
  return text.replace(curiePattern, '<code class="linkml-curie">$1</code>');
};

/**
 * ASCII Tree Diagram transformer.
 * Converts ASCII tree diagrams (using ├── └── │ characters) into styled HTML trees.
 * 
 * Detects patterns like:
 * ```
 * Archives nationales (national)
 *   └── Archives régionales (regional)
 *         └── Archives départementales (THIS TYPE)
 *               └── Archives communales (municipal)
 * ```
 * 
 * Or more complex trees:
 * ```
 * Custodian (hub)
 *   │
 *   └── CustodianCollection (aspect)
 *         ├── CollectionType (classification)
 *         ├── AccessPolicy (access restrictions)
 *         └── sub_collections → Collection[]
 * ```
 */
const transformTreeDiagrams = (text: string): string => {
  if (!text) return text;
  
  const codeBlockPattern = /```\n?([\s\S]*?)\n?```/g;
  
  return text.replace(codeBlockPattern, (match, codeContent: string) => {
    // Check if this code block contains tree characters but NOT box diagrams
    const hasTreeChars = /[├└│]──/.test(codeContent) || /\s+└──/.test(codeContent);
    const hasBoxChars = /[┌┐]/.test(codeContent); // Only top box corners indicate box diagram
    
    if (!hasTreeChars || hasBoxChars) {
      // Not a tree diagram (or is a box diagram), skip
      return match;
    }
    
    const lines = codeContent.split('\n').filter(l => l.trim());
    if (lines.length === 0) return match;
    
    // First pass: detect unique indentation levels to build proper hierarchy
    const indentLevels = new Set<number>();
    for (const line of lines) {
      const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
      indentLevels.add(leadingSpaces);
    }
    // Sort indent levels to map them to 0, 1, 2, 3...
    const sortedIndents = Array.from(indentLevels).sort((a, b) => a - b);
    const indentMap = new Map<number, number>();
    sortedIndents.forEach((indent, index) => indentMap.set(indent, index));
    
    // Build tree structure
    let html = '<div class="linkml-tree-diagram">';
    
    for (const line of lines) {
      // Get actual indent level from map
      const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
      const indentLevel = indentMap.get(leadingSpaces) || 0;
      
      // Check for tree branch characters
      const hasBranch = /├──|└──/.test(line);
      const isLastBranch = /└──/.test(line);
      const hasVerticalLine = /│/.test(line) && !/[├└]/.test(line);
      
      // Check if this line is highlighted (THIS CLASS, THIS TYPE, etc.)
      const isHighlighted = /\(THIS\s*(CLASS|TYPE|LEVEL)?\)/.test(line);
      
      // Extract the content after tree characters
      let content = line
        .replace(/^[\s│├└─]+/, '') // Remove leading spaces and tree chars
        .replace(/→/g, '<span class="linkml-tree-arrow">→</span>') // Style arrows
        .trim();
      
      // Clean up and add highlighting
      if (isHighlighted) {
        content = content.replace(/\(THIS\s*(CLASS|TYPE|LEVEL)?\)/g, '');
        content = `<span class="linkml-tree-highlight">${content.trim()}</span>`;
      }
      
      // Skip empty vertical connector lines
      if (hasVerticalLine && !content) {
        html += `<div class="linkml-tree-connector" style="margin-left: ${indentLevel * 1.5}rem;">│</div>`;
        continue;
      }
      
      // Determine the branch character to display
      let branchChar = '';
      if (isLastBranch) {
        branchChar = '<span class="linkml-tree-branch">└──</span>';
      } else if (hasBranch) {
        branchChar = '<span class="linkml-tree-branch">├──</span>';
      }
      
      if (content) {
        const itemClass = isHighlighted ? 'linkml-tree-item linkml-tree-item--highlighted' : 'linkml-tree-item';
        html += `<div class="${itemClass}" style="margin-left: ${indentLevel * 1.5}rem;">${branchChar}${content}</div>`;
      }
    }
    
    html += '</div>';
    return html;
  });
};

/**
 * Arrow-based Flow Diagram transformer.
 * Converts simple arrow-based flow diagrams (using ↓ → ← characters) into styled HTML.
 * 
 * Detects patterns like:
 * ```
 * Current Archive (active use)
 *   ↓
 * DEPOSIT ARCHIVE (semi-current) ← THIS TYPE
 *   ↓
 * Historical Archive (permanent preservation)
 *   or
 * Destruction (per retention schedule)
 * ```
 * 
 * Or horizontal flows:
 * ```
 * DepositArchive (custodian type)
 *   │
 *   └── operates_storage → Storage (facility instance)
 *                            │
 *                            └── has_storage_type → StorageType
 *                                  └── DEPOSIT_STORAGE
 * ```
 */
const transformFlowDiagrams = (text: string): string => {
  if (!text) return text;
  
  const codeBlockPattern = /```\n?([\s\S]*?)\n?```/g;
  
  return text.replace(codeBlockPattern, (match, codeContent: string) => {
    // Skip if it has tree branch characters (├── └──) - handled by tree transformer
    const hasTreeChars = /[├└]──/.test(codeContent);
    // Skip if it has box characters (┌─┐) - handled by lifecycle transformer
    const hasBoxChars = /[┌┐]/.test(codeContent);
    
    if (hasTreeChars || hasBoxChars) {
      return match; // Let other transformers handle these
    }
    
    // Check if this is an arrow-based flow diagram
    // Must have vertical arrows (↓) or be a simple vertical flow with indented items
    const hasVerticalArrows = /↓/.test(codeContent);
    const hasHorizontalArrows = /[←→]/.test(codeContent);
    const hasVerticalPipe = /│/.test(codeContent);
    
    // Must have at least arrows to be considered a flow diagram
    if (!hasVerticalArrows && !hasHorizontalArrows && !hasVerticalPipe) {
      return match;
    }
    
    const lines = codeContent.split('\n');
    const elements: Array<{
      type: 'node' | 'arrow' | 'branch' | 'connector';
      content: string;
      annotation?: string;
      isHighlighted?: boolean;
      indentLevel?: number;
    }> = [];
    
    // Detect indent levels
    const indentLevels = new Set<number>();
    for (const line of lines) {
      if (line.trim()) {
        const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
        indentLevels.add(leadingSpaces);
      }
    }
    const sortedIndents = Array.from(indentLevels).sort((a, b) => a - b);
    const indentMap = new Map<number, number>();
    sortedIndents.forEach((indent, index) => indentMap.set(indent, index));
    
    for (const line of lines) {
      const trimmedLine = line.trim();
      if (!trimmedLine) continue;
      
      const leadingSpaces = line.match(/^(\s*)/)?.[1]?.length || 0;
      const indentLevel = indentMap.get(leadingSpaces) || 0;
      
      // Check for pure arrow lines
      if (/^[↓⬇]+$/.test(trimmedLine)) {
        elements.push({ type: 'arrow', content: '↓', indentLevel });
        continue;
      }
      
      // Check for pure vertical connector lines
      if (/^│+$/.test(trimmedLine)) {
        elements.push({ type: 'connector', content: '│', indentLevel });
        continue;
      }
      
      // Check for "or" / "and" branching keywords
      if (/^(or|and|OR|AND)$/i.test(trimmedLine)) {
        elements.push({ type: 'branch', content: trimmedLine.toLowerCase(), indentLevel });
        continue;
      }
      
      // This is a node - check for highlighting markers and annotations
      let content = trimmedLine;
      let annotation = '';
      let isHighlighted = false;
      
      // Check for THIS TYPE/CLASS markers
      if (/←\s*(THIS\s*(TYPE|CLASS)?|DEZE\s*(TYPE|KLASSE)?)/i.test(content)) {
        isHighlighted = true;
        // Extract the marker as annotation
        const markerMatch = content.match(/←\s*(THIS\s*(TYPE|CLASS)?|DEZE\s*(TYPE|KLASSE)?)/i);
        if (markerMatch) {
          annotation = markerMatch[1];
          content = content.replace(/\s*←\s*(THIS\s*(TYPE|CLASS)?|DEZE\s*(TYPE|KLASSE)?)/gi, '');
        }
      }
      
      // Check for parenthetical annotations like "(active use)"
      const parenMatch = content.match(/\(([^)]+)\)\s*$/);
      if (parenMatch && !isHighlighted) {
        annotation = parenMatch[1];
        content = content.replace(/\s*\([^)]+\)\s*$/, '');
      }
      
      // Clean up any remaining tree characters that might have slipped through
      content = content.replace(/^[│\s]+/, '').trim();
      
      if (content) {
        elements.push({ 
          type: 'node', 
          content, 
          annotation: annotation || undefined,
          isHighlighted, 
          indentLevel 
        });
      }
    }
    
    // If we didn't parse meaningful elements, return original
    if (elements.length < 2) {
      return match;
    }
    
    // Build HTML output
    let html = '<div class="linkml-flow-diagram">';
    
    for (const element of elements) {
      const indentStyle = element.indentLevel ? ` style="margin-left: ${element.indentLevel * 1.5}rem;"` : '';
      
      if (element.type === 'node') {
        const highlightClass = element.isHighlighted ? ' linkml-flow-node--highlighted' : '';
        html += `<div class="linkml-flow-node${highlightClass}"${indentStyle}>`;
        html += `<span class="linkml-flow-node__content">${element.content}</span>`;
        if (element.annotation) {
          html += `<span class="linkml-flow-node__annotation">${element.annotation}</span>`;
        }
        html += '</div>';
      } else if (element.type === 'arrow') {
        html += `<div class="linkml-flow-arrow"${indentStyle}>↓</div>`;
      } else if (element.type === 'connector') {
        html += `<div class="linkml-flow-connector"${indentStyle}>│</div>`;
      } else if (element.type === 'branch') {
        html += `<div class="linkml-flow-branch"${indentStyle}>${element.content}</div>`;
      }
    }
    
    html += '</div>';
    return html;
  });
};

/**
 * ASCII Lifecycle Diagram transformer.
 * Converts ASCII box diagrams (using ┌─┐│└─┘ characters) into styled HTML cards.
 * 
 * Detects patterns like:
 * ```
 * ┌─────────────────────────────────────┐
 * │  CustodianAdministration            │
 * │  ═════════════════════════          │
 * │  ACTIVE records in daily use        │
 * └─────────────────────────────────────┘
 *                   ↓
 * ┌─────────────────────────────────────┐
 * │  CustodianArchive                   │
 * └─────────────────────────────────────┘
 * ```
 * 
 * And converts them to styled HTML cards with flow arrows.
 */
const transformLifecycleDiagrams = (text: string): string => {
  if (!text) return text;
  
  // First transform tree diagrams, then flow diagrams
  text = transformTreeDiagrams(text);
  text = transformFlowDiagrams(text);
  
  // Pattern to match markdown code blocks containing ASCII box diagrams
  // Looks for ``` followed by content containing box-drawing characters
  const codeBlockPattern = /```\n?([\s\S]*?)\n?```/g;
  
  return text.replace(codeBlockPattern, (match, codeContent: string) => {
    // Check if this code block contains ASCII box diagrams (with top corners)
    const hasBoxChars = /[┌┐└┘│─═]/.test(codeContent) && /[┌┐]/.test(codeContent);
    if (!hasBoxChars) {
      // Not a box diagram, return original code block
      return match;
    }
    
    // Parse the diagram into boxes and connectors
    const lines = codeContent.split('\n');
    const elements: Array<{ type: 'box' | 'arrow' | 'text'; content: string; title?: string; isHighlighted?: boolean }> = [];
    let currentBox: string[] = [];
    let inBox = false;
    
    for (const line of lines) {
      const trimmedLine = line.trim();
      
      // Check for box start
      if (trimmedLine.startsWith('┌') && trimmedLine.endsWith('┐')) {
        inBox = true;
        currentBox = [];
        continue;
      }
      
      // Check for box end
      if (trimmedLine.startsWith('└') && trimmedLine.endsWith('┘')) {
        if (currentBox.length > 0) {
          // Extract title (first non-empty line or line with ═)
          let title = '';
          let content: string[] = [];
          let isHighlighted = false;
          
          for (let i = 0; i < currentBox.length; i++) {
            const boxLine = currentBox[i];
            // Check if this is a title line (followed by ═ underline or contains "(THIS CLASS)")
            if (i === 0 || boxLine.includes('═') || boxLine.includes('THIS CLASS')) {
              if (boxLine.includes('═')) {
                // This is an underline, title was previous line
                continue;
              }
              if (boxLine.includes('THIS CLASS') || boxLine.includes('(THIS')) {
                isHighlighted = true;
                title = boxLine.replace(/\(THIS CLASS\)/g, '').replace(/\(THIS\)/g, '').trim();
              } else if (!title && boxLine.trim()) {
                title = boxLine.trim();
              } else {
                content.push(boxLine);
              }
            } else {
              content.push(boxLine);
            }
          }
          
          // Clean up title - remove leading/trailing special characters
          title = title.replace(/^[═\s]+|[═\s]+$/g, '').trim();
          
          elements.push({
            type: 'box',
            title: title || 'Untitled',
            content: content.filter(l => l.trim() && !l.includes('═')).join('\n'),
            isHighlighted
          });
        }
        inBox = false;
        continue;
      }
      
      // Inside a box - collect content
      if (inBox) {
        // Remove box side characters and clean up
        const cleanLine = trimmedLine
          .replace(/^│\s*/, '')
          .replace(/\s*│$/, '')
          .trim();
        if (cleanLine) {
          currentBox.push(cleanLine);
        }
        continue;
      }
      
      // Check for arrow/connector lines
      if (trimmedLine.includes('↓') || trimmedLine.includes('→') || trimmedLine.includes('⬇')) {
        elements.push({ type: 'arrow', content: '↓' });
        continue;
      }
      
      // Check for text between boxes (descriptions of transitions)
      if (trimmedLine.startsWith('(') && (trimmedLine.endsWith(')') || trimmedLine.includes(')'))) {
        // This is explanatory text in parentheses
        const textContent = trimmedLine.replace(/^\(|\)$/g, '').trim();
        elements.push({ type: 'text', content: textContent });
        continue;
      }
      
      // Other non-empty lines between boxes (transition descriptions)
      if (trimmedLine && !trimmedLine.match(/^[─┌┐└┘│═]+$/)) {
        // Continuation of previous text or standalone text
        const lastElement = elements[elements.length - 1];
        if (lastElement && lastElement.type === 'text') {
          lastElement.content += ' ' + trimmedLine.replace(/^\(|\)$/g, '');
        } else if (trimmedLine.length > 2) {
          elements.push({ type: 'text', content: trimmedLine.replace(/^\(|\)$/g, '') });
        }
      }
    }
    
    // If no elements were parsed, return original
    if (elements.length === 0) {
      return match;
    }
    
    // Build HTML output
    let html = '<div class="linkml-lifecycle-diagram">';
    
    for (const element of elements) {
      if (element.type === 'box') {
        const highlightClass = element.isHighlighted ? ' linkml-lifecycle-box--highlighted' : '';
        html += `<div class="linkml-lifecycle-box${highlightClass}">`;
        html += `<div class="linkml-lifecycle-box__title">${element.title}</div>`;
        if (element.content) {
          // Convert content lines to list items or paragraphs
          const contentLines = element.content.split('\n').filter(l => l.trim());
          if (contentLines.length > 0) {
            html += '<div class="linkml-lifecycle-box__content">';
            for (const line of contentLines) {
              // Check if it's a list item (starts with -)
              if (line.trim().startsWith('-')) {
                html += `<div class="linkml-lifecycle-box__item">${line.trim().substring(1).trim()}</div>`;
              } else if (line.trim().startsWith('✅') || line.trim().startsWith('❌')) {
                html += `<div class="linkml-lifecycle-box__item">${line.trim()}</div>`;
              } else {
                html += `<div class="linkml-lifecycle-box__line">${line}</div>`;
              }
            }
            html += '</div>';
          }
        }
        html += '</div>';
      } else if (element.type === 'arrow') {
        html += '<div class="linkml-lifecycle-arrow">↓</div>';
      } else if (element.type === 'text') {
        html += `<div class="linkml-lifecycle-text">${element.content}</div>`;
      }
    }
    
    html += '</div>';
    return html;
  });
};

/**
 * Combined content transformer that applies all text transformations.
 * Order matters: 
 * 1. Lifecycle diagrams first (process code blocks before other transformations)
 * 2. CURIEs (so they don't get broken by admonition spans)
 * 3. Admonitions last
 */
const transformContent = (text: string): string => {
  if (!text) return text;
  return transformAdmonitions(highlightCuries(transformLifecycleDiagrams(text)));
};

/**
 * Build a filtered UML diagram showing the current class and related classes up to a specified depth.
 * Used in the per-class UML accordion to provide a focused view of class relationships.
 * 
 * @param centerClassName - The central class to build the diagram around
 * @param allExportInfo - Map of class name to ClassExportInfo for all loaded classes
 * @param depth - How many hops from center to include (1 = direct only, 2 = secondary, etc.)
 * @param parentClasses - Map of class name to parent class (from is_a inheritance)
 * @returns UMLDiagram with filtered nodes and links
 */
const buildFilteredUMLDiagram = (
  centerClassName: string,
  allExportInfo: Record<string, ClassExportInfo>,
  depth: number = 1,
  parentClasses: Record<string, string> = {}
): UMLDiagram => {
  const nodes: UMLNode[] = [];
  const links: UMLLink[] = [];
  const addedNodes = new Set<string>();
  const addedLinks = new Set<string>(); // Track links to avoid duplicates
  const processedAtDepth = new Map<string, number>(); // Track at what depth each class was processed
  
  // Helper to add a node if not already added
  const addNode = (name: string, _distanceFromCenter: number, type: 'class' | 'enum' = 'class') => {
    if (!addedNodes.has(name)) {
      addedNodes.add(name);
      nodes.push({
        id: name,
        name: name,
        type: type,
        attributes: [],
        methods: [],
      });
    }
  };
  
  // Helper to add a link if not already added (checking both directions)
  const addLink = (source: string, target: string, type: UMLLink['type'], label: string) => {
    const linkKey1 = `${source}->${target}:${type}`;
    const linkKey2 = `${target}->${source}:${type}`;
    // For inheritance, direction matters; for others, check both
    if (type === 'inheritance') {
      if (!addedLinks.has(linkKey1)) {
        addedLinks.add(linkKey1);
        links.push({ source, target, type, label });
      }
    } else {
      if (!addedLinks.has(linkKey1) && !addedLinks.has(linkKey2)) {
        addedLinks.add(linkKey1);
        links.push({ source, target, type, label });
      }
    }
  };
  
  // Get all related classes from export info
  const getRelatedClasses = (className: string): string[] => {
    const exportInfo = allExportInfo[className];
    if (!exportInfo) return [];
    
    const related: string[] = [];
    
    // Parent class
    if (parentClasses[className]) {
      related.push(parentClasses[className]);
    }
    
    // Subclasses
    related.push(...exportInfo.subclasses);
    
    // Mixin users
    related.push(...exportInfo.mixinUsers);
    
    // Classes using slots with this range
    related.push(...exportInfo.classesUsingSlotWithThisRange.map(c => c.className));
    
    // Classes referencing in slot_usage
    related.push(...exportInfo.classesReferencingInSlotUsage);
    
    return [...new Set(related)]; // Dedupe
  };
  
  // Add relationships from a class's export info
  const addRelationshipsFromClass = (className: string) => {
    const exportInfo = allExportInfo[className];
    if (!exportInfo) return;
    
    // Parent class (is_a inheritance)
    if (parentClasses[className] && addedNodes.has(parentClasses[className])) {
      addLink(className, parentClasses[className], 'inheritance', 'is_a');
    }
    
    // Subclasses
    for (const subclass of exportInfo.subclasses) {
      if (addedNodes.has(subclass)) {
        addLink(subclass, className, 'inheritance', 'is_a');
      }
    }
    
    // Mixin users
    for (const mixinUser of exportInfo.mixinUsers) {
      if (addedNodes.has(mixinUser)) {
        addLink(mixinUser, className, 'association', 'mixin');
      }
    }
    
    // Classes using slots with this range
    for (const { className: usingClass, slotName } of exportInfo.classesUsingSlotWithThisRange) {
      if (addedNodes.has(usingClass)) {
        addLink(usingClass, className, 'aggregation', slotName);
      }
    }
    
    // Classes referencing in slot_usage
    for (const refClass of exportInfo.classesReferencingInSlotUsage) {
      if (addedNodes.has(refClass)) {
        addLink(refClass, className, 'association', 'slot_usage');
      }
    }
  };
  
  // BFS to add nodes up to specified depth
  const queue: Array<{ className: string; currentDepth: number }> = [
    { className: centerClassName, currentDepth: 0 }
  ];
  
  while (queue.length > 0) {
    const { className, currentDepth } = queue.shift()!;
    
    // Skip if already processed at same or lower depth
    const existingDepth = processedAtDepth.get(className);
    if (existingDepth !== undefined && existingDepth <= currentDepth) {
      continue;
    }
    processedAtDepth.set(className, currentDepth);
    
    // Add node
    addNode(className, currentDepth);
    
    // If we haven't reached max depth, queue related classes
    if (currentDepth < depth) {
      const related = getRelatedClasses(className);
      for (const relatedClass of related) {
        // Only queue if not already processed at a lower depth
        const relDepth = processedAtDepth.get(relatedClass);
        if (relDepth === undefined || relDepth > currentDepth + 1) {
          queue.push({ className: relatedClass, currentDepth: currentDepth + 1 });
        }
      }
    }
  }
  
  // Now add all links between added nodes
  for (const className of addedNodes) {
    addRelationshipsFromClass(className);
  }
  
  return {
    nodes,
    links,
    title: `${centerClassName} Relationships (Depth: ${depth})`,
  };
};

// Bilingual text content
const TEXT = {
  sidebarTitle: { nl: 'LinkML-schema\'s', en: 'LinkML Schemas' },
  pageTitle: { nl: 'LinkML-schemaviewer', en: 'LinkML Schema Viewer' },
  visualView: { nl: 'Visuele weergave', en: 'Visual View' },
  rawYaml: { nl: 'Ruwe YAML', en: 'Raw YAML' },
  loading: { nl: 'Schema laden...', en: 'Loading schema...' },
  noSchemasFound: { nl: 'Geen schema\'s gevonden. Zorg dat het manifest is gegenereerd.', en: 'No schemas found. Make sure the manifest is generated.' },
  failedToInit: { nl: 'Initialiseren van schemalijst mislukt', en: 'Failed to initialize schema list' },
  failedToLoad: { nl: 'Schema laden mislukt:', en: 'Failed to load schema:' },
  unnamedSchema: { nl: 'Naamloos schema', en: 'Unnamed Schema' },
  prefixes: { nl: 'Prefixen', en: 'Prefixes' },
  classes: { nl: 'Klassen', en: 'Classes' },
  slots: { nl: 'Slots', en: 'Slots' },
  enumerations: { nl: 'Enumeraties', en: 'Enumerations' },
  imports: { nl: 'Imports', en: 'Imports' },
  abstract: { nl: 'abstract', en: 'abstract' },
  required: { nl: 'verplicht', en: 'required' },
  multivalued: { nl: 'meervoudig', en: 'multivalued' },
  uri: { nl: 'URI:', en: 'URI:' },
  id: { nl: 'ID:', en: 'ID:' },
  version: { nl: 'Versie:', en: 'Version:' },
  range: { nl: 'Bereik:', en: 'Range:' },
  pattern: { nl: 'Patroon:', en: 'Pattern:' },
  slotsLabel: { nl: 'Slots:', en: 'Slots:' },
  exactMappings: { nl: 'Exacte mappings:', en: 'Exact Mappings:' },
  closeMappings: { nl: 'Vergelijkbare mappings:', en: 'Close Mappings:' },
  permissibleValues: { nl: 'Toegestane waarden:', en: 'Permissible Values:' },
  meaning: { nl: 'betekenis:', en: 'meaning:' },
  searchPlaceholder: { nl: 'Zoeken in waarden...', en: 'Search values...' },
  showAll: { nl: 'Alles tonen', en: 'Show all' },
  showLess: { nl: 'Minder tonen', en: 'Show less' },
  showing: { nl: 'Toont', en: 'Showing' },
  of: { nl: 'van', en: 'of' },
  noResults: { nl: 'Geen resultaten gevonden', en: 'No results found' },
  searchSchemas: { nl: 'Zoeken in schema\'s...', en: 'Search schemas...' },
  mainSchema: { nl: 'Hoofdschema', en: 'Main Schema' },
  noMatchingSchemas: { nl: 'Geen overeenkomende schema\'s', en: 'No matching schemas' },
  copyToClipboard: { nl: 'Kopieer naar klembord', en: 'Copy to clipboard' },
  copied: { nl: 'Gekopieerd!', en: 'Copied!' },
  use3DPolygon: { nl: '3D-polygoon', en: '3D Polygon' },
  use2DBadge: { nl: '2D-badge', en: '2D Badge' },
};

// Dynamically discover schema files from the modules directory
interface SchemaCategory {
  name: string;
  displayName: string;
  files: SchemaFile[];
}

const LinkMLViewerPage: React.FC = () => {
  const { language } = useLanguage();
  const t = (key: keyof typeof TEXT) => TEXT[key][language];
  
  const [searchParams, setSearchParams] = useSearchParams();
  const [categories, setCategories] = useState<SchemaCategory[]>([]);
  const [selectedSchema, setSelectedSchema] = useState<SchemaFile | null>(null);
  const [schema, setSchema] = useState<LinkMLSchema | null>(null);
  const [rawYaml, setRawYaml] = useState<string | null>(null);
  const [viewMode, setViewMode] = useState<'visual' | 'raw'>('visual');
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['classes', 'enums', 'slots']));
  const [highlightedClass, setHighlightedClass] = useState<string | null>(null);
  const highlightedRef = useRef<HTMLDivElement>(null);
  
  // State for expandable enum ranges in slots
  const [expandedEnumRanges, setExpandedEnumRanges] = useState<Set<string>>(new Set());
  const [loadedEnums, setLoadedEnums] = useState<Record<string, LinkMLEnum | null>>({});
  const [enumSearchFilters, setEnumSearchFilters] = useState<Record<string, string>>({});
  const [enumShowAll, setEnumShowAll] = useState<Record<string, boolean>>({});
  
  // State for pre-loaded custodian types (loaded async from schema annotations)
  // Maps element name -> custodian type codes
  const [classCustodianTypes, setClassCustodianTypes] = useState<Record<string, CustodianTypeCode[]>>({});
  const [slotCustodianTypes, setSlotCustodianTypes] = useState<Record<string, CustodianTypeCode[]>>({});
  const [enumCustodianTypes, setEnumCustodianTypes] = useState<Record<string, CustodianTypeCode[]>>({});
  const [_custodianTypesLoaded, setCustodianTypesLoaded] = useState(false);
  
  // State for sidebar search and category filters
  const [sidebarSearch, setSidebarSearch] = useState<string>('');
  const [categoryFilters, setCategoryFilters] = useState<Record<string, boolean>>({
    main: true,
    class: true,
    enum: true,
    slot: true,
  });
  
  // State for copy to clipboard feedback
  const [copyFeedback, setCopyFeedback] = useState(false);
  
  // State for expandable Exports section in class details
  const [expandedExports, setExpandedExports] = useState<Set<string>>(new Set());
  const [classExports, setClassExports] = useState<Record<string, ClassExportInfo>>({});
  const [loadingExports, setLoadingExports] = useState<Set<string>>(new Set());
  
  // State for expandable UML diagram section in class details
  const [expandedUML, setExpandedUML] = useState<Set<string>>(new Set());
  
  // State for UML depth slider per class (1 = direct relationships only, 2 = secondary, etc.)
  const [umlDepth, setUmlDepth] = useState<Record<string, number>>({});
  
  // State for parent class mapping (className -> parentClassName from is_a inheritance)
  // Built incrementally as classes are loaded for UML diagrams
  const [parentClassMap, setParentClassMap] = useState<Record<string, string>>({});
  
  // State for 3D polygon indicator toggle
  // Persisted in localStorage - defaults to false (2D SVG) if not set
  const [use3DIndicator, setUse3DIndicator] = useState(() => {
    try {
      const saved = localStorage.getItem('linkml-viewer-use-3d');
      return saved === 'true';
    } catch {
      return false;
    }
  });
  
  // Persist 3D mode preference to localStorage
  useEffect(() => {
    try {
      localStorage.setItem('linkml-viewer-use-3d', use3DIndicator ? 'true' : 'false');
    } catch {
      // Ignore localStorage errors (e.g., private browsing)
    }
  }, [use3DIndicator]);
  
  // State for bidirectional hover sync between 3D polyhedrons and legend bar
  const [hoveredCustodianType, setHoveredCustodianType] = useState<CustodianTypeCode | null>(null);
  
  // State for filtering schema elements by custodian type (click on polyhedron face or legend)
  // Multi-select: Set of selected type codes (empty = no filter)
  // Initialize from URL params (e.g., ?custodian=G,L,A)
  const [custodianTypeFilter, setCustodianTypeFilter] = useState<Set<CustodianTypeCode>>(() => {
    const param = searchParams.get('custodian');
    if (!param) return new Set();
    const codes = param.split(',').filter((c): c is CustodianTypeCode => 
      CUSTODIAN_TYPE_CODES.includes(c as CustodianTypeCode)
    );
    return new Set(codes);
  });
  
  // Sync custodian filter to URL params
  useEffect(() => {
    const currentParam = searchParams.get('custodian');
    const filterCodes = Array.from(custodianTypeFilter).sort().join(',');
    
    // Only update if different to avoid loops
    if (custodianTypeFilter.size === 0 && currentParam) {
      // Remove param if filter is empty
      searchParams.delete('custodian');
      setSearchParams(searchParams, { replace: true });
    } else if (custodianTypeFilter.size > 0 && currentParam !== filterCodes) {
      searchParams.set('custodian', filterCodes);
      setSearchParams(searchParams, { replace: true });
    }
  }, [custodianTypeFilter, searchParams, setSearchParams]);
  
  // Ref for main content (used by navigation-synced collapsible header)
  const mainContentRef = useRef<HTMLElement>(null);
  
  // Schema loading progress tracking
  const { progress: schemaProgress, isLoading: isSchemaServiceLoading, isComplete: isSchemaServiceComplete } = useSchemaLoadingProgress();
  
  // Handler for filtering by custodian type (clicking polyhedron face or legend item)
  // Multi-select toggle behavior: clicking type adds/removes from set
  const handleCustodianTypeFilter = useCallback((typeCode: CustodianTypeCode) => {
    setCustodianTypeFilter(prev => {
      const newSet = new Set(prev);
      if (newSet.has(typeCode)) {
        newSet.delete(typeCode);
      } else {
        newSet.add(typeCode);
      }
      return newSet;
    });
  }, []);

  // Toggle exports section for a class and load export data on demand
  const toggleExports = useCallback(async (className: string) => {
    // Toggle expansion state
    setExpandedExports(prev => {
      const next = new Set(prev);
      if (next.has(className)) {
        next.delete(className);
      } else {
        next.add(className);
      }
      return next;
    });
    
    // Load export data if not already loaded and schema service is ready
    if (!classExports[className] && !loadingExports.has(className) && isSchemaServiceComplete) {
      setLoadingExports(prev => new Set(prev).add(className));
      try {
        const exportInfo = await linkmlSchemaService.getClassExportInfo(className);
        setClassExports(prev => ({ ...prev, [className]: exportInfo }));
      } catch (error) {
        console.error(`Error loading export info for ${className}:`, error);
      } finally {
        setLoadingExports(prev => {
          const next = new Set(prev);
          next.delete(className);
          return next;
        });
      }
    }
  }, [classExports, loadingExports, isSchemaServiceComplete]);

  // Toggle UML diagram section for a class
  // Reuses already-loaded classExports data from the Exports accordion
  const toggleUML = useCallback((className: string) => {
    setExpandedUML(prev => {
      const next = new Set(prev);
      if (next.has(className)) {
        next.delete(className);
      } else {
        next.add(className);
      }
      return next;
    });
    
    // If exports data not loaded yet, trigger the load
    // (UML depends on classExports data)
    if (!classExports[className] && !loadingExports.has(className) && isSchemaServiceComplete) {
      toggleExports(className);
    }
  }, [classExports, loadingExports, isSchemaServiceComplete, toggleExports]);

  // Load export info for related classes when increasing depth
  // Uses BFS to find all classes within the specified depth and loads their export info
  const loadExportInfoForDepth = useCallback(async (centerClassName: string, depth: number) => {
    if (!isSchemaServiceComplete) return;
    
    // Get currently loaded export info for center class
    const centerExport = classExports[centerClassName];
    if (!centerExport) return;
    
    // BFS to find all classes we need to load
    const classesToLoad = new Set<string>();
    const visited = new Set<string>();
    const queue: Array<{ className: string; currentDepth: number }> = [
      { className: centerClassName, currentDepth: 0 }
    ];
    
    while (queue.length > 0) {
      const { className, currentDepth } = queue.shift()!;
      
      if (visited.has(className) || currentDepth > depth) continue;
      visited.add(className);
      
      const exportInfo = classExports[className];
      
      // If we don't have export info for this class, mark it for loading
      if (!exportInfo && !loadingExports.has(className)) {
        classesToLoad.add(className);
        continue; // Can't traverse further without export info
      }
      
      if (!exportInfo) continue;
      
      // Get related classes
      const related: string[] = [];
      related.push(...exportInfo.subclasses);
      related.push(...exportInfo.mixinUsers);
      related.push(...exportInfo.classesUsingSlotWithThisRange.map(c => c.className));
      related.push(...exportInfo.classesReferencingInSlotUsage);
      
      // Also check parent class from semantic info if available
      const parentClass = parentClassMap[className];
      if (parentClass) {
        related.push(parentClass);
      }
      
      // Queue related classes for next depth level
      if (currentDepth < depth) {
        for (const relatedClass of related) {
          if (!visited.has(relatedClass)) {
            queue.push({ className: relatedClass, currentDepth: currentDepth + 1 });
          }
        }
      }
    }
    
    // Load all classes that need loading
    if (classesToLoad.size === 0) return;
    
    // Mark as loading
    setLoadingExports(prev => {
      const next = new Set(prev);
      classesToLoad.forEach(c => next.add(c));
      return next;
    });
    
    // Load in parallel
    const loadPromises = Array.from(classesToLoad).map(async (className) => {
      try {
        const exportInfo = await linkmlSchemaService.getClassExportInfo(className);
        
        // Also try to get parent class info
        try {
          const semanticInfo = await linkmlSchemaService.getClassSemanticInfo(className);
          if (semanticInfo?.parentClass) {
            setParentClassMap(prev => ({ ...prev, [className]: semanticInfo.parentClass! }));
          }
        } catch {
          // Semantic info not available for all classes
        }
        
        return { className, exportInfo };
      } catch (error) {
        console.error(`Error loading export info for ${className}:`, error);
        return { className, exportInfo: null };
      }
    });
    
    const results = await Promise.all(loadPromises);
    
    // Update state with loaded export info
    setClassExports(prev => {
      const next = { ...prev };
      for (const { className, exportInfo } of results) {
        if (exportInfo) {
          next[className] = exportInfo;
        }
      }
      return next;
    });
    
    // Clear loading state
    setLoadingExports(prev => {
      const next = new Set(prev);
      classesToLoad.forEach(c => next.delete(c));
      return next;
    });
    
    // If we loaded new classes, we may need to traverse further
    // Schedule another pass if we're not at full depth yet
    const hasNewClasses = results.some(r => r.exportInfo !== null);
    if (hasNewClasses && depth > 1) {
      // Re-run to potentially load more classes at deeper levels
      setTimeout(() => loadExportInfoForDepth(centerClassName, depth), 100);
    }
  }, [classExports, loadingExports, parentClassMap, isSchemaServiceComplete]);

  // Debounced version of loadExportInfoForDepth for slider interaction
  // This prevents excessive API calls when dragging the slider
  const debouncedLoadExportInfo = useMemo(
    () => debounce((className: string, depth: number) => {
      loadExportInfoForDepth(className, depth);
    }, 300),
    [loadExportInfoForDepth]
  );

  // Cleanup debounced function on unmount
  useEffect(() => {
    return () => {
      debouncedLoadExportInfo.cancel();
    };
  }, [debouncedLoadExportInfo]);

  // Memoized UML diagram builder to prevent recalculation on every render
  // Key is className + depth combination
  const getMemoizedUMLDiagram = useCallback((className: string, depth: number) => {
    return buildFilteredUMLDiagram(className, classExports, depth, parentClassMap);
  }, [classExports, parentClassMap]);

  // Navigate to a class by updating URL params and selecting the schema file
  const navigateToClass = useCallback((className: string) => {
    setSearchParams({ class: className });
    setHighlightedClass(className);
    
    // Find and select the class file
    const classFile = categories
      .find(cat => cat.name === 'class')
      ?.files.find(file => file.name === className);
    
    if (classFile) {
      setSelectedSchema(classFile);
      // Ensure classes section is expanded
      setExpandedSections(prev => new Set([...prev, 'classes']));
    }
  }, [categories, setSearchParams]);

  // Track if initialization has already happened (prevents re-init on URL param changes)
  const isInitializedRef = useRef(false);
  
  // Handle URL parameters for deep linking (only used on initial mount)
  const handleUrlParams = useCallback((cats: SchemaCategory[], currentSearchParams: URLSearchParams) => {
    const classParam = currentSearchParams.get('class');
    
    if (classParam) {
      setHighlightedClass(classParam);
      
      // Find the schema file that contains this class
      const classFile = cats
        .find(cat => cat.name === 'class')
        ?.files.find(file => file.name === classParam);
      
      if (classFile) {
        setSelectedSchema(classFile);
        // Ensure classes section is expanded
        setExpandedSections(prev => new Set([...prev, 'classes']));
      }
    }
  }, []);

  // Initialize schema file list from manifest - RUNS ONLY ONCE on mount
  // Note: Does NOT depend on searchParams to prevent re-initialization when
  // custodian filter changes the URL. Deep linking for ?class= is handled
  // by reading searchParams directly inside the effect on initial mount only.
  useEffect(() => {
    // Skip if already initialized (prevents re-init on searchParams changes from filter)
    if (isInitializedRef.current) {
      return;
    }
    
    const initializeSchemas = async () => {
      setIsLoading(true);
      try {
        // Fetch the schema manifest (dynamically generated at build time)
        const cats = await fetchSchemaManifest();
        
        if (cats.length === 0) {
          setError(t('noSchemasFound'));
          return;
        }
        
        setCategories(cats);
        
        // Check URL params for deep linking (read searchParams directly, don't depend on it)
        handleUrlParams(cats, searchParams);
        
        // Select main schema by default if no URL param set the schema
        const classParam = searchParams.get('class');
        if (!classParam && cats[0]?.files.length > 0) {
          setSelectedSchema(cats[0].files[0]);
        }
        
        // Mark as initialized to prevent re-running
        isInitializedRef.current = true;
      } catch (err) {
        setError(t('failedToInit'));
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    };
    
    initializeSchemas();
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []); // Empty deps - run only on mount

  // Scroll to highlighted class when it changes
  useEffect(() => {
    if (highlightedClass && highlightedRef.current) {
      setTimeout(() => {
        highlightedRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }, 100);
    }
  }, [highlightedClass, schema]);

  // Fetch schema manifest from dynamically generated JSON file
  const fetchSchemaManifest = async (): Promise<SchemaCategory[]> => {
    try {
      const response = await fetch('/schemas/20251121/linkml/manifest.json');
      if (!response.ok) {
        throw new Error(`Failed to fetch manifest: ${response.status}`);
      }
      const manifest = await response.json();
      
      // Transform manifest categories to our format
      return manifest.categories.map((cat: { name: string; displayName: string; files: Array<{ name: string; path: string; category: string }> }) => ({
        name: cat.name,
        displayName: cat.displayName,
        files: cat.files.map((file: { name: string; path: string; category: string }) => ({
          name: file.name,
          path: file.path,
          category: cat.name as SchemaFile['category']
        }))
      }));
    } catch (err) {
      console.error('Failed to fetch schema manifest:', err);
      // Return empty categories on error
      return [];
    }
  };

  // Load selected schema
  useEffect(() => {
    if (!selectedSchema) return;
    
    const loadSelectedSchema = async () => {
      setIsLoading(true);
      setError(null);
      
      try {
        const [schemaData, yamlContent] = await Promise.all([
          loadSchema(selectedSchema.path),
          loadSchemaRaw(selectedSchema.path)
        ]);
        
        setSchema(schemaData);
        setRawYaml(yamlContent);
      } catch (err) {
        setError(`${t('failedToLoad')} ${selectedSchema.name}`);
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    };
    
    loadSelectedSchema();
  }, [selectedSchema]);

  // Load custodian types from schema annotations when schema changes
  // This pre-loads types asynchronously so they're available for rendering
  // IMPORTANT: Wait for schema service to complete loading before fetching custodian types
  // to avoid race condition where annotations aren't available yet
  useEffect(() => {
    if (!schema) {
      setCustodianTypesLoaded(false);
      return;
    }
    
    // Don't load custodian types until schema service has finished loading all class files
    // This prevents the race condition where we try to read annotations before they're loaded
    if (!isSchemaServiceComplete) {
      console.log('[LinkMLViewerPage] Waiting for schema service to complete before loading custodian types...');
      return;
    }
    
    const loadCustodianTypes = async () => {
      const classes = extractClasses(schema);
      const slots = extractSlots(schema);
      const enums = extractEnums(schema);
      
      console.log('[LinkMLViewerPage] Schema service complete, loading custodian types for', {
        classes: classes.length,
        slots: slots.length,
        enums: enums.length
      });
      
      // Load types for all classes in parallel
      const classTypesPromises = classes.map(async (cls) => {
        const types = await getCustodianTypesForClassAsync(cls.name);
        return [cls.name, types] as const;
      });
      
      // Load types for all slots in parallel
      const slotTypesPromises = slots.map(async (slot) => {
        const types = await getCustodianTypesForSlotAsync(slot.name);
        return [slot.name, types] as const;
      });
      
      // Load types for all enums in parallel
      const enumTypesPromises = enums.map(async (enumDef) => {
        const types = await getCustodianTypesForEnumAsync(enumDef.name);
        return [enumDef.name, types] as const;
      });
      
      try {
        const [classResults, slotResults, enumResults] = await Promise.all([
          Promise.all(classTypesPromises),
          Promise.all(slotTypesPromises),
          Promise.all(enumTypesPromises)
        ]);
        
        // Convert to records
        const classTypesMap: Record<string, CustodianTypeCode[]> = {};
        for (const [name, types] of classResults) {
          classTypesMap[name] = types;
        }
        
        const slotTypesMap: Record<string, CustodianTypeCode[]> = {};
        for (const [name, types] of slotResults) {
          slotTypesMap[name] = types;
        }
        
        const enumTypesMap: Record<string, CustodianTypeCode[]> = {};
        for (const [name, types] of enumResults) {
          enumTypesMap[name] = types;
        }
        
        setClassCustodianTypes(classTypesMap);
        setSlotCustodianTypes(slotTypesMap);
        setEnumCustodianTypes(enumTypesMap);
        setCustodianTypesLoaded(true);
        
        console.log('[LinkMLViewerPage] Loaded custodian types from schema annotations:', {
          classes: Object.keys(classTypesMap).length,
          slots: Object.keys(slotTypesMap).length,
          enums: Object.keys(enumTypesMap).length
        });
      } catch (error) {
        console.error('[LinkMLViewerPage] Error loading custodian types:', error);
        // Fall back to sync functions (will use defaults)
        setCustodianTypesLoaded(true);
      }
    };
    
    loadCustodianTypes();
  }, [schema, isSchemaServiceComplete]);

  const toggleSection = (section: string) => {
    setExpandedSections(prev => {
      const next = new Set(prev);
      if (next.has(section)) {
        next.delete(section);
      } else {
        next.add(section);
      }
      return next;
    });
  };

  // Check if a range is an enum type
  const isEnumRange = (range: string): boolean => {
    return range.endsWith('Enum');
  };

  // Toggle enum range expansion and load enum data if needed
  const toggleEnumRange = async (slotName: string, enumName: string) => {
    const key = `${slotName}:${enumName}`;
    
    // Toggle expansion state
    setExpandedEnumRanges(prev => {
      const next = new Set(prev);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.add(key);
      }
      return next;
    });
    
    // Load enum data if not already loaded
    if (!loadedEnums[enumName]) {
      try {
        const enumSchema = await loadSchema(`modules/enums/${enumName}.yaml`);
        if (enumSchema?.enums) {
          const enumDef = Object.values(enumSchema.enums)[0] as LinkMLEnum;
          if (enumDef) {
            setLoadedEnums(prev => ({ ...prev, [enumName]: { ...enumDef, name: enumName } }));
          }
        }
      } catch (err) {
        console.error(`Failed to load enum ${enumName}:`, err);
        setLoadedEnums(prev => ({ ...prev, [enumName]: null }));
      }
    }
  };

  // Render enum values for a slot range
  const renderEnumValues = (_slotName: string, enumName: string) => {
    const enumDef = loadedEnums[enumName];
    
    if (!enumDef?.permissible_values) {
      return <div className="linkml-viewer__enum-loading">{t('loading')}</div>;
    }
    
    const allValues = Object.entries(enumDef.permissible_values);
    const searchFilter = enumSearchFilters[enumName] || '';
    const showAll = enumShowAll[enumName] || false;
    const displayCount = 20; // Show first 20 values initially
    
    // Filter values based on search
    const filteredValues = searchFilter
      ? allValues.filter(([value, details]) => {
          const searchLower = searchFilter.toLowerCase();
          return (
            value.toLowerCase().includes(searchLower) ||
            (details.description?.toLowerCase().includes(searchLower)) ||
            (details.meaning?.toLowerCase().includes(searchLower))
          );
        })
      : allValues;
    
    // Determine how many to show
    const valuesToShow = showAll || searchFilter ? filteredValues : filteredValues.slice(0, displayCount);
    
    return (
      <div className="linkml-viewer__range-enum-values">
        <div className="linkml-viewer__range-enum-header">
          {/* Search input */}
          <div className="linkml-viewer__range-enum-search">
            <input
              type="text"
              placeholder={t('searchPlaceholder')}
              value={searchFilter}
              onChange={(e) => setEnumSearchFilters(prev => ({ ...prev, [enumName]: e.target.value }))}
              className="linkml-viewer__range-enum-search-input"
            />
            {searchFilter && (
              <button
                className="linkml-viewer__range-enum-search-clear"
                onClick={() => setEnumSearchFilters(prev => ({ ...prev, [enumName]: '' }))}
                title="Clear search"
              >
                ×
              </button>
            )}
          </div>
          
          {/* Count display */}
          <span className="linkml-viewer__range-enum-count">
            {searchFilter ? (
              <>
                {t('showing')} {filteredValues.length} {t('of')} {allValues.length}
              </>
            ) : showAll ? (
              <>
                {allValues.length} {t('permissibleValues').replace(':', '')}
              </>
            ) : (
              <>
                {t('showing')} {Math.min(displayCount, allValues.length)} {t('of')} {allValues.length}
              </>
            )}
          </span>
        </div>
        
        <div className="linkml-viewer__range-enum-list">
          {valuesToShow.length > 0 ? (
            valuesToShow.map(([value, details]) => (
              <div key={value} className="linkml-viewer__range-enum-item">
                <code className="linkml-viewer__range-enum-name">{value}</code>
                {details.meaning && (
                  <a 
                    href={details.meaning.startsWith('wikidata:')
                      ? `https://www.wikidata.org/wiki/${details.meaning.replace('wikidata:', '')}`
                      : details.meaning}
                    target="_blank"
                    rel="noopener noreferrer"
                    className="linkml-viewer__range-enum-link"
                    title={details.meaning}
                  >
                    🔗
                  </a>
                )}
                {details.description && (
                  <span className="linkml-viewer__range-enum-desc">{details.description}</span>
                )}
              </div>
            ))
          ) : (
            <div className="linkml-viewer__range-enum-no-results">
              {t('noResults')}
            </div>
          )}
        </div>
        
        {/* Show all / Show less button */}
        {!searchFilter && allValues.length > displayCount && (
          <button
            className="linkml-viewer__range-enum-toggle-all"
            onClick={() => setEnumShowAll(prev => ({ ...prev, [enumName]: !showAll }))}
          >
            {showAll ? (
              <>▲ {t('showLess')}</>
            ) : (
              <>▼ {t('showAll')} ({allValues.length - displayCount} {language === 'nl' ? 'meer' : 'more'})</>
            )}
          </button>
        )}
      </div>
    );
  };

  const renderClassDetails = (cls: LinkMLClass) => {
    const isHighlighted = highlightedClass === cls.name;
    // Use pre-loaded types from schema annotations, fall back to sync function if not yet loaded
    const custodianTypes = classCustodianTypes[cls.name] || getCustodianTypesForClass(cls.name);
    const isUniversal = isUniversalElement(custodianTypes);
    
    // Check if this class matches the current custodian type filter (multi-select)
    const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t));
    
    return (
      <div 
        key={cls.name} 
        ref={isHighlighted ? highlightedRef : null}
        className={`linkml-viewer__item ${isHighlighted ? 'linkml-viewer__item--highlighted' : ''} ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''}`}
      >
        <h4 className="linkml-viewer__item-name">
          {cls.name}
          {cls.abstract && <span className="linkml-viewer__badge linkml-viewer__badge--abstract">{t('abstract')}</span>}
          {/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
          {use3DIndicator ? (
            <CustodianTypeIndicator3D 
              types={custodianTypes} 
              size={32}
              animate={true}
              showTooltip={true}
              hoveredType={hoveredCustodianType}
              onTypeHover={setHoveredCustodianType}
              // Removed onFaceClick - cube should be decorative, not trigger filtering
              className="linkml-viewer__custodian-indicator"
            />
          ) : (
            !isUniversal && (
              <CustodianTypeBadge 
                types={custodianTypes} 
                size="small" 
                className="linkml-viewer__custodian-badge"
              />
            )
          )}
        </h4>
        {cls.class_uri && (
          <div className="linkml-viewer__uri">
            <span className="linkml-viewer__label">{t('uri')}</span>
            <code>{cls.class_uri}</code>
          </div>
        )}
        {cls.description && (
          <div className="linkml-viewer__description linkml-viewer__markdown">
            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(cls.description)}</ReactMarkdown>
          </div>
        )}
        {cls.slots && cls.slots.length > 0 && (
          <div className="linkml-viewer__slots-list">
            <span className="linkml-viewer__label">{t('slotsLabel')}</span>
            <div className="linkml-viewer__tag-list">
              {cls.slots.map(slot => (
                <span key={slot} className="linkml-viewer__tag">{slot}</span>
              ))}
            </div>
          </div>
        )}
        {cls.exact_mappings && cls.exact_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label">{t('exactMappings')}</span>
          <div className="linkml-viewer__tag-list">
            {cls.exact_mappings.map(mapping => (
              <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping">{mapping}</span>
            ))}
          </div>
        </div>
      )}
        {cls.close_mappings && cls.close_mappings.length > 0 && (
          <div className="linkml-viewer__mappings">
            <span className="linkml-viewer__label">{t('closeMappings')}</span>
            <div className="linkml-viewer__tag-list">
              {cls.close_mappings.map(mapping => (
                <span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--close">{mapping}</span>
              ))}
            </div>
          </div>
        )}
        {/* Exports Section - Shows reverse dependencies (what references this class) */}
        {isSchemaServiceComplete && (
          <div className="linkml-viewer__exports-section">
            <button 
              className={`linkml-viewer__exports-toggle ${expandedExports.has(cls.name) ? 'linkml-viewer__exports-toggle--expanded' : ''}`}
              onClick={() => toggleExports(cls.name)}
            >
              <span className="linkml-viewer__exports-icon">{expandedExports.has(cls.name) ? '▼' : '▶'}</span>
              <span className="linkml-viewer__label">Exports</span>
              {loadingExports.has(cls.name) && <span className="linkml-viewer__exports-loading">Loading...</span>}
            </button>
            {expandedExports.has(cls.name) && classExports[cls.name] && (
              <div className="linkml-viewer__exports-content">
                {/* Subclasses */}
                {classExports[cls.name].subclasses.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Subclasses ({classExports[cls.name].subclasses.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].subclasses.map(subclass => (
                        <button 
                          key={subclass} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(subclass)}
                        >
                          {subclass}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Mixin Users */}
                {classExports[cls.name].mixinUsers.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Used as Mixin by ({classExports[cls.name].mixinUsers.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].mixinUsers.map(user => (
                        <button 
                          key={user} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(user)}
                        >
                          {user}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Slots with this Range */}
                {classExports[cls.name].slotsWithThisRange.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Slots with this Range ({classExports[cls.name].slotsWithThisRange.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].slotsWithThisRange.map(slot => (
                        <span key={slot} className="linkml-viewer__exports-item linkml-viewer__exports-item--slot">
                          {slot}
                        </span>
                      ))}
                    </div>
                  </div>
                )}
                {/* Classes using slots with this range */}
                {classExports[cls.name].classesUsingSlotWithThisRange.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Classes Using Slots with this Range ({classExports[cls.name].classesUsingSlotWithThisRange.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].classesUsingSlotWithThisRange.map(({ className, slotName }) => (
                        <button 
                          key={`${className}-${slotName}`} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(className)}
                          title={`via slot: ${slotName}`}
                        >
                          {className} <span className="linkml-viewer__exports-via">via {slotName}</span>
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* Classes referencing in slot_usage */}
                {classExports[cls.name].classesReferencingInSlotUsage.length > 0 && (
                  <div className="linkml-viewer__exports-category">
                    <span className="linkml-viewer__exports-category-label">Referenced in slot_usage ({classExports[cls.name].classesReferencingInSlotUsage.length})</span>
                    <div className="linkml-viewer__exports-list">
                      {classExports[cls.name].classesReferencingInSlotUsage.map(refClass => (
                        <button 
                          key={refClass} 
                          className="linkml-viewer__exports-link"
                          onClick={() => navigateToClass(refClass)}
                        >
                          {refClass}
                        </button>
                      ))}
                    </div>
                  </div>
                )}
                {/* No exports message */}
                {classExports[cls.name].subclasses.length === 0 &&
                 classExports[cls.name].mixinUsers.length === 0 &&
                 classExports[cls.name].slotsWithThisRange.length === 0 &&
                 classExports[cls.name].classesUsingSlotWithThisRange.length === 0 &&
                 classExports[cls.name].classesReferencingInSlotUsage.length === 0 && (
                  <div className="linkml-viewer__exports-empty">
                    No other schema elements reference this class.
                  </div>
                )}
              </div>
            )}
          </div>
        )}
        
        {/* UML Diagram Section - Shows filtered class relationship diagram */}
        {isSchemaServiceComplete && (
          <div className="linkml-viewer__uml-section">
            <button 
              className={`linkml-viewer__uml-toggle ${expandedUML.has(cls.name) ? 'linkml-viewer__uml-toggle--expanded' : ''}`}
              onClick={() => toggleUML(cls.name)}
            >
              <span className="linkml-viewer__uml-icon">{expandedUML.has(cls.name) ? '▼' : '▶'}</span>
              <span className="linkml-viewer__label">UML Diagram</span>
              {loadingExports.has(cls.name) && <span className="linkml-viewer__uml-loading">Loading...</span>}
            </button>
            {expandedUML.has(cls.name) && classExports[cls.name] && (
              <div className="linkml-viewer__uml-content">
                {/* Depth slider */}
                <div className="linkml-viewer__uml-depth-control">
                  <label className="linkml-viewer__uml-depth-label">
                    <span>Relationship Depth:</span>
                    <input
                      type="range"
                      min="1"
                      max="4"
                      value={umlDepth[cls.name] || 1}
                      onChange={(e) => {
                        const newDepth = parseInt(e.target.value, 10);
                        setUmlDepth(prev => ({ ...prev, [cls.name]: newDepth }));
                        // Use debounced loading to prevent excessive API calls while dragging
                        if (newDepth > 1) {
                          debouncedLoadExportInfo(cls.name, newDepth);
                        }
                      }}
                      className="linkml-viewer__uml-depth-slider"
                    />
                    <span className="linkml-viewer__uml-depth-value">{umlDepth[cls.name] || 1}</span>
                  </label>
                  <span className="linkml-viewer__uml-depth-hint">
                    {(umlDepth[cls.name] || 1) === 1 ? 'Direct relationships' : 
                     (umlDepth[cls.name] || 1) === 2 ? 'Secondary connections' :
                     (umlDepth[cls.name] || 1) === 3 ? 'Tertiary connections' : 'Extended network'}
                  </span>
                </div>
                <UMLVisualization
                  diagram={getMemoizedUMLDiagram(cls.name, umlDepth[cls.name] || 1)}
                  width={600}
                  height={500}
                  layoutType="dagre"
                  dagreDirection="TB"
                />
              </div>
            )}
            {expandedUML.has(cls.name) && !classExports[cls.name] && !loadingExports.has(cls.name) && (
              <div className="linkml-viewer__uml-empty">
                Click to load class relationships.
              </div>
            )}
          </div>
        )}
      </div>
    );
  };

  const renderSlotDetails = (slot: LinkMLSlot) => {
    const rangeIsEnum = slot.range && isEnumRange(slot.range);
    const enumKey = slot.range ? `${slot.name}:${slot.range}` : '';
    const isExpanded = expandedEnumRanges.has(enumKey);
    // Use pre-loaded types from schema annotations, fall back to sync function if not yet loaded
    const custodianTypes = slotCustodianTypes[slot.name] || getCustodianTypesForSlot(slot.name);
    const isUniversal = isUniversalElement(custodianTypes);
    
    // Check if this slot matches the current custodian type filter (multi-select)
    const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t));
    
    return (
      <div key={slot.name} className={`linkml-viewer__item ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''}`}>
        <h4 className="linkml-viewer__item-name">
          {slot.name}
          {slot.required && <span className="linkml-viewer__badge linkml-viewer__badge--required">{t('required')}</span>}
          {slot.multivalued && <span className="linkml-viewer__badge linkml-viewer__badge--multi">{t('multivalued')}</span>}
          {/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
          {use3DIndicator ? (
            <CustodianTypeIndicator3D 
              types={custodianTypes} 
              size={32}
              animate={true}
              showTooltip={true}
              hoveredType={hoveredCustodianType}
              onTypeHover={setHoveredCustodianType}
              // Removed onFaceClick - cube should be decorative, not trigger filtering
              className="linkml-viewer__custodian-indicator"
            />
          ) : (
            !isUniversal && (
              <CustodianTypeBadge 
                types={custodianTypes} 
                size="small" 
                className="linkml-viewer__custodian-badge"
              />
            )
          )}
        </h4>
        {slot.range && (
          <div className="linkml-viewer__range">
            <span className="linkml-viewer__label">{t('range')}</span>
            {rangeIsEnum ? (
              <button 
                className={`linkml-viewer__range-enum-toggle ${isExpanded ? 'linkml-viewer__range-enum-toggle--expanded' : ''}`}
                onClick={() => toggleEnumRange(slot.name, slot.range!)}
              >
                <span className="linkml-viewer__range-enum-icon">{isExpanded ? '▼' : '▶'}</span>
                <code>{slot.range}</code>
                <span className="linkml-viewer__badge linkml-viewer__badge--enum">enum</span>
              </button>
            ) : (
              <code>{slot.range}</code>
            )}
          </div>
        )}
        {/* Expandable enum values */}
        {rangeIsEnum && isExpanded && slot.range && renderEnumValues(slot.name, slot.range)}
        {slot.description && (
          <div className="linkml-viewer__description linkml-viewer__markdown">
            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(slot.description)}</ReactMarkdown>
          </div>
        )}
        {slot.pattern && (
          <div className="linkml-viewer__pattern">
            <span className="linkml-viewer__label">{t('pattern')}</span>
            <code>{slot.pattern}</code>
          </div>
        )}
      </div>
    );
  };

  const renderEnumDetails = (enumDef: LinkMLEnum) => {
    const enumName = enumDef.name;
    const allValues = enumDef.permissible_values ? Object.entries(enumDef.permissible_values) : [];
    const searchFilter = enumSearchFilters[enumName] || '';
    const showAll = enumShowAll[enumName] || false;
    const displayCount = 20;
    const custodianTypes = enumCustodianTypes[enumDef.name] || getCustodianTypesForEnum(enumDef.name);
    const isUniversal = isUniversalElement(custodianTypes);
    
    // Filter values based on search
    const filteredValues = searchFilter
      ? allValues.filter(([value, details]) => {
          const searchLower = searchFilter.toLowerCase();
          return (
            value.toLowerCase().includes(searchLower) ||
            (details.description?.toLowerCase().includes(searchLower)) ||
            (details.meaning?.toLowerCase().includes(searchLower))
          );
        })
      : allValues;
    
    // Determine how many to show
    const valuesToShow = showAll || searchFilter ? filteredValues : filteredValues.slice(0, displayCount);
    
    // Check if this enum matches the current custodian type filter (multi-select)
    const matchesFilter = custodianTypeFilter.size === 0 || custodianTypes.some(t => custodianTypeFilter.has(t));
    
    return (
      <div key={enumDef.name} className={`linkml-viewer__item ${!matchesFilter ? 'linkml-viewer__item--filtered-out' : ''}`}>
        <h4 className="linkml-viewer__item-name">
          {enumDef.name}
          {/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
          {use3DIndicator ? (
            <CustodianTypeIndicator3D 
              types={custodianTypes} 
              size={32}
              animate={true}
              showTooltip={true}
              hoveredType={hoveredCustodianType}
              onTypeHover={setHoveredCustodianType}
              // Removed onFaceClick - cube should be decorative, not trigger filtering
              className="linkml-viewer__custodian-indicator"
            />
          ) : (
            !isUniversal && (
              <CustodianTypeBadge 
                types={custodianTypes} 
                size="small" 
                className="linkml-viewer__custodian-badge"
              />
            )
          )}
        </h4>
        {enumDef.description && (
          <div className="linkml-viewer__description linkml-viewer__markdown">
            <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(enumDef.description)}</ReactMarkdown>
          </div>
        )}
        {allValues.length > 0 && (
          <div className="linkml-viewer__enum-values">
            {/* Search input */}
            <div className="linkml-viewer__range-enum-search">
              <input
                type="text"
                placeholder={t('searchPlaceholder')}
                value={searchFilter}
                onChange={(e) => setEnumSearchFilters(prev => ({ ...prev, [enumName]: e.target.value }))}
                className="linkml-viewer__range-enum-search-input"
              />
              {searchFilter && (
                <button
                  className="linkml-viewer__range-enum-search-clear"
                  onClick={() => setEnumSearchFilters(prev => ({ ...prev, [enumName]: '' }))}
                  title="Clear search"
                >
                  ×
                </button>
              )}
            </div>
            
            {/* Count display */}
            <span className="linkml-viewer__range-enum-count" style={{ display: 'block', marginBottom: '0.5rem' }}>
              {searchFilter ? (
                <>
                  {t('showing')} {filteredValues.length} {t('of')} {allValues.length}
                </>
              ) : showAll ? (
                <>
                  {allValues.length} {t('permissibleValues').replace(':', '')}
                </>
              ) : (
                <>
                  {t('showing')} {Math.min(displayCount, allValues.length)} {t('of')} {allValues.length}
                </>
              )}
            </span>
            
            <div className="linkml-viewer__value-list">
              {valuesToShow.length > 0 ? (
                valuesToShow.map(([value, details]) => (
                  <div key={value} className="linkml-viewer__value-item">
                    <code className="linkml-viewer__value-name">{value}</code>
                    {details.description && (
                      <div className="linkml-viewer__value-desc linkml-viewer__markdown linkml-viewer__markdown--compact">
                        <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(details.description)}</ReactMarkdown>
                      </div>
                    )}
                    {details.meaning && (
                      <span className="linkml-viewer__value-meaning">
                        <span className="linkml-viewer__label">{t('meaning')}</span> {details.meaning}
                      </span>
                    )}
                  </div>
                ))
              ) : (
                <div className="linkml-viewer__range-enum-no-results">
                  {t('noResults')}
                </div>
              )}
            </div>
            
            {/* Show all / Show less button */}
            {!searchFilter && allValues.length > displayCount && (
              <button
                className="linkml-viewer__range-enum-toggle-all"
                onClick={() => setEnumShowAll(prev => ({ ...prev, [enumName]: !showAll }))}
              >
                {showAll ? (
                  <>▲ {t('showLess')}</>
                ) : (
                  <>▼ {t('showAll')} ({allValues.length - displayCount} {language === 'nl' ? 'meer' : 'more'})</>
                )}
              </button>
            )}
          </div>
        )}
      </div>
    );
  };

  const renderVisualView = () => {
    if (!schema) return null;
    
    const classes = extractClasses(schema);
    const slots = extractSlots(schema);
    const enums = extractEnums(schema);
    
    // Count matching items when filter is active (for display purposes)
    const matchingClassCount = custodianTypeFilter.size > 0
      ? classes.filter(cls => {
          const types = classCustodianTypes[cls.name] || getCustodianTypesForClass(cls.name);
          return types.some(t => custodianTypeFilter.has(t));
        }).length
      : classes.length;
    
    const matchingSlotCount = custodianTypeFilter.size > 0
      ? slots.filter(slot => {
          const types = slotCustodianTypes[slot.name] || getCustodianTypesForSlot(slot.name);
          return types.some(t => custodianTypeFilter.has(t));
        }).length
      : slots.length;
    
    const matchingEnumCount = custodianTypeFilter.size > 0
      ? enums.filter(enumDef => {
          const types = enumCustodianTypes[enumDef.name] || getCustodianTypesForEnum(enumDef.name);
          return types.some(t => custodianTypeFilter.has(t));
        }).length
      : enums.length;
    
    return (
      <div className="linkml-viewer__visual">
        {/* Schema Metadata */}
        <div className="linkml-viewer__metadata">
          <h2 className="linkml-viewer__schema-name">{formatDisplayName(schema.name || schema.title || '') || t('unnamedSchema')}</h2>
          {schema.description && (
            <div className="linkml-viewer__schema-desc linkml-viewer__markdown">
              <ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{transformContent(schema.description)}</ReactMarkdown>
            </div>
          )}
          {schema.id && (
            <div className="linkml-viewer__schema-id">
              <span className="linkml-viewer__label">{t('id')}</span>
              <code>{schema.id}</code>
            </div>
          )}
          {schema.version && (
            <div className="linkml-viewer__schema-version">
              <span className="linkml-viewer__label">{t('version')}</span>
              <code>{schema.version}</code>
            </div>
          )}
        </div>

        {/* Prefixes */}
        {schema.prefixes && Object.keys(schema.prefixes).length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('prefixes')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('prefixes') ? '▼' : '▶'}
              </span>
              {t('prefixes')} ({Object.keys(schema.prefixes).length})
            </button>
            {expandedSections.has('prefixes') && (
              <div className="linkml-viewer__prefix-list">
                {Object.entries(schema.prefixes).map(([prefix, uri]) => (
                  <div key={prefix} className="linkml-viewer__prefix-item">
                    <code className="linkml-viewer__prefix-name">{prefix}:</code>
                    <span className="linkml-viewer__prefix-uri">{uri}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}

        {/* Classes */}
        {classes.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('classes')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('classes') ? '▼' : '▶'}
              </span>
              {t('classes')} ({custodianTypeFilter.size > 0 ? `${matchingClassCount}/${classes.length}` : classes.length})
            </button>
            {expandedSections.has('classes') && (
              <div className="linkml-viewer__section-content">
                {classes.map(renderClassDetails)}
              </div>
            )}
          </div>
        )}

        {/* Slots */}
        {slots.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('slots')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('slots') ? '▼' : '▶'}
              </span>
              {t('slots')} ({custodianTypeFilter.size > 0 ? `${matchingSlotCount}/${slots.length}` : slots.length})
            </button>
            {expandedSections.has('slots') && (
              <div className="linkml-viewer__section-content">
                {slots.map(renderSlotDetails)}
              </div>
            )}
          </div>
        )}

        {/* Enums */}
        {enums.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('enums')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('enums') ? '▼' : '▶'}
              </span>
              {t('enumerations')} ({custodianTypeFilter.size > 0 ? `${matchingEnumCount}/${enums.length}` : enums.length})
            </button>
            {expandedSections.has('enums') && (
              <div className="linkml-viewer__section-content">
                {enums.map(renderEnumDetails)}
              </div>
            )}
          </div>
        )}

        {/* Imports */}
        {schema.imports && schema.imports.length > 0 && (
          <div className="linkml-viewer__section">
            <button 
              className="linkml-viewer__section-header"
              onClick={() => toggleSection('imports')}
            >
              <span className="linkml-viewer__section-icon">
                {expandedSections.has('imports') ? '▼' : '▶'}
              </span>
              {t('imports')} ({schema.imports.length})
            </button>
            {expandedSections.has('imports') && (
              <div className="linkml-viewer__import-list">
                {schema.imports.map(imp => (
                  <div key={imp} className="linkml-viewer__import-item">
                    <code>{imp}</code>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>
    );
  };

  const renderRawView = () => {
    if (!rawYaml) return null;
    
    const handleCopyYaml = async () => {
      try {
        await navigator.clipboard.writeText(rawYaml);
        setCopyFeedback(true);
        setTimeout(() => setCopyFeedback(false), 2000);
      } catch (err) {
        console.error('Failed to copy YAML:', err);
      }
    };
    
    return (
      <div className="linkml-viewer__raw">
        <div className="linkml-viewer__raw-header">
          <button
            className={`linkml-viewer__copy-btn ${copyFeedback ? 'linkml-viewer__copy-btn--copied' : ''}`}
            onClick={handleCopyYaml}
            title={copyFeedback ? t('copied') : t('copyToClipboard')}
          >
            {copyFeedback ? '✓' : '⧉'} {copyFeedback ? t('copied') : t('copyToClipboard')}
          </button>
        </div>
        <pre className="linkml-viewer__yaml">{rawYaml}</pre>
      </div>
    );
  };

  return (
    <div className="linkml-viewer-page">
      {/* Left Sidebar - Schema Files */}
      <aside className="linkml-viewer-page__sidebar">
        <h2 className="linkml-viewer-page__sidebar-title">{t('sidebarTitle')}</h2>
        
        {/* Sidebar Search */}
        <div className="linkml-viewer-page__sidebar-search">
          <input
            type="text"
            placeholder={t('searchSchemas')}
            value={sidebarSearch}
            onChange={(e) => setSidebarSearch(e.target.value)}
            className="linkml-viewer-page__sidebar-search-input"
          />
          {sidebarSearch && (
            <button
              className="linkml-viewer-page__sidebar-search-clear"
              onClick={() => setSidebarSearch('')}
              title="Clear search"
            >
              ×
            </button>
          )}
        </div>
        
        {/* Category Filters */}
        <div className="linkml-viewer-page__sidebar-filters">
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.main}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, main: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--main">
              {t('mainSchema')}
            </span>
          </label>
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.class}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, class: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--class">
              {t('classes')}
            </span>
          </label>
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.enum}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, enum: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--enum">
              {t('enumerations')}
            </span>
          </label>
          <label className="linkml-viewer-page__filter-checkbox">
            <input
              type="checkbox"
              checked={categoryFilters.slot}
              onChange={(e) => setCategoryFilters(prev => ({ ...prev, slot: e.target.checked }))}
            />
            <span className="linkml-viewer-page__filter-label linkml-viewer-page__filter-label--slot">
              {t('slots')}
            </span>
          </label>
        </div>
        
        {categories
          .filter(category => categoryFilters[category.name] !== false)
          .map(category => {
            // Filter files based on search
            const filteredFiles = sidebarSearch
              ? category.files.filter(file => 
                  file.name.toLowerCase().includes(sidebarSearch.toLowerCase())
                )
              : category.files;
            
            // Don't render empty categories
            if (filteredFiles.length === 0) return null;
            
            return (
              <div key={category.name} className="linkml-viewer-page__category">
                <h3 className="linkml-viewer-page__category-title">
                  {category.displayName}
                  <span className="linkml-viewer-page__category-count">
                    ({filteredFiles.length}{sidebarSearch ? `/${category.files.length}` : ''})
                  </span>
                </h3>
                <div className="linkml-viewer-page__file-list">
                  {filteredFiles.map(file => (
                    <button
                      key={file.path}
                      className={`linkml-viewer-page__file-item ${
                        selectedSchema?.path === file.path ? 'linkml-viewer-page__file-item--active' : ''
                      }`}
                      onClick={() => setSelectedSchema(file)}
                    >
                      {file.name}
                    </button>
                  ))}
                </div>
              </div>
            );
          })}
        
        {/* No results message */}
        {sidebarSearch && categories.every(category => 
          !categoryFilters[category.name] || 
          category.files.filter(file => 
            file.name.toLowerCase().includes(sidebarSearch.toLowerCase())
          ).length === 0
        ) && (
          <div className="linkml-viewer-page__no-results">
            {t('noMatchingSchemas')}
          </div>
        )}

      </aside>

      {/* Main Content */}
      <main className="linkml-viewer-page__main" ref={mainContentRef}>
        {/* Collapsible header with title - hides when navigation collapses */}
        <header className="linkml-viewer-page__header">
          <h1 className="linkml-viewer-page__title">
            {selectedSchema ? formatDisplayName(selectedSchema.name) : t('pageTitle')}
          </h1>
        </header>
        
        {/* Sticky subheader with tabs - always visible */}
        <div className="linkml-viewer-page__subheader">
          <div className="linkml-viewer-page__tabs">
            <button
              className={`linkml-viewer-page__tab ${viewMode === 'visual' ? 'linkml-viewer-page__tab--active' : ''}`}
              onClick={() => setViewMode('visual')}
            >
              {t('visualView')}
            </button>
            <button
              className={`linkml-viewer-page__tab ${viewMode === 'raw' ? 'linkml-viewer-page__tab--active' : ''}`}
              onClick={() => setViewMode('raw')}
            >
              {t('rawYaml')}
            </button>
            <span className="linkml-viewer-page__tab-separator">|</span>
            <button
              className={`linkml-viewer-page__tab linkml-viewer-page__tab--indicator ${use3DIndicator ? 'linkml-viewer-page__tab--active' : ''}`}
              onClick={() => setUse3DIndicator(!use3DIndicator)}
              title={use3DIndicator ? t('use2DBadge') : t('use3DPolygon')}
            >
              {use3DIndicator ? '🔷 3D' : '🏷️ 2D'}
            </button>
          </div>
          
          {/* Legend bar for 3D mode - shows all 19 custodian types with bidirectional hover sync */}
          {use3DIndicator && (
            <div className="linkml-viewer-page__legend-bar">
              <CustodianTypeLegendBar
                hoveredType={hoveredCustodianType}
                onTypeHover={setHoveredCustodianType}
                onTypeClick={handleCustodianTypeFilter}
                highlightTypes={Array.from(custodianTypeFilter)}
                size="small"
              />
              {custodianTypeFilter.size > 0 && (
                <button
                  className="linkml-viewer-page__filter-clear"
                  onClick={() => setCustodianTypeFilter(new Set())}
                  title={language === 'nl' ? 'Filter wissen' : 'Clear filter'}
                >
                  ✕ {language === 'nl' ? 'Filter wissen' : 'Clear filter'} ({Array.from(custodianTypeFilter).join(', ')})
                </button>
              )}
            </div>
          )}
        </div>

        <div className="linkml-viewer-page__content">
          {isLoading || isSchemaServiceLoading ? (
            <LoadingScreen
              message={schemaProgress?.message || t('loading')}
              progress={schemaProgress?.percent}
              size="medium"
              fullscreen={false}
            />
          ) : error ? (
            <div className="linkml-viewer-page__error">{error}</div>
          ) : viewMode === 'visual' ? (
            renderVisualView()
          ) : (
            renderRawView()
          )}
        </div>
      </main>
    </div>
  );
};

export default LinkMLViewerPage;
