import type { HfColorGradingTarget } from "../colorGrading";

export type RuntimeJson =
  | string
  | number
  | boolean
  | null
  | RuntimeJson[]
  | { [key: string]: RuntimeJson };

import type { HyperframeControlAction } from "../inline-scripts/runtimeContract.js";
import type { HyperframePickerElementInfo } from "../inline-scripts/pickerApi.js";

export type RuntimeBridgeControlAction =
  | HyperframeControlAction
  | "tick"
  | "set-volume"
  | "set-media-output-muted"
  | "set-native-media-sync-disabled"
  | "set-web-audio-media-disabled"
  | "set-root-duration"
  | "stop-media"
  | "flash-elements";

export type RuntimeBridgeControlMessage = {
  source: "hf-parent";
  type: "control";
  action: RuntimeBridgeControlAction;
  frame?: number;
  muted?: boolean;
  volume?: number;
  durationSeconds?: number;
  disabled?: boolean;
  playbackRate?: number;
  target?: HfColorGradingTarget | string | null;
  grading?: RuntimeJson;
  compare?: RuntimeJson;
  seekMode?: "drag" | "commit";
};

export type RuntimeStateMessage = {
  source: "hf-preview";
  type: "state";
  frame: number;
  isPlaying: boolean;
  muted: boolean;
  playbackRate: number;
};

export type RuntimeTimelineClip = {
  id: string | null;
  label: string;
  start: number;
  duration: number;
  track: number;
  zIndex: number;
  stackingContextId: string | null;
  kind: "video" | "audio" | "image" | "element" | "composition";
  tagName: string | null;
  compositionId: string | null;
  compositionAncestors: string[];
  parentCompositionId: string | null;
  nodePath: string | null;
  compositionSrc: string | null;
  assetUrl: string | null;
  timelineRole: string | null;
  timelineLabel: string | null;
  timelineGroup: string | null;
  timelinePriority: number | null;
};

export type RuntimeTimelineScene = {
  id: string;
  label: string;
  start: number;
  duration: number;
  thumbnailUrl: string | null;
  avatarName: string | null;
};

export type RuntimeTimelineMessage = {
  source: "hf-preview";
  type: "timeline";
  durationInFrames: number;
  clips: RuntimeTimelineClip[];
  scenes: RuntimeTimelineScene[];
  compositionWidth: number;
  compositionHeight: number;
};

export type RuntimeDiagnosticMessage = {
  source: "hf-preview";
  type: "diagnostic";
  code: string;
  details: Record<string, RuntimeJson>;
};

export type RuntimePickerElementInfo = HyperframePickerElementInfo;

export type RuntimePickerHoveredMessage = {
  source: "hf-preview";
  type: "element-hovered";
  elementInfo: RuntimePickerElementInfo;
};

export type RuntimePickerCandidatesMessage = {
  source: "hf-preview";
  type: "element-pick-candidates";
  candidates: RuntimePickerElementInfo[];
  selectedIndex: number;
  point: { x: number; y: number };
};

export type RuntimePickerPickedMessage = {
  source: "hf-preview";
  type: "element-picked";
  elementInfo: RuntimePickerElementInfo;
};

export type RuntimePickerPickedManyMessage = {
  source: "hf-preview";
  type: "element-picked-many";
  elementInfos: RuntimePickerElementInfo[];
};

export type RuntimePickerCancelledMessage = {
  source: "hf-preview";
  type: "pick-mode-cancelled";
};

export type RuntimeStageSizeMessage = {
  source: "hf-preview";
  type: "stage-size";
  width: number;
  height: number;
};

/**
 * Fired once per session when the runtime's attempt to play a timed media
 * element is rejected with `NotAllowedError`. The parent (web component / host
 * app) uses this as the signal to promote to parent-frame audio proxies —
 * iframes lose autoplay privileges when the user gesture originated in the
 * parent frame, so the host has to take over audible playback there.
 */
export type RuntimeMediaAutoplayBlockedMessage = {
  source: "hf-preview";
  type: "media-autoplay-blocked";
};

/**
 * Posted by the runtime when `installRuntimeControlBridge` finishes registering
 * its message listener — signals that subsequent control messages
 * (`set-muted`, `set-volume`, `set-playback-rate`, etc.) will now be received
 * and processed. The parent (web component / host app) listens for this and
 * replays current playback state to repair any race where bridge messages
 * were posted before the listener was installed. Emitted again on every iframe
 * reload because the new runtime instance starts with no state.
 */
export type RuntimeReadyMessage = {
  source: "hf-preview";
  type: "ready";
};

/**
 * Analytics events emitted by the runtime.
 *
 * The host app receives these via postMessage and forwards to its analytics
 * provider (PostHog, Mixpanel, Amplitude, custom logging, etc.).
 * No analytics SDK runs inside this iframe.
 */
export type RuntimeAnalyticsMessage = {
  source: "hf-preview";
  type: "analytics";
  event:
    | "composition_loaded"
    | "composition_played"
    | "composition_paused"
    | "composition_seeked"
    | "composition_ended"
    | "element_picked";
  properties: Record<string, string | number | boolean | null>;
};

