/**
 * The demo registry: discovery by existence.
 *
 * There is no list of demos anywhere in this repo. A demo exists because
 * `src/demos/<slug>/meta.ts` and `src/demos/<slug>/demo.tsx` exist. Adding one
 * is `mkdir` plus two files; the header, the gallery, the vertical pages and
 * the router all pick it up with no edit to shared code. That property is the
 * whole reason this file is a glob and not an array, so resist the urge to
 * "just add an import" for the one awkward demo.
 *
 * Two globs, deliberately different:
 *   - `meta.ts` is EAGER. Every page needs every meta (the header lists them),
 *     it is plain serialisable data, and the contract forbids React or icon
 *     components in it precisely so this eager glob stays cheap.
 *   - `demo.tsx` is LAZY. It is the expensive half — components, adapters,
 *     the reward source imported with `?raw` — and only one of them is ever
 *     needed at a time.
 *
 * A demo whose meta is malformed is QUARANTINED: dropped from the registry with
 * a console error. One bad demo must never be able to white-page the site.
 */

import type { DemoMeta, DemoModule, Vertical } from './types';

/**
 * The shell is generic over each demo's board type and never inspects it, but
 * the registry has to hand back demos of *different* board types from one
 * function. `DemoModule<unknown>` does not work: `Surface` takes `{ state: T }`
 * in a contravariant position, so `DemoModule<WordleState>` is not assignable
 * to `DemoModule<unknown>` and the shell could not pass a state back in either.
 * `any` is the one thing that is assignable in both directions here. Each demo
 * is still fully checked against `DemoModule<TState>` at its own `defineDemo()`
 * call site, which is where the type actually protects anyone.
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyDemoModule = DemoModule<any>;

/** Every vertical in the contract, in the order the site presents them. */
export const VERTICAL_ORDER: readonly Vertical[] = [
  'reference',
  'support',
  'healthcare',
  'insurance',
  'financial-crime',
  'energy',
  'logistics',
  'code',
  'retail',
  'telecom',
  'data',
  'legal',
];

/** Exec-facing names. The union member is a slug; this is what a person reads. */
export const VERTICAL_LABELS: Readonly<Record<Vertical, string>> = {
  reference: 'Reference',
  support: 'Customer support',
  healthcare: 'Healthcare',
  insurance: 'Insurance',
  'financial-crime': 'Financial crime',
  energy: 'Energy',
  logistics: 'Logistics',
  code: 'Software',
  retail: 'Retail',
  telecom: 'Telecom',
  data: 'Data',
  legal: 'Legal',
};

const VERTICAL_SET = new Set<string>(VERTICAL_ORDER);

export interface VerticalGroup {
  vertical: Vertical;
  label: string;
  demos: DemoMeta[];
}

/**
 * Demos may export their meta as `meta` or as the default. Both are accepted
 * because the alternative is a build that compiles and a site that is empty.
 */
interface MetaModuleShape {
  readonly meta?: unknown;
  readonly default?: unknown;
}

interface DemoModuleShape {
  readonly demo?: unknown;
  readonly default?: unknown;
}

/**
 * A leading underscore marks scaffolding, not a demo — the same convention
 * `scripts/_lib.mjs` uses for `demoSlugs()` and `scripts/new-demo.mjs` copies
 * from. It is excluded in the GLOB rather than filtered after, so the template
 * is never even imported: it holds unsubstituted `__slug__` tokens, which the
 * validator below would quarantine with a console error on every page load,
 * and an eager glob would ship it to every visitor to say so.
 */
const metaModules = import.meta.glob<MetaModuleShape>(
  ['../../demos/*/meta.ts', '!../../demos/_*/meta.ts'],
  { eager: true },
);

const demoLoaders = import.meta.glob<DemoModuleShape>([
  '../../demos/*/demo.tsx',
  '!../../demos/_*/demo.tsx',
]);

/** `../../demos/wordle-five/meta.ts` -> `wordle-five` */
function slugFromPath(path: string): string | null {
  const match = /\/demos\/([^/]+)\/[^/]+$/.exec(path);
  return match?.[1] ?? null;
}

function isNonEmptyString(value: unknown): value is string {
  return typeof value === 'string' && value.trim().length > 0;
}

/**
 * Returns the reason this meta is unusable, or `null` if it is fine.
 *
 * Everything checked here is read without a guard by some surface. `slug` is
 * checked against the directory name rather than merely being present, because
 * a slug that disagrees with its directory produces a card that links to a
 * route that 404s — the single most confusing failure this registry can have.
 */
