#!/usr/bin/env node
/**
 * manifest-diff.mjs
 *
 * Compares Fluent Commerce Mystique manifest settings (deployed in account)
 * against CDN baselines. Identifies which fragments exist, which are customized,
 * and produces a structural diff report — all locally to save LLM tokens.
 *
 * Zero npm dependencies — uses only Node.js built-ins.
 *
 * Usage:
 *   node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE>
 *   node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE> --app oms
 *   node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE> --deep
 *   node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE> --fetch   # force re-download from CDN + account
 *   node fluent-ai-skills/tools/manifest-diff.mjs --help
 *
 * Outputs:
 *   accounts/<PROFILE>/analysis/manifests/<app>-diff-report.json  — full report per app
 *   Console summary — fragment inventory, customization status, diff highlights
 */

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';
import https from 'node:https';

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

const CDN_BASE = 'https://fluent.apps.fluentcommerce.com/_plugins';

// ─── OOTB Component Registry (from fluent-mystique-validate/validator.mjs) ──
const OOTB_COMPONENTS = new Set([
  // Layout & Container
  'fc.page','fc.page.wizard','fc.page.wizard.summary','fc.page.section',
  'fc.page.section.column','fc.page.section.header','fc.page.refresh',
  'fc.conditional','fc.repeater','fc.provider.graphql','fc.workflow.provider',
  'fc.tabs','fc.tabs.card','fc.tab.content','fc.accordion','fc.filterPanel',
  // Content & Display
  'fc.list','fc.card.attribute','fc.card.image','fc.card.product','fc.card.multi',
  'fc.card.attributes.grid','fc.card.map.point','fc.tile.metric',
  'fc.attribute.json','fc.attribute.column','fc.attribute.locationId',
  'fc.attribute.retailerId','fc.mystique.collapsible.text',
  'fc.mystique.collapsible.attributes','fc.mystique.link','fc.progress.circular',
  'fc.dashboard.threshold','fc.stepper','fc.json.viewer','fc.json.editor',
  'fc.quantity.list','fc.scanner.barcode','fc.scanner.camera',
  'fc.scanner.barcodeFilter','fc.settingForm','fc.action.inline',
  'fc.mutation.inline','fc.list.customAction',
  // Charts
  'fc.chart.area','fc.chart.area.wrapper.feed','fc.chart.bar',
  'fc.chart.bar.wrapper.source','fc.chart.line','fc.chart.gauge',
  'fc.chart.gauge.wrapper.threshold',
  // List wrappers
  'fc.list.wrapper.feed','fc.list.wrapper.source','fc.list.wrapper.bppmetrics',
  // Actions & Buttons
  'fc.button.print','fc.button.print.pick','fc.button.print.inline',
  'fc.button.print.inline.compatibility','fc.button.print.download',
  'fc.modal.button','fc.modal.button.addItem','fc.drawer.button',
  'fc.buttons.add.reject','fc.button.bar',
  'fc.sourcing.profile.drawer.button','fc.sourcing.profile.modal.button',
  'fc.sourcing.strategy.modal.button',
  // Fields
  'select','retailer','fc.field.daterange','daterange','fc.field.intrange',
  'intrange','fc.field.filterComplex','fc.field.multistring',
  'fc.page.filter.select','fc.attribute.jsoneditor','AttributeInput.list',
  'ExtendedAddressInput','fc.barcode.scanner','fc.action.field.wavepick',
  'fc.action.field.fulfilmentpack','FULFILMENT_PACK',
  'fc.action.field.multiparcel','MULTI_PARCEL_FIELD',
  'fc.action.field.returnitems','RETURN_ITEMS','FULFILMENT_IDS',
  'fc.daterange.wrapper.forwarder',
  'fc.field.select','fc.field.text','fc.field.textarea','fc.field.number',
  'fc.field.checkbox','fc.field.radio','fc.field.toggle','fc.field.switch',
  // OMS
  'fc.events.search','fc.event.detail','fc.activity.entity',
  'fc.order.itemDetails','fc.order.shipmentDetails','fc.return.rowExpansion',
  'fc.reports.ipuipc',
  // Analytics
  'fc.analytics.viz',
]);

