// src/lisp/vlm/VLMEntryPoint.ts
// VLM Entry Point - User-facing API for distributed evaluation
//
// Users send expressions here. The entry point decides whether to
// parallelize (via atomizer) or evaluate directly (via single node).
//
// Reference: Vision/18-VIRTUAL-LISP-MACHINE.md

import { MatrixActor } from '../../core/MatrixActor';
import { RequestReply } from '../../engine/remoting/RequestReply';
import type { SExpr } from '../SExpr';
import type { IAtomizedMessage } from './IAtomizedMessage';
import { canAtomize, allArgsAreAtoms } from './AtomizationUtils';
import { getVLMNamespace } from './VLMRouter';
import { METACIRCULAR_EVAL } from './MetacircularEval';
import { parseSExprSingle } from '../core/SExprParser';

// Helper to make RPC calls
async function rpc<TReq, TRes>(
  ctx: unknown,
  targetMount: string,
  op: string,
  params: TReq
): Promise<TRes> {
  return RequestReply.execute<TReq, TRes>(
    ctx as Parameters<typeof RequestReply.execute>[0],
    targetMount,
    op,
    params,
    { timeoutMs: 30000 }
  );
}

// Auto-detect and parse S-expression strings
function autoparse(expr: SExpr | string): SExpr {
  if (typeof expr === 'string' && expr.trim().startsWith('(')) {
    return parseSExprSingle(expr);
  }
  return expr;
}

/**
 * VLMEntryPoint - User sends expressions here
 *
 * Provides the main eval interface for the Virtual LISP Machine:
 * - Parses S-expression strings if needed
 * - Decides on parallel vs sequential evaluation
 * - Uses traveling interpreter (METACIRCULAR_EVAL) by default
 * - Supports custom traveling interpreters
 *
 * Mount: {namespace}.vlm-eval
 *
 * @example
 * // With traveling interpreter (default)
 * await callComponent('flowpad.vlm-eval', 'eval', {
 *   expr: '(+ 1 2 (* 3 4))'
 * })
 * // → 15
 *
 * @example
 * // Bootstrap-only (no traveling interpreter)
 * await callComponent('flowpad.vlm-eval', 'eval', {
 *   expr: ['+', 1, 2, 3],
 *   eval: null  // Use bootstrap directly
 * })
 */
export class VLMEntryPoint extends MatrixActor {
  private _pendingEvals: Map<string, {
    resolve: (value: unknown) => void;
    reject: (error: Error) => void;
    timeout: ReturnType<typeof setTimeout>;
  }> = new Map();
  protected _ns!: ReturnType<typeof getVLMNamespace>;
  private _responseTopicBase!: string;

  static accepts = {
    eval: {
      expr: 'any',
      eval: 'any?',
      options: 'object?',
    },
    receiveResult: {
      traceId: 'string',
      result: 'any',
    },
  };

  static emits = {
    evaluationStarted: { traceId: 'string', expr: 'any' },
    evaluationComplete: { traceId: 'string', result: 'any', durationMs: 'number' },
    evaluationError: { traceId: 'string', error: 'string' },
  };

  protected async onBootstrap(): Promise<void> {
    this._ns = getVLMNamespace(this._context!.mount);
    // Derive response topic base from our mount
    const parts = this._context!.mount.split('.');
    parts[parts.length - 1] = parts[parts.length - 1].replace('eval', 'response');
    this._responseTopicBase = parts.join('.');

    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log('[VLMEntryPoint] Initialized at', this._context!.mount);
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log('[VLMEntryPoint] Response base:', this._responseTopicBase);
  }

  async onReceiveResult(params: { traceId: string; result: unknown }): Promise<{ success: boolean }> {
    const { traceId, result } = params;
    const pending = this._pendingEvals.get(traceId);
    if (pending) {
      clearTimeout(pending.timeout);
      this._pendingEvals.delete(traceId);
      pending.resolve(result);
      return { success: true };
    }
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.warn(`[VLMEntryPoint] No pending eval for traceId: ${traceId}`);
    return { success: false };
  }

  async onEval(params: {
    expr: SExpr | string;
    eval?: SExpr | null;  // null = bootstrap only, undefined = METACIRCULAR_EVAL
    options?: { parallel?: boolean; timeout?: number };
  }): Promise<unknown> {
    const traceId = `eval-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
    const startTime = performance.now();
    const expr = autoparse(params.expr);
    const { eval: customEval, options } = params;
    const timeoutMs = options?.timeout || 30000;

    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log(`[VLMEntryPoint] Starting evaluation ${traceId}: ${JSON.stringify(expr)}`);
    this.emit('evaluationStarted', { traceId, expr });

    const responseTopic = `${this._responseTopicBase}.${traceId}`;

    // Set up promise for result
    const resultPromise = new Promise<unknown>((resolve, reject) => {
      const timeout = setTimeout(() => {
        this._pendingEvals.delete(traceId);
        const error = `Evaluation timeout after ${timeoutMs}ms`;
        this.emit('evaluationError', { traceId, error });
        reject(new Error(error));
      }, timeoutMs);

      this._pendingEvals.set(traceId, { resolve, reject, timeout });
    });

    // Prepare message
    // If eval is explicitly null, use bootstrap directly
    // If eval is undefined, use METACIRCULAR_EVAL (traveling interpreter)
    // If eval is a value, use that value
    const travelingEval = customEval === null ? null : (customEval || METACIRCULAR_EVAL);
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log(`[VLMEntryPoint] Using ${travelingEval ? 'traveling interpreter' : 'bootstrap directly'}`);

    const message: IAtomizedMessage = {
      expr,
      eval: travelingEval!,
      continuation: responseTopic,
      trace: { id: traceId, depth: 0, createdAt: Date.now() },
    };

    // Check if parallelizable
    // PERF: Skip atomization for expressions where all args are atoms (no nested calls)
    // For (+ 1 2 3 4 5), parallelizing 5 atomic values has more overhead than just summing locally
    const shouldAtomize = options?.parallel !== false && canAtomize(expr) && !allArgsAreAtoms(expr);

    let result: unknown;
    if (shouldAtomize) {
      // Parallel path - send to atomizer (only for complex nested expressions)
      // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
      console.log(`[VLMEntryPoint] Expression has nested calls, sending to atomizer`);
      await rpc(this._context!, this._ns.atomizer, 'atomize', message);
      result = await resultPromise;
    } else {
      // Sequential path - send directly to a node
      // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
      console.log(`[VLMEntryPoint] Expression not parallelizable, sending to single node`);
      const nodeInfo = await rpc<object, { mount: string } | null>(
        this._context!,
        this._ns.router,
        'getNode',
        {}
      );
      if (nodeInfo) {
        // Clear the pending timeout since we'll get result directly
        const pending = this._pendingEvals.get(traceId);
        if (pending) {
          clearTimeout(pending.timeout);
          this._pendingEvals.delete(traceId);
        }
        result = await rpc(this._context!, nodeInfo.mount, 'evalAtomized', message);
      } else {
        throw new Error('No nodes available');
      }
    }

    const durationMs = performance.now() - startTime;
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log(`[VLMEntryPoint] Evaluation complete ${traceId}: ${JSON.stringify(result)} (${durationMs.toFixed(1)}ms)`);
    this.emit('evaluationComplete', { traceId, result, durationMs });

    return result;
  }
}