function validationError(candidate: unknown, dirName: string): string | null {
  if (candidate === null || typeof candidate !== 'object') {
    return 'meta.ts must export a `meta` object (or a default export).';
  }
  const meta = candidate as Partial<DemoMeta>;

  if (!isNonEmptyString(meta.slug)) return '`slug` is missing or empty.';
  if (meta.slug !== dirName) {
    return `\`slug\` is "${meta.slug}" but the directory is "${dirName}". They must match.`;
  }
  if (!isNonEmptyString(meta.title)) return '`title` is missing or empty.';
  if (!isNonEmptyString(meta.tagline)) return '`tagline` is missing or empty.';
  if (!isNonEmptyString(meta.icon)) return '`icon` is missing or empty.';
  if (!isNonEmptyString(meta.persona)) return '`persona` is missing or empty.';
  if (!isNonEmptyString(meta.rewardLine)) return '`rewardLine` is missing or empty.';
  if (!isNonEmptyString(meta.ogImage)) return '`ogImage` is missing or empty.';
  if (!isNonEmptyString(meta.vertical) || !VERTICAL_SET.has(meta.vertical)) {
    return `\`vertical\` is "${String(meta.vertical)}", which is not one of: ${VERTICAL_ORDER.join(', ')}.`;
  }
  if (meta.status !== 'live' && meta.status !== 'spec') {
    return `\`status\` is "${String(meta.status)}"; expected "live" or "spec".`;
  }
  if (typeof meta.order !== 'number' || !Number.isFinite(meta.order)) {
    return '`order` must be a finite number.';
  }
  return null;
}

function byOrderThenSlug(a: DemoMeta, b: DemoMeta): number {
  return a.order - b.order || a.slug.localeCompare(b.slug);
}

/** Built once at module load. Quarantine decisions are logged exactly once. */
const demosBySlug: ReadonlyMap<string, DemoMeta> = (() => {
  const accepted = new Map<string, DemoMeta>();

  for (const [path, module] of Object.entries(metaModules)) {
    const dirName = slugFromPath(path);
    if (dirName === null) {
      console.error(`[demo-kit] Ignoring "${path}": could not read a slug from the path.`);
      continue;
    }

    const candidate = module.meta ?? module.default;
    const problem = validationError(candidate, dirName);
    if (problem !== null) {
      console.error(`[demo-kit] Quarantined demo "${dirName}": ${problem}`);
      continue;
    }

    const meta = candidate as DemoMeta;
    if (!Object.hasOwn(demoLoaders, `../../demos/${dirName}/demo.tsx`)) {
      console.error(
        `[demo-kit] Quarantined demo "${dirName}": meta.ts exists but demo.tsx does not.`,
      );
      continue;
    }

    accepted.set(meta.slug, meta);
  }

  return accepted;
})();

const orderedDemos: readonly DemoMeta[] = [...demosBySlug.values()].sort(byOrderThenSlug);

/**
 * The same list as `listDemos()`, as a frozen constant.
 *
 * Handy where a module wants the lineup at import time rather than in a render.
 * It is the SAME array every caller sees — never sort or splice it in place.
 */
export const demos: readonly DemoMeta[] = orderedDemos;

/** Every demo that survived validation, sorted by `order` then slug. */
export function listDemos(): DemoMeta[] {
  return [...orderedDemos];
}

/** One demo's meta, or `undefined` for an unknown or quarantined slug. */
export function getDemo(slug: string): DemoMeta | undefined {
  return demosBySlug.get(slug);
}

export function hasDemo(slug: string): boolean {
  return demosBySlug.has(slug);
}

/**
 * Demos grouped by vertical, in `VERTICAL_ORDER`. Verticals with no demos are
 * omitted — an empty "Telecom" heading reads as a broken page, not a roadmap.
 */
export function listVerticals(): VerticalGroup[] {
  const groups = new Map<Vertical, DemoMeta[]>();
  for (const demo of orderedDemos) {
    const bucket = groups.get(demo.vertical);
    if (bucket) bucket.push(demo);
    else groups.set(demo.vertical, [demo]);
  }

  return VERTICAL_ORDER.flatMap((vertical) => {
    const demos = groups.get(vertical);
    if (!demos || demos.length === 0) return [];
    return [{ vertical, label: VERTICAL_LABELS[vertical], demos }];
  });
}

export function getVertical(slug: string): VerticalGroup | undefined {
  return listVerticals().find((group) => group.vertical === slug);
}

/**
 * Promise cache, not value cache: two callers in the same tick (the route
 * loader and the page itself) share one dynamic import instead of racing.
 * A rejection is evicted so a failed chunk fetch can be retried.
 */
const moduleCache = new Map<string, Promise<AnyDemoModule>>();

/** Load a demo's heavy half. Rejects for an unknown or quarantined slug. */
export function loadDemoModule(slug: string): Promise<AnyDemoModule> {
  const cached = moduleCache.get(slug);
  if (cached) return cached;

  const meta = demosBySlug.get(slug);
  const loader = demoLoaders[`../../demos/${slug}/demo.tsx`];
  if (!meta || !loader) {
    return Promise.reject(new Error(`No demo named "${slug}".`));
  }

  const pending = loader()
    .then((module) => {
      const candidate = module.demo ?? module.default;
      if (candidate === null || typeof candidate !== 'object') {
        throw new Error(`Demo "${slug}" does not export a demo object from demo.tsx.`);
      }
      const demo = candidate as AnyDemoModule;
      if (demo.meta?.slug !== slug) {
        throw new Error(
          `Demo "${slug}" exports a module whose meta.slug is "${String(demo.meta?.slug)}".`,
        );
      }
      return demo;
    })
    .catch((error: unknown) => {
      moduleCache.delete(slug);
      throw error;
    });

  moduleCache.set(slug, pending);
  return pending;
}
