/**
 * Icon Registry — pluggable icon lookup map.
 *
 * Usage:
 *   <icon-ui name="paper-plane-right"></icon-ui>
 *   <icon-ui name="star" weight="fill"></icon-ui>
 *   <icon-ui name="heart" weight="bold"></icon-ui>
 *
 * ## Why this module is small
 *
 * This file holds the registry surface only. The Phosphor auto-glob
 * (`@phosphor-icons/core/assets/{weight}/*.svg` × 6 weights, ~9000 SVGs)
 * lives in the sibling side-effect module `./icons-phosphor.js`. Splitting
 * keeps `core/icons.js` cheap to import — primitives that consume the
 * registry API (button, input, icon) pull in the API surface without
 * triggering Vite's per-icon code-split chunks.
 *
 * ## Loading icons
 *
 * Three paths, pick whichever matches your bundle-size budget:
 *
 *   1. **Zero-config (recommended for prototypes + small apps).** Import
 *      the main barrel — it side-effect-imports `icons-phosphor.js` for
 *      you and every Phosphor name resolves on demand:
 *
 *          import '@adia-ai/web-components';
 *
 *   2. **Opt-in full Phosphor with piecemeal component imports.** If you
 *      import components one at a time but want every Phosphor name to
 *      work, side-effect-import the phosphor module yourself:
 *
 *          import '@adia-ai/web-components/components/button';
 *          import '@adia-ai/web-components/core/icons-phosphor';
 *
 *   3. **Scoped registration (recommended for production bundle-size
 *      SLOs).** Skip `icons-phosphor.js` entirely and register only the
 *      icons you actually use. `import.meta.glob` with a brace-list emits
 *      one chunk per matched file — for a 4-icon app, that's ~4 chunks
 *      instead of the ~9000 the full set would produce:
 *
 *          import { installIconLoaders } from '@adia-ai/web-components/core/icons';
 *          installIconLoaders({
 *            regular: import.meta.glob(
 *              '/node_modules/@phosphor-icons/core/assets/regular/{caret-right,caret-up-down,moon,sun}.svg',
 *              { query: '?raw', import: 'default', eager: true }
 *            ),
 *          });
 *
 *      Or register single SVGs directly:
 *
 *          import { registerIcon } from '@adia-ai/web-components/core/icons';
 *          import caretRightSvg from './assets/caret-right.svg?raw';
 *          registerIcon('caret-right', caretRightSvg);
 */

export const ICON_WEIGHTS = ['regular', 'thin', 'light', 'bold', 'fill', 'duotone'];
const DEFAULT_WEIGHT = 'regular';

/** Composite cache key so `star@regular` and `star@fill` can coexist. */
const cacheKey = (name, weight) => `${weight}:${name}`;

const registry = new Map();
const pending = new Set();

/** Register a single icon. */
export function registerIcon(name, svg, weight = DEFAULT_WEIGHT) {
  registry.set(cacheKey(name, weight), svg);
}

/** Register multiple icons at a given weight. */
export function registerIcons(icons, weight = DEFAULT_WEIGHT) {
  for (const [name, svg] of Object.entries(icons)) {
    registry.set(cacheKey(name, weight), svg);
  }
}

/** Get icon SVG by name + weight. Triggers async load if not cached. */
export function getIcon(name, weight = DEFAULT_WEIGHT) {
  if (!name) return '';
  const key = cacheKey(name, weight);
  if (registry.has(key)) return registry.get(key);

  loadIcon(name, weight);
  return '';
}

/** Check if an icon is registered (sync). */
export function hasIcon(name, weight = DEFAULT_WEIGHT) {
  return registry.has(cacheKey(name, weight));
}

/** Get all registered icon keys. */
export function listIcons() {
  return [...registry.keys()];
}

// ── Loader-map plumbing ──
//
// `weightModules` maps weight → ({ path → loader }). A loader is either an
// SVG string (eager) or a `() => Promise<string>` (lazy). The default empty
// map means lookups all miss; callers either register icons directly via
// `registerIcon`/`registerIcons` or install a loader map via
// `installIconLoaders` (called by `icons-phosphor.js` in the zero-config path).

