// src/lisp/vlm/VLMJoinCoordinator.ts
// VLM Join Coordinator - Collects parallel results
//
// Waits for N slot results from atomized children, then combines them
// using the parent operator and sends the result to the continuation.
//
// 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 { IJoinState } from './IJoinState';
import type { IVLMTrace } from './IVLMTrace';
import { getVLMNamespace } from './VLMRouter';

// Debug flag - set to true for verbose logging (EXPENSIVE - uses JSON.stringify)
const VLM_DEBUG = false;

// 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 }
  );
}

/**
 * VLMJoinCoordinator - Waits for N slot results, then continues computation
 *
 * When all children complete, combines their results using the parent operator
 * and sends the final result to either:
 * - A parent join (for nested parallel operations)
 * - The entry point (for top-level results)
 *
 * Mount: {namespace}.vlm-join
 */
export class VLMJoinCoordinator extends MatrixActor {
  static accepts = {
    createJoin: {
      id: 'string',
      totalSlots: 'number',
      parentOp: 'string',
      parentContinuation: 'string',
      travelingEval: 'any',
      trace: 'object?',
    },
    slotResult: {
      joinId: 'string',
      slot: 'number',
      value: 'any',
    },
    getJoinState: { joinId: 'string' },
    listJoins: {},
  };

  static emits = {
    joinCreated: { id: 'string', totalSlots: 'number' },
    slotReceived: { joinId: 'string', slot: 'number', value: 'any', received: 'number', total: 'number' },
    joinComplete: { joinId: 'string', result: 'any', durationMs: 'number' },
    joinTimeout: { joinId: 'string', receivedSlots: 'array', missingSlots: 'array' },
  };

  private _joins: Map<string, IJoinState> = new Map();
  private _timeoutMs = 30000;
  protected _ns!: ReturnType<typeof getVLMNamespace>;

  protected async onBootstrap(): Promise<void> {
    this._ns = getVLMNamespace(this._context!.mount);
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log('[VLMJoinCoordinator] Initialized at', this._context!.mount);
  }

  async onCreateJoin(params: {
    id: string;
    totalSlots: number;
    parentOp: string;
    parentContinuation: string;
    travelingEval: SExpr;
    trace?: IVLMTrace;
  }): Promise<{ success: boolean }> {
    const state: IJoinState = {
      id: params.id,
      totalSlots: params.totalSlots,
      receivedSlots: new Map(),
      parentOp: params.parentOp,
      parentContinuation: params.parentContinuation,
      travelingEval: params.travelingEval,
      createdAt: Date.now(),
      timeout: this._timeoutMs,
      trace: params.trace,
    };

    this._joins.set(params.id, state);
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    if (VLM_DEBUG) console.log(`[VLMJoinCoordinator] Join created: ${params.id} waiting for ${params.totalSlots} slots`);
    this.emit('joinCreated', { id: params.id, totalSlots: params.totalSlots });

    // Set timeout
    setTimeout(() => this._checkTimeout(params.id), this._timeoutMs);

    return { success: true };
  }

  async onSlotResult(params: { joinId: string; slot: number; value: unknown }): Promise<void> {
    const state = this._joins.get(params.joinId);
    if (!state) {
      // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
      console.warn(`[VLMJoinCoordinator] Unknown join: ${params.joinId}`);
      return;
    }

    // Store result
    state.receivedSlots.set(params.slot, params.value);
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    if (VLM_DEBUG) console.log(`[VLMJoinCoordinator] Slot ${params.slot} received for ${params.joinId}: ${JSON.stringify(params.value)} (${state.receivedSlots.size}/${state.totalSlots})`);

    this.emit('slotReceived', {
      joinId: params.joinId,
      slot: params.slot,
      value: params.value,
      received: state.receivedSlots.size,
      total: state.totalSlots,
    });

    // Check if complete
    if (state.receivedSlots.size === state.totalSlots) {
      await this._completeJoin(state);
    }
  }

