// src/runtime/SchemaEvaluator.ts
//
// Vision 45 §4: The SchemaEvaluator - EVAL for Matrix's Homoiconicity
//
// John McCarthy (LISP Creator, Turing Award):
//   "EVAL is the soul of LISP. It takes quoted expressions and gives them meaning.
//    SchemaEvaluator is Matrix's EVAL - it takes component schemas and gives them life."
//
// Guy Steele (Scheme Design):
//   "The key insight is that code and data share the same representation.
//    SchemaEvaluator bridges the gap - IComponentSchema is the quoted form,
//    MatrixActor is the evaluated form."

import type {
  IComponentSchema,
  IHandlerSchema,
  ITrackSchema,
} from '../serialization/IComponentSchema';
import type { IMatrixContext } from '../engine/core/IMatrixContext';
import type { IMatrixRuntime } from '../engine/core/IMatrixRuntime';
import { MatrixActor } from '../core/MatrixActor';
import { TopicRouter } from '../engine/messaging/TopicRouter';
import type { MxEnvelope } from '../engine/messaging/MxEnvelope';
import { toHandlerCase } from '../engine/utils/NameUtils';

type ShadowRootCarrier = {
  _shellRoot?: ShadowRoot | null;
};

/**
 * Result of schema evaluation.
 */
export interface ISchemaEvalResult {
  /** Mount path of the created component */
  mount: string;

  /** The created component instance */
  component: MatrixActor;

  /** External adapter ID (e.g., shell tag) if applicable */
  adapterId?: string;

  /** Mount paths of child components */
  childMounts: string[];
}

/**
 * Options for schema evaluation.
 */
export interface ISchemaEvalOptions {
  /** Parent context to derive child context from */
  parentContext?: IMatrixContext;

  /** Slot to place component in */
  slot?: string;

  /** Override lifecycle mode */
  lifecycleMode?: 'matrix-owned' | 'external-owned';

  /** Skip shell creation (headless mode) */
  headless?: boolean;
}

/**
 * SchemaEvaluator - The EVAL step of Matrix's homoiconicity.
 *
 * Transforms IComponentSchema (data) into running MatrixActor (execution).
 *
 * TIMING GUARANTEES (Vision 45 §6):
 * 1. Component is ALWAYS created before DOM shell (matrix-owned scenario)
 * 2. Children evaluated in order (serial by default for predictability)
 * 3. Ready promise resolves only when component AND dependencies are ready
 * 4. Parent sees childAdded event BEFORE shell appears in DOM
 *
 * @example
 * ```typescript
 * const evaluator = new SchemaEvaluator(context);
 * const result = await evaluator.eval(schema);
 * console.log(result.mount); // "parent.child"
 * ```
 */
export class SchemaEvaluator {
  private readonly context: IMatrixContext;
  private readonly runtime: IMatrixRuntime;

  constructor(context: IMatrixContext) {
    if (!context.runtime) {
      throw new Error('[SchemaEvaluator] Context must have a runtime');
    }
    this.context = context;
    this.runtime = context.runtime;
  }