/**
 * Numeric performance metrics emitted by the runtime — scrub latency, sustained
 * fps, dropped frames, decoder count, composition load time, media sync drift.
 * The host aggregates per-session values (p50/p95) and forwards to its
 * observability pipeline. Distinct from `analytics` events because perf data
 * is continuous and numeric, not discrete.
 */
export type RuntimePerformanceMessage = {
  source: "hf-preview";
  type: "perf";
  name: string;
  value: number;
  tags: Record<string, string | number | boolean | null>;
};

export type RuntimeOutboundMessage =
  | RuntimeStateMessage
  | RuntimeTimelineMessage
  | RuntimeDiagnosticMessage
  | RuntimePickerHoveredMessage
  | RuntimePickerCandidatesMessage
  | RuntimePickerPickedMessage
  | RuntimePickerPickedManyMessage
  | RuntimePickerCancelledMessage
  | RuntimeStageSizeMessage
  | RuntimeMediaAutoplayBlockedMessage
  | RuntimeReadyMessage
  | RuntimeAnalyticsMessage
  | RuntimePerformanceMessage;

export type RuntimePlayer = {
  _timeline: RuntimeTimelineLike | null;
  play: () => void;
  pause: () => void;
  seek: (timeSeconds: number, options?: { keepPlaying?: boolean }) => void;
  renderSeek: (timeSeconds: number, options?: RuntimeSeekOptions) => void;
  getTime: () => number;
  getDuration: () => number;
  isPlaying: () => boolean;
  setPlaybackRate: (rate: number) => void;
  getPlaybackRate: () => number;
};

export type RuntimeSeekOptions = {
  suppressEvents?: boolean;
};

export type RuntimeTimelineChildLike = {
  targets?: () => unknown[];
  vars?: unknown;
  startTime?: () => number;
  duration?: () => number;
  parent?: RuntimeTimelineChildLike;
};

export type RuntimeTimelineLike = {
  play: () => void;
  pause: () => void;
  seek: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
  totalTime?: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
  progress?: (value?: number, suppressEvents?: boolean) => unknown;
  time: () => number;
  duration: () => number;
  add: (timeline: RuntimeTimelineLike, startAtSeconds: number) => void;
  paused: (paused?: boolean) => void;
  timeScale?: (rate: number) => void;
  set: (target: RuntimeGsapSetTarget, vars: RuntimeGsapSetVars, atSeconds?: number) => void;
  getChildren?: (
    nested?: boolean,
    tweens?: boolean,
    timelines?: boolean,
    ignoreBeforeTime?: number,
  ) => RuntimeTimelineChildLike[];
};

export type RuntimeDeterministicAdapter = {
  name: string;
  discover: () => void;
  seek: (ctx: { time: number; suppressEvents?: boolean }) => void;
  pause: () => void;
  play?: () => void;
  revert?: () => void;
  /**
   * Optional async readiness gate. If the adapter has outstanding async work
   * (e.g. Three.js's `DefaultLoadingManager` still loading models/textures),
   * return a promise that settles when the work is done. The runtime waits
   * for the returned promise to settle before publishing
   * `window.__renderReady = true`, so the engine doesn't capture empty
   * frames while assets are still loading.
   *
   * Return `null` (or omit the method) when nothing is pending. The runtime
   * calls this on every readiness-publish evaluation and tracks promise
   * identity, so returning the same promise on repeated calls is the
   * expected contract — return a fresh promise only when a new wait is
   * actually needed (e.g. a new batch of items has been queued).
   *
   * Throwing or rejecting is safe: the runtime swallows the error and
   * proceeds to publish (matching the existing failure-doesn't-block-render
   * convention).
   */
  getReadyPromise?: () => PromiseLike<unknown> | null;
  /**
   * Optional duration auto-inference. Non-GSAP runtimes (CSS, WAAPI, Lottie)
   * have no `window.__timelines` entry, so the runtime has no authored source
   * of truth for total composition length unless the author sets
   * `data-duration` on the root element. This hook lets an adapter report the
   * longest end time it can discover from its own animations, so the runtime
   * can fold it into the duration floor (see `resolveAdapterDurationFloorSeconds`
   * in `init.ts`) and treat `data-duration` as optional rather than required.
   *
   * Return the inferred duration in seconds, or `null` when nothing usable
   * was discovered (e.g. no animations yet, or an animation with unbounded /
   * infinite iteration count that can't be resolved to a finite end time —
   * those compositions must keep declaring `data-duration` explicitly).
   *
   * Called on every adapter-discovery cycle (same cadence as `discover`), so
   * it's safe — and expected — to return a growing value as async work
   * (Lottie JSON fetch, etc.) resolves.
   */
  getInferredDurationSeconds?: () => number | null;
};

export type RuntimeGsapSetTarget = string | Element | Element[] | null;

export type RuntimeGsapSetVars = Record<string, string | number | boolean | null | undefined>;
