/**
 * Low-level wrapper around the WASM engine
 * Node.js / Bundler target
 */

// WASM module types
import { WasmEngine } from "../wasm-node/hypen_engine.js";
import { frameworkLoggers } from "./logger.js";
import { classifyEngineError } from "./result.js";
import type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";

// Re-export types so existing consumers of "./engine.js" still work
export type { Patch, Action, RenderCallback, ActionHandler, ResolvedComponent, ComponentResolver } from "./types.js";

const log = frameworkLoggers.engine;

/**
 * Unwrap proxy objects to plain values for WASM serialization.
 * Uses structuredClone when available (Node 17+, Bun, modern browsers),
 * falls back to JSON round-trip for proxy objects or older environments.
 */
function unwrapForWasm<T>(value: T): T {
  // Fast path: primitives don't need cloning
  if (value === null || typeof value !== 'object') {
    return value;
  }

  // Check if the object has a snapshot method (our proxy convention)
  if (typeof (value as any).__getSnapshot === 'function') {
    return (value as any).__getSnapshot() as T;
  }

  // Try structuredClone first (fastest for plain objects)
  try {
    return structuredClone(value);
  } catch {
    // Fallback for proxy objects or unsupported types
    return JSON.parse(JSON.stringify(value));
  }
}

/**
 * Engine wraps the WASM engine and provides a TypeScript-friendly API
 */
export class Engine {
  private wasmEngine: WasmEngine | null = null;
  private initialized = false;

  /**
   * Initialize the WASM module
   */
  async init(): Promise<void> {
    if (this.initialized) return;

    // For bundler target, WASM is auto-initialized
    this.wasmEngine = new WasmEngine();
    this.initialized = true;
  }

  /**
   * Ensure the engine is initialized before operations
   */
  private ensureInitialized(): WasmEngine {
    if (!this.wasmEngine) {
      throw new Error("Engine not initialized. Call init() first.");
    }
    return this.wasmEngine;
  }

  /**
   * Set the render callback that receives patches
   */
  setRenderCallback(callback: RenderCallback): void {
    const engine = this.ensureInitialized();
    engine.setRenderCallback((patches: Patch[]) => {
      callback(patches);
    });
  }

  /**
   * Set the component resolver for dynamic component composition
   */
  setComponentResolver(resolver: ComponentResolver): void {
    const engine = this.ensureInitialized();
    engine.setComponentResolver((componentName: string, contextPath: string | null) => {
      const result = resolver(componentName, contextPath);
      return result;
    });
  }

  /**
   * Parse and render Hypen DSL source code
   * @throws {ParseError} if the source fails to parse
   * @throws {RenderError} if rendering fails
   */
  renderSource(source: string): void {
    const engine = this.ensureInitialized();
    try {
      engine.renderSource(source);
    } catch (err) {
      throw classifyEngineError(err);
    }
  }

  /**
   * Render a lazy component (for lazy route loading)
   */
  renderLazyComponent(source: string): void {
    const engine = this.ensureInitialized();
    engine.renderLazyComponent(source);
  }

  /**
   * Render a component into a specific parent node (subtree rendering)
   * @throws {ParseError} if the source fails to parse
   * @throws {RenderError} if the parent node is not found or rendering fails
   */
  renderInto(source: string, parentNodeId: string, state: Record<string, any>): void {
    const engine = this.ensureInitialized();
    try {
      engine.renderInto(source, parentNodeId, unwrapForWasm(state));
    } catch (err) {
      throw classifyEngineError(err);
    }
  }

  /**
   * Notify the engine of state changes using sparse updates
   * @throws {StateError} if the state patch is invalid
   */
  notifyStateChange(paths: string[], values: Record<string, any>): void {
    const engine = this.ensureInitialized();

    if (paths.length === 0) {
      return;
    }

    try {
      engine.updateStateSparse(paths, unwrapForWasm(values));
    } catch (err) {
      throw classifyEngineError(err);
    }
    log.debug("State changed (sparse):", paths);
  }

  /**
   * Notify the engine of state changes using full state replacement
   * @deprecated Use notifyStateChange with sparse values instead
   */
  notifyStateChangeFull(paths: string[], currentState: Record<string, any>): void {
    const engine = this.ensureInitialized();
    engine.updateState(unwrapForWasm(currentState));

    if (paths.length > 0) {
      log.debug("State changed (full):", paths);
    }
  }

  /**
   * Update state (triggers re-render of affected nodes)
   * @deprecated Use notifyStateChange instead
   */
  updateState(statePatch: Record<string, any>): void {
    const engine = this.ensureInitialized();
    engine.updateState(unwrapForWasm(statePatch));
  }

  /**
   * Dispatch an action
   * @throws {HypenError} if the action dispatch fails
   */
  dispatchAction(name: string, payload?: any): void {
    const engine = this.ensureInitialized();
    try {
      engine.dispatchAction(name, payload ?? null);
    } catch (err) {
      throw classifyEngineError(err);
    }
  }

  /**
   * Register an action handler
   */
  private _onErrorHandler?: (error: Error) => void;

  /**
   * Set error handler for action errors
   */
  setOnError(handler: (error: Error) => void): void {
    this._onErrorHandler = handler;
  }

  onAction(actionName: string, handler: ActionHandler): void {
    const engine = this.ensureInitialized();
    const handleError = (err: unknown) => {
      log.error("Action handler error:", err);
      if (this._onErrorHandler) {
        this._onErrorHandler(err instanceof Error ? err : new Error(String(err)));
      }
    };
    engine.onAction(actionName, (action: Action) => {
      try {
        const result = handler(action);
        // If handler returns a promise, catch async errors too
        if (result && typeof (result as any).catch === "function") {
          (result as any).catch(handleError);
        }
      } catch (err) {
        handleError(err);
      }
    });
  }

  /**
   * Initialize a module
   */
  setModule(
    name: string,
    actions: string[],
    stateKeys: string[],
    initialState: Record<string, any>
  ): void {
    const engine = this.ensureInitialized();
    engine.setModule(name, actions, stateKeys, initialState);
  }

  /**
   * Get the current revision number
   */
  getRevision(): number {
    const engine = this.ensureInitialized();
    return Number(engine.getRevision());
  }

  /**
   * Clear the engine tree
   */
  clearTree(): void {
    const engine = this.ensureInitialized();
    engine.clearTree();
  }

  /**
   * Debug method to inspect parsed components
   */
  debugParseComponent(source: string): string {
    const engine = this.ensureInitialized();
    return engine.debugParseComponent(source);
  }

  // ── Data Source Context ────────────────────────────────────────

  /**
   * Set (or replace) a named data source context.
   *
   * Registers the provider in the dependency graph, stores the data,
   * and re-renders every node bound to `$name.*`.
   * Sparse merging (if needed) should happen at the SDK layer before
   * calling this method with the merged object.
   *
   * @param name - Provider name (e.g., "spacetime", "firebase")
   * @param data - Full state object for this provider
   */
  setContext(name: string, data: Record<string, unknown>): void {
    const engine = this.ensureInitialized();
    try {
      engine.setContext(name, unwrapForWasm(data));
    } catch (err) {
      throw classifyEngineError(err);
    }
    log.debug(`Data source "${name}" context set`);
  }

  /**
   * Remove a data source context entirely.
   * Drops the provider's state and re-renders bound nodes (they resolve to null).
   */
  removeContext(name: string): void {
    const engine = this.ensureInitialized();
    engine.removeContext(name);
  }
}