const EMPTY_WEIGHTS = { regular: {}, thin: {}, light: {}, bold: {}, fill: {}, duotone: {} };
const WEIGHT_KEYS = new Set(['regular', 'thin', 'light', 'bold', 'fill', 'duotone']);
let weightModules = EMPTY_WEIGHTS;

/* Shape-detect a flat `import.meta.glob` result: zero recognized weight
   keys (`regular`/`thin`/`light`/`bold`/`fill`/`duotone`) AND at least one
   key that looks like an SVG path (`.svg` suffix). Used to auto-promote
   the documented flat-glob form (FB-09) to `{ regular: <flat> }` so the
   substrate stops silently failing on the most common malformed shape
   (FB-07). */
function isFlatGlobShape(modules) {
  const keys = Object.keys(modules);
  if (keys.length === 0) return false;
  for (const k of keys) if (WEIGHT_KEYS.has(k)) return false;
  for (const k of keys) if (k.endsWith('.svg')) return true;
  return false;
}

/* Warn-once cache so the FB-07 auto-promote message doesn't spam in HMR
   environments where `installIconLoaders` re-runs on every hot reload. */
const warnedShapes = new WeakSet();

/* `whenIconRegistryReady` lets a component defer icon-name decisions until
   the registry is sync-checkable. <icon-ui> doesn't need it — it owns the
   <name → svg> render and re-stamps elements when the loader map arrives.
   But components that branch on whether a string IS an icon (e.g.
   <input-ui>'s prefix/suffix, which accept both icon names and text labels
   like "@" or "$") need to re-evaluate once loaders arrive, otherwise a
   kebab-case name baked into the DOM as text never recovers. */
let registryReady = false;
let resolveRegistryReady;
export const whenIconRegistryReady = new Promise((r) => { resolveRegistryReady = r; });

/**
 * Install a per-weight loader map and mark the registry ready. The map
 * shape matches `import.meta.glob` output: `{ '/path/to/star.svg': loader }`.
 *
 * Either a lazy glob (Vite emits a chunk per icon) or `{ eager: true }`
 * (Vite inlines the strings) works — the loader can be the SVG string
 * directly or a thunk returning `Promise<string>`.
 *
 * Calling this re-stamps any `<icon-ui>` elements that asked for an icon
 * before loaders arrived. Idempotent — calling it twice replaces the map.
 */
export function installIconLoaders(modules) {
  // FB-07: validate shape before installing. The previous behavior was
  // `weightModules = { ...EMPTY_WEIGHTS, ...modules }` which silently
  // accepted any object — including the documented-but-broken flat
  // path→svg glob (FB-09), which spread path keys at the top level
  // and left every weight slot empty. Three shapes are now recognized:
  //
  //   1. Per-weight map { regular: { path: svg }, bold: { ... }, ... }
  //      — canonical, installs verbatim.
  //   2. Flat glob { '/path/foo.svg': '<svg>', ... } — auto-promoted to
  //      { regular: <flat> } with a one-time warn so the caller knows
  //      to wrap explicitly (and so bold/fill/etc. aren't silently lost).
  //   3. Anything else — logged via console.error, install aborted.
  if (!modules || typeof modules !== 'object') {
    console.error(
      `[icons] installIconLoaders: expected per-weight map { regular: { ... }, bold: { ... }, ... }, ` +
      `got ${modules === null ? 'null' : typeof modules}. Install aborted; registry unchanged.`
    );
    return;
  }
  let installable = modules;
  if (isFlatGlobShape(modules)) {
    if (!warnedShapes.has(modules)) {
      warnedShapes.add(modules);
      console.warn(
        `[icons] installIconLoaders received a flat path→svg map. ` +
        `Auto-promoting to { regular: <flat> }. Wrap explicitly to silence ` +
        `this warning and to load bold/fill/etc. weights:\n` +
        `  installIconLoaders({ regular: import.meta.glob('.../regular/*.svg', { ... }) });`
      );
    }
    installable = { regular: modules };
  } else {
    const hasAnyWeight = Object.keys(modules).some(k => WEIGHT_KEYS.has(k));
    if (!hasAnyWeight) {
      const sampleKeys = Object.keys(modules).slice(0, 5).join(', ');
      console.error(
        `[icons] installIconLoaders: shape unrecognized. ` +
        `Expected per-weight map keyed by regular/thin/light/bold/fill/duotone, ` +
        `or a flat path→svg map. Received keys: ${sampleKeys || '(empty)'}. ` +
        `Install aborted; registry unchanged.`
      );
      return;
    }
  }
  weightModules = { ...EMPTY_WEIGHTS, ...installable };
  registryReady = true;
  if (typeof document !== 'undefined') {
    for (const el of document.querySelectorAll('icon-ui[name]')) {
      const name = el.getAttribute('name');
      const weight = el.getAttribute('weight') || DEFAULT_WEIGHT;
      if (name) loadIcon(name, weight);
    }
  }
  resolveRegistryReady();
}

