/**
 * React-side contracts of the renderer (CONTRACT §5.5, §5.6): the props every widget
 * component receives, the adapters an app supplies, and the page context the
 * ElementRenderer threads through the tree.
 */

import type { ComponentType, CSSProperties, ReactElement, ReactNode } from 'react';
import type {
  BreakpointId,
  Element,
  Kit,
  Link,
  Media,
  MenuItem,
  Rendered,
  ResolvedElement,
  Settings,
  WidgetDef,
} from './schema';

/** Extra attributes merged onto a root node (editor data-* attributes, lightbox flags…). */
export type Attrs = Record<string, string | number | boolean | undefined>;

export interface LinkAdapterProps {
  /** The `url` control value; the adapter keeps target/rel and parses `attributes`. */
  link: Link;
  className?: string;
  id?: string;
  title?: string;
  'aria-label'?: string;
  style?: CSSProperties;
  tabIndex?: number;
  attrs?: Attrs;
  children?: ReactNode;
}

export interface ImageAdapterProps {
  /** Skip rendering when `media.url` is empty. */
  media: Media;
  className?: string;
  /** Rendered size hint (next/image); defaults to the media's own width/height. */
  width?: number;
  height?: number;
  sizes?: string;
  priority?: boolean;
  loading?: 'lazy' | 'eager';
  style?: CSSProperties;
  attrs?: Attrs;
}

/** What the 3D Model widget's viewer shows (CONTRACT §25), read from the widget's settings. */
export interface Model3DOptions {
  autoRotate: boolean;
  /** 1 = one turn in about 30 seconds. */
  autoRotateSpeed: number;
  /** What the visitor may do with the pointer: rotate (drag), zoom (wheel, pinch), pan. */
  rotate: boolean;
  zoom: boolean;
  pan: boolean;
  /** Initial camera: degrees around the model (0 = the front), degrees above it (-85…85). */
  azimuth: number;
  elevation: number;
  /** Initial zoom in percent: 100 = the whole model in view. */
  zoomLevel: number;
  lighting: 'studio' | 'soft' | 'neutral' | 'dramatic';
  /** Tone-mapping exposure (1 = neutral). */
  exposure: number;
  /** Image-based lighting from three's RoomEnvironment (no file is downloaded). */
  environment: boolean;
  shadow: boolean;
  /** 0…1 */
  shadowOpacity: number;
  /** Play the model's first animation clip; loop it. */
  animation: boolean;
  loop: boolean;
  /** Load the viewer and the model only once the widget is near the viewport. */
  lazy: boolean;
}

export interface Model3DAdapterProps {
  /** The .glb to load; '' when there is none (then only `poster` shows). */
  src: string;
  /** Tried when `src` cannot be fetched: the attachment's own URL behind WordPress's stream. */
  fallbackSrc?: string;
  /** The poster image or the neutral placeholder: shown until the model is ready, and instead of it when it cannot load. */
  poster: ReactNode;
  options: Model3DOptions;
}

/**
 * `{ Link, Image }` — client components; the Next.js app wraps next/link + next/image.
 * `Model3D` (optional) is the 3D Model widget's live viewer; without one it shows its poster.
 */
export interface Adapters {
  Link: ComponentType<LinkAdapterProps>;
  Image: ComponentType<ImageAdapterProps>;
  Model3D?: ComponentType<Model3DAdapterProps>;
}

/** Props of every widget component (§5.5). The component renders INSIDE the Wrapper. */
export interface WidgetProps {
  element: Element;
  def: WidgetDef;
  kit: Kit;
  /** Defaults merged, DeviceMaps intact, `resolved[id].settings` overlaid. */
  settings: Settings;
  /** Non-responsive → the value; responsive → the desktop value. */
  get<T = unknown>(name: string): T;
  /** Desktop + every kit breakpoint, inheritance applied. */
  perDevice<T = unknown>(name: string): Record<BreakpointId, T>;
  /** Editor only (the device switcher); undefined on the frontend. */
  device?: BreakpointId;
  editor: boolean;
  resolved?: ResolvedElement;
  rendered?: Rendered;
  /** "lucide:star" → svg inner markup. */
  icons: Record<string, string>;
  /** nav-menu widgets: element id → items. */
  menus: Record<string, MenuItem[]>;
  adapters: Adapters;
  /** Containers only: the rendered child elements. */
  children?: ReactNode;
  /** Content the page owner hands to theme widgets (CONTRACT §16.5), e.g. the post content. */
  slots?: PageSlots;
}

/**
 * Slots a theme template's widgets render into: the frontend (and the editor's template
 * preview) passes the current post's own content here for the post-content widget.
 */
export interface PageSlots {
  postContent?: ReactNode;
}

export type WidgetComponent = ComponentType<WidgetProps>;

/**
 * Editor hook: receives the element and its root node (a `<Wrapper>` for widgets, a
 * `<Container>` for containers — both accept an `attrs` prop) and returns what to render,
 * e.g. `cloneElement(node, { attrs: { 'data-nxe-id': element.id } })` or a sortable wrapper.
 */
export type DecorateHook = (element: Element, node: ReactElement) => ReactNode;

/** Editor hook: render one child element yourself (the canvas's sortable tree). */
export type RenderElementHook = (element: Element, index: number, parent: Element | null) => ReactNode;

/** Everything the ElementRenderer needs besides the element itself. Serialisable on the frontend. */
export interface PageContext {
  pageId: number | string;
  kit: Kit;
  editor: boolean;
  device?: BreakpointId;
  icons: Record<string, string>;
  menus: Record<string, MenuItem[]>;
  resolved: Record<string, ResolvedElement>;
  rendered: Record<string, Rendered>;
  adapters: Adapters;
  /** Editor-only text shown by the "widget not available" placeholder. */
  messages?: { widgetUnavailable?: string };
  decorate?: DecorateHook;
  renderElement?: RenderElementHook;
  slots?: PageSlots;
}
