#!/usr/bin/env node
/**
 * manifest-selector-map.mjs
 *
 * Generates a selector cache from Fluent Commerce Mystique manifest settings.
 * Pre-analyzes manifests into lightweight caches so the LLM can skip expensive
 * DOM snapshots (~30K tokens each) during UI testing.
 *
 * Zero npm dependencies — uses only Node.js built-ins.
 *
 * Usage:
 *   node fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE>
 *   node fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE> --verbose
 *   node fluent-ai-skills/tools/manifest-selector-map.mjs --manifest path/to/fragment.json
 *   node fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE> --helper  # outputs browser helper JS only
 *   node fluent-ai-skills/tools/manifest-selector-map.mjs --help
 *
 * Outputs three files in accounts/<PROFILE>/manifests/:
 *   selector-cache.json       — Full analysis (reference, not for LLM)
 *   selector-cache-slim.json  — Navigation, buttons, cards, lists (~58KB ~15K tokens)
 *   selector-cache-helper.js  — Browser helper JS (deprecated — methods now merged into __fcTest)
 */

import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync, statSync } from 'node:fs';
import { join, basename, resolve, dirname } from 'node:path';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const WORKSPACE_ROOT = process.cwd();
const ACCOUNTS_DIR = join(WORKSPACE_ROOT, 'accounts');

// ─── CLI Argument Parsing ───────────────────────────────────────────────────

function parseArgs() {
  const args = process.argv.slice(2);
  const opts = {
    profile: null,
    manifest: null,
    output: null,
    helper: false,
    verbose: false,
  };

  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case '--profile': case '-p':
        opts.profile = args[++i];
        break;
      case '--manifest':
        opts.manifest = args[++i];
        break;
      case '--output': case '-o':
        opts.output = args[++i];
        break;
      case '--helper':
        opts.helper = true;
        break;
      case '--verbose': case '-v':
        opts.verbose = true;
        break;
      case '--help': case '-h':
        printHelp();
        process.exit(0);
        break;
      default:
        if (!args[i].startsWith('-')) {
          // Positional arg treated as profile
          opts.profile = args[i];
        } else {
          console.error(`Unknown option: ${args[i]}`);
          console.error('Run with --help for usage.');
          process.exit(1);
        }
    }
  }

  return opts;
}

function printHelp() {
  console.log(`
Manifest Selector Cache Generator

Generates a pre-analyzed selector cache from Fluent Commerce Mystique manifest
settings. Produces three files for token-efficient UI testing:

  selector-cache.json       Full analysis (reference, not for LLM context)
  selector-cache-slim.json  Navigation, buttons, cards, lists (~15K tokens)
  selector-cache-helper.js  Browser helper JS (deprecated, merged into __fcTest)

Usage:
  node fluent-ai-skills/tools/manifest-selector-map.mjs --profile <PROFILE>
  node fluent-ai-skills/tools/manifest-selector-map.mjs --manifest <path/to/fragment.json>

Options:
  --profile, -p <NAME>  Profile name (required unless --manifest is used)
  --manifest <PATH>     Parse a single manifest file instead of scanning profile
  --output, -o <PATH>   Override output path for selector-cache.json
  --helper              Print browser helper JS to stdout and exit
  --verbose, -v         Show detailed parsing progress
  --help, -h            Show this help

Manifest search order (when using --profile):
  1. accounts/<PROFILE>/manifests/  (local JSON files, excluding backups/)
  2. accounts/<PROFILE>/manifests/backups/  (fallback to backup copies)
  3. accounts/<PROFILE>/settings/   (settings directory fallback)
  4. CLI fetch: fluent setting list + fluent setting get (last resort)

Examples:
  node fluent-ai-skills/tools/manifest-selector-map.mjs --profile ACME
  node fluent-ai-skills/tools/manifest-selector-map.mjs --profile ACME --verbose
  node fluent-ai-skills/tools/manifest-selector-map.mjs --manifest accounts/ACME/manifests/fc.mystique.manifest.oms.fragment.ordermanagement.json
  node fluent-ai-skills/tools/manifest-selector-map.mjs --profile ACME --helper > helper.js
`);
}

// ─── Manifest File Discovery ────────────────────────────────────────────────

/**
 * Checks if a filename is a manifest file (not a cache, deploy artifact, or non-manifest).
 */
function isManifestFile(filename) {
  if (!filename.endsWith('.json')) return false;
  // Must contain 'fc.mystique.manifest' to be a manifest setting
  if (!filename.includes('fc.mystique.manifest')) return false;
  // Exclude generated files
  if (filename.startsWith('selector-cache')) return false;
  if (filename.startsWith('verify-')) return false;
  return true;
}

