import type { AstroComponentFactory } from 'astro/runtime/server/index.js';

/**
 * A renderable Astro component (shell or screen body). Aliased behind ONE local name:
 * `astro/runtime/server` IS reachable via astro's `./runtime/*` exports glob (verified
 * 2026-06-22 against astro@6.4.8 package.json `exports`) — the established ecosystem
 * pattern for typing component-valued vars (Starlight, astro-icon do the same). It is
 * NOT a top-level NAMED export, so aliasing keeps a future astro type-path change a
 * ONE-line fix confined to this file. Never spread the raw import path across the app.
 */
export type ThemeComponent = AstroComponentFactory;

/**
 * Public content screens whose BODY a theme may override. Source = real routes (§2).
 * SINGLE-SOURCE: the `as const` array is canonical; the union is DERIVED from it
 * (`typeof SCREEN_IDS[number]`) so the two can never drift — adding a screen in one
 * place and not the other is structurally impossible. This is the ecosystem-standard
 * const-tuple idiom; it matters here because the set explicitly GROWS (WIRED_SCREENS
 * note) and SP4 generates against it (a type-silent drift would ship broken themes).
 */
export const SCREEN_IDS = [
  'index', 'post', 'blueprint', 'module', 'contact', 'search', 'privacy', 'not-found',
] as const;
export type ScreenId = (typeof SCREEN_IDS)[number];

/**
 * Screens ACTUALLY wired to `resolveScreen` in pages (§7). A theme override on a
 * screen NOT in this set is a SILENT NO-OP — the contract would lie about what it
 * honors (the agent-legibility defect the north-star forbids; SP4's make-theme skill
 * authors against this contract and would emit silently-failing themes). SP2 wires
 * exactly `post`. GROW THIS SET IN THE SAME WAVE that wires a new screen's page to
 * `resolveScreen` — never ahead of the wiring.
 */
export const WIRED_SCREENS: ReadonlySet<ScreenId> = new Set<ScreenId>([
  'post',
  'index',
  'search',
  'not-found',
]);

/**
 * PURE theme projection (id + display name). NO component references → importable in
 * vitest/.tsx without the astro vite plugin. The admin picker (§7 W6) and
 * `parseActiveSelection` (§5) consume ONLY this; never the component registry.
 */
export interface ThemeMeta {
  id: string;
  name: string;
  desc?: string;
}

/**
 * A presentation theme = meta + components. Lives ONLY in the .astro-importing registry
 * (`src/themes/registry.ts`, §6) — never imported by pure/tested code. The registry
 * guard asserts `MOD_CMS_THEMES` keys exactly match the pure `THEME_REGISTRY_META` ids.
 */
export interface ModTheme extends ThemeMeta {
  /** Page chrome (header/nav/main-slot/footer). Always present. */
  Shell: ThemeComponent;
  /** Sparse — a missing screenId falls back to the host default body (the resolver). */
  overrides: Partial<Record<ScreenId, ThemeComponent>>;
}

/** The shell is always the active theme's. */
export function resolveShell(theme: ModTheme): ThemeComponent {
  return theme.Shell;
}

/** override-template → else host default. The whole SP2 fallback contract. */
export function resolveScreen(
  theme: ModTheme,
  screen: ScreenId,
  hostDefault: ThemeComponent,
): ThemeComponent {
  return theme.overrides[screen] ?? hostDefault;
}

/**
 * Override keys that are NOT wired (would silently no-op). Pure → the registry test
 * (W3) asserts this returns [] for EVERY registered theme, so the contract can never
 * advertise capability the wiring doesn't honor. SP4-generated themes inherit the test.
 */
export function findUnwiredOverrides(theme: ModTheme): ScreenId[] {
  return (Object.keys(theme.overrides) as ScreenId[])
    .filter((screen) => !WIRED_SCREENS.has(screen));
}