/**
 * Aggregate `static requiredIcons` declarations from all currently-defined
 * `customElements`, install loaders for the union, and return the list of
 * discovered icon names. Replaces the trial-and-error icon-glob authoring
 * loop FEEDBACK-06 §4 + FEEDBACK-07 §4 reported.
 *
 * Each AdiaUI primitive that auto-stamps icons declares them via:
 *
 *   export class UISelect extends UIFormElement {
 *     static requiredIcons = ['caret-up-down'];
 *     // ...
 *   }
 *
 * Consumer usage:
 *
 *   import '@adia-ai/web-components';  // side-effect — defines all primitives
 *   import { installIconLoadersForRegistered } from '@adia-ai/web-components/core/icons';
 *
 *   // Build the Phosphor loader map from your bundler (Vite, Webpack, etc.).
 *   const modules = import.meta.glob('/node_modules/@phosphor-icons/core/assets/regular/*.svg', { query: '?raw' });
 *
 *   // Aggregate requiredIcons across all defined primitives + install.
 *   const discovered = installIconLoadersForRegistered(modules);
 *
 * Or, to limit the icon set to ONLY what's needed (smaller bundle):
 *
 *   const discovered = installIconLoadersForRegistered.discover();
 *   // discovered: ['caret-up-down', 'x', 'caret-right', ...]
 *   // Now build a narrower glob from this list.
 *
 * Added v0.5.3 §154 per FEEDBACK-06 §4 + FEEDBACK-07 §4.
 *
 * @param {Record<string, () => Promise<string>>} modules - Phosphor loader map
 * @returns {string[]} The union of all primitives' `requiredIcons`
 */
export function installIconLoadersForRegistered(modules) {
  const discovered = discoverRequiredIcons();
  installIconLoaders(modules);
  return discovered;
}

/**
 * Walk customElements and collect the union of `static requiredIcons` across
 * all defined primitives. Useful for building a Vite glob brace-list that
 * matches exactly what the registered primitives need.
 *
 * Returns an empty array if `customElements` is unavailable (Node SSR)
 * or if no defined primitive exposes `requiredIcons`.
 */
