// kernel/feature-registry.js — the single source of truth for "what pages exist".
//
// This is the first piece of the feature-module architecture (see
// docs/architecture/webui-architecture.md). It replaces the four hand-maintained
// registries the SPA grew over time:
//   1. the eager <script> list in index.html,
//   2. spa.js's hardcoded `routes` Map (setupRoutes),
//   3. system-loader.js's component registry,
//   4. config-manager.js's renderConfig() category if-chain.
//
// It loads a checked-in manifest describing the pages and exposes a routes Map
// plus the runtime registry. Each page's index.js calls LP.features.register({...})
// at load time and the kernel drives its mount()/unmount(); a page that only has
// a legacy handleX() method falls back to that (strangler coexistence).
(function () {
  const LP = (window.LP = window.LP || {});

  LP.features = {
    // Runtime registrations — each page's index.js calls register() at load time;
    // buildRouteTable()/get() consume them to drive the kernel lifecycle.
    _modules: new Map(),

    // The parsed manifest (the generated/checked-in description of all pages).
    _manifest: null,

    // Register a live feature module. id must be unique; last-wins with a warn
    // so a duplicate during migration is loud, not silent.
    register(def) {
      if (!def || !def.id) {
        console.error('[features] register() called without an id', def);
        return;
      }
      if (this._modules.has(def.id)) {
        console.warn(`[features] duplicate registration for "${def.id}" — last wins`);
      }
      this._modules.set(def.id, def);
      return def;
    },

    get(id) {
      return this._modules.get(id) || null;
    },

    // Fetch + cache the page manifest. Prefers the live scan endpoint
    // (/api/features/list — folder-discovered, exactly like /api/themes/list),
    // and falls back to the checked-in static manifest if the endpoint is
    // unavailable (e.g. the backend hasn't restarted to pick up the route yet).
    // Returns the manifest or null on total failure (callers fall back to their
    // built-in defaults).
    async loadManifest() {
      if (this._manifest) return this._manifest;
      const sources = ['/api/features/list', '/components/manifest.dev.json'];
      for (const url of sources) {
        try {
          const res = await fetch(url, { cache: 'no-cache' });
          if (!res.ok) continue;
          const data = await res.json();
          if (data && Array.isArray(data.features) && data.features.length) {
            this._manifest = data;
            return this._manifest;
          }
        } catch (err) {
          console.warn(`[features] manifest source ${url} failed:`, err.message);
        }
      }
      console.warn('[features] no manifest source available, callers will fall back');
      return null;
    },

    // The manifest entries, or [] if not loaded.
    list() {
      return (this._manifest && this._manifest.features) || [];
    },

    // Build a route-pattern -> entry Map from the manifest. The pattern strings
    // (including the trailing-'*' wildcards) are kept verbatim so the consumer's
    // matching logic is unchanged from spa.js's findRouteHandler.
    buildRouteTable() {
      const table = new Map();
      for (const f of this.list()) {
        for (const route of (f.routes || [])) {
          if (table.has(route)) {
            console.warn(`[features] duplicate route "${route}" (feature "${f.id}") — last wins`);
          }
          table.set(route, f);
        }
      }
      return table;
    },

    // Nav entries (topbar) derived from the manifest, sorted by nav.order.
    // Phase 0 keeps topbar.html authoritative; this is here for the nav kernel
    // that lands with the topbar migration.
    navItems() {
      return this.list()
        .filter(f => f.nav && f.nav.label)
        .sort((a, b) => (a.nav.order || 0) - (b.nav.order || 0))
        .map(f => ({ id: f.id, ...f.nav, routes: f.routes }));
    },
  };
})();
