// src/engine/remoting/ComponentProxy.ts

import type { IMatrixContext } from '../core/IMatrixContext';
import { RequestReply } from './RequestReply';
import { TopicRouter } from '../messaging/TopicRouter';
import type { IIntrospectResult } from './IIntrospectResult';
import type { IMountableElement } from './IMountableElement';
import type { IRenderOptions } from './IRenderOptions';

interface IEventEnvelope {
  op?: unknown;
  payload?: unknown;
}

/**
 * ComponentProxy - Proxy object wrapping introspect data.
 *
 * Provides:
 * - All accepts commands as callable methods
 * - `.render()` for ghost rendering
 * - Access to introspect metadata
 *
 * Usage:
 * ```typescript
 * const proxy = await ComponentProxy.fromIntrospect(context, 'app.tree');
 * await proxy.setLabel({ label: 'New Label' });
 * proxy.render(document.getElementById('results'));
 * ```
 */
export class ComponentProxy {
  private _introspect: IIntrospectResult;
  private _context: IMatrixContext;
  private _children?: ComponentProxy[];

  constructor(introspect: IIntrospectResult, context: IMatrixContext) {
    this._introspect = introspect;
    this._context = context;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Static Factory Methods
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Create a ComponentProxy by calling introspect on a remote mount.
   */
  static async fromIntrospect(
    context: IMatrixContext,
    mount: string
  ): Promise<ComponentProxy> {
    const result = await RequestReply.execute<Record<string, never>, IIntrospectResult>(
      context,
      mount,
      'introspect',
      {},
      { timeoutMs: 5000 }
    );

    return new ComponentProxy(result, context);
  }

  /**
   * Create a ComponentProxy from existing introspect data.
   */
  static fromData(
    introspect: IIntrospectResult,
    context: IMatrixContext
  ): ComponentProxy {
    return new ComponentProxy(introspect, context);
  }

  /**
   * Create a JS Proxy that wraps this ComponentProxy with dynamic method access.
   * Allows calling accepts commands directly: proxy.setLabel({ label: 'foo' })
   */
  asProxy(): this & Record<string, (...args: unknown[]) => Promise<unknown>> {
    const self = this as Record<string | symbol, unknown>;
    return new Proxy(this, {
      get: (target, key: string | symbol) => {
        // Return own properties/methods first
        if (typeof key === 'symbol') return self[key];
        if (key in target) return self[key];

        // Check if it's an accept command
        const accepts = target._introspect.accepts ?? [];
        if (accepts.includes(key)) {
          // Return a method that calls the remote command
          return async (...args: unknown[]) => {
            const payload = args.length === 1 ? args[0] : args.length === 0 ? undefined : args;
            return target.send(key, payload);
          };
        }

        // Check if it's a special introspect property
        if (key in target._introspect) {
          return target._introspect[key as keyof IIntrospectResult];
        }

        return undefined;
      }
    }) as this & Record<string, (...args: unknown[]) => Promise<unknown>>;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Introspect Data Access
  // ═══════════════════════════════════════════════════════════════════════

  /** Get the component's mount path */
  get mount(): string {
    return this._introspect.mount;
  }

  /** Get the component class name */
  get componentClass(): string {
    return this._introspect.componentClass;
  }

  /** Get list of accepted commands */
  get accepts(): string[] {
    return this._introspect.accepts ?? [];
  }

  /** Get list of emitted events */
  get emits(): string[] {
    return this._introspect.emits ?? [];
  }

  /** Get the shell tag (custom element name) */
  get shellTag(): string | undefined {
    return this._introspect.shellTag;
  }

  /** Get the module URL for dynamic loading */
  get moduleUrl(): string | undefined {
    return this._introspect.moduleUrl;
  }

  /** Get the full introspect data */
  get introspect(): IIntrospectResult {
    return this._introspect;
  }

  /** Get child proxies (if component has children) */
  get children(): ComponentProxy[] {
    if (!this._children && this._introspect.children) {
      this._children = this._introspect.children.map(
        child => new ComponentProxy(child, this._context)
      );
    }
    return this._children ?? [];
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Remote Communication
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Send a command to the remote component.
   */
  async send<TParams = unknown, TResult = unknown>(
    command: string,
    params?: TParams
  ): Promise<TResult> {
    return RequestReply.execute<TParams | undefined, TResult>(
      this._context,
      this._introspect.mount,
      command,
      params,
      { timeoutMs: 10000 }
    );
  }

  /**
   * Fire-and-forget publish to the component's $inbox.
   */
  publish(command: string, payload?: unknown): void {
    this._context.transport.publish(TopicRouter.inbox(this._introspect.mount), {
      op: command,
      payload: payload ?? {},
    });
  }

  /**
   * Subscribe to an event from the remote component via $events.
   */
  subscribe(
    event: string,
    handler: (payload: unknown, meta: { topic: string; ts: number }) => void
  ): () => void {
    const eventsTopic = TopicRouter.events(this._introspect.mount);
    return this._context.transport.subscribe(eventsTopic, (p, meta) => {
      const envelope = p as IEventEnvelope;
      if (envelope?.op === event) handler(envelope.payload, meta);
    });
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Ghost Rendering
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Check if this component can be rendered (has shell info).
   */
  get canRender(): boolean {
    return !!this._introspect.shellTag;
  }

  /**
   * Render a ghost of this component into a container.
   *
   * The ghost is a local Shell element wired to the remote component
   * via transport. Actions on the ghost control the real component.
   *
   * @param container - The DOM element to render into
   * @param options - Render options
   * @returns The created shell element, or null if rendering is not possible
   */
  async render(
    container: HTMLElement,
    options: IRenderOptions = {}
  ): Promise<HTMLElement | null> {
    const shellTag = this._introspect.shellTag;
    const moduleUrl = this._introspect.moduleUrl;

    if (!shellTag) {
      // eslint-disable-next-line no-console -- kernel component proxy has no actor context when ghost rendering cannot start
      console.warn(
        `[ComponentProxy] Cannot render ${this._introspect.componentClass}: no shellTag`
      );
      return null;
    }

    // Load the module if needed and custom element isn't defined
    if (moduleUrl && !customElements.get(shellTag)) {
      try {
        await import(/* @vite-ignore */ moduleUrl);
        // Wait for element to be defined
        await customElements.whenDefined(shellTag);
      } catch (err) {
        // eslint-disable-next-line no-console -- kernel component proxy has no actor context for dynamic shell load failures
        console.error(
          `[ComponentProxy] Failed to load module for ${shellTag}:`,
          err
        );
        return null;
      }
    }

    // Check if custom element is defined
    if (!customElements.get(shellTag)) {
      // eslint-disable-next-line no-console -- kernel component proxy has no actor context when shell definitions are missing
      console.warn(
        `[ComponentProxy] Cannot render ${shellTag}: custom element not defined. ` +
        `Set static moduleUrl = import.meta.url on the Shell class.`
      );
      return null;
    }

    // Create the shell element
    const ghost = document.createElement(shellTag);

    // Set remote mount attribute for ghost mode
    const remoteMount = options.remoteMount ?? this._introspect.mount;
    ghost.setAttribute('data-remote-mount', remoteMount);

    // Apply additional attributes
    if (options.attributes) {
      for (const [key, value] of Object.entries(options.attributes)) {
        ghost.setAttribute(key, value);
      }
    }

    // Append to container
    container.appendChild(ghost);

    // Mount with a derived context that routes to the remote
    // The shell will use data-remote-mount to wire up remote communication
    const ghostContext = this._context.derive(`ghost-${Date.now()}`);

    // If the element has a mount() method, call it
    const mountable = ghost as IMountableElement | HTMLElement;
    if ('mount' in mountable && typeof mountable.mount === 'function') {
      await (mountable as IMountableElement).mount(ghostContext);
    }

    return ghost;
  }

  /**
   * Render a CLONE of this component into a container.
   *
   * ═══════════════════════════════════════════════════════════════════════════
   * ✅ UI CONTROL INVERSION: Uses transport for replica binding
   * ═══════════════════════════════════════════════════════════════════════════
   *
   * Unlike `render()` which creates a proxy (command forwarding only),
   * `renderClone()` creates a full replica that:
   * - Has its own local mount (for FlowPad-specific addressing)
   * - Binds to the original's topic tree (receives same commands)
   * - Emits to both local and original topic trees
   * - Achieves state convergence via deterministic handlers
   *
   * This is the "true clone" mode from VISION-INLINE-RENDERING.md
   *
   * @param container - The DOM element to render into
   * @param options - Render options
   * @returns The created shell element, or null if rendering is not possible
   */
  async renderClone(
    container: HTMLElement,
    options: IRenderOptions = {}
  ): Promise<HTMLElement | null> {
    // First render the ghost shell at a local mount
    const ghost = await this.render(container, options);
    if (!ghost) return null;

    // ═══════════════════════════════════════════════════════════════════════
    // ✅ NO GHOST CASTING: Use transport to send addReplicaBind command
    // Instead of accessing ghost._component, we send via transport.
    // The shell's component will receive the command via its accepts handler.
    // ═══════════════════════════════════════════════════════════════════════
    const ghostMount = ghost.getAttribute('data-mount');
    if (ghostMount) {
      try {
        // Send addReplicaBind via transport instead of direct method call
        this._context.transport.publish(
          TopicRouter.inbox(ghostMount),
          { op: 'addReplicaBind', payload: { mount: this._introspect.mount } }
        );
      } catch (err) {
        // eslint-disable-next-line no-console -- kernel component proxy has no actor context for replica bind failures
        console.error(
          `[ComponentProxy] Failed to add replica bind for ${this._introspect.mount}:`,
          err
        );
      }
    } else {
      // eslint-disable-next-line no-console -- kernel component proxy has no actor context when clone mounting falls back to proxy mode
      console.warn(
        `[ComponentProxy] renderClone: Ghost element has no data-mount. ` +
        `Falling back to proxy mode.`
      );
    }

    return ghost;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // String Representation
  // ═══════════════════════════════════════════════════════════════════════

  toString(): string {
    return `ComponentProxy<${this._introspect.componentClass}@${this._introspect.mount}>`;
  }

  toJSON(): IIntrospectResult {
    return this._introspect;
  }
}

/**
 * Create a ComponentProxy from introspect data.
 * Convenience function for use in FlowRunner results.
 */
export function createProxy(
  introspect: IIntrospectResult,
  context: IMatrixContext
): ComponentProxy & Record<string, (...args: unknown[]) => Promise<unknown>> {
  return new ComponentProxy(introspect, context).asProxy();
}
