// src/runtime/BrowserDomStrategy.ts
// Browser DOM strategy for MatrixRuntime.
// Handles custom element registration, creation, mounting, and unmounting.

import type { IMatrixContext } from '../engine/core/IMatrixContext';
import type { IDomStrategy } from './IDomStrategy';
import { ShellElementRegistry } from './ComponentRegistry';
import { HeadlessHost } from './HeadlessHost';
import type { IRuntimeHost } from './IRuntimeHost';
import type { IRuntimeLogger } from './IRuntimeLogger';
import { ConsoleLogger, NullLogger } from './ConsoleLogger';

/**
 * Configuration for BrowserDomStrategy.
 */
export interface IBrowserDomStrategyConfig {
  /** Optional runtime host (defaults to HeadlessHost which auto-detects DOM). */
  host?: IRuntimeHost;
  /** Enable logging. */
  logging?: boolean;
}

/**
 * BrowserDomStrategy — IDomStrategy implementation for browser environments.
 *
 * Encapsulates all DOM concerns:
 * - Custom element registration via ShellElementRegistry
 * - Element creation (prefers constructor over document.createElement)
 * - Element mounting (duck-types mount/setContext/fallback)
 * - Element unmounting (DOM removal)
 *
 * The MatrixRuntime delegates to this when domStrategy is provided.
 * When domStrategy is null/undefined, runtime operates headless.
 */
export class BrowserDomStrategy implements IDomStrategy {
  private readonly host: IRuntimeHost;
  private readonly registry: ShellElementRegistry;
  private readonly logger: IRuntimeLogger;

  constructor(config?: IBrowserDomStrategyConfig) {
    this.host = config?.host ?? new HeadlessHost();
    this.logger = config?.logging
      ? new ConsoleLogger(true, '[BrowserDomStrategy]')
      : new NullLogger();
    this.registry = new ShellElementRegistry(this.host.customElements, this.logger);
  }

  registerElement(tag: string, constructor: CustomElementConstructor): void {
    const errors = this.registry.validate(tag, constructor);
    if (errors.length) {
      throw new Error(`Component validation failed: ${errors.join(', ')}`);
    }
    this.registry.register(tag, constructor);
  }

  isElementRegistered(tag: string): boolean {
    return this.registry.isRegistered(tag);
  }

  createElement(tag: string, attributes?: Record<string, string>): HTMLElement {
    if (!this.registry.isRegistered(tag)) {
      throw new Error(`Component '${tag}' is not registered`);
    }

    // Prefer constructing from the registered custom element class.
    // This avoids environments where document.createElement doesn't
    // invoke the custom element constructor (common in headless/mocked DOMs).
    const ctor = this.registry.getConstructor(tag);
    let element: HTMLElement;

    if (ctor) {
      element = new ctor() as unknown as HTMLElement;
    } else {
      element = this.host.createElement(tag) as unknown as HTMLElement;
    }

    if (attributes) {
      for (const [name, value] of Object.entries(attributes)) {
        if (typeof element.setAttribute === 'function') {
          element.setAttribute(name, value);
        } else {
          (element as unknown as Record<string, string>)[name] = value;
        }
      }
    }

    this.logger.debug(`Created element: ${tag}`, attributes);
    return element;
  }

  async mountElement(element: HTMLElement, context: IMatrixContext): Promise<void> {
    const tag = element.localName;

    // Duck-type detection for different element patterns
    const el = element as unknown as Record<string, unknown>;

    // Matrix element: mount(context) + ready
    const hasMount = typeof el.mount === 'function' && typeof el._receiveContext === 'function';
    // FabricElement: setContext() before appendChild
    const hasSetContext = typeof el.setContext === 'function';

    if (hasMount) {
      this.host.document.body.appendChild(element);
      await (el.mount as (ctx: IMatrixContext) => Promise<void>)(context);
      await (el.ready as Promise<void>);
    } else if (hasSetContext) {
      (el.setContext as (ctx: IMatrixContext) => void)(context);
      this.host.document.body.appendChild(element);
      const readyPromise = el.ready;
      if (readyPromise && typeof (readyPromise as Promise<void>).then === 'function') {
        await (readyPromise as Promise<void>);
      }
    } else {
      // Fallback: just append
      this.host.document.body.appendChild(element);
    }

    const mountPath = context.mount;
    this.registry.trackInstance(tag, element, { context: { mount: mountPath } }, mountPath);
    this.logger.info(`Mounted element: ${tag} at ${mountPath}`);
  }

  async unmountElement(element: HTMLElement): Promise<void> {
    const tag = element.localName;
    const el = element as unknown as Record<string, unknown>;
    const mountPath = (el.context as { mount?: string } | undefined)?.mount ?? 'unknown';

    if (element.parentNode) {
      element.parentNode.removeChild(element);
    }

    this.registry.untrackInstance(element);
    this.logger.info(`Unmounted element: ${tag} from ${mountPath}`);
  }

  /** Get the underlying host (for shutdown cleanup). */
  getHost(): IRuntimeHost {
    return this.host;
  }

  /** Get the element registry (for stats, tracking, etc.). */
  getRegistry(): ShellElementRegistry {
    return this.registry;
  }

  /** Dispose the host. */
  dispose(): void {
    this.host.dispose();
  }
}