function discoverRequiredIcons() {
  const union = new Set();
  if (typeof customElements === 'undefined') return [];
  // customElements has no Object.entries-style iterator in the standard API.
  // We need to track which tag names have been defined. The AdiaUI convention:
  // every primitive's tag ends in `-ui` (or `-n` per yaml `tag:` pattern). We
  // walk a known list of tag names — passed via opt-in. For SSR safety the
  // consumer can also pass `tags` explicitly:
  // discoverRequiredIcons(['select-ui', 'tag-ui', ...]).
  //
  // For browser usage with the default barrel import, the consumer's
  // `import '@adia-ai/web-components'` defines all 93+ primitives at the
  // global custom-element registry; we walk the DOM-known tag names by
  // checking customElements.get() on the AdiaUI prefix convention.
  //
  // Strategy: query all defined tags by walking the document's primitive
  // surface OR accept an explicit tag list. The simpler API: iterate over
  // a known whitelist of tag names. We embed the whitelist for known
  // auto-stamping primitives.
  const KNOWN_AUTO_STAMPING_TAGS = [
    'accordion-ui',
    'agent-questions-ui',
    'agent-reasoning-ui',
    'agent-trace-ui',
    'calendar-picker-ui',
    'color-picker-ui',
    'command-ui',
    'pane-ui',
    'select-ui',
    'table-ui',
    'tag-ui',
    'tree-ui',
  ];
  for (const tag of KNOWN_AUTO_STAMPING_TAGS) {
    const ctor = customElements.get(tag);
    if (ctor && Array.isArray(ctor.requiredIcons)) {
      for (const name of ctor.requiredIcons) union.add(name);
    }
  }
  return [...union];
}

/**
 * Public discovery — same shape as installIconLoadersForRegistered but
 * doesn't install. Returns the union of `static requiredIcons` across
 * all currently-defined auto-stamping AdiaUI primitives.
 */
installIconLoadersForRegistered.discover = discoverRequiredIcons;

/* Track which (name, weight) pairs we've already warned about so the
   console stays readable even when a missing icon is used many times on
   a page (e.g. a broken Phosphor name in a list row repeated 50×). */
const warnedMissingIcons = new Set();
function warnMissingIcon(name, weight) {
  const key = cacheKey(name, weight);
  if (warnedMissingIcons.has(key)) return;
  warnedMissingIcons.add(key);
  const weightNote = weight !== DEFAULT_WEIGHT ? ` at weight="${weight}"` : '';
  const alias = ICON_ALIASES[name];
  const aliasNote = alias ? ` (alias "${name}" → "${alias}" — also not found)` : '';
  console.warn(
    `[icon-ui] Icon "${name}"${weightNote} not found in the registry${aliasNote}. ` +
    `Check the name against @phosphor-icons/core, use an alias in ICON_ALIASES, ` +
    `or register a custom SVG via registerIcon().`
  );
}

/**
 * Phosphor filename convention: `star.svg` for regular, `star-fill.svg` for
 * fill, `star-bold.svg` for bold, etc. (each non-regular weight suffixes the
 * weight to the filename).
 */
function filenameFor(name, weight) {
  if (weight === 'regular') return `${name}.svg`;
  return `${name}-${weight}.svg`;
}

/* Resolve a loader by name + weight, tolerant of path-prefix variation.
 *
 * Two-step lookup, fast-path first:
 *
 *   1. **Fast path**: try the canonical `/node_modules/@phosphor-icons/core/
 *      assets/<weight>/<filename>` key directly. This is what `icons-phosphor.js`
 *      installs when Vite's project root sits above an npm-style node_modules,
 *      and what the chat-ui dev convention assumes.
 *
 *   2. **Suffix fallback**: if the fast path misses, scan `modules` keys for
 *      one ending with `/<filename>`. Handles:
 *        - pnpm: `/node_modules/.pnpm/@phosphor-icons+core@.../node_modules/.../assets/...`
 *        - yarn berry / PnP: virtual paths like `/.yarn/cache/...`
 *        - custom Vite root: `/packages/my-app/node_modules/...`
 *        - brace-list globs with absolute paths
 *        - manually-built loader maps with arbitrary keys
 *
 * The fast path stays O(1); the fallback is O(N) over the weight's module
 * map but only runs once per (name, weight) miss (results aren't cached, but
 * misses fall through to a registry hit via `loadIcon`'s `registry.set`).
 *
 * Same two-step shape applies to the `-fill` back-compat name suffix.
 *
 * Fix history: pre-§140 the function was fast-path-only. FEEDBACK-06 from
 * color-app reported `installIconLoaders` → `warnMissingIcon` firing for
 * `caret-right` / `x` / `caret-up-down` even though the consumer had installed
 * a loader map. Root cause: color-app's `modules` keys didn't match the
 * canonical `/node_modules/...` prefix (different package-manager or Vite
 * root). The suffix fallback closes that gap without changing the public API.
 */