// Known OMS fragments (platform baseline)
const OMS_FRAGMENTS = [
  'dashboard', 'ordermanagement', 'mixedbaskets', 'availability',
  'products', 'inventory', 'unifiedinventory', 'stores', 'insights', 'admin',
];

// Known Store fragments (common ones)
const STORE_FRAGMENTS = [
  'dashboard', 'fulfilment', 'returns', 'inventory', 'waves',
];

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

function parseArgs() {
  const args = process.argv.slice(2);
  const opts = { profile: null, app: null, deep: false, fetch: false, verbose: false, components: false };

  for (let i = 0; i < args.length; i++) {
    switch (args[i]) {
      case '--profile': case '-p': opts.profile = args[++i]; break;
      case '--app': case '-a': opts.app = args[++i]; break;
      case '--deep': opts.deep = true; break;
      case '--fetch': case '--refresh-cdn': opts.fetch = true; break;
      case '--verbose': case '-v': opts.verbose = true; break;
      case '--components': case '-c': opts.components = true; break;
      case '--help': case '-h': printHelp(); process.exit(0);
      default:
        if (!args[i].startsWith('-')) opts.profile = args[i];
        else { console.error(`Unknown option: ${args[i]}`); process.exit(1); }
    }
  }
  if (!opts.profile) {
    opts.profile = process.env.FLUENT_PROFILE;
    if (!opts.profile) {
      console.error('Error: --profile <PROFILE> is required (or set FLUENT_PROFILE).');
      process.exit(1);
    }
  }
  return opts;
}

function printHelp() {
  console.log(`
Manifest CDN vs Setting Comparison Tool

Compares deployed Mystique manifest settings against CDN baselines.
Shows which fragments are customized, added routes, modified components, etc.

Usage:
  node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE>
  node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE> --app oms
  node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE> --deep --verbose
  node fluent-ai-skills/tools/manifest-diff.mjs --profile <PROFILE> --refresh-cdn   # force re-download baselines

Options:
  --profile, -p <NAME>  Profile name (required)
  --app, -a <APP>       Limit to one app: oms, store (default: all discovered)
  --deep                Include per-route component-level diffs
  --fetch, --refresh-cdn  Force re-download CDN baselines
  --components, -c      Component inventory: classify OOTB vs custom
  --verbose, -v         Show detailed progress
  --help, -h            Show this help

Output:
  accounts/<PROFILE>/analysis/manifests/<app>-diff-report.json
`);
}

// ─── HTTP Fetch (Node.js built-in) ──────────────────────────────────────────

function httpGet(url) {
  return new Promise((resolve, reject) => {
    const req = https.get(url, { timeout: 15000 }, (res) => {
      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        httpGet(res.headers.location).then(resolve, reject);
        return;
      }
      if (res.statusCode !== 200) {
        res.resume();
        reject(new Error(`HTTP ${res.statusCode} for ${url}`));
        return;
      }
      const chunks = [];
      res.on('data', (c) => chunks.push(c));
      res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
      res.on('error', reject);
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error(`Timeout: ${url}`)); });
  });
}

// ─── CDN Baseline Fetching ──────────────────────────────────────────────────

async function fetchCdnBaseline(appName, settingName, cacheDir, force) {
  const cacheFile = join(cacheDir, `${settingName}.json`);
  if (!force && existsSync(cacheFile)) {
    try {
      return JSON.parse(readFileSync(cacheFile, 'utf8'));
    } catch { /* fall through to re-fetch */ }
  }

  const url = `${CDN_BASE}/${appName}/manifests/${settingName}.json`;
  try {
    const body = await httpGet(url);
    const parsed = JSON.parse(body);
    mkdirSync(cacheDir, { recursive: true });
    writeFileSync(cacheFile, JSON.stringify(parsed, null, 2), 'utf8');
    return parsed;
  } catch (e) {
    return null; // CDN doesn't have this one
  }
}

