/**
 * VL Metadata Extractor v1.0
 *
 * Extracts structured metadata directly from VL source files (.vx, .sc, .cp, .vs, .vdb, .vth)
 * Zero dependencies, browser-compatible ES Module.
 *
 * Usage:
 *   const metadata = VLMetadataExtractor.extract(fileTree);
 *   const { uiMap, serviceMap } = VLMetadataExtractor.toProcessEditorFormat(metadata);
 *   const graph = VLMetadataExtractor.buildDependencyGraph(metadata);
 *
 * fileTree: { 'Apps/PlayerDesktop.vx': '...content...', ... }
 */

const VLMetadataExtractor = (() => {

  // ─── Core Utilities ────────────────────────────────────────────────

  const FILE_TYPES = {
    '.vx':  'app',
    '.sc':  'section',
    '.cp':  'component',
    '.vs':  'service',
    '.vdb': 'database',
    '.vth': 'theme'
  };

  function classifyFile(path) {
    for (const [ext, type] of Object.entries(FILE_TYPES)) {
      if (path.endsWith(ext)) return { type, ext };
    }
    return null;
  }

  function getDepth(line) {
    const m = line.match(/^(-*)/);
    return m ? m[1].length : 0;
  }

  function stripDepth(line) {
    return line.replace(/^-+/, '');
  }

  function splitSections(content) {
    const lines = content.split('\n');
    const sections = {};
    let currentSection = '__header__';
    sections[currentSection] = [];

    for (const line of lines) {
      const trimmed = line.trim();
      if (/^#\s+/.test(trimmed)) {
        currentSection = trimmed.replace(/^#\s+/, '').trim();
        sections[currentSection] = [];
      } else {
        sections[currentSection].push(line);
      }
    }
    return sections;
  }

  // Extract VL version from file content
  function extractVersion(content) {
    const m = content.match(/\/\/\s*VL_VERSION:(\d+\.\d+)/);
    return m ? m[1] : null;
  }

  // Extract preview annotation
  function extractPreview(content) {
    const m = content.match(/\/\/\s*Preview:\s*(.+)/);
    if (!m) return null;
    const props = {};
    const pairs = m[1].match(/[\w-]+:[^\s]+/g) || [];
    for (const pair of pairs) {
      const [k, ...v] = pair.split(':');
      props[k] = v.join(':');
    }
    return props;
  }

  // Extract root component: <Type-Name "id"> ...props
  function extractRoot(content) {
    const m = content.match(/<(App|Section|Component|ServiceDomain|Database|Theme)-(\w+)(?:\s+"(\w+)")?/);
    if (!m) return null;
    return { type: m[1], name: m[2], id: m[3] || null };
  }

  // Find the index of the matching closing paren/bracket for the opener at pos
  function findMatchingClose(str, pos) {
    const open = str[pos];
    const close = open === '(' ? ')' : open === '[' ? ']' : open === '{' ? '}' : null;
    if (!close) return -1;
    let depth = 1;
    for (let i = pos + 1; i < str.length; i++) {
      if (str[i] === open) depth++;
      else if (str[i] === close) { depth--; if (depth === 0) return i; }
    }
    return -1;
  }

  // Extract the balanced content inside the outermost parens starting at pos
  function extractBalancedParens(str, startPos) {
    const openIdx = str.indexOf('(', startPos);
    if (openIdx < 0) return { content: '', endIdx: -1 };
    const closeIdx = findMatchingClose(str, openIdx);
    if (closeIdx < 0) return { content: str.slice(openIdx + 1), endIdx: str.length };
    return { content: str.slice(openIdx + 1, closeIdx), endIdx: closeIdx };
  }

  // Parse typed parameter list: "param1(TYPE), param2(TYPE)"
  // Handles nested parens: legs([{matchId:INT}])
  function parseParams(str) {
    if (!str || !str.trim()) return [];
    const params = [];
    let i = 0;
    while (i < str.length) {
      // Skip whitespace and commas
      while (i < str.length && (str[i] === ' ' || str[i] === ',' || str[i] === '\t')) i++;
      if (i >= str.length) break;
      // Read param name
      let name = '';
      while (i < str.length && /\w/.test(str[i])) { name += str[i]; i++; }
      if (!name) { i++; continue; }
      // Expect (TYPE)
      if (i < str.length && str[i] === '(') {
        const closeIdx = findMatchingClose(str, i);
        if (closeIdx > 0) {
          const type = str.slice(i + 1, closeIdx);
          params.push({ name, type });
          i = closeIdx + 1;
        } else {
          params.push({ name, type: str.slice(i + 1) });
          break;
        }
      }
    }
    return params;
  }

  // Parse return type: "{field:TYPE, field:TYPE}" or complex types
  function parseReturnFields(str) {
    if (!str || !str.trim()) return [];
    const trimmed = str.trim();
    // Handle {field:TYPE, ...} format
    if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
      const inner = trimmed.slice(1, -1);
      return parseFieldList(inner);
    }
    // Otherwise it's a simple type
    return [{ name: '_value', type: trimmed }];
  }

  // Parse "field:TYPE, field:TYPE" or "field:TYPE, field:[{...}]"
  function parseFieldList(str) {
    const fields = [];
    let depth = 0;
    let current = '';
    for (let i = 0; i < str.length; i++) {
      const ch = str[i];
      if (ch === '{' || ch === '[' || ch === '(') depth++;
      else if (ch === '}' || ch === ']' || ch === ')') depth--;
      if (ch === ',' && depth === 0) {
        const f = parseOneField(current.trim());
        if (f) fields.push(f);
        current = '';
      } else {
        current += ch;
      }
    }
    if (current.trim()) {
      const f = parseOneField(current.trim());
      if (f) fields.push(f);
    }
    return fields;
  }

  function parseOneField(str) {
    const idx = str.indexOf(':');
    if (idx < 0) return null;
    return { name: str.slice(0, idx).trim(), type: str.slice(idx + 1).trim() };
  }

  // Parse inline props from element line: key:value key:"quoted" key:(expr)
  function parseInlineProps(propsStr) {
    const props = {};
    if (!propsStr) return props;
    const regex = /(\w[\w-]*):(?:"([^"]*)"|\(([^)]*)\)|(\[[^\]]*\])|(\{[^}]*\})|(\S+))/g;
    let m;
    while ((m = regex.exec(propsStr)) !== null) {
      const key = m[1];
      const val = m[2] !== undefined ? m[2] : (m[3] || m[4] || m[5] || m[6]);
      props[key] = val;
    }
    return props;
  }

  // ─── Variable Declaration Parser ──────────────────────────────────

  function parseVarDecl(line) {
    // $name(TYPE) = default   or   $name(complex type) = value
    const m = line.match(/^\$(\w+)\(([^)]*(?:\([^)]*\)[^)]*)*(?:\[[^\]]*(?:\{[^}]*\}[^\]]*)*\][^)]*)*)\)\s*=\s*(.+)$/);
    if (!m) {
      // Try simpler pattern
      const m2 = line.match(/^\$(\w+)\((.+?)\)\s*=\s*(.+)$/);
      if (m2) return { name: m2[1], type: m2[2], default: m2[3].trim() };
      return null;
    }
    return { name: m[1], type: m[2], default: m[3].trim() };
  }

  // Parse variables from a section block (lines)
  function parseVarsFromLines(lines) {
    const vars = [];
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('$')) {
        const v = parseVarDecl(trimmed);
        if (v) vars.push(v);
      }
    }
    return vars;
  }

  // ─── Event Declaration Parser ─────────────────────────────────────

  function parseEventDecl(line) {
    // EVENT @name(param1(TYPE), param2(TYPE))
    const m = line.match(/^EVENT\s+@(\w+)/);
    if (!m) return null;
    const name = m[1];
    const parenStart = line.indexOf('(', m.index + m[0].length);
    if (parenStart < 0) return { name, params: [] };
    const parenEnd = findMatchingClose(line, parenStart);
    if (parenEnd < 0) return { name, params: [] };
    const paramStr = line.slice(parenStart + 1, parenEnd);
    return { name, params: parseParams(paramStr) };
  }

  function parseEventsFromLines(lines) {
    const events = [];
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('EVENT')) {
        const e = parseEventDecl(trimmed);
        if (e) events.push(e);
      }
    }
    return events;
  }

  // ─── Method/Service Declaration Parser ────────────────────────────

  function parseMethodDecl(line) {
    // Handles: METHOD, METHOD_PUB, SERVICE, PUBLIC_SERVICE, TRANSACTION, PIPE
    // Format: KEYWORD name(params); RETURN {fields}
    const kwMatch = line.match(/^(METHOD_PUB|METHOD|PUBLIC_SERVICE|SERVICE|TRANSACTION|PIPE)\s+(\w+)/);
    if (!kwMatch) return null;

    const kind = kwMatch[1];
    const name = kwMatch[2];
    const afterName = line.slice(kwMatch.index + kwMatch[0].length);

    // Find params inside balanced parens
    const parenStart = afterName.indexOf('(');
    if (parenStart < 0) return { kind, name, params: [], returns: null };
    const parenEnd = findMatchingClose(afterName, parenStart);
    if (parenEnd < 0) return { kind, name, params: [], returns: null };

    const paramStr = afterName.slice(parenStart + 1, parenEnd);
    const params = parseParams(paramStr);

    // Find RETURN part after the closing paren
    const afterParams = afterName.slice(parenEnd + 1).trim().replace(/^;\s*/, '');
    let returns = null;
    const retMatch = afterParams.match(/^RETURN\s+(.+)$/);
    if (retMatch) {
      returns = retMatch[1].trim();
    }

    const result = { kind, name, params, returns };
    parseExposeIfPresent(result);
    return result;
  }

  function parseExposeIfPresent(result) {
    if (result.returns && result.returns.includes('EXPOSE')) {
      const parts = result.returns.split(/;\s*EXPOSE\s+/);
      result.returns = parts[0].trim();
      if (parts[1]) {
        result.expose = parseInlineProps(parts[1].replace(/^\{|\}$/g, ''));
      }
    }
  }

  function parseMethodsFromLines(lines, kinds) {
    const methods = [];
    for (const line of lines) {
      const stripped = stripDepth(line.trim()).trim();
      for (const kind of kinds) {
        if (stripped.startsWith(kind + ' ')) {
          const m = parseMethodDecl(stripped);
          if (m) methods.push(m);
          break;
        }
      }
    }
    return methods;
  }

  // ─── Element Reference Parser ─────────────────────────────────────

  function findElementRefs(lines, category) {
    // Find <Category-Name "instanceId"> in lines
    const refs = [];
    const seen = new Set();
    const regex = new RegExp(`<${category}-(\\w+)(?:\\s+"(\\w+)")?`, 'g');
    for (const line of lines) {
      let m;
      while ((m = regex.exec(line)) !== null) {
        const key = `${m[1]}:${m[2] || ''}`;
        if (!seen.has(key)) {
          seen.add(key);
          refs.push({ name: m[1], instanceId: m[2] || null });
        }
      }
    }
    return refs;
  }

  // ─── Interactive Element Parser ──────────────────────────────────

  const INTERACTIVE_CATEGORIES = [
    'Button', 'Input', 'Textarea', 'Select',
    'SingleSelect_Dropdown', 'MultiSelect_Dropdown',
    'Modal', 'Image', 'Chart', 'Icon'
  ];

  const INTERACTIVE_REGEX = new RegExp(
    '<(' + INTERACTIVE_CATEGORIES.join('|') + ')-(\\w+)(?:\\s+"(\\w+)")?>\\s*(.*)',
    'g'
  );

  function parseInteractiveElements(treeLines) {
    const elements = [];
    for (const line of treeLines) {
      const depth = getDepth(line.trim());
      const stripped = stripDepth(line.trim()).trim();
      if (!stripped) continue;

      INTERACTIVE_REGEX.lastIndex = 0;
      const m = INTERACTIVE_REGEX.exec(stripped);
      if (!m) continue;

      const category = m[1];
      const name = m[2];
      const instanceId = m[3] || null;
      const propsStr = m[4] || '';
      const props = parseInlineProps(propsStr);

      // Determine element type for test generation
      let elType = 'other';
      if (category === 'Button') elType = 'button';
      else if (category === 'Input') elType = 'input';
      else if (category === 'Textarea') elType = 'textarea';
      else if (category === 'Select' || category.includes('Select_Dropdown')) elType = 'select';
      else if (category === 'Modal') elType = 'modal';

      const el = { category, name, instanceId, type: elType, depth };
      if (props.value !== undefined) el.label = props.value;
      if (props.placeholder) el.placeholder = props.placeholder;
      if (props.type) el.inputType = props.type;
      if (props.disabled) el.disabled = props.disabled;
      if (props.StyleClass) el.styleClass = props.StyleClass;

      elements.push(el);
    }
    return elements;
  }

  // ─── Event Binding Parser (for .vx files) ─────────────────────────

  function parseEventBindings(lines) {
    const bindings = [];
    let current = null;

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#')) continue;

      // Match: <Section-Name "id">.@eventName(params)
      // Or: <Component-Name "id">.@eventName(params)
      const m = trimmed.match(/^<(Section|Component)-(\w+)\s+"(\w+)">\s*\.@(\w+)\(([^)]*)\)$/);
      if (m) {
        if (current) bindings.push(current);
        current = {
          source: { type: m[1].toLowerCase(), id: m[2], instanceId: m[3], event: m[4], params: m[5] ? m[5].split(',').map(s => s.trim()).filter(Boolean) : [] },
          actions: []
        };
        continue;
      }

      // Collect handler body lines
      if (current && trimmed.startsWith('-')) {
        current.actions.push(stripDepth(trimmed).trim());
      } else if (current && trimmed === '') {
        // Empty line may end the handler
      } else if (current && !trimmed.startsWith('-') && !trimmed.startsWith('<')) {
        // Non-dash non-element line in handler
        // Still part of the handler if we're at depth
      }
    }
    if (current) bindings.push(current);
    return bindings;
  }

  // ─── .vdb Parser ──────────────────────────────────────────────────

  function parseVDB(content, filePath) {
    const version = extractVersion(content);
    const root = extractRoot(content);
    const lines = content.split('\n');

    const tables = [];
    const relations = [];
    let currentTable = null;

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;

      // Table start: <Table-Name> data:[...]
      const tableMatch = trimmed.match(/^<Table-(\w+)>(?:\s+data:(.+))?$/);
      if (tableMatch) {
        currentTable = {
          name: tableMatch[1],
          fields: [],
          indexes: [],
          seedData: tableMatch[2] || null
        };
        tables.push(currentTable);
        continue;
      }

      // Close table
      if (trimmed.match(/^<\/Table-/)) {
        currentTable = null;
        continue;
      }

      // Field in table
      const fieldMatch = stripDepth(trimmed).match(/^<Field-(\w+)>\s*(.*)$/);
      if (fieldMatch && currentTable) {
        const props = parseInlineProps(fieldMatch[2]);
        currentTable.fields.push({
          name: fieldMatch[1],
          type: props.type || 'STRING',
          notNull: props.notNull === 'true',
          default: props.default || null
        });
        continue;
      }

      // Index in table
      const indexMatch = stripDepth(trimmed).match(/^<Index-(\w+)>\s*(.*)$/);
      if (indexMatch && currentTable) {
        const props = parseInlineProps(indexMatch[2]);
        let fields = [];
        if (props.fields) {
          try { fields = JSON.parse(props.fields); } catch(e) { fields = [props.fields]; }
        }
        currentTable.indexes.push({
          name: indexMatch[1],
          type: props.type || 'NORMAL',
          fields
        });
        continue;
      }

      // Relation: <Relation-T1&T2> T1.field<<T2.field
      const relMatch = trimmed.match(/^<Relation-(\w+&\w+)>\s+(\w+)\.([(\w,)]+)(<<|>>|--)(\w+)\.([(\w,)]+)$/);
      if (relMatch) {
        const cardMap = { '<<': 'oneToMany', '>>': 'manyToOne', '--': 'oneToOne' };
        relations.push({
          name: relMatch[1],
          from: { table: relMatch[2], field: relMatch[3] },
          to: { table: relMatch[5], field: relMatch[6] },
          cardinality: cardMap[relMatch[4]] || relMatch[4]
        });
      }
    }

    return {
      name: root ? root.name : filePath.split('/').pop().replace('.vdb', ''),
      vlVersion: version,
      tables,
      relations
    };
  }

  // ─── .vs Parser ───────────────────────────────────────────────────

  function parseVS(content, filePath) {
    const version = extractVersion(content);
    const root = extractRoot(content);
    const sections = splitSections(content);
    const domainId = root ? root.name : filePath.split('/').pop().replace('.vs', '');
    const fileName = filePath.split('/').pop();

    // Parse environment vars
    const envVars = [];
    const envLines = sections['Backend Environment Vars'] || [];
    for (const line of envLines) {
      const trimmed = line.trim();
      const m = trimmed.match(/^ENV\s+(\w+)\((\w+)\)\s+"([^"]*)"$/);
      if (m) {
        envVars.push({ name: m[1], type: m[2], description: m[3] });
      }
    }

    // Parse Backend Tree: VirtualTables and backend components
    const virtualTables = [];
    const backendComponents = [];
    const treeLines = sections['Backend Tree'] || [];
    let currentVT = null;

    for (const line of treeLines) {
      const trimmed = line.trim();
      const stripped = stripDepth(trimmed);
      if (!stripped) continue;

      // VirtualTable: <VirtualTable-Name "id"> sourceTable:X
      const vtMatch = stripped.match(/^<VirtualTable-(\w+)\s+"(\w+)">\s*(.*)$/);
      if (vtMatch) {
        const props = parseInlineProps(vtMatch[3]);
        currentVT = {
          name: vtMatch[1],
          instanceId: vtMatch[2],
          sourceTable: props.sourceTable || '',
          fields: [],
          extraSpecs: {}
        };
        // Parse extraSpecs if present
        if (props.extraSpecs) {
          try { currentVT.extraSpecs = JSON.parse(props.extraSpecs); } catch(e) {}
        }
        virtualTables.push(currentVT);
        continue;
      }

      // Field in VT
      const fieldMatch = stripped.match(/^<Field-(\w+)>\s*(.*)$/);
      if (fieldMatch && currentVT) {
        const props = parseInlineProps(fieldMatch[2]);
        currentVT.fields.push({
          name: fieldMatch[1],
          type: props.type || 'STRING',
          sourceField: props.sourceField || null
        });
        continue;
      }

      // Backend components: <ServerApi-X "id">, <MQ-X "id">, etc.
      const compMatch = stripped.match(/^<(ServerApi|MQ|TokenIssuer|Encryption|Email|ServerCache)-(\w+)(?:\s+"(\w+)")?/);
      if (compMatch) {
        backendComponents.push({
          type: compMatch[1],
          name: compMatch[2],
          instanceId: compMatch[3] || null
        });
        currentVT = null;
      }
    }

    // Parse Services
    const services = [];
    const serviceLines = sections['Services'] || [];
    for (const line of serviceLines) {
      const stripped = stripDepth(line.trim()).trim();
      if (stripped.startsWith('SERVICE ') || stripped.startsWith('PUBLIC_SERVICE ')) {
        const m = parseMethodDecl(stripped);
        if (m) {
          services.push({
            serviceId: `${domainId}.${m.name}`,
            name: m.name,
            type: m.kind === 'PUBLIC_SERVICE' ? 'PUBLIC_SERVICE' : 'SERVICE',
            params: m.params,
            returns: { raw: m.returns, fields: parseReturnFields(m.returns) },
            expose: m.expose || null
          });
        }
      }
    }

    // Parse Transactions
    const transactions = [];
    const txLines = sections['Transactions'] || [];
    for (const line of txLines) {
      const stripped = stripDepth(line.trim()).trim();
      if (stripped.startsWith('TRANSACTION ')) {
        const m = parseMethodDecl(stripped);
        if (m) {
          transactions.push({
            name: m.name,
            params: m.params,
            returns: { raw: m.returns, fields: parseReturnFields(m.returns) }
          });
        }
      }
    }

    return {
      domainId,
      fileName,
      filePath,
      vlVersion: version,
      envVars,
      virtualTables,
      services,
      transactions,
      backendComponents
    };
  }

  // ─── .cp Parser ───────────────────────────────────────────────────

  function parseCP(content, filePath) {
    const version = extractVersion(content);
    const preview = extractPreview(content);
    const root = extractRoot(content);
    const sections = splitSections(content);
    const componentId = root ? root.name : filePath.split('/').pop().replace('.cp', '');
    const fileName = filePath.split('/').pop();

    // Parse interactive elements from Frontend Tree
    const treeLines = sections['Frontend Tree'] || [];
    const interactiveElements = parseInteractiveElements(treeLines);

    return {
      componentId,
      fileName,
      filePath,
      vlVersion: version,
      previewSize: preview,
      publicProps: parseVarsFromLines(sections['Frontend Public Props'] || []),
      publicEvents: parseEventsFromLines(sections['Frontend Public Events'] || []),
      derivedVars: parseVarsFromLines(sections['Frontend Derived Vars'] || []),
      interactiveElements,
      internalMethods: parseMethodsFromLines(
        sections['Frontend Internal Methods'] || [],
        ['METHOD']
      ),
      pipeFuncs: parseMethodsFromLines(
        sections['Frontend Pipeline Funcs'] || [],
        ['PIPE']
      )
    };
  }

  // ─── .sc Parser ───────────────────────────────────────────────────

  function parseSC(content, filePath) {
    const version = extractVersion(content);
    const preview = extractPreview(content);
    const root = extractRoot(content);
    const sections = splitSections(content);
    const sectionId = root ? root.name : filePath.split('/').pop().replace('.sc', '');
    const fileName = filePath.split('/').pop();

    // Parse ServiceDomain references from Frontend Tree
    const treeLines = sections['Frontend Tree'] || [];
    const serviceDomainRefs = [];
    const sdMap = new Map(); // domainId -> { domainId, instanceId, services }
    let currentSD = null;

    for (const line of treeLines) {
      const stripped = stripDepth(line.trim()).trim();
      if (!stripped) continue;

      // <ServiceDomain-Name "instanceId">
      const sdMatch = stripped.match(/^<ServiceDomain-(\w+)\s+"(\w+)">$/);
      if (sdMatch) {
        currentSD = { domainId: sdMatch[1], domainInstanceId: sdMatch[2], services: [] };
        sdMap.set(sdMatch[1], currentSD);
        continue;
      }

      // <Service-Name> params:(...) returns:(...)
      const svcMatch = stripped.match(/^<Service-(\w+)>\s*(.*)$/);
      if (svcMatch && currentSD) {
        currentSD.services.push({
          serviceId: `${currentSD.domainId}.${svcMatch[1]}`,
          serviceName: svcMatch[1]
        });
        continue;
      }

      // If we encounter a non-service child, reset currentSD context
      if (!stripped.startsWith('<Service-') && !stripped.startsWith('<Field-')) {
        if (currentSD && !stripped.startsWith('<ServiceDomain-')) {
          // Check if the line is still under a ServiceDomain (by depth)
          const depth = getDepth(line.trim());
          if (depth === 0) currentSD = null;
        }
      }
    }

    const servicesUsed = Array.from(sdMap.values());

    // Parse Component references from Frontend Tree
    const componentRefs = findElementRefs(treeLines, 'Component').map(r => ({
      componentId: r.name,
      instanceId: r.instanceId
    }));

    // Parse interactive elements (buttons, inputs, etc.) from Frontend Tree
    const interactiveElements = parseInteractiveElements(treeLines);

    // Parse methods sections
    const pubMethodLines = sections['Frontend Public Methods'] || [];
    const publicMethods = [];
    for (const line of pubMethodLines) {
      const stripped = stripDepth(line.trim()).trim();
      if (stripped.startsWith('METHOD_PUB ')) {
        const m = parseMethodDecl(stripped);
        if (m) publicMethods.push({
          name: m.name,
          params: m.params,
          returns: { raw: m.returns, fields: m.returns ? parseReturnFields(m.returns) : [] }
        });
      }
    }

    // ── Nav section detection ──────────────────────────────────────
    // A nav section has: PUBLIC EVENT @someEvent(routePath(STRING))
    // and renders items with data-key attribute via a For loop
    // e.g. SidebarNav.sc has $menuItems with key/label pairs
    const publicEvents = parseEventsFromLines(sections['Frontend Public Events'] || []);
    const globalVarsAll = parseVarsFromLines(sections['Frontend Global Vars'] || []);

    let isNavSection = false;
    let navMenuItems = [];    // [{ key: '/campaigns', label: 'Campaigns' }, ...]
    let navItemInstanceId = null; // e.g. 'menuItem'

    // Detect nav event: EVENT @xxx(routePath(STRING)) or similar
    const hasNavEvent = publicEvents.some(e =>
      e.params?.some(p => p.name === 'routePath' || p.name?.toLowerCase().includes('route'))
    );

    if (hasNavEvent) {
      isNavSection = true;

      // Parse $menuItems variable: [{key:STRING,label:STRING,...}] = [{key:"/",label:"Dashboard",...},...]
      const menuVar = globalVarsAll.find(v =>
        v.name?.toLowerCase().includes('menu') || v.name?.toLowerCase().includes('nav')
      );
      if (menuVar?.default) {
        // Extract key/label pairs from default value string
        const pairRegex = /\{[^}]*key:"([^"]+)"[^}]*label:"([^"]+)"[^}]*\}/g;
        let m;
        while ((m = pairRegex.exec(menuVar.default)) !== null) {
          navMenuItems.push({ key: m[1], label: m[2] });
        }
        // Also try label before key order
        if (navMenuItems.length === 0) {
          const pairRegex2 = /\{[^}]*label:"([^"]+)"[^}]*key:"([^"]+)"[^}]*\}/g;
          while ((m = pairRegex2.exec(menuVar.default)) !== null) {
            navMenuItems.push({ key: m[2], label: m[1] });
          }
        }
      }

      // Find the For loop's rendered item instanceId (the element with data-key binding)
      for (const line of treeLines) {
        const stripped = stripDepth(line.trim()).trim();
        // Look for <Row-ItemName "instanceId"> ... data-key:...
        const rowMatch = stripped.match(/^<(?:Row|Col|Div)-(\w+)\s+"(\w+)">[^>]*data-key:/);
        if (rowMatch) {
          navItemInstanceId = rowMatch[2];
          break;
        }
      }
    }

    return {
      sectionId,
      fileName,
      filePath,
      vlVersion: version,
      previewSize: preview,
      publicProps: parseVarsFromLines(sections['Frontend Public Props'] || []),
      publicEvents,
      publicMethods,
      globalVars: globalVarsAll,
      derivedVars: parseVarsFromLines(sections['Frontend Derived Vars'] || []),
      servicesUsed,
      componentRefs,
      interactiveElements,
      internalMethods: parseMethodsFromLines(
        sections['Frontend Internal Methods'] || [],
        ['METHOD']
      ),
      pipeFuncs: parseMethodsFromLines(
        sections['Frontend Pipeline Funcs'] || [],
        ['PIPE']
      ),
      // Nav section info (set if this section provides app-level navigation)
      isNavSection,
      navMenuItems,    // [{ key: '/campaigns', label: 'Campaigns' }, ...]
      navItemInstanceId, // 'menuItem' — the instance-id of rendered nav items
    };
  }

  // ─── .vx Parser ───────────────────────────────────────────────────

  function parseVX(content, filePath) {
    const version = extractVersion(content);
    const root = extractRoot(content);
    const sections = splitSections(content);
    const appId = root ? root.name : filePath.split('/').pop().replace('.vx', '');
    const fileName = filePath.split('/').pop();

    // Parse SysConfig
    const sysConfig = { deviceTarget: 'PC', screenResolution: '1920x1080' };
    const sysLines = sections['SysConfig'] || [];
    for (const line of sysLines) {
      const trimmed = line.trim();
      const dtMatch = trimmed.match(/^DEVICE_TARGET:"([^"]+)"$/);
      if (dtMatch) sysConfig.deviceTarget = dtMatch[1];
      const srMatch = trimmed.match(/^SCREEN_RESOLUTION:"([^"]+)"$/);
      if (srMatch) sysConfig.screenResolution = srMatch[1];
    }

    // Parse Global Vars
    const globalVars = parseVarsFromLines(sections['Frontend Global Vars'] || []);

    // Parse Frontend Tree: Pages with Sections and Components
    const treeLines = sections['Frontend Tree'] || [];
    const pages = [];
    let currentPage = null;

    // Route map: extract <If-X> conditions:($routeVar == "/path") → sectionId
    // Pattern used in VL SPA apps with route-based navigation ($activeRoute variable)
    const routeMap = {};     // { '/campaigns': 'CampaignManagement', ... }
    let pendingIfRoute = null; // set when we see an <If-...> with a route condition

    // Also detect the routing variable name (e.g., $activeRoute)
    let routeVarName = null;

    // Detect nav section: which section receives menuSelect event (sets $routeVar)
    const navSectionRefs = []; // instanceIds of sections that emit route-changing events

    for (const line of treeLines) {
      const stripped = stripDepth(line.trim()).trim();
      if (!stripped) continue;

      // <Page-Name "id"> path:"route"
      const pageMatch = stripped.match(/^<Page-(\w+)\s+"(\w+)">\s*(.*)$/);
      if (pageMatch) {
        const props = parseInlineProps(pageMatch[3]);
        currentPage = {
          pageId: pageMatch[1],
          instanceId: pageMatch[2],
          route: props.path || '',
          sectionRefs: [],
          componentRefs: []
        };
        pages.push(currentPage);
        pendingIfRoute = null;
        continue;
      }

      // <If-Name> conditions:($routeVar == "/path")  or  conditions:($routeVar == "path")
      const ifMatch = stripped.match(/^<If-\w+>\s*conditions?:\((\$\w+)\s*==\s*"([^"]+)"\)/);
      if (ifMatch) {
        routeVarName = routeVarName || ifMatch[1]; // record routing variable
        pendingIfRoute = ifMatch[2]; // e.g. "/campaigns"
        continue;
      }

      // <Section-Name "instanceId"> immediately following an <If> with route condition
      if (pendingIfRoute !== null) {
        const secInIfMatch = stripped.match(/^<Section-(\w+)\s+"(\w+)">/);
        if (secInIfMatch) {
          routeMap[pendingIfRoute] = secInIfMatch[1]; // route → sectionId
          pendingIfRoute = null;
          if (currentPage) {
            currentPage.sectionRefs.push({
              sectionId: secInIfMatch[1],
              instanceId: secInIfMatch[2],
              layoutProps: {}
            });
          }
          continue;
        }
        // Non-section line after If → clear pending
        pendingIfRoute = null;
      }

      if (currentPage) {
        // <Section-Name "instanceId"> ...layoutProps (direct section ref, no If)
        const secMatch = stripped.match(/^<Section-(\w+)\s+"(\w+)">?\s*(.*)$/);
        if (secMatch) {
          // Avoid duplicates (might have been added via If route above)
          if (!currentPage.sectionRefs.some(r => r.sectionId === secMatch[1])) {
            currentPage.sectionRefs.push({
              sectionId: secMatch[1],
              instanceId: secMatch[2],
              layoutProps: parseInlineProps(secMatch[3])
            });
          }
          continue;
        }

        // <Component-Name "instanceId"> ...props
        const compMatch = stripped.match(/^<Component-(\w+)\s+"(\w+)">?\s*(.*)$/);
        if (compMatch) {
          currentPage.componentRefs.push({
            componentId: compMatch[1],
            instanceId: compMatch[2]
          });
        }
      }
    }

    // Parse Event Handlers — detect nav section by pattern:
    // <Section-SidebarNav "sidebarNav">.@menuSelect(routePath)
    // followed by: -$activeRoute = routePath
    const handlerLines = sections['Frontend Event Handlers'] || [];
    const eventBindings = parseEventBindings(handlerLines);

    // Find which section instanceId provides navigation via @menuSelect → $routeVar assignment
    let navSectionInstanceId = null;
    let navEventName = null;
    for (let i = 0; i < handlerLines.length; i++) {
      const line = handlerLines[i].trim();
      // Match: <Section-XXX "instanceId">.@eventName(params)
      const m = line.match(/^<Section-(\w+)\s+"(\w+)">\s*\.@(\w+)\(([^)]*)\)$/);
      if (m) {
        // Look at the handler body lines for "$routeVar = <param>"
        const param = m[4] ? m[4].split(',')[0].trim() : '';
        for (let j = i + 1; j < handlerLines.length && j < i + 5; j++) {
          const bodyLine = handlerLines[j].trim().replace(/^-+/, '').trim();
          if (routeVarName && bodyLine.startsWith(routeVarName + ' = ') && param && bodyLine.includes(param)) {
            navSectionInstanceId = m[2];
            navEventName = m[3];
            break;
          }
        }
        if (navSectionInstanceId) break;
      }
    }

    // Build home route (route that appears first, or '/')
    const homeRoute = Object.keys(routeMap).find(r => r === '/') || Object.keys(routeMap)[0] || '/';

    // Parse interactive elements from Frontend Tree
    const interactiveElements = parseInteractiveElements(treeLines);

    // Parse Internal Methods
    const internalMethods = parseMethodsFromLines(
      sections['Frontend Internal Methods'] || [],
      ['METHOD']
    );

    return {
      appId,
      fileName,
      filePath,
      vlVersion: version,
      sysConfig,
      globalVars,
      pages,
      interactiveElements,
      eventBindings,
      internalMethods,
      // Route-based navigation info (key addition for SPA test generation)
      routeMap,          // { '/': 'DashboardOverview', '/campaigns': 'CampaignManagement', ... }
      routeVarName,      // '$activeRoute'
      homeRoute,         // '/'
      navSectionInstanceId, // 'sidebarNav' — the section that emits route changes
      navEventName,      // 'menuSelect'
    };
  }

  // ─── .vth Parser ──────────────────────────────────────────────────

  function parseVTH(content, filePath) {
    const version = extractVersion(content);
    const root = extractRoot(content);
    const sections = splitSections(content);
    const themeName = root ? root.name : filePath.split('/').pop().replace('.vth', '');
    const meta = {};
    const metaLines = sections['Meta'] || [];
    for (const line of metaLines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('//')) continue;
      const m = trimmed.match(/^([A-Za-z_][\w-]*):(.+)$/);
      if (!m) continue;
      const [, key, value] = m;
      meta[key] = value.trim().replace(/^"(.*)"$/, '$1');
    }

    const slots = {};
    const pointSlotLines = sections['Point Slot Values'] || sections['Coordinate Values'] || [];
    for (const line of pointSlotLines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('//')) continue;
      const sep = trimmed.indexOf(':');
      if (sep <= 0) continue;
      const key = trimmed.slice(0, sep).trim();
      const value = trimmed.slice(sep + 1).trim();
      if (!key || !value) continue;
      slots[key] = value;
    }

    // Parse Design Tokens
    const designTokens = [];
    const tokenLines = sections['Design Tokens'] || [];
    for (const line of tokenLines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('//')) continue;

      // <TokenGroup-Name> key:value key:value ...
      const m = trimmed.match(/^<(\w+)-(\w+)>\s+(.+)$/);
      if (m) {
        const group = `${m[1]}-${m[2]}`;
        const tokensStr = m[3];
        const tokens = [];
        // Parse key:value pairs - tokens can have complex values like rgba(...)
        const tokenRegex = /(\w+):([^\s]+(?:\s+[^\s:]+)*?)(?=\s+\w+:|$)/g;
        let tm;
        // Simpler approach: split by camelCase boundaries after first match
        const pairs = [];
        let remaining = tokensStr;
        // Use a smarter regex that matches tokenName:value
        const smartRegex = /([a-z]\w*):(.+?)(?=\s+[a-z]\w*:|$)/gi;
        while ((tm = smartRegex.exec(remaining)) !== null) {
          pairs.push({ name: tm[1], value: tm[2].trim() });
        }
        designTokens.push({ group, tokens: pairs });
      }
    }

    // Parse Component Styles / Component Variants
    const componentVariants = [];
    const styleLines = sections['Component Styles'] || sections['Component Variants'] || [];
    let currentComp = null;
    let currentVariant = null;

    for (const line of styleLines) {
      const trimmed = line.trim();
      const stripped = stripDepth(trimmed);
      if (!stripped || stripped.startsWith('//')) continue;

      const depth = getDepth(trimmed);

      // Top-level element: <CompType-VariantName> styles...
      if (depth === 0) {
        const m = stripped.match(/^<(\w+)-(\w+)>?\s*(.*)$/);
        if (m) {
          const compType = m[1];
          const variantName = m[2];
          const styles = m[3] ? parseInlineProps(m[3]) : {};
          currentVariant = { name: variantName, styles, stateStyles: [] };

          // Find or create component entry
          let comp = componentVariants.find(c => c.component === compType);
          if (!comp) {
            comp = { component: compType, default: null, variants: [], bindingRules: [] };
            componentVariants.push(comp);
          }
          comp.variants.push(currentVariant);
        }
        continue;
      }

      // StateStyle nested under variant
      if (depth > 0 && currentVariant) {
        const ssMatch = stripped.match(/^<StateStyle-(\w+)(?:\s+trigger:(\w+))?>\s*(.*)$/);
        if (ssMatch) {
          currentVariant.stateStyles.push({
            name: ssMatch[1],
            trigger: ssMatch[2] || null,
            styles: parseInlineProps(ssMatch[3])
          });
        }
      }
    }

    // Parse Overrides
    const overrides = [];
    const overrideLines = sections['Overrides'] || [];
    for (const line of overrideLines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('//')) continue;
      // <Section-X>.#elementId prop:value
      const m = trimmed.match(/^<(\w+-\w+)>\.#(\w+)\s+(.+)$/);
      if (m) {
        overrides.push({
          target: `${m[1]}.#${m[2]}`,
          properties: parseInlineProps(m[3])
        });
      }
    }

    return {
      name: themeName,
      vlVersion: version,
      rootTag: root ? root.type : null,
      meta,
      mode: meta.mode || null,
      version: meta.version || null,
      styleSpaceVersion: meta.styleSpaceVersion || null,
      base_theme: meta.base_theme || null,
      profile: meta.profile || null,
      slots,
      pointSlotValues: slots,
      designTokens,
      componentVariants,
      bindingRules: [], // Would need more parsing for binding rules
      overrides
    };
  }

  // ─── Main Extract Function ────────────────────────────────────────

  function extract(fileTree) {
    const metadata = {
      project: {
        name: '',
        vlVersion: '',
        fileManifest: []
      },
      database: null,
      serviceDomains: [],
      components: [],
      sections: [],
      apps: [],
      theme: null,
      dependencyGraph: { nodes: [], edges: [] }
    };

    // Normalize paths: strip leading ./ or /
    const normalized = {};
    for (const [path, content] of Object.entries(fileTree)) {
      const normPath = path.replace(/^\.?\//, '');
      normalized[normPath] = content;
    }

    // Classify and parse each file
    for (const [path, content] of Object.entries(normalized)) {
      const info = classifyFile(path);
      if (!info) continue;

      const { type } = info;
      metadata.project.fileManifest.push({
        path,
        type,
        id: null // filled below
      });

      try {
        switch (type) {
          case 'database': {
            const db = parseVDB(content, path);
            metadata.database = db;
            metadata.project.name = db.name;
            metadata.project.vlVersion = db.vlVersion || metadata.project.vlVersion;
            metadata.project.fileManifest[metadata.project.fileManifest.length - 1].id = db.name;
            break;
          }
          case 'service': {
            const sd = parseVS(content, path);
            metadata.serviceDomains.push(sd);
            metadata.project.vlVersion = sd.vlVersion || metadata.project.vlVersion;
            metadata.project.fileManifest[metadata.project.fileManifest.length - 1].id = sd.domainId;
            break;
          }
          case 'component': {
            const cp = parseCP(content, path);
            metadata.components.push(cp);
            metadata.project.vlVersion = cp.vlVersion || metadata.project.vlVersion;
            metadata.project.fileManifest[metadata.project.fileManifest.length - 1].id = cp.componentId;
            break;
          }
          case 'section': {
            const sc = parseSC(content, path);
            metadata.sections.push(sc);
            metadata.project.vlVersion = sc.vlVersion || metadata.project.vlVersion;
            metadata.project.fileManifest[metadata.project.fileManifest.length - 1].id = sc.sectionId;
            break;
          }
          case 'app': {
            const vx = parseVX(content, path);
            metadata.apps.push(vx);
            metadata.project.vlVersion = vx.vlVersion || metadata.project.vlVersion;
            metadata.project.fileManifest[metadata.project.fileManifest.length - 1].id = vx.appId;
            break;
          }
          case 'theme': {
            const th = parseVTH(content, path);
            metadata.theme = th;
            metadata.project.vlVersion = th.vlVersion || metadata.project.vlVersion;
            metadata.project.fileManifest[metadata.project.fileManifest.length - 1].id = th.name;
            break;
          }
        }
      } catch (err) {
        console.warn(`[VLMetadataExtractor] Error parsing ${path}:`, err);
      }
    }

    // Build dependency graph
    metadata.dependencyGraph = buildDependencyGraph(metadata);

    return metadata;
  }

  // ─── Dependency Graph Builder ─────────────────────────────────────

  function buildDependencyGraph(meta) {
    const nodes = [];
    const edges = [];
    const nodeSet = new Set();

    function addNode(id, type, label, sub) {
      if (!nodeSet.has(id)) {
        nodeSet.add(id);
        nodes.push({ id, type, label, sub: sub || '' });
      }
    }

    // Add database/theme nodes
    if (meta.database) {
      const dbPath = meta.project.fileManifest.find(f => f.type === 'database')?.path;
      addNode(dbPath || `Database/${meta.database.name}.vdb`, 'database', meta.database.name, '.vdb');
    }
    if (meta.theme) {
      const thPath = meta.project.fileManifest.find(f => f.type === 'theme')?.path;
      addNode(thPath || `Theme/${meta.theme.name}.vth`, 'theme', meta.theme.name, '.vth');
    }

    // Add service domain nodes
    for (const sd of meta.serviceDomains) {
      addNode(sd.filePath, 'service', sd.domainId, sd.fileName);
    }

    // Add component nodes
    for (const cp of meta.components) {
      addNode(cp.filePath, 'component', cp.componentId, cp.fileName);
    }

    // Add section nodes
    for (const sc of meta.sections) {
      addNode(sc.filePath, 'section', sc.sectionId, sc.fileName);
    }

    // Add app nodes
    for (const app of meta.apps) {
      addNode(app.filePath, 'app', app.appId, app.fileName);
    }

    // Build edges

    // 1. Section -> ServiceDomain (calls)
    for (const sc of meta.sections) {
      for (const svcRef of sc.servicesUsed) {
        const sdMeta = meta.serviceDomains.find(sd => sd.domainId === svcRef.domainId);
        if (sdMeta) {
          const edgeKey = `${sc.filePath}|${sdMeta.filePath}|calls`;
          if (!edges.find(e => `${e.from}|${e.to}|${e.type}` === edgeKey)) {
            edges.push({ from: sc.filePath, to: sdMeta.filePath, type: 'calls' });
          }
        }
      }
    }

    // 2. Section -> Component (uses)
    for (const sc of meta.sections) {
      for (const compRef of sc.componentRefs) {
        const cpMeta = meta.components.find(cp => cp.componentId === compRef.componentId);
        if (cpMeta) {
          edges.push({ from: sc.filePath, to: cpMeta.filePath, type: 'uses' });
        }
      }
    }

    // 3. App -> Section (hosts)
    for (const app of meta.apps) {
      const hostedSections = new Set();
      for (const page of app.pages) {
        for (const secRef of page.sectionRefs) {
          hostedSections.add(secRef.sectionId);
        }
      }
      // Also check event bindings for section references
      for (const binding of app.eventBindings) {
        if (binding.source.type === 'section') {
          hostedSections.add(binding.source.id);
        }
      }
      for (const secId of hostedSections) {
        const scMeta = meta.sections.find(sc => sc.sectionId === secId);
        if (scMeta) {
          edges.push({ from: app.filePath, to: scMeta.filePath, type: 'hosts' });
        }
      }
    }

    // 4. App -> Component (uses)
    for (const app of meta.apps) {
      const usedComps = new Set();
      for (const page of app.pages) {
        for (const compRef of page.componentRefs) {
          usedComps.add(compRef.componentId);
        }
      }
      // Also check event bindings
      for (const binding of app.eventBindings) {
        if (binding.source.type === 'component') {
          usedComps.add(binding.source.id);
        }
      }
      for (const compId of usedComps) {
        const cpMeta = meta.components.find(cp => cp.componentId === compId);
        if (cpMeta) {
          edges.push({ from: app.filePath, to: cpMeta.filePath, type: 'uses' });
        }
      }
    }

    return { nodes, edges };
  }

  // ─── Process Editor Format Converter ──────────────────────────────

  function toProcessEditorFormat(meta) {
    // Build UIMap
    const uiMap = {
      apps: meta.apps.map(a => ({ appId: a.appId })),
      pages: [],
      sections: [],
      components: []
    };

    // Pages
    for (const app of meta.apps) {
      for (const page of app.pages) {
        uiMap.pages.push({
          pageId: page.pageId,
          route: page.route,
          rootSections: page.sectionRefs.map(s => s.sectionId),
          sectionLayout: {}
        });
      }
    }

    // Build section->component reverse map for reusedIn
    const compUsageMap = new Map(); // componentId -> [sectionId]
    for (const sc of meta.sections) {
      for (const ref of sc.componentRefs) {
        if (!compUsageMap.has(ref.componentId)) compUsageMap.set(ref.componentId, []);
        compUsageMap.get(ref.componentId).push(sc.sectionId);
      }
    }

    // Sections
    for (const sc of meta.sections) {
      uiMap.sections.push({
        sectionId: sc.sectionId,
        fileName: sc.fileName,
        servicesUsed: sc.servicesUsed.flatMap(d =>
          d.services.map(svc => ({ serviceId: svc.serviceId }))
        ),
        componentRefs: sc.componentRefs.map(c => c.componentId),
        publicEvents: sc.publicEvents,
        publicMethods: sc.publicMethods,
        keyStates: [...sc.globalVars, ...sc.derivedVars].map(v => ({
          name: v.name,
          type: v.type,
          initialValue: v.default
        }))
      });
    }

    // Components
    for (const cp of meta.components) {
      uiMap.components.push({
        componentId: cp.componentId,
        fileName: cp.fileName,
        props: cp.publicProps,
        events: cp.publicEvents,
        reusedIn: compUsageMap.get(cp.componentId) || []
      });
    }

    // Build ServiceMap
    const serviceMap = {
      serviceDomains: meta.serviceDomains.map(sd => {
        // Build service->vtable mapping
        // We can't perfectly map which services use which VTables without deep analysis
        // But we list all VTables under the domain
        return {
          domainId: sd.domainId,
          fileName: sd.fileName,
          description: '',
          services: sd.services.map(svc => ({
            serviceId: svc.serviceId,
            inputModel: Object.fromEntries(svc.params.map(p => [p.name, p.type])),
            outputModel: svc.returns && svc.returns.fields
              ? Object.fromEntries(svc.returns.fields.map(f => [f.name, f.type]))
              : {},
            virtualTablesUsed: sd.virtualTables.map(vt => vt.instanceId || vt.name),
            operations: []
          })),
          virtualTables: sd.virtualTables.map(vt => ({
            vtId: vt.instanceId || vt.name,
            sourceTable: vt.sourceTable,
            fields: vt.fields
          }))
        };
      })
    };

    // Build VDB text (for Process Editor compatibility)
    let vdbText = '';
    if (meta.database) {
      vdbText = `// VL_VERSION:${meta.project.vlVersion || '2.8'}\n`;
      vdbText += `<Database-${meta.database.name}>\n\n`;
      for (const table of meta.database.tables) {
        vdbText += `<Table-${table.name}>\n`;
        for (const field of table.fields) {
          let fieldLine = `-<Field-${field.name}> type:${field.type}`;
          if (field.notNull) fieldLine += ' notNull:true';
          if (field.default) fieldLine += ` default:${field.default}`;
          vdbText += fieldLine + '\n';
        }
        for (const idx of table.indexes) {
          vdbText += `-<Index-${idx.name}> type:${idx.type} fields:${JSON.stringify(idx.fields)}\n`;
        }
        vdbText += '\n';
      }
      for (const rel of meta.database.relations) {
        const cardMap = { oneToMany: '<<', manyToOne: '>>', oneToOne: '--' };
        vdbText += `<Relation-${rel.name}> ${rel.from.table}.${rel.from.field}${cardMap[rel.cardinality] || '<<'}${rel.to.table}.${rel.to.field}\n`;
      }
      vdbText += `\n</Database-${meta.database.name}>\n`;
    }

    return { uiMap, serviceMap, vdbText };
  }

  // ─── Public API ───────────────────────────────────────────────────

  return {
    extract,
    buildDependencyGraph,
    toProcessEditorFormat,
    // Expose individual parsers for advanced usage
    parsers: {
      parseVDB,
      parseVS,
      parseCP,
      parseSC,
      parseVX,
      parseVTH
    },
    // Expose utilities
    utils: {
      classifyFile,
      splitSections,
      extractVersion,
      extractPreview,
      extractRoot,
      parseParams,
      parseReturnFields,
      parseInlineProps
    }
  };

})();

// Support both ES Module and global usage
if (typeof module !== 'undefined' && module.exports) {
  module.exports = VLMetadataExtractor;
}