function resolveLoader(name, weight = DEFAULT_WEIGHT) {
  const modules = weightModules[weight];
  if (!modules) return null;
  const filename = filenameFor(name, weight);

  // 1. Fast path — canonical phosphor npm-style path.
  const fastKey = `/node_modules/@phosphor-icons/core/assets/${weight}/${filename}`;
  if (modules[fastKey]) return modules[fastKey];

  // 2. Suffix fallback — match any key ending with the filename.
  const suffix = `/${filename}`;
  for (const key in modules) {
    if (key.endsWith(suffix)) return modules[key];
  }

  // Back-compat: allow the caller to put the -fill suffix directly in the
  // name (e.g. `name="circle-fill"`) without specifying weight. Two-step
  // lookup applies here too.
  if (weight === DEFAULT_WEIGHT && name.endsWith('-fill')) {
    const fillFilename = `${name}.svg`;
    const fillFastKey = `/node_modules/@phosphor-icons/core/assets/fill/${fillFilename}`;
    if (weightModules.fill[fillFastKey]) return weightModules.fill[fillFastKey];
    const fillSuffix = `/${fillFilename}`;
    for (const key in weightModules.fill) {
      if (key.endsWith(fillSuffix)) return weightModules.fill[key];
    }
  }
  return null;
}

/**
 * Sync check: is `name` a loadable icon at the given weight? True if the
 * asset exists in the installed loader map OR has an alias that does. Does
 * NOT trigger loading. Used by components that accept either an icon name
 * or a text label (e.g. input-ui's prefix/suffix) to disambiguate.
 */
export function isIconName(name, weight = DEFAULT_WEIGHT) {
  if (!name || typeof name !== 'string') return false;
  if (resolveLoader(name, weight)) return true;
  const alias = ICON_ALIASES[name];
  if (alias && resolveLoader(alias, weight)) return true;
  return false;
}