// ─── Account Setting Loading ────────────────────────────────────────────────

function loadAccountSettings(profile) {
  const settings = {};
  const dirs = [
    join(ACCOUNTS_DIR, profile, 'manifests'),
    join(ACCOUNTS_DIR, profile, 'manifests', 'settings'),
    join(ACCOUNTS_DIR, profile, 'settings'),
  ];

  for (const dir of dirs) {
    if (!existsSync(dir)) continue;
    for (const f of readdirSync(dir)) {
      if (!f.endsWith('.json')) continue;
      if (!f.includes('fc.mystique.manifest')) continue;
      // Skip caches and backups
      if (f.startsWith('selector-cache') || f.startsWith('verify-')) continue;
      const fullPath = join(dir, f);
      try {
        if (!statSync(fullPath).isFile()) continue;
      } catch { continue; }

      const name = f.replace('.json', '')
        // Strip date suffixes from backup filenames
        .replace(/_\d{4}-\d{2}-\d{2}$/, '');

      if (settings[name]) continue; // first-found wins (manifests/ before settings/)

      try {
        const raw = JSON.parse(readFileSync(fullPath, 'utf8'));
        settings[name] = extractManifestBody(raw);
      } catch { /* skip unparseable */ }
    }
  }

  return settings;
}

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;
  }
  if (typeof manifest === 'string') {
    try { manifest = JSON.parse(manifest); } catch { return null; }
  }
  if (!manifest || (!manifest.routes && !manifest.manifestVersion)) return null;
  return manifest;
}

function deriveAppName(settingName) {
  const prefix = 'fc.mystique.manifest.';
  if (!settingName.startsWith(prefix)) return 'unknown';
  const rest = settingName.slice(prefix.length);
  const dotIdx = rest.indexOf('.');
  return dotIdx >= 0 ? rest.slice(0, dotIdx) : rest;
}

function deriveFragmentName(settingName) {
  // fc.mystique.manifest.oms.fragment.ordermanagement -> ordermanagement
  const match = settingName.match(/\.fragment\.(.+)$/);
  return match ? match[1] : null;
}

// ─── Structural Comparison ──────────────────────────────────────────────────

function extractRoutes(manifest) {
  const routes = [];
  function walk(items, parentPath) {
    if (!Array.isArray(items)) return;
    for (const item of items) {
      const route = {
        type: item.type || 'unknown',
        path: item.path || null,
        component: item.component || null,
        navLabel: item.nav?.label || null,
        hasQuery: !!(item.data?.query),
        queryFields: item.data?.query ? countQueryFields(item.data.query) : 0,
        descendantCount: countDescendants(item.descendants),
        roles: item.roles || null,
        userActions: extractUserActions(item),
      };
      routes.push(route);

      // Recurse
      if (item.pages) walk(item.pages, item.path || parentPath);
      if (item.routes) walk(item.routes, item.path || parentPath);
      if (item.type === 'section' && item.pages) walk(item.pages, item.path || parentPath);
    }
  }
  walk(manifest.routes || [], '');
  return routes;
}

function countDescendants(descendants) {
  if (!Array.isArray(descendants)) return 0;
  let count = descendants.length;
  for (const d of descendants) {
    count += countDescendants(d.descendants);
  }
  return count;
}