  /**
   * Evaluate a schema into a running component.
   *
   * EXECUTION ORDER (critical for timing):
   * 1. Create component instance (before any DOM)
   * 2. Compile and attach handlers
   * 3. Initialize component
   * 4. Evaluate children recursively (components first, then shells)
   * 5. Register external adapter (shell) with existing component
   * 6. Wire bindings
   * 7. Wait for ready based on readyMode
   */
  async eval(
    schema: IComponentSchema,
    options?: ISchemaEvalOptions
  ): Promise<ISchemaEvalResult> {
    const parentContext = options?.parentContext ?? this.context;
    const id = schema.id ?? schema.name.toLowerCase();
    const childContext = schema.mount
      ? this._createContextForMount(schema.mount)
      : parentContext.derive(id);
    const mount = childContext.mount;

    // ═══════════════════════════════════════════════════════════════════
    // PHASE 1: Create component (BEFORE any DOM)
    // ═══════════════════════════════════════════════════════════════════
    const component = await this._createComponent(schema);

    // ═══════════════════════════════════════════════════════════════════
    // PHASE 2: Compile and attach handlers
    // ═══════════════════════════════════════════════════════════════════
    if (schema.handlers) {
      this._attachHandlers(component, schema);
    }

    // ═══════════════════════════════════════════════════════════════════
    // PHASE 3: Initialize component
    // ═══════════════════════════════════════════════════════════════════
    await component.initialize(childContext, id);

    // Apply initial state
    if (schema.state) {
      this._applyState(component, schema.state);
    }

    // ═══════════════════════════════════════════════════════════════════
    // PHASE 4: Evaluate children BEFORE creating shell
    // ═══════════════════════════════════════════════════════════════════
    // This ensures child components exist before their shells
    const childMounts: string[] = [];
    if (schema.children && schema.children.length > 0 && component instanceof MatrixActor) {
      for (const childSchema of schema.children) {
        const childResult = await this.eval(childSchema, {
          parentContext: childContext,
          slot: childSchema.slot,
          lifecycleMode: options?.lifecycleMode ?? 'matrix-owned',
        });
        childMounts.push(childResult.mount);
      }
    }

    // ═══════════════════════════════════════════════════════════════════
    // PHASE 5: Register external adapter (shell) - OPTIONAL
    // ═══════════════════════════════════════════════════════════════════
    let adapterId: string | undefined;
    if (!options?.headless && schema.template) {
      adapterId = await this._registerAdapter(schema, component, options?.slot);
    }

    // ═══════════════════════════════════════════════════════════════════
    // PHASE 6: Wire bindings
    // ═══════════════════════════════════════════════════════════════════
    if (schema.tracks) {
      this._setupTracking(component, schema.tracks, childContext);
    }
    if (schema.bindings) {
      this._setupBindings(component, schema.bindings, childContext);
    }

    // ═══════════════════════════════════════════════════════════════════
    // PHASE 7: Wait for ready
    // ═══════════════════════════════════════════════════════════════════
    await this._waitForReady(component, schema.readyMode ?? 'immediate', childMounts);

    return {
      mount,
      component,
      adapterId,
      childMounts,
    };
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Component Creation Strategies
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Create component from schema.
   *
   * Strategy selection:
   * 1. componentType → Use registered class
   * 2. handlers → Dynamic compilation
   * 3. Neither → Anonymous structural component
   */
  private async _createComponent(schema: IComponentSchema): Promise<MatrixActor> {
    // Strategy 1: Registered component type
    if (schema.componentType) {
      // Registry is on context, not runtime (Vision 45 fix)
      const registry = this.context.registry;
      // Use getConstructor() - ComponentRegistry's method for type lookup
      const ComponentClass = registry?.getConstructor?.(schema.componentType);
      if (ComponentClass) {
        return new ComponentClass() as MatrixActor;
      }
      // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
      console.warn(
        `[SchemaEvaluator] Component type "${schema.componentType}" not found in registry`
      );
    }

    // Strategy 2: Dynamic compilation from handlers
    if (schema.handlers && Object.keys(schema.handlers).length > 0) {
      return this._compileDynamicComponent(schema);
    }

    // Strategy 3: Anonymous structural component (composition only)
    return this._createStructuralComponent(schema);
  }

  /**
   * Compile dynamic component from handlers.
   *
   * Creates an anonymous class with handlers attached.
   */
  private _compileDynamicComponent(schema: IComponentSchema): MatrixActor {
    // Build accepts/emits from schema
    const accepts = schema.accepts ?? {};
    const emits = schema.emits ?? {};
    const hasChildren = schema.children && schema.children.length > 0;

    // Create dynamic class extending MatrixActor (concrete)
    // Note: We create a subclass dynamically and attach metadata via defineProperty
    // to avoid static property type conflicts.
    // State keys are captured so getCognitiveState() can expose them to the oracle.
    const stateKeys = Object.keys(schema.state ?? {});
    class DynamicComponent extends MatrixActor {
      /** Expose shell's shadowRoot so handler code can use this.shadowRoot */
      get shadowRoot(): ShadowRoot | null {
        return (this as unknown as ShadowRootCarrier)._shellRoot ?? null;
      }

      getCognitiveState(): Record<string, unknown> {
        const state: Record<string, unknown> = {};
        const self = this as unknown as Record<string, unknown>;
        for (const key of stateKeys) {
          if (key in self) state[key] = self[key];
        }
        return state;
      }
    }

    // Attach schema metadata dynamically (avoids static type conflicts)
    Object.defineProperty(DynamicComponent, 'schemaAccepts', { value: accepts });
    Object.defineProperty(DynamicComponent, 'schemaEmits', { value: emits });
    // Also set `accepts` and `emits` so MatrixActor's inbox dispatch recognizes domain ops
    Object.defineProperty(DynamicComponent, 'accepts', { value: accepts, configurable: true });
    Object.defineProperty(DynamicComponent, 'emits', { value: emits, configurable: true });
    Object.defineProperty(DynamicComponent, 'template', { value: schema.template });
    Object.defineProperty(DynamicComponent, 'name', { value: `Dynamic${schema.name}`, configurable: true });

    return new DynamicComponent();
  }

  /**
   * Create structural component (no handlers, just composition).
   */
  private _createStructuralComponent(schema: IComponentSchema): MatrixActor {
    // Create structural class extending MatrixActor (concrete)
    // Attach metadata via defineProperty to avoid static type conflicts
    class StructuralComponent extends MatrixActor {}

    Object.defineProperty(StructuralComponent, 'schemaAccepts', { value: schema.accepts ?? {} });
    Object.defineProperty(StructuralComponent, 'schemaEmits', { value: schema.emits ?? {} });
    // Also set `accepts` and `emits` so MatrixActor's inbox dispatch recognizes domain ops
    Object.defineProperty(StructuralComponent, 'accepts', { value: schema.accepts ?? {}, configurable: true });
    Object.defineProperty(StructuralComponent, 'emits', { value: schema.emits ?? {}, configurable: true });
    Object.defineProperty(StructuralComponent, 'template', { value: schema.template });
    Object.defineProperty(StructuralComponent, 'name', { value: `Structural${schema.name}`, configurable: true });

    return new StructuralComponent();
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Handler Compilation
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Attach handlers from schema to component.
   */
  private _attachHandlers(
    component: MatrixActor,
    schema: IComponentSchema
  ): void {
    if (!schema.handlers) return;

    const componentRecord = component as unknown as Record<string, unknown>;

    for (const [commandName, handlerDef] of Object.entries(schema.handlers)) {
      const handler = this._compileHandler(handlerDef, commandName);
      if (handler) {
        // Attach as onCommandName method
        const methodName = `on${toHandlerCase(commandName)}`;
        componentRecord[methodName] = handler;
      }
    }
  }

  /**
   * Compile a handler definition into a callable function.
   */
  private _compileHandler(
    handlerDef: IHandlerSchema | string,
    commandName: string
  ): ((...args: unknown[]) => unknown) | null {
    // Normalize to IHandlerSchema
    const def: IHandlerSchema = typeof handlerDef === 'string'
      ? { code: handlerDef, language: 'javascript' }
      : handlerDef;

    const language = def.language ?? 'javascript';

    if (language === 'javascript') {
      return this._compileJavaScriptHandler(def.code, commandName);
    } else if (language === 'lisp') {
      return this._compileLispHandler(def.code, commandName);
    }

    // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
    console.warn(`[SchemaEvaluator] Unknown handler language: ${language}`);
    return null;
  }

  /**
   * Compile JavaScript handler code.
   */
  private _compileJavaScriptHandler(
    code: string,
    commandName: string
  ): ((...args: unknown[]) => unknown) | null {
    try {
      // The code should be a function body that receives (payload, meta)
      // and has access to 'this' (the component)
      // eslint-disable-next-line no-new-func
      const handler = new Function('payload', 'meta', code);
      return function(this: unknown, payload: unknown, meta: unknown) {
        return handler.call(this, payload, meta);
      };
    } catch (err) {
      // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
      console.error(
        `[SchemaEvaluator] Failed to compile JavaScript handler for '${commandName}':`,
        err
      );
      return null;
    }
  }

  /**
   * Compile LISP handler code.
   *
   * NOTE: Full LISP integration requires the LISP interpreter.
   * This is a stub that logs a warning.
   */
  private _compileLispHandler(
    code: string,
    commandName: string
  ): ((...args: unknown[]) => unknown) | null {
    // TODO: Integrate with LISP interpreter from src/lisp
    // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
    console.warn(
      `[SchemaEvaluator] LISP handler compilation not yet implemented for '${commandName}'`
    );
    return null;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // State Management
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Apply initial state to component.
   */
  private _applyState(component: MatrixActor, state: Record<string, unknown>): void {
    const componentRecord = component as unknown as Record<string, unknown>;
    for (const [key, value] of Object.entries(state)) {
      // Some keys (e.g. "name") collide with read-only getters on Function/HTMLElement.
      // Always use defineProperty to guarantee a writable own-property — this ensures
      // both initial state AND handler code (this.name = val) work without crashing.
      const desc = Object.getOwnPropertyDescriptor(componentRecord, key);
      if (desc && !desc.writable && !desc.set) {
        // Existing non-writable own property — override it
        Object.defineProperty(componentRecord, key, { value, writable: true, enumerable: true, configurable: true });
      } else {
        try {
          componentRecord[key] = value;
        } catch {
          Object.defineProperty(componentRecord, key, { value, writable: true, enumerable: true, configurable: true });
        }
      }
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Adapter Registration
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Register external adapter (shell) for the component.
   */
  private async _registerAdapter(
    schema: IComponentSchema,
    component: MatrixActor,
    slot?: string
  ): Promise<string | undefined> {
    if (!this.runtime.registerExternalAdapter) {
      return undefined;
    }

    const adapterId = `schema-${schema.name.toLowerCase()}`;
    // Cast to concrete constructor - the component was already instantiated so it's not abstract
    const ComponentClass = component.constructor as new () => unknown;

    return this.runtime.registerExternalAdapter(
      adapterId,
      ComponentClass,
      {
        template: schema.template,
        styles: schema.styles,
        tag: adapterId,
      }
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Binding & Tracking
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Set up reactive tracking subscriptions.
   */
  private _setupTracking(
    component: MatrixActor,
    tracks: ITrackSchema[],
    context: IMatrixContext
  ): void {
    const componentRecord = component as unknown as Record<string, unknown>;

    // Group tracks by source mount for efficient single-subscription-per-mount
    const tracksByMount = new Map<string, Array<{ event: string; handler: Function }>>();

    for (const track of tracks) {
      const resolvedMount = this._resolveTrackMount(track.source, context);
      const handler = componentRecord[track.handler];

      if (typeof handler !== 'function') {
        // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
        console.warn(
          `[SchemaEvaluator] Track handler '${track.handler}' not found on component`
        );
        continue;
      }

      let entries = tracksByMount.get(resolvedMount);
      if (!entries) { entries = []; tracksByMount.set(resolvedMount, entries); }
      entries.push({ event: track.event, handler: handler as Function });
    }

    for (const [mount, entries] of tracksByMount) {
      const eventsTopic = TopicRouter.events(mount);
      context.transport.subscribe(eventsTopic, (payload, meta) => {
        const envelope = payload as MxEnvelope;
        if (!envelope?.op) return;
        for (const entry of entries) {
          if (envelope.op === entry.event) {
            try {
              entry.handler.call(component, envelope.payload, meta);
              // Trigger shell DOM update after handler mutates state (FP-0.5)
              if (typeof component._shellUpdateCallback === 'function') {
                component._shellUpdateCallback();
              }
            } catch (err) {
              // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
              console.error(`[SchemaEvaluator] Error in track handler:`, err);
            }
          }
        }
      });
    }
  }

  /**
   * Resolve track source to a mount path.
   *
   * Supported formats:
   *   "/absolute.mount"       → "absolute.mount"
   *   "../sibling-name"       → parent + sibling-name  (sibling resolution)
   *   "../../uncle/cousin"    → grandparent + uncle.cousin
   *   "child-name"            → context.mount + child-name
   */
  private _resolveTrackMount(source: string, context: IMatrixContext): string {
    if (source.startsWith('/')) {
      return source.slice(1);
    }

    // Handle ../ relative paths (sibling/ancestor resolution)
    let mount = context.mount;
    let remaining = source;
    while (remaining.startsWith('../')) {
      remaining = remaining.slice(3);
      const lastDot = mount.lastIndexOf('.');
      mount = lastDot >= 0 ? mount.slice(0, lastDot) : mount;
    }
    // Remove leading / if any after ../ processing
    if (remaining.startsWith('/')) remaining = remaining.slice(1);

    return remaining ? `${mount}.${remaining.replace(/\//g, '.')}` : mount;
  }

  /**
   * Set up event bindings.
   */
  private _setupBindings(
    component: MatrixActor,
    bindings: Record<string, unknown>,
    context: IMatrixContext
  ): void {
    // Bindings are typically wired by the shell/composite
    // This method handles declarative bindings from schema
    // Format: { 'childId.event': 'handlerName' } or { 'childId.event': { event, handler } }

    const componentRecord = component as unknown as Record<string, unknown>;

    // Group bindings by childMount for efficient single-subscription-per-mount
    const bindingsByMount = new Map<string, Map<string, Function>>();

    for (const [key, value] of Object.entries(bindings)) {
      const [childId, event] = key.split('.');
      const handlerName = typeof value === 'string' ? value : (value as { handler?: string })?.handler;

      if (!handlerName) continue;

      const childMount = `${context.mount}.${childId}`;
      const handler = componentRecord[handlerName];

      if (typeof handler !== 'function') {
        // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
        console.warn(
          `[SchemaEvaluator] Binding handler '${handlerName}' not found on component`
        );
        continue;
      }

      let eventMap = bindingsByMount.get(childMount);
      if (!eventMap) { eventMap = new Map(); bindingsByMount.set(childMount, eventMap); }
      eventMap.set(event, handler as Function);
    }

    for (const [childMount, eventMap] of bindingsByMount) {
      const eventsTopic = TopicRouter.events(childMount);
      context.transport.subscribe(eventsTopic, (payload, meta) => {
        const envelope = payload as MxEnvelope;
        if (!envelope?.op) return;
        const handler = eventMap.get(envelope.op);
        if (!handler) return;
        try {
          handler.call(component, envelope.payload, meta);
        } catch (err) {
          // eslint-disable-next-line no-console -- kernel infrastructure, no actor context
          console.error(`[SchemaEvaluator] Error in binding handler:`, err);
        }
      });
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Ready State
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Wait for component to be ready based on readyMode.
   */
  private async _waitForReady(
    component: MatrixActor,
    readyMode: 'immediate' | 'children' | 'custom',
    childMounts: string[]
  ): Promise<void> {
    switch (readyMode) {
      case 'immediate':
        // Already ready
        return;

      case 'children':
        // Wait for all children (they were already evaluated, so just resolve)
        // In a full implementation, we'd track child ready states
        return;

      case 'custom':
        // Wait for component to emit 'ready'
        // This requires subscribing to the component's ready event
        // For now, return immediately (component should signal when ready)
        return;
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Utility Methods
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Create a context for an absolute mount path.
   */
  private _createContextForMount(mount: string): IMatrixContext {
    // Create a new context with the specified mount
    return {
      ...this.context,
      mount,
    };
  }
}