  async onGetJoinState(params: { joinId: string }): Promise<{
    id: string;
    totalSlots: number;
    receivedCount: number;
    slots: Record<string, unknown>;
  } | null> {
    const state = this._joins.get(params.joinId);
    if (!state) return null;
    return {
      id: state.id,
      totalSlots: state.totalSlots,
      receivedCount: state.receivedSlots.size,
      slots: Object.fromEntries(state.receivedSlots),
    };
  }

  async onListJoins(): Promise<{ joins: Array<{
    id: string;
    totalSlots: number;
    receivedCount: number;
    createdAt: number;
  }> }> {
    return {
      joins: [...this._joins.values()].map(j => ({
        id: j.id,
        totalSlots: j.totalSlots,
        receivedCount: j.receivedSlots.size,
        createdAt: j.createdAt,
      }))
    };
  }

  private async _completeJoin(state: IJoinState): Promise<void> {
    const durationMs = Date.now() - state.createdAt;

    // Collect values in slot order
    const values: unknown[] = [];
    for (let i = 0; i < state.totalSlots; i++) {
      values.push(state.receivedSlots.get(i));
    }

    // Compute the final result
    const result = this._evalPrimitive(state.parentOp, values);
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    if (VLM_DEBUG) console.log(`[VLMJoinCoordinator] Join complete: ${state.id} - (${state.parentOp} ${values.join(' ')}) = ${result}`);

    // Clean up
    this._joins.delete(state.id);

    this.emit('joinComplete', { joinId: state.id, result, durationMs });

    // Send result based on continuation type
    if (state.parentContinuation.startsWith('join:')) {
      // Nested join: format is "join:${parentJoinId}:${slot}"
      const parts = state.parentContinuation.split(':');
      const parentJoinId = parts[1];
      const parentSlot = parseInt(parts[2], 10);
      // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
      if (VLM_DEBUG) console.log(`[VLMJoinCoordinator] Nested join complete, reporting to parent ${parentJoinId} slot ${parentSlot}`);
      await rpc(this._context!, this._ns.join, 'slotResult', {
        joinId: parentJoinId,
        slot: parentSlot,
        value: result,
      }).catch(err => {
        // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
        console.error(`[VLMJoinCoordinator] Failed to report to parent join: ${err}`);
      });
    } else {
      // Top-level join: continuation is response topic
      const traceId = state.parentContinuation.split('.').pop();
      if (traceId) {
        await rpc(this._context!, this._ns.eval, 'receiveResult', {
          traceId,
          result,
        }).catch(err => {
          // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
          console.error(`[VLMJoinCoordinator] Failed to send result: ${err}`);
        });
      }
    }
  }

  private _evalPrimitive(op: string, values: unknown[]): unknown {
    switch (op) {
      case '+': return (values as number[]).reduce((a, b) => a + b, 0);
      case '*': return (values as number[]).reduce((a, b) => a * b, 1);
      case 'and': return values.every(Boolean);
      case 'or': return values.some(Boolean);
      case 'list': return values;
      case 'parallel': return values;
      default:
        // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
        console.warn(`[VLMJoinCoordinator] Unknown op: ${op}, returning list`);
        return values;
    }
  }

  private _checkTimeout(joinId: string): void {
    const state = this._joins.get(joinId);
    if (!state) return; // Already completed

    const received = [...state.receivedSlots.keys()];
    const missing: number[] = [];
    for (let i = 0; i < state.totalSlots; i++) {
      if (!state.receivedSlots.has(i)) missing.push(i);
    }

    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.error(`[VLMJoinCoordinator] Join TIMEOUT: ${joinId} - missing slots: ${missing.join(', ')}`);
    this.emit('joinTimeout', { joinId, receivedSlots: received, missingSlots: missing });
    this._joins.delete(joinId);
  }
}