/**
 * Finds manifest files for the given profile.
 * Search order:
 *   1. accounts/<PROFILE>/manifests/ (top-level JSON files, not in subdirs)
 *   2. accounts/<PROFILE>/manifests/backups/ (fallback)
 *   3. accounts/<PROFILE>/settings/ (fallback)
 *   4. CLI fetch via fluent setting list + get (last resort)
 */
function findManifestFiles(profile, verbose) {
  const manifestDir = join(ACCOUNTS_DIR, profile, 'manifests');
  const backupsDir = join(manifestDir, 'backups');
  const settingsDir = join(ACCOUNTS_DIR, profile, 'settings');

  // Strategy 1: Top-level manifest files in accounts/<PROFILE>/manifests/
  if (existsSync(manifestDir)) {
    const files = readdirSync(manifestDir)
      .filter(f => {
        // Only top-level files (not directories)
        const fullPath = join(manifestDir, f);
        try {
          return statSync(fullPath).isFile() && isManifestFile(f);
        } catch { return false; }
      })
      .map(f => join(manifestDir, f));

    if (files.length > 0) {
      if (verbose) console.log(`Found ${files.length} manifest file(s) in ${manifestDir}`);
      return files;
    }
  }

  // Strategy 2: Backup copies in accounts/<PROFILE>/manifests/backups/
  if (existsSync(backupsDir)) {
    const files = readdirSync(backupsDir)
      .filter(f => f.endsWith('.json') && f.includes('fc.mystique.manifest'))
      .map(f => join(backupsDir, f));

    if (files.length > 0) {
      if (verbose) console.log(`Found ${files.length} manifest backup(s) in ${backupsDir}`);
      return files;
    }
  }

  // Strategy 3: Settings directory
  if (existsSync(settingsDir)) {
    const files = readdirSync(settingsDir)
      .filter(f => f.endsWith('.json') && f.includes('fc.mystique.manifest'))
      .map(f => join(settingsDir, f));

    if (files.length > 0) {
      if (verbose) console.log(`Found ${files.length} manifest setting(s) in ${settingsDir}`);
      return files;
    }
  }

  // Strategy 4: CLI fetch
  if (verbose) console.log('No local manifests found. Attempting CLI fetch...');
  return fetchManifestsViaCli(profile, manifestDir, verbose);
}

/**
 * Fetches manifest settings via the Fluent CLI as a last resort.
 */