// Common aliases — brand names without -logo suffix
const ICON_ALIASES = {
  // Brand logos (LLMs often omit -logo suffix)
  'google': 'google-logo', 'github': 'github-logo', 'microsoft': 'windows-logo',
  'apple': 'apple-logo', 'twitter': 'twitter-logo', 'facebook': 'facebook-logo',
  'discord': 'discord-logo', 'slack': 'slack-logo', 'linkedin': 'linkedin-logo',
  'youtube': 'youtube-logo', 'instagram': 'instagram-logo', 'figma': 'figma-logo',
  'dribbble': 'dribbble-logo', 'stripe': 'stripe-logo', 'paypal': 'paypal-logo',
  // Common LLM-generated names that don't match Phosphor
  'alert-circle': 'warning-circle', 'alert': 'warning', 'error': 'x-circle',
  'success': 'check-circle', 'close': 'x', 'delete': 'trash',
  'edit': 'pencil-simple', 'save': 'floppy-disk', 'search': 'magnifying-glass',
  'settings': 'gear', 'home': 'house', 'mail': 'envelope', 'email': 'envelope',
  'phone': 'phone', 'location': 'map-pin', 'calendar': 'calendar-blank',
  'time': 'clock', 'user': 'user', 'users': 'users-three',
  'menu': 'list', 'more': 'dots-three', 'dots-vertical': 'dots-three-vertical', 'filter': 'funnel',
  'inbox': 'tray',
  'text-bold': 'text-b', 'bold': 'text-b', 'italic': 'text-italic', 'underline': 'text-underline',
  'reset': 'arrow-counter-clockwise', 'undo': 'arrow-counter-clockwise', 'redo': 'arrow-clockwise',
  'success-checkmark': 'seal-check', 'circuit': 'circuitry',
  'contrast': 'circle-half', 'theme': 'circle-half', 'dark-mode': 'moon', 'light-mode': 'sun',
  'alert-triangle': 'warning', 'alert-octagon': 'warning-octagon', 'alert-diamond': 'warning-diamond',
  'number': 'hash', 'numbered-list': 'list-numbers', 'numeric': 'hash',
  // Phosphor uses suffix-circle for these, but consumers reach for the
  // prefix form ("circle-warning") just as often. Resolve both shapes.
  'circle-warning': 'warning-circle', 'circle-x': 'x-circle',
  'circle-check': 'check-circle', 'circle-info': 'info',
  'circle-question': 'question', 'circle-plus': 'plus-circle', 'circle-minus': 'minus-circle',
  'sort': 'sort-ascending', 'refresh': 'arrow-clockwise', 'download': 'download-simple',
  'upload': 'upload-simple', 'share': 'share-network', 'copy': 'copy-simple',
  'link': 'link-simple', 'external-link': 'arrow-square-out',
  'chevron-down': 'caret-down', 'chevron-up': 'caret-up',
  'chevron-left': 'caret-left', 'chevron-right': 'caret-right',
  'arrow-left': 'arrow-left', 'arrow-right': 'arrow-right',
  'plus': 'plus', 'minus': 'minus',
  'send': 'paper-plane-right', 'submit': 'paper-plane-right',
  'attachment': 'paperclip', 'attach': 'paperclip',
  'expand': 'arrows-out', 'collapse': 'arrows-in',
  'fullscreen': 'arrows-out', 'exit-fullscreen': 'arrows-in',
  'fade': 'gradient', 'fade-presence': 'gradient',
  'notification': 'bell', 'notifications': 'bell',
  'bookmark': 'bookmark-simple', 'favorite': 'heart', 'like': 'heart',
  'comment': 'chat-circle', 'chat': 'chat-circle',
  'image': 'image', 'photo': 'image', 'picture': 'image',
  'video': 'video-camera', 'camera': 'camera',
  'file': 'file', 'document': 'file-text', 'folder': 'folder',
  'lock': 'lock-simple', 'unlock': 'lock-simple-open',
  'eye-off': 'eye-slash', 'hide': 'eye-slash', 'show': 'eye',
  'logout': 'sign-out', 'login': 'sign-in',
  // Shape primitives — useful as visual markers (legend dots, status pips,
  // bullet substitutes). Default to filled variants since unfilled circles
  // tend to read as outlined buttons rather than markers.
  'dot': 'circle-fill', 'circle': 'circle-fill', 'marker': 'circle-fill',
};

async function loadIcon(name, weight = DEFAULT_WEIGHT) {
  const key = cacheKey(name, weight);
  if (pending.has(key)) return;
  pending.add(key);

  try {
    let loader = resolveLoader(name, weight);

    // Try alias if direct name not found
    if (!loader && ICON_ALIASES[name]) {
      loader = resolveLoader(ICON_ALIASES[name], weight);
    }

    if (!loader) {
      pending.delete(key);
      // Warn once per (name, weight) — but only if the registry is
      // known-complete. Pre-loader-install the map is empty + every lookup
      // fails legitimately; warning there would be noise. `installIconLoaders`
      // re-stamps every <icon-ui> element after install, so misses that
      // survive the re-query are the ones worth warning about.
      if (registryReady) warnMissingIcon(name, weight);
      return;
    }

    // Loader may be a string (eager glob) or a thunk returning string /
    // Promise<string> (lazy glob or fetch).
    const svg = typeof loader === 'function' ? await loader() : loader;
    registry.set(key, svg);

    // Re-render any icon-ui elements showing this name+weight. Regular has
    // no explicit weight attr, so match both forms.
    const selector = weight === DEFAULT_WEIGHT
      ? `icon-ui[name="${name}"]:not([weight]), icon-ui[name="${name}"][weight="regular"]`
      : `icon-ui[name="${name}"][weight="${weight}"]`;
    for (const el of document.querySelectorAll(selector)) {
      el.innerHTML = svg;
    }
  } catch {
    // Icon not found — stays blank
  } finally {
    pending.delete(key);
  }
}
