/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ComponentType } from "react";

/**
 * How a theme replaces parts of the site.
 *
 * A theme is code, not a stylesheet: your components ship to the customer
 * and run as you wrote them. That's what makes a bought theme look exactly
 * like the one you designed, animations and all.
 *
 * The cost is a build. Files dropped into a running install do nothing
 * until `npm run build` runs, because Next compiles. There's no way round
 * that, and the admin panel says so rather than pretending otherwise.
 */

/**
 * Every piece a theme may replace.
 *
 * A theme provides as few or as many as it likes. Anything it doesn't
 * provide falls back to the built-in one, so a half-finished theme still
 * gives a working site rather than a broken one.
 */
export type ThemeSlots = {
  // ── The frame ──────────────────────────────────────
  /** Wraps every page. Where a theme decides its column layout. */
  Shell?: ComponentType<{ children: React.ReactNode }>;
  Header?: ComponentType;
  LeftMenu?: ComponentType;
  RightRail?: ComponentType;
  Footer?: ComponentType;

  // ── The pieces that appear everywhere ──────────────
  // Loosely typed on purpose: a theme's own component declares whatever
  // props it actually wants, and shouldn't have to satisfy a shape it
  // doesn't use.
  PostCard?: ComponentType<any>;
  Composer?: ComponentType<any>;
  Avatar?: ComponentType<any>;
  Button?: ComponentType<any>;

  // ── Whole pages ────────────────────────────────────
  /** Keyed by route: "/" , "/[username]", "/messages" and so on. */
  pages?: Record<string, ComponentType<any>>;

  // ── The states people forget ───────────────────────
  Loading?: ComponentType<{ what?: string }>;
  Empty?: ComponentType<{ what?: string }>;
};

export type ThemeModule = {
  /** Must match the folder name and the slug in the shop. */
  slug: string;
  name: string;
  version: string;
  author?: string;
  /** Loaded before the theme's own components. */
  css?: string;
  slots: ThemeSlots;
};

/**
 * Every theme this build compiled in.
 *
 * All of them, rather than one: a reader chooses which they see, so each
 * has to be present. That means page weight grows with each installed
 * theme — fine for a handful, not for dozens.
 *
 * A theme installed but not yet built isn't here, which is why the panel
 * says to build rather than claiming it applied.
 */
const themes = new Map<string, ThemeModule>();

/** Whichever the reader is looking at. */
let showing: string | null = null;

export function registerTheme(theme: ThemeModule): void {
  themes.set(theme.slug, theme);
}

/** Every theme compiled into this build. */
export function builtInThemes(): ThemeModule[] {
  return [...themes.values()];
}

/** Switches to a theme. An unknown one falls back to the default. */
export function showTheme(slug: string | null): void {
  showing = slug && themes.has(slug) ? slug : null;
}

export function activeTheme(): ThemeModule | null {
  return showing ? themes.get(showing) ?? null : null;
}

/**
 * A themed component, or the built-in one.
 *
 * Used at every point a theme may take over:
 *
 *   const Card = slot("PostCard", DefaultPostCard);
 *
 * A theme that doesn't provide one gets the built-in, so nothing has to
 * check whether a theme exists.
 */
export function slot<K extends keyof ThemeSlots>(
  name: K,
  fallback: NonNullable<ThemeSlots[K]>
): NonNullable<ThemeSlots[K]> {
  const provided = activeTheme()?.slots?.[name];
  return (provided ?? fallback) as NonNullable<ThemeSlots[K]>;
}

/** A whole page, or the built-in one. */
export function pageSlot(
  route: string,
  fallback: ComponentType<any>
): ComponentType<any> {
  return activeTheme()?.slots?.pages?.[route] ?? fallback;
}