function countQueryFields(query) {
  if (!query) return 0;
  // Rough count of field names in a GraphQL query string
  if (typeof query === 'string') {
    return (query.match(/\w+\s*[:{(]/g) || []).length;
  }
  return 0;
}

function extractUserActions(route) {
  const actions = [];
  const primary = route.props?.actions?.primary || [];
  const secondary = route.props?.actions?.secondary || [];
  for (const a of [...primary, ...secondary]) {
    if (a.type === 'userAction') {
      actions.push(a.name);
    }
  }
  return actions;
}

function extractPlugins(manifest) {
  return (manifest.plugins || []).map(p => ({
    name: p.name || null,
    type: p.type || null,
    src: p.src || null,
  }));
}

// ─── Component Inventory ─────────────────────────────────────────────────────

function extractAllComponents(manifest) {
  const components = new Map(); // alias -> { count, locations[] }
  function walk(node, path) {
    if (!node) return;
    if (node.component) {
      const alias = node.component;
      if (!components.has(alias)) components.set(alias, { count: 0, locations: [] });
      const entry = components.get(alias);
      entry.count++;
      if (entry.locations.length < 3) entry.locations.push(path); // keep max 3 sample paths
    }
    // Recurse into all child arrays
    for (const key of ['routes', 'pages', 'descendants']) {
      if (Array.isArray(node[key])) {
        for (let i = 0; i < node[key].length; i++) {
          walk(node[key][i], path + '/' + (node[key][i].path || node[key][i].component || key + '[' + i + ']'));
        }
      }
    }
    // User action overrides can reference custom field components
    const actions = node.props?.actions;
    if (actions) {
      for (const group of ['primary', 'secondary']) {
        if (Array.isArray(actions[group])) {
          for (const action of actions[group]) {
            if (action.overrides) {
              for (const [fieldName, fieldDef] of Object.entries(action.overrides)) {
                if (fieldDef.component) {
                  const alias = fieldDef.component;
                  if (!components.has(alias)) components.set(alias, { count: 0, locations: [] });
                  const entry = components.get(alias);
                  entry.count++;
                  if (entry.locations.length < 3) entry.locations.push(path + '/action.' + fieldName);
                }
              }
            }
          }
        }
      }
    }
  }
  walk({ routes: manifest.routes || [] }, '');
  return components;
}

// Legacy alias → modern alias map
const LEGACY_ALIASES = {
  'shared.components.material.DynamicPage': 'fc.page',
  'shared.components.material.MaterialTabsSet': 'fc.tabs',
  'shared.components.material.SubLayoutContent': 'fc.tab.content',
  'shared.components.material.DynamicList': 'fc.list',
  'shared.components.material.DynamicCard': 'fc.card.attribute',
};

function classifyComponents(componentMap) {
  const ootb = [];
  const custom = [];
  for (const [alias, info] of componentMap) {
    const entry = { alias, count: info.count, locations: info.locations };
    const resolved = LEGACY_ALIASES[alias] || alias;
    if (OOTB_COMPONENTS.has(resolved)) {
      if (resolved !== alias) entry.alias = alias + ' (legacy: ' + resolved + ')';
      ootb.push(entry);
    } else {
      custom.push(entry);
    }
  }
  ootb.sort((a, b) => b.count - a.count);
  custom.sort((a, b) => b.count - a.count);
  return { ootb, custom };
}

function extractReferences(manifest) {
  const refs = [];
  function walk(routes) {
    if (!Array.isArray(routes)) return;
    for (const r of routes) {
      if (r.type === 'reference' && r.settingName) {
        refs.push(r.settingName);
      }
      if (r.routes) walk(r.routes);
      if (r.pages) walk(r.pages);
    }
  }
  walk(manifest.routes || []);
  return refs;
}

function compareManifests(baseline, deployed, deep) {
  if (!baseline && !deployed) return null;

  const result = {
    status: 'UNKNOWN',
    baselineRouteCount: 0,
    deployedRouteCount: 0,
    addedRoutes: [],
    removedRoutes: [],
    modifiedRoutes: [],
    unchangedRoutes: [],
    pluginChanges: { added: [], removed: [], modified: [] },
    referenceChanges: { added: [], removed: [] },
    summary: '',
  };

  if (!baseline && deployed) {
    const routes = extractRoutes(deployed);
    result.status = 'ACCOUNT_ONLY';
    result.deployedRouteCount = routes.length;
    result.addedRoutes = routes.map(r => r.path || r.navLabel || r.type);
    result.summary = `Custom setting with ${routes.length} routes (no CDN baseline exists)`;
    return result;
  }

  if (baseline && !deployed) {
    const routes = extractRoutes(baseline);
    result.status = 'BASELINE_ONLY';
    result.baselineRouteCount = routes.length;
    result.summary = `Using CDN baseline (${routes.length} routes, no account override)`;
    return result;
  }

  // Both exist — structural comparison
  const baseRoutes = extractRoutes(baseline);
  const deplRoutes = extractRoutes(deployed);

  result.baselineRouteCount = baseRoutes.length;
  result.deployedRouteCount = deplRoutes.length;

  // Compare by path (for pages) or navLabel (for sections)
  const baseByKey = new Map();
  for (const r of baseRoutes) {
    const key = r.path || r.navLabel || `${r.type}:${baseRoutes.indexOf(r)}`;
    baseByKey.set(key, r);
  }

  const deplByKey = new Map();
  for (const r of deplRoutes) {
    const key = r.path || r.navLabel || `${r.type}:${deplRoutes.indexOf(r)}`;
    deplByKey.set(key, r);
  }

  for (const [key, deplRoute] of deplByKey) {
    if (!baseByKey.has(key)) {
      result.addedRoutes.push(deep ? { key, ...deplRoute } : key);
    } else {
      const baseRoute = baseByKey.get(key);
      const diffs = diffRoute(baseRoute, deplRoute);
      if (diffs.length > 0) {
        result.modifiedRoutes.push(deep ? { key, diffs } : { key, changes: diffs.length });
      } else {
        result.unchangedRoutes.push(key);
      }
    }
  }

  for (const [key] of baseByKey) {
    if (!deplByKey.has(key)) {
      result.removedRoutes.push(key);
    }
  }

  // Plugin comparison
  const basePlugins = extractPlugins(baseline);
  const deplPlugins = extractPlugins(deployed);
  const basePluginNames = new Set(basePlugins.map(p => p.name || p.src));
  const deplPluginNames = new Set(deplPlugins.map(p => p.name || p.src));

  for (const p of deplPlugins) {
    const id = p.name || p.src;
    if (!basePluginNames.has(id)) result.pluginChanges.added.push(p);
  }
  for (const p of basePlugins) {
    const id = p.name || p.src;
    if (!deplPluginNames.has(id)) result.pluginChanges.removed.push(p);
  }

  // Reference comparison (root manifests reference fragments)
  const baseRefs = extractReferences(baseline);
  const deplRefs = extractReferences(deployed);
  const baseRefSet = new Set(baseRefs);
  const deplRefSet = new Set(deplRefs);

  for (const r of deplRefs) {
    if (!baseRefSet.has(r)) result.referenceChanges.added.push(r);
  }
  for (const r of baseRefs) {
    if (!deplRefSet.has(r)) result.referenceChanges.removed.push(r);
  }

  // Determine status
  const totalChanges = result.addedRoutes.length + result.removedRoutes.length +
    result.modifiedRoutes.length + result.pluginChanges.added.length +
    result.pluginChanges.removed.length + result.referenceChanges.added.length +
    result.referenceChanges.removed.length;

  if (totalChanges === 0) {
    result.status = 'IDENTICAL';
    result.summary = `Deployed setting matches CDN baseline (${baseRoutes.length} routes)`;
  } else {
    result.status = 'CUSTOMIZED';
    const parts = [];
    if (result.addedRoutes.length) parts.push(`+${result.addedRoutes.length} routes`);
    if (result.removedRoutes.length) parts.push(`-${result.removedRoutes.length} routes`);
    if (result.modifiedRoutes.length) parts.push(`~${result.modifiedRoutes.length} modified`);
    if (result.pluginChanges.added.length) parts.push(`+${result.pluginChanges.added.length} plugins`);
    if (result.pluginChanges.removed.length) parts.push(`-${result.pluginChanges.removed.length} plugins`);
    result.summary = parts.join(', ');
  }

  return result;
}

function diffRoute(base, deployed) {
  const diffs = [];
  if (base.component !== deployed.component) {
    diffs.push({ field: 'component', from: base.component, to: deployed.component });
  }
  if (base.descendantCount !== deployed.descendantCount) {
    diffs.push({ field: 'descendantCount', from: base.descendantCount, to: deployed.descendantCount });
  }
  if (base.queryFields !== deployed.queryFields) {
    diffs.push({ field: 'queryFields', from: base.queryFields, to: deployed.queryFields });
  }
  if (JSON.stringify(base.roles) !== JSON.stringify(deployed.roles)) {
    diffs.push({ field: 'roles', from: base.roles, to: deployed.roles });
  }
  if (JSON.stringify(base.userActions) !== JSON.stringify(deployed.userActions)) {
    diffs.push({ field: 'userActions', from: base.userActions, to: deployed.userActions });
  }
  if (base.navLabel !== deployed.navLabel) {
    diffs.push({ field: 'navLabel', from: base.navLabel, to: deployed.navLabel });
  }
  return diffs;
}

// ─── Discover Apps from Account Settings ────────────────────────────────────

function discoverApps(accountSettings) {
  const apps = new Map(); // appName -> { root: settingName|null, fragments: Set }

  for (const settingName of Object.keys(accountSettings)) {
    const appName = deriveAppName(settingName);
    if (!apps.has(appName)) apps.set(appName, { root: null, fragments: new Set() });

    const fragName = deriveFragmentName(settingName);
    if (fragName) {
      apps.get(appName).fragments.add(fragName);
    } else {
      apps.get(appName).root = settingName;
    }
  }

  return apps;
}

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

async function main() {
  const opts = parseArgs();
  const profile = opts.profile;
  const cdnCacheDir = join(ACCOUNTS_DIR, profile, 'manifests', 'cdn-baselines');
  const outDir = join(ACCOUNTS_DIR, profile, 'analysis', 'manifests');

  console.log(`\n=== Manifest CDN vs Setting Comparison ===`);
  console.log(`Profile: ${profile}\n`);

  // Step 1: Load account settings
  if (opts.verbose) console.log('Loading account manifest settings...');
  const accountSettings = loadAccountSettings(profile);
  const settingCount = Object.keys(accountSettings).length;

  if (settingCount === 0) {
    console.log('No manifest settings found locally.');
    console.log(`Looked in:`);
    console.log(`  accounts/${profile}/manifests/`);
    console.log(`  accounts/${profile}/manifests/settings/`);
    console.log(`  accounts/${profile}/settings/`);
    console.log('');
    console.log('To populate, either:');
    console.log('  1. Use MCP setting.get with outputFile to download settings');
    console.log('  2. Use CLI: fluent setting get -p <PROFILE> -n "fc.mystique.manifest.<name>" > file.json');
    console.log('  3. Use --fetch flag once settings are available via CLI');
    process.exit(1);
  }

  if (opts.verbose) console.log(`Found ${settingCount} manifest setting(s)\n`);

  // Step 2: Discover apps and fragments
  const discoveredApps = discoverApps(accountSettings);

  // Filter by --app if specified
  if (opts.app) {
    for (const [name] of discoveredApps) {
      if (name !== opts.app) discoveredApps.delete(name);
    }
  }

  // Step 3: Build known fragment lists per app
  const knownFragments = {
    oms: OMS_FRAGMENTS,
    store: STORE_FRAGMENTS,
  };

  // Step 4: Fetch CDN baselines and compare
  const report = {
    _generated: new Date().toISOString(),
    _profile: profile,
    apps: {},
    totals: {
      totalSettings: settingCount,
      totalFragments: 0,
      customizedFragments: 0,
      baselineOnlyFragments: 0,
      identicalFragments: 0,
      accountOnlyFragments: 0,
      totalPluginsAdded: 0,
      totalRoutesAdded: 0,
      totalRoutesModified: 0,
      totalRoutesRemoved: 0,
    },
  };

  for (const [appName, appInfo] of discoveredApps) {
    console.log(`── ${appName.toUpperCase()} App ──`);

    const appReport = {
      rootSetting: appInfo.root,
      rootComparison: null,
      knownFragments: knownFragments[appName] || [],
      deployedFragments: [...appInfo.fragments],
      fragmentComparisons: {},
      undeployedFragments: [], // known but no setting override
    };

    // Compare root manifest
    if (appInfo.root) {
      const rootSettingName = appInfo.root;
      if (opts.verbose) console.log(`  Comparing root: ${rootSettingName}`);
      const baseline = await fetchCdnBaseline(appName, rootSettingName, cdnCacheDir, opts.fetch);
      const deployed = accountSettings[rootSettingName];
      appReport.rootComparison = compareManifests(baseline, deployed, opts.deep);

      if (appReport.rootComparison) {
        const rc = appReport.rootComparison;
        console.log(`  Root manifest: ${rc.status}`);
        if (rc.summary) console.log(`    ${rc.summary}`);
        if (rc.pluginChanges?.added?.length) {
          console.log(`    Custom plugins:`);
          for (const p of rc.pluginChanges.added) {
            console.log(`      + ${p.name || p.src}`);
          }
          report.totals.totalPluginsAdded += rc.pluginChanges.added.length;
        }
      }
    }

    // Compare each fragment
    const allFragments = new Set([
      ...(knownFragments[appName] || []),
      ...appInfo.fragments,
    ]);

    report.totals.totalFragments += allFragments.size;

    for (const fragName of [...allFragments].sort()) {
      const settingName = `fc.mystique.manifest.${appName}.fragment.${fragName}`;
      if (opts.verbose) console.log(`  Comparing fragment: ${fragName}`);

      const baseline = await fetchCdnBaseline(appName, settingName, cdnCacheDir, opts.fetch);
      const deployed = accountSettings[settingName] || null;
      const comparison = compareManifests(baseline, deployed, opts.deep);

      if (comparison) {
        appReport.fragmentComparisons[fragName] = comparison;

        switch (comparison.status) {
          case 'CUSTOMIZED':
            report.totals.customizedFragments++;
            report.totals.totalRoutesAdded += comparison.addedRoutes.length;
            report.totals.totalRoutesModified += comparison.modifiedRoutes.length;
            report.totals.totalRoutesRemoved += comparison.removedRoutes.length;
            break;
          case 'IDENTICAL':
            report.totals.identicalFragments++;
            break;
          case 'BASELINE_ONLY':
            report.totals.baselineOnlyFragments++;
            appReport.undeployedFragments.push(fragName);
            break;
          case 'ACCOUNT_ONLY':
            report.totals.accountOnlyFragments++;
            break;
        }
      } else if (!baseline && !deployed) {
        // Fragment doesn't exist anywhere — skip
        continue;
      }
    }

    report.apps[appName] = appReport;

    // Print fragment summary table
    console.log('');
    console.log('  Fragment Inventory:');
    console.log('  ' + '-'.repeat(72));
    console.log(`  ${'Fragment'.padEnd(28)} ${'Status'.padEnd(16)} ${'Details'.padEnd(28)}`);
    console.log('  ' + '-'.repeat(72));

    for (const [fragName, comp] of Object.entries(appReport.fragmentComparisons).sort(([a], [b]) => a.localeCompare(b))) {
      const statusIcon = {
        CUSTOMIZED: '*',
        IDENTICAL: '=',
        BASELINE_ONLY: '-',
        ACCOUNT_ONLY: '+',
      }[comp.status] || '?';

      console.log(`  ${statusIcon} ${fragName.padEnd(26)} ${comp.status.padEnd(16)} ${comp.summary.slice(0, 28)}`);
    }

    if (appReport.undeployedFragments.length > 0) {
      for (const frag of appReport.undeployedFragments) {
        if (!appReport.fragmentComparisons[frag]) {
          console.log(`  - ${frag.padEnd(26)} ${'BASELINE_ONLY'.padEnd(16)} Using CDN default`);
        }
      }
    }

    console.log('  ' + '-'.repeat(72));
    console.log('  Legend: * customized  = identical  - baseline only  + account only');
    console.log('');

    // Component inventory (if --components flag)
    if (opts.components) {
      const allComponents = new Map();
      // Scan all deployed settings for this app
      for (const settingName of Object.keys(accountSettings)) {
        if (deriveAppName(settingName) !== appName) continue;
        const manifest = accountSettings[settingName];
        if (!manifest) continue;
        const comps = extractAllComponents(manifest);
        for (const [alias, info] of comps) {
          if (!allComponents.has(alias)) allComponents.set(alias, { count: 0, locations: [] });
          const entry = allComponents.get(alias);
          entry.count += info.count;
          for (const loc of info.locations) {
            if (entry.locations.length < 5) entry.locations.push(settingName.split('.fragment.')[1] || settingName);
          }
        }
      }

      const classified = classifyComponents(allComponents);
      appReport.components = classified;

      console.log('  Component Inventory:');
      console.log('  ' + '-'.repeat(72));
      console.log(`  ${'Component'.padEnd(40)} ${'Type'.padEnd(10)} ${'Uses'.padEnd(6)} ${'Found In'}`);
      console.log('  ' + '-'.repeat(72));

      // Show custom components first (most interesting)
      for (const c of classified.custom) {
        const locs = [...new Set(c.locations)].slice(0, 3).join(', ');
        console.log(`  ! ${c.alias.padEnd(38)} ${'CUSTOM'.padEnd(10)} ${String(c.count).padEnd(6)} ${locs}`);
      }
      // Then OOTB
      for (const c of classified.ootb) {
        const locs = [...new Set(c.locations)].slice(0, 3).join(', ');
        console.log(`    ${c.alias.padEnd(38)} ${'OOTB'.padEnd(10)} ${String(c.count).padEnd(6)} ${locs}`);
      }
      console.log('  ' + '-'.repeat(72));
      console.log(`  Total: ${allComponents.size} unique components (${classified.ootb.length} OOTB, ${classified.custom.length} custom)`);
      console.log('  Legend: ! = custom component (from SDK plugin)');
      console.log('');

      // Accumulate totals
      report.totals.totalOotbComponents = (report.totals.totalOotbComponents || 0) + classified.ootb.length;
      report.totals.totalCustomComponents = (report.totals.totalCustomComponents || 0) + classified.custom.length;
    }
  }

  // Write reports — one per app plus a combined report
  mkdirSync(outDir, { recursive: true });
  const reportPaths = [];

  for (const [appName, appReport] of Object.entries(report.apps)) {
    const appPath = join(outDir, `${appName}-diff-report.json`);
    writeFileSync(appPath, JSON.stringify({
      _generated: report._generated,
      _profile: profile,
      app: appName,
      ...appReport,
    }, null, 2), 'utf8');
    reportPaths.push(appPath);
  }

  // Combined report
  const combinedPath = join(outDir, 'manifest-comparison.json');
  writeFileSync(combinedPath, JSON.stringify(report, null, 2), 'utf8');
  reportPaths.push(combinedPath);

  // Print totals
  console.log('── Summary ──');
  console.log(`  Total manifest settings:  ${report.totals.totalSettings}`);
  console.log(`  Total fragments tracked:  ${report.totals.totalFragments}`);
  console.log(`  Customized:               ${report.totals.customizedFragments}`);
  console.log(`  Identical to CDN:         ${report.totals.identicalFragments}`);
  console.log(`  Using CDN default:        ${report.totals.baselineOnlyFragments}`);
  console.log(`  Account-only (no CDN):    ${report.totals.accountOnlyFragments}`);
  console.log(`  Custom plugins added:     ${report.totals.totalPluginsAdded}`);
  console.log(`  Routes added:             ${report.totals.totalRoutesAdded}`);
  console.log(`  Routes modified:          ${report.totals.totalRoutesModified}`);
  console.log(`  Routes removed:           ${report.totals.totalRoutesRemoved}`);
  if (opts.components) {
    console.log(`  OOTB components:          ${report.totals.totalOotbComponents || 0}`);
    console.log(`  Custom components:        ${report.totals.totalCustomComponents || 0}`);
  }
  console.log('');
  console.log('Reports:');
  for (const p of reportPaths) {
    console.log(`  ${p}`);
  }
}

main().catch(e => {
  console.error(`Fatal: ${e.message}`);
  process.exit(1);
});