function fetchManifestsViaCli(profile, outputDir, verbose) {
  try {
    // List all manifest settings
    const listOutput = execSync(
      `fluent setting list -p ${profile} --filter "fc.mystique.manifest" --output json`,
      { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
    );

    let settings;
    try {
      settings = JSON.parse(listOutput);
    } catch {
      console.error('Failed to parse CLI setting list output.');
      return [];
    }

    if (!Array.isArray(settings) || settings.length === 0) {
      console.error('No manifest settings found via CLI.');
      return [];
    }

    // Ensure output directory exists
    mkdirSync(outputDir, { recursive: true });

    const files = [];
    for (const setting of settings) {
      const name = setting.name || setting;
      if (typeof name !== 'string' || !name.includes('fc.mystique.manifest')) continue;

      if (verbose) console.log(`  Fetching ${name} via CLI...`);
      try {
        const getOutput = execSync(
          `fluent setting get -p ${profile} -n "${name}" --output json`,
          { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
        );

        const outFile = join(outputDir, `${name}.json`);
        writeFileSync(outFile, getOutput, 'utf8');
        files.push(outFile);
      } catch (e) {
        if (verbose) console.error(`  Failed to fetch ${name}: ${e.message}`);
      }
    }

    if (files.length > 0 && verbose) {
      console.log(`Fetched ${files.length} manifest setting(s) via CLI.`);
    }
    return files;
  } catch (e) {
    if (verbose) console.error(`CLI fetch failed: ${e.message}`);
    return [];
  }
}

// ─── Manifest Parser ────────────────────────────────────────────────────────

/**
 * Extracts the manifest body from various wrapper formats:
 *   1. Raw manifest: { manifestVersion, routes }
 *   2. Setting object: { lobValue: { manifestVersion, routes } }
 *   3. Setting array: [{ lobValue: { manifestVersion, routes } }]
 *   4. Stringified lobValue
 */
function extractManifestBody(raw) {
  let manifest;
  if (Array.isArray(raw)) {
    const item = raw[0];
    if (!item) return null;
    manifest = item.lobValue || item;
  } else {
    manifest = raw.lobValue || raw;
  }
  // Handle stringified JSON
  if (typeof manifest === 'string') {
    try { manifest = JSON.parse(manifest); } catch { return null; }
  }
  if (!manifest || (!manifest.routes && !manifest.manifestVersion)) return null;
  return manifest;
}

/**
 * Derives the app name from a manifest setting name.
 * e.g. "fc.mystique.manifest.oms.fragment.ordermanagement" -> "oms"
 *      "fc.mystique.manifest.store" -> "store"
 */
function deriveAppName(settingName) {
  const prefix = 'fc.mystique.manifest.';
  if (!settingName.startsWith(prefix)) return 'unknown';
  const rest = settingName.slice(prefix.length);
  // App name is the first segment after the prefix
  const dotIdx = rest.indexOf('.');
  return dotIdx >= 0 ? rest.slice(0, dotIdx) : rest;
}

/**
 * Parses a single manifest file and extracts pages, buttons, cards, lists, tabs.
 */
function parseManifest(filePath) {
  let raw;
  try {
    raw = JSON.parse(readFileSync(filePath, 'utf8'));
  } catch (e) {
    return null;
  }

  const name = basename(filePath, '.json')
    // Strip date suffixes from backup filenames (e.g. _2026-03-01)
    .replace(/_\d{4}-\d{2}-\d{2}$/, '');
  const appName = deriveAppName(name);
  const manifest = extractManifestBody(raw);
  if (!manifest) return null;

  const pages = [];
  const buttons = [];
  const cards = [];
  const lists = [];
  const tabs = [];

  /**
   * Walks descendant components within a page, extracting cards, lists, and tabs.
   */
  function walkDescendants(descendants, pagePath, pageTitle, depth) {
    if (!Array.isArray(descendants)) return;
    depth = depth || 0;

    for (const desc of descendants) {
      const comp = desc.component || '';
      const props = desc.props || {};

      // ── fc.list — extract columns and filters ──
      if (comp === 'fc.list' || comp.endsWith('.list')) {
        const attrs = props.attributes || [];
        lists.push({
          page: pagePath,
          pageTitle,
          component: comp,
          dataSource: props.dataSource || desc.dataSource || null,
          columns: attrs.map(a => ({
            label: a.label || null,
            template: a.template || a.value || null,
          })),
          hasFilter: !!(props.filter && props.filter.enabled),
          rowLink: props.rowLink || null,
          selector: {
            strategy: 'structural',
            find: `table within page "${pageTitle}"`,
            css: '.MuiTable-root, [role="grid"]',
            columnHeaders: attrs.map(a => a.label).filter(Boolean),
          },
        });
      }

      // ── fc.card.attribute / fc.card — extract card title and fields ──
      if (comp === 'fc.card.attribute' || comp === 'fc.card' ||
          comp.endsWith('.card.attribute') || comp === 'fc.tabs.card') {
        const attrs = props.attributes || [];
        const title = props.title || props['data.title'] || null;
        cards.push({
          page: pagePath,
          pageTitle,
          component: comp,
          title,
          width: props.width || 'full',
          fields: attrs.map(a => ({
            label: a.label || null,
            template: a.template || a.value || null,
          })),
          selector: {
            strategy: 'heading-text',
            find: `card titled "${title}" on page "${pageTitle}"`,
            js: title
              ? `__fcTest.findCard(${JSON.stringify(title)})`
              : null,
          },
        });
      }

      // ── fc.tabs — extract tab labels from props.layouts[] ──
      if (comp === 'fc.tabs' || comp.endsWith('.tabs')) {
        const layouts = props.layouts || [];
        const tabItems = layouts.map(l => ({
          label: l.label || l.title || 'Unknown',
        }));
        // Also check descendants for tab-level metadata
        if (tabItems.length === 0 && Array.isArray(desc.descendants)) {
          for (const t of desc.descendants) {
            const tProps = t.props || {};
            tabItems.push({
              label: tProps.title || tProps.label || 'Unknown',
              component: t.component || null,
            });
          }
        }
        if (tabItems.length > 0) {
          tabs.push({
            page: pagePath,
            pageTitle,
            items: tabItems,
            selector: {
              strategy: 'mui-tabs',
              css: '.MuiTabs-root',
            },
          });
        }
      }

      // Recurse into nested descendants
      if (desc.descendants) {
        walkDescendants(desc.descendants, pagePath, pageTitle, depth + 1);
      }
    }
  }

  /**
   * Walks the route tree, extracting page metadata and action buttons.
   */
  function walkPages(routes, sectionLabel) {
    if (!Array.isArray(routes)) return;
    sectionLabel = sectionLabel || '';

    for (const route of routes) {
      // Section routes contain pages
      if (route.type === 'section') {
        const label = route.nav?.label || '';
        walkPages(route.pages || [], label);
        // Some sections have nested routes instead of pages
        if (route.routes) walkPages(route.routes, label);
      }

      // Page routes are the main extraction target
      if (route.type === 'page' || route.type === 'tab') {
        const path = route.path || '';
        const title = route.props?.title || route.nav?.label || path;
        const isDetail = path.includes(':');
        const actions = route.props?.actions || {};

        const page = {
          section: sectionLabel,
          path,
          title,
          isDetail,
          component: route.component || 'fc.page',
          navLabel: route.nav?.label || null,
          selector: {
            strategy: 'navigation',
            find: isDetail ? `detail page at /${path}` : `list page "${title}"`,
            navClick: route.nav?.label
              ? `__fcTest.clickNav(${JSON.stringify(route.nav.label)})`
              : `__fcTest.navigateTo(${JSON.stringify('/' + path)})`,
          },
        };

        pages.push(page);

        // Extract user action buttons from primary/secondary actions
        const primary = actions.primary || [];
        const secondary = actions.secondary || [];

        for (const action of [...primary, ...secondary]) {
          if (action.type === 'userAction') {
            const isPrimary = primary.includes(action);
            buttons.push({
              page: path,
              pageTitle: title,
              name: action.name,
              type: isPrimary ? 'PRIMARY' : 'SECONDARY',
              hasOverrides: !!(action.overrides && Object.keys(action.overrides).length),
              overrideFields: action.overrides ? Object.keys(action.overrides) : [],
              selector: {
                strategy: 'button-text',
                find: `${isPrimary ? 'primary' : 'secondary'} button "${action.name}" on "${title}"`,
                js: `__fcTest.findActionButton(${JSON.stringify(action.name)})`,
              },
            });
          }
        }

        // Walk descendants for cards, lists, tabs
        if (route.descendants) {
          walkDescendants(route.descendants, path, title);
        }
      }

      // Recurse into sub-routes (e.g. pages array within a route)
      if (route.pages) walkPages(route.pages, sectionLabel);
    }
  }

  walkPages(manifest.routes);

  return { name, appName, pages, buttons, cards, lists, tabs };
}

// ─── Slim Cache Builder ─────────────────────────────────────────────────────

/**
 * Builds the app-grouped slim cache from parsed manifest results.
 * Output format:
 *   {
 *     generated, profile,
 *     apps: {
 *       oms: {
 *         navigation: [{ label, path }],
 *         pages: {
 *           "/oms/orders": { listColumns, buttons },
 *           "/oms/orders/:id": { cardTitles, tabs, actionButtons }
 *         }
 *       }
 *     }
 *   }
 *
 * Also produces the flat legacy format for backward compatibility.
 */
function buildSlimCache(manifests, profile) {
  const now = new Date().toISOString();

  // ── App-grouped format (new) ──
  const apps = {};

  for (const [, m] of Object.entries(manifests)) {
    const app = m.appName || 'unknown';
    if (!apps[app]) {
      apps[app] = { navigation: [], pages: {} };
    }

    for (const page of m.pages) {
      // Navigation entries (non-detail pages with nav labels)
      if (!page.isDetail && page.navLabel) {
        apps[app].navigation.push({
          label: page.navLabel || page.title,
          path: `/${app}/${page.path}`,
        });
      }

      // Page entries — keyed by path
      const pageKey = `/${app}/${page.path}`;
      if (!apps[app].pages[pageKey]) {
        apps[app].pages[pageKey] = {};
      }
    }

    // Populate page data from buttons, cards, lists, tabs
    for (const btn of m.buttons) {
      const pageKey = `/${app}/${btn.page}`;
      if (!apps[app].pages[pageKey]) apps[app].pages[pageKey] = {};
      const pg = apps[app].pages[pageKey];
      if (!pg.actionButtons) pg.actionButtons = [];
      // Deduplicate by name+type
      const btnKey = `${btn.name}|${btn.type}`;
      if (!pg.actionButtons.some(b => `${b.eventName}|${b.type}` === btnKey)) {
        pg.actionButtons.push({
          label: btn.name,
          eventName: btn.name,
          type: btn.type,
          fields: btn.overrideFields,
        });
      }
    }

    for (const card of m.cards) {
      const pageKey = `/${app}/${card.page}`;
      if (!apps[app].pages[pageKey]) apps[app].pages[pageKey] = {};
      const pg = apps[app].pages[pageKey];
      if (!pg.cardTitles) pg.cardTitles = [];
      if (card.title && !pg.cardTitles.includes(card.title)) {
        pg.cardTitles.push(card.title);
      }
    }

    for (const list of m.lists) {
      const pageKey = `/${app}/${list.page}`;
      if (!apps[app].pages[pageKey]) apps[app].pages[pageKey] = {};
      const pg = apps[app].pages[pageKey];
      if (!pg.listColumns) pg.listColumns = [];
      const cols = list.columns.map(c => c.label).filter(Boolean);
      // Cap at 6 columns, add "+N more" if truncated
      const capped = cols.length > 6
        ? [...cols.slice(0, 6), `+${cols.length - 6} more`]
        : cols;
      // Deduplicate columns within a page
      for (const col of capped) {
        if (!pg.listColumns.includes(col)) {
          pg.listColumns.push(col);
        }
      }
    }

    for (const tab of m.tabs) {
      const pageKey = `/${app}/${tab.page}`;
      if (!apps[app].pages[pageKey]) apps[app].pages[pageKey] = {};
      const pg = apps[app].pages[pageKey];
      if (!pg.tabs) pg.tabs = [];
      for (const item of tab.items) {
        if (item.label && !pg.tabs.includes(item.label)) {
          pg.tabs.push(item.label);
        }
      }
    }
  }

  // Deduplicate navigation entries
  for (const app of Object.values(apps)) {
    const seen = new Set();
    app.navigation = app.navigation.filter(n => {
      const key = n.path;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });

    // Remove empty page entries (no data extracted)
    for (const [key, pg] of Object.entries(app.pages)) {
      if (!pg.actionButtons && !pg.cardTitles && !pg.listColumns && !pg.tabs) {
        delete app.pages[key];
      }
    }
  }

  // ── Flat legacy format (backward compat) ──
  const flat = {
    _generated: now,
    _purpose: 'Slim navigation + interaction map — feed to LLM instead of raw manifests',
    navigation: [],
    actionButtons: [],
    cardTitles: [],
    listPages: [],
  };

  for (const [, m] of Object.entries(manifests)) {
    for (const page of m.pages) {
      flat.navigation.push({
        section: page.section,
        path: page.path,
        label: page.navLabel || page.title,
        isDetail: page.isDetail,
      });
    }
    for (const btn of m.buttons) {
      flat.actionButtons.push({
        page: btn.pageTitle,
        name: btn.name,
        type: btn.type,
        fields: btn.overrideFields,
      });
    }
    for (const card of m.cards) {
      flat.cardTitles.push({
        page: card.pageTitle,
        title: card.title,
      });
    }
    for (const list of m.lists) {
      flat.listPages.push({
        page: list.pageTitle,
        columns: list.columns.map(c => c.label).filter(Boolean),
        hasFilter: list.hasFilter,
      });
    }
  }

  // Deduplicate flat arrays
  const seenPaths = new Set();
  flat.navigation = flat.navigation.filter(n => {
    const key = n.path;
    if (seenPaths.has(key)) return false;
    seenPaths.add(key);
    return true;
  });

  const seenCards = new Set();
  flat.cardTitles = flat.cardTitles.filter(c => {
    const key = `${c.page}|${c.title}`;
    if (seenCards.has(key)) return false;
    seenCards.add(key);
    return true;
  });

  const seenLists = new Set();
  flat.listPages = flat.listPages.filter(l => {
    const key = `${l.page}|${l.columns.slice(0, 3).join(',')}`;
    if (seenLists.has(key)) return false;
    seenLists.add(key);
    return true;
  }).map(l => ({
    ...l,
    columns: l.columns.length > 6
      ? [...l.columns.slice(0, 6), `+${l.columns.length - 6} more`]
      : l.columns,
  }));

  const seenBtns = new Set();
  flat.actionButtons = flat.actionButtons.filter(b => {
    const key = `${b.page}|${b.name}`;
    if (seenBtns.has(key)) return false;
    seenBtns.add(key);
    return true;
  });

  return {
    grouped: {
      generated: now,
      profile,
      apps,
    },
    flat,
  };
}

// ─── Browser Helper Library ─────────────────────────────────────────────────

function generateBrowserHelper() {
  return `// Fluent UI Test Helper — inject once via evaluate_script
// Merged into __fcTest namespace (shared with BROWSER_OPTIMIZATIONS.md)
// Also sets window.__fc as alias for backward compatibility
// Total: ~2KB injected, then each call is ~50 tokens
(function() {
  if (window.__fcTest && window.__fcTest._MUI) return; // already injected with MUI finders

  const MUI = {
    card: '.MuiPaper-root.MuiCard-root, .MuiPaper-root',
    heading: 'h3.MuiTypography-root, h2.MuiTypography-root, h4.MuiTypography-root',
    button: 'button.MuiButton-root, button.MuiButtonBase-root',
    table: '.MuiTable-root, [role="grid"]',
    tabs: '.MuiTabs-root',
    tab: '.MuiTab-root, [role="tab"]',
    input: '.MuiInputBase-input, input.MuiInput-input',
    select: '.MuiSelect-root',
    dialog: '.MuiDialog-root',
    navItem: 'a[href], [role="menuitem"], .MuiListItem-root',
  };

  // If __fcTest already exists (from BROWSER_OPTIMIZATIONS.md full library),
  // extend it with MUI finders. Otherwise create a standalone version.
  const target = window.__fcTest || {};
  Object.assign(target, {
    _version: '2.0.0',
    _highlights: target._highlights || [],
    _MUI: MUI,

    // ─── Finders (return element or null) ───

    findCard(title) {
      const headings = document.querySelectorAll(MUI.heading);
      for (const h of headings) {
        if (h.textContent.trim().toLowerCase().includes(title.toLowerCase())) {
          let el = h;
          for (let i = 0; i < 5; i++) {
            el = el.parentElement;
            if (!el) return null;
            if (el.matches && el.matches(MUI.card)) return el;
          }
          return h.parentElement?.parentElement || null;
        }
      }
      return null;
    },

    findButton(label) {
      const buttons = document.querySelectorAll(MUI.button);
      for (const b of buttons) {
        const text = b.textContent.trim();
        if (text.toLowerCase() === label.toLowerCase()) return b;
      }
      return document.querySelector(\`[aria-label="\${label}"]\`);
    },

    findActionButton(eventName) {
      const buttons = document.querySelectorAll(MUI.button);
      for (const b of buttons) {
        const text = b.textContent.trim();
        const readable = eventName.replace(/([A-Z])/g, ' $1').trim();
        if (text.toLowerCase().includes(readable.toLowerCase()) ||
            text.toLowerCase().includes(eventName.toLowerCase())) {
          return b;
        }
      }
      return null;
    },

    findTab(label) {
      const tabs = document.querySelectorAll(MUI.tab);
      for (const t of tabs) {
        if (t.textContent.trim().toLowerCase().includes(label.toLowerCase())) return t;
      }
      return null;
    },

    findNav(label) {
      const items = document.querySelectorAll(MUI.navItem);
      for (const item of items) {
        if (item.textContent.trim().toLowerCase().includes(label.toLowerCase())) return item;
      }
      return null;
    },

    findInput(label) {
      const labels = document.querySelectorAll('label');
      for (const lbl of labels) {
        if (lbl.textContent.trim().toLowerCase().includes(label.toLowerCase())) {
          const forId = lbl.getAttribute('for');
          if (forId) return document.getElementById(forId);
          return lbl.querySelector('input, textarea, select') ||
                 lbl.parentElement?.querySelector('input, textarea, select');
        }
      }
      return null;
    },

    findTableColumn(headerText) {
      const headers = document.querySelectorAll('th, [role="columnheader"]');
      for (const h of headers) {
        if (h.textContent.trim().toLowerCase().includes(headerText.toLowerCase())) {
          return { element: h, index: Array.from(h.parentElement.children).indexOf(h) };
        }
      }
      return null;
    },

    findTableRow(columnHeader, cellValue) {
      const col = this.findTableColumn(columnHeader);
      if (!col) return null;
      const rows = document.querySelectorAll('tbody tr, [role="row"]');
      for (const row of rows) {
        const cells = row.querySelectorAll('td, [role="gridcell"]');
        if (cells[col.index]?.textContent.trim().includes(cellValue)) return row;
      }
      return null;
    },

    // ─── Actions (find + interact) ───

    clickNav(label) {
      const el = this.findNav(label);
      if (el) { el.click(); return true; }
      return false;
    },

    clickButton(label) {
      const el = this.findButton(label);
      if (el) { el.click(); return true; }
      return false;
    },

    clickTab(label) {
      const el = this.findTab(label);
      if (el) { el.click(); return true; }
      return false;
    },

    clickTableRow(columnHeader, cellValue) {
      const row = this.findTableRow(columnHeader, cellValue);
      if (row) {
        const link = row.querySelector('a') || row;
        link.click();
        return true;
      }
      return false;
    },

    fillInput(label, value) {
      const el = this.findInput(label);
      if (!el) return false;
      const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
        window.HTMLInputElement.prototype, 'value'
      ).set;
      nativeInputValueSetter.call(el, value);
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
      return true;
    },

    navigateTo(path) {
      window.location.hash = path.startsWith('#') ? path : '#' + path;
      return true;
    },

    // ─── Visual feedback (click telemetry) ───

    highlight(selector, label, color) {
      color = color || '#ff0000';
      const el = typeof selector === 'string'
        ? document.querySelector(selector)
        : selector;
      if (!el) return false;

      const id = 'fc-hl-' + Date.now();
      el.style.outline = '3px solid ' + color;
      el.style.outlineOffset = '2px';
      el.style.transition = 'outline 0.3s ease';
      el.setAttribute('data-fc-highlight', id);

      if (label) {
        const badge = document.createElement('div');
        badge.id = id;
        badge.textContent = label;
        Object.assign(badge.style, {
          position: 'absolute', zIndex: '99999',
          background: color, color: '#fff', fontSize: '11px',
          padding: '2px 6px', borderRadius: '3px', fontWeight: '700',
          pointerEvents: 'none', whiteSpace: 'nowrap',
        });
        const rect = el.getBoundingClientRect();
        badge.style.top = (window.scrollY + rect.top - 18) + 'px';
        badge.style.left = (window.scrollX + rect.left) + 'px';
        document.body.appendChild(badge);
      }

      this._highlights.push(id);
      return true;
    },

    clearHighlights() {
      document.querySelectorAll('[data-fc-highlight]').forEach(el => {
        el.style.outline = '';
        el.style.outlineOffset = '';
        el.removeAttribute('data-fc-highlight');
      });
      this._highlights.forEach(id => {
        const badge = document.getElementById(id);
        if (badge) badge.remove();
      });
      this._highlights = [];
    },

    // ─── Batch operations (biggest token saver) ───

    batch(operations) {
      const results = [];
      for (const op of operations) {
        try {
          const fn = this[op.action];
          if (typeof fn === 'function') {
            results.push({ action: op.action, success: fn.apply(this, op.args || []) });
          } else {
            results.push({ action: op.action, success: false, error: 'unknown action' });
          }
        } catch (e) {
          results.push({ action: op.action, success: false, error: e.message });
        }
      }
      return results;
    },

    // ─── Page state snapshot (lightweight alternative to full DOM) ───

    pageState() {
      const headings = Array.from(document.querySelectorAll(MUI.heading))
        .map(h => h.textContent.trim()).filter(Boolean);
      const buttons = Array.from(document.querySelectorAll(MUI.button))
        .map(b => b.textContent.trim()).filter(Boolean);
      const tabs = Array.from(document.querySelectorAll(MUI.tab))
        .map(t => ({ label: t.textContent.trim(), active: t.classList.contains('Mui-selected') }));
      const tables = document.querySelectorAll(MUI.table).length;
      const dialogs = document.querySelectorAll(MUI.dialog).length;
      const url = window.location.hash || window.location.pathname;

      return { url, headings, buttons, tabs, tables, dialogs };
    },
  });

  window.__fcTest = target;
  window.__fc = target; // backward-compat alias

  console.log('[FC Helper] v' + target._version + ' injected — ' +
    Object.keys(target).filter(k => !k.startsWith('_')).length + ' methods available');
})();`;
}

// ─── Main ───────────────────────────────────────────────────────────────────

function main() {
  const opts = parseArgs();

  // Helper-only mode: print JS to stdout and exit
  if (opts.helper) {
    console.log(generateBrowserHelper());
    return;
  }

  // Determine manifest files
  let files;
  if (opts.manifest) {
    const absPath = resolve(opts.manifest);
    if (!existsSync(absPath)) {
      console.error(`Manifest file not found: ${absPath}`);
      process.exit(1);
    }
    files = [absPath];
  } else if (opts.profile) {
    files = findManifestFiles(opts.profile, opts.verbose);
  } else {
    // Try FLUENT_PROFILE env var
    const envProfile = process.env.FLUENT_PROFILE;
    if (envProfile) {
      opts.profile = envProfile;
      files = findManifestFiles(envProfile, opts.verbose);
    } else {
      console.error('Error: --profile <PROFILE> is required (or set FLUENT_PROFILE env var).');
      console.error('Run with --help for usage.');
      process.exit(1);
    }
  }

  if (!files || files.length === 0) {
    console.error('No manifest files found. Ensure manifests exist in:');
    console.error(`  accounts/${opts.profile}/manifests/`);
    console.error(`  accounts/${opts.profile}/manifests/backups/`);
    console.error(`  accounts/${opts.profile}/settings/`);
    console.error('Or the Fluent CLI is available for fetching.');
    process.exit(1);
  }

  if (opts.verbose) console.log(`Processing ${files.length} manifest file(s)...\n`);

  // Parse all manifests
  const cache = {
    _generated: new Date().toISOString(),
    _version: '2.0.0',
    _purpose: 'Pre-analyzed manifest cache for LLM token savings during UI testing',
    _helperSize: '~2KB (inject once via evaluate_script)',
    manifests: {},
    summary: { totalPages: 0, totalButtons: 0, totalCards: 0, totalLists: 0, totalTabs: 0 },
  };

  for (const f of files) {
    if (opts.verbose) console.log(`  Parsing ${basename(f)}...`);
    try {
      const result = parseManifest(f);
      if (!result) {
        if (opts.verbose) console.log(`    Skipped (not a valid manifest)`);
        continue;
      }
      cache.manifests[result.name] = result;
      cache.summary.totalPages += result.pages.length;
      cache.summary.totalButtons += result.buttons.length;
      cache.summary.totalCards += result.cards.length;
      cache.summary.totalLists += result.lists.length;
      cache.summary.totalTabs += result.tabs.length;
    } catch (e) {
      console.error(`  Error parsing ${basename(f)}: ${e.message}`);
    }
  }

  if (Object.keys(cache.manifests).length === 0) {
    console.error('No valid manifests found in the provided files.');
    process.exit(1);
  }

  // Embed the browser helper
  cache._browserHelper = generateBrowserHelper();

  // Token savings estimate
  const manifestTokens = files.reduce((sum, f) => {
    try {
      const size = readFileSync(f, 'utf8').length;
      return sum + Math.round(size / 4); // ~4 chars per token
    } catch { return sum; }
  }, 0);

  const cacheStr = JSON.stringify(cache, null, 2);
  const cacheTokens = Math.round(cacheStr.length / 4);

  cache._tokenSavings = {
    rawManifestTokens: manifestTokens,
    cacheTokens,
    reductionPercent: manifestTokens > 0 ? Math.round((1 - cacheTokens / manifestTokens) * 100) : 0,
    perSnapshotSaving: '~30K tokens (DOM snapshot avoided when using selector cache)',
    helperInjectionCost: '~500 tokens (one-time)',
    perInteractionCost: '~50 tokens (vs ~300 for crafted JS)',
  };

  // Build slim cache (both grouped and flat formats)
  const profile = opts.profile || 'unknown';
  const { grouped: slimGrouped, flat: slimFlat } = buildSlimCache(cache.manifests, profile);

  // The slim output combines the app-grouped format with backward-compatible flat arrays
  const slim = {
    ...slimGrouped,
    // Flat arrays for backward compatibility
    navigation: slimFlat.navigation,
    actionButtons: slimFlat.actionButtons,
    cardTitles: slimFlat.cardTitles,
    listPages: slimFlat.listPages,
  };

  const slimStr = JSON.stringify(slim, null, 2);
  const slimTokens = Math.round(slimStr.length / 4);

  cache._tokenSavings.slimCacheTokens = slimTokens;
  cache._tokenSavings.slimReductionPercent = manifestTokens > 0
    ? Math.round((1 - slimTokens / manifestTokens) * 100)
    : 0;

  // Determine output paths
  const outPath = opts.output
    ? resolve(opts.output)
    : (opts.profile
        ? resolve(ACCOUNTS_DIR, opts.profile, 'manifests', 'selector-cache.json')
        : resolve('selector-cache.json'));

  const outDir = dirname(outPath);
  mkdirSync(outDir, { recursive: true });

  const slimPath = outPath.replace('.json', '-slim.json');
  const helperPath = outPath.replace('.json', '-helper.js');

  // Write output files
  writeFileSync(outPath, JSON.stringify(cache, null, 2), 'utf8');
  writeFileSync(slimPath, JSON.stringify(slim, null, 2), 'utf8');
  writeFileSync(helperPath, generateBrowserHelper(), 'utf8');

  // Print summary
  const appNames = [...new Set(Object.values(cache.manifests).map(m => m.appName))];
  const navCount = slim.navigation.length;
  const pageCount = Object.values(slim.apps || {}).reduce(
    (sum, app) => sum + Object.keys(app.pages || {}).length, 0
  );

  console.log('\n=== Manifest Selector Cache ===\n');
  console.log(`Profile:    ${profile}`);
  console.log(`Apps:       ${appNames.join(', ')}`);
  console.log(`Manifests:  ${Object.keys(cache.manifests).length}`);
  console.log('');
  console.log(`Navigation: ${navCount} sidebar entries`);
  console.log(`Pages:      ${cache.summary.totalPages} (${pageCount} with extracted data)`);
  console.log(`Buttons:    ${cache.summary.totalButtons} user action buttons`);
  console.log(`Cards:      ${cache.summary.totalCards} card components`);
  console.log(`Lists:      ${cache.summary.totalLists} list/table components`);
  console.log(`Tabs:       ${cache.summary.totalTabs} tab groups`);
  console.log('');
  console.log(`Token savings:`);
  console.log(`  Raw manifests: ~${manifestTokens.toLocaleString()} tokens`);
  console.log(`  Full cache:    ~${cacheTokens.toLocaleString()} tokens (${cache._tokenSavings.reductionPercent}% reduction)`);
  console.log(`  Slim cache:    ~${slimTokens.toLocaleString()} tokens (${cache._tokenSavings.slimReductionPercent}% reduction)`);
  console.log(`  Per snapshot:  ~30K tokens saved (DOM snapshot skipped)`);
  console.log('');
  console.log(`Output:`);
  console.log(`  ${outPath}`);
  console.log(`  ${slimPath}  <-- feed to LLM`);
  console.log(`  ${helperPath}`);
}

main();
