// src/lisp/core/StreamRuntime.ts
// Stream Runtime - Reactive stream execution engine
//
// Implements the full reactive stream runtime for LISP-on-Protocol.
// Handles stream activation, deactivation, and operator processing.
//
// Reference: Vision/17-REACTIVE-STREAMS.md

import type { IStream } from '../IStream';
import type { IStreamOperator } from '../IStreamOperator';
import type { IActiveSubscription } from '../IActiveSubscription';
import type { ILambdaClosure } from '../ILambdaClosure';
import type { ITransportAdapter } from '../../engine/remoting/ITransportAdapter';
import { getLispConfig } from '../config';

/**
 * Lambda application function type
 * This should be provided by the evaluator context
 */
export type ApplyLambdaFn = (fn: ILambdaClosure, args: unknown[]) => Promise<unknown>;

/**
 * Event emitter function type
 */
export type EmitFn = (event: string, data: Record<string, unknown>) => void;

/**
 * StreamRuntime - Manages reactive stream subscriptions and processing
 *
 * Extracted from LispServices.ts to provide standalone stream runtime.
 */
export class StreamRuntime {
  private _activeSubscriptions: Map<string, IActiveSubscription> = new Map();
  private _subscriptionCounter = 0;
  private _transport: ITransportAdapter;
  private _applyLambda: ApplyLambdaFn;
  private _emit: EmitFn;

  constructor(
    transport: ITransportAdapter,
    applyLambda: ApplyLambdaFn,
    emit: EmitFn
  ) {
    this._transport = transport;
    this._applyLambda = applyLambda;
    this._emit = emit;
  }

  /**
   * Get the number of active subscriptions
   */
  get activeCount(): number {
    return this._activeSubscriptions.size;
  }

  /**
   * Get all active subscription IDs
   */
  get activeIds(): string[] {
    return [...this._activeSubscriptions.keys()];
  }

  /**
   * Activate a stream pipeline - subscribe to source and process through operators
   * Returns a subscription ID that can be used to unsubscribe later
   */
  async activateStream(stream: IStream, outputTopic?: string): Promise<string> {
    const config = getLispConfig();

    // Check subscription limit
    if (this._activeSubscriptions.size >= config.maxSubscriptions) {
      throw new Error(`Maximum subscriptions (${config.maxSubscriptions}) exceeded`);
    }

    const subId = `stream-${++this._subscriptionCounter}`;
    const unsubs: (() => void)[] = [];
    const state: Record<string, unknown> = {};

    // Initialize state for operators that need it
    for (let i = 0; i < stream.operators.length; i++) {
      const op = stream.operators[i];
      if (op.type === 'scan') {
        state[`scan_${i}`] = op.initial;
      }
      if (op.type === 'distinct') {
        state[`distinct_${i}`] = Symbol('uninitialized');
      }
      if (op.type === 'take') {
        state[`take_${i}`] = 0;
      }
      if (op.type === 'debounce' || op.type === 'throttle') {
        state[`timer_${i}`] = null;
        state[`lastEmit_${i}`] = 0;
      }
    }

    // Process a value through the operator pipeline
    const processValue = async (value: unknown): Promise<void> => {
      let current = value;
      let shouldEmit = true;

      for (let i = 0; i < stream.operators.length && shouldEmit; i++) {
        const op = stream.operators[i];

        switch (op.type) {
          case 'map':
            if (op.fn) {
              current = await this._applyLambda(op.fn, [current]);
            }
            break;

          case 'filter':
            if (op.fn) {
              const keep = await this._applyLambda(op.fn, [current]);
              if (!keep) shouldEmit = false;
            }
            break;

          case 'scan':
            if (op.fn) {
              const acc = state[`scan_${i}`];
              current = await this._applyLambda(op.fn, [acc, current]);
              state[`scan_${i}`] = current;
            }
            break;

          case 'distinct': {
            const prev = state[`distinct_${i}`];
            if (JSON.stringify(prev) === JSON.stringify(current)) {
              shouldEmit = false;
            } else {
              state[`distinct_${i}`] = current;
            }
            break;
          }

          case 'take':
            state[`take_${i}`] = (state[`take_${i}`] as number) + 1;
            if ((state[`take_${i}`] as number) > (op.n ?? op.count ?? 0)) {
              shouldEmit = false;
              // Auto-unsubscribe when take limit reached
              this.deactivateStream(subId);
            }
            break;

          case 'tap':
            // Side effect only, don't change value
            if (op.fn) {
              await this._applyLambda(op.fn, [current]);
            }
            break;

          case 'debounce': {
            // Clear previous timer
            const prevTimer = state[`timer_${i}`];
            if (prevTimer) {
              clearTimeout(prevTimer as ReturnType<typeof setTimeout>);
            }
            // Set new timer - emit after silence
            const debounceValue = current;
            state[`timer_${i}`] = setTimeout(async () => {
              if (outputTopic) {
                this._transport.publish(outputTopic, debounceValue);
              }
              this._emit('stream.value', { subscriptionId: subId, value: debounceValue });
            }, op.ms ?? 0);
            shouldEmit = false;  // Don't emit immediately
            break;
          }

          case 'throttle': {
            const now = Date.now();
            const lastEmit = (state[`lastEmit_${i}`] as number) || 0;
            if (now - lastEmit < (op.ms ?? 0)) {
              shouldEmit = false;  // Too soon, skip
            } else {
              state[`lastEmit_${i}`] = now;
            }
            break;
          }

          case 'skip':
            if (!state[`skip_${i}`]) {
              state[`skip_${i}`] = 0;
            }
            state[`skip_${i}`] = (state[`skip_${i}`] as number) + 1;
            if ((state[`skip_${i}`] as number) <= (op.n ?? op.count ?? 0)) {
              shouldEmit = false;
            }
            break;

          case 'delay':
            await new Promise(resolve => setTimeout(resolve, op.ms ?? 0));
            break;

          case 'buffer':
            if (!state[`buffer_${i}`]) {
              state[`buffer_${i}`] = [];
            }
            (state[`buffer_${i}`] as unknown[]).push(current);
            if ((state[`buffer_${i}`] as unknown[]).length >= (op.count ?? 1)) {
              current = state[`buffer_${i}`];
              state[`buffer_${i}`] = [];
            } else {
              shouldEmit = false;
            }
            break;
        }
      }

      // Emit the final value (if not handled by debounce)
      if (shouldEmit) {
        if (outputTopic) {
          this._transport.publish(outputTopic, current);
        }
        this._emit('stream.value', { subscriptionId: subId, value: current });
      }
    };

    // Subscribe to source(s)
    if (typeof stream.source === 'string') {
      // Single topic source
      const unsub = this._transport.subscribe(stream.source, async (msg: unknown) => {
        try {
          await processValue(msg);
        } catch (err) {
          // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
          console.error(`[IStream ${subId}] Error processing value:`, err);
          this._emit('stream.error', { subscriptionId: subId, error: (err as Error).message });
        }
      });
      unsubs.push(unsub);
    } else if (Array.isArray(stream.source)) {
      // Multiple streams (merge)
      const activateNestedIStream = async (nested: IStream): Promise<void> => {
        if (typeof nested.source === 'string') {
          const unsub = this._transport.subscribe(nested.source, async (msg: unknown) => {
            // Process through nested operators first
            let current = msg;
            for (const op of nested.operators) {
              if (op.type === 'map' && op.fn) {
                current = await this._applyLambda(op.fn, [current]);
              } else if (op.type === 'filter' && op.fn) {
                if (!await this._applyLambda(op.fn, [current])) return;
              }
            }
            // Then through main pipeline
            await processValue(current);
          });
          unsubs.push(unsub);
        }
      };

      for (const nestedIStream of stream.source as IStream[]) {
        await activateNestedIStream(nestedIStream);
      }
    }

    // Store subscription
    const subscription: IActiveSubscription = {
      id: subId,
      topic: typeof stream.source === 'string' ? stream.source : 'merged',
      stream,
      outputTopic,
      unsubs,
      state,
      unsubscribe: () => this.deactivateStream(subId),
      createdAt: Date.now(),
    };
    this._activeSubscriptions.set(subId, subscription);

    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log(`[StreamRuntime] Stream activated: ${subId}${outputTopic ? ` -> ${outputTopic}` : ''}`);
    this._emit('stream.subscribed', { subscriptionId: subId, outputTopic: outputTopic ?? null });

    return subId;
  }

  /**
   * Deactivate a stream subscription
   */
  deactivateStream(subId: string): boolean {
    const sub = this._activeSubscriptions.get(subId);
    if (!sub) {
      return false;
    }

    // Clear any pending timers
    if (sub.state) {
      for (const [key, value] of Object.entries(sub.state)) {
        if (key.startsWith('timer_') && value) {
          clearTimeout(value as ReturnType<typeof setTimeout>);
        }
      }
    }

    // Unsubscribe from all sources
    if (sub.unsubs) {
      for (const unsub of sub.unsubs) {
        unsub();
      }
    }

    this._activeSubscriptions.delete(subId);
    // eslint-disable-next-line no-console -- standalone lisp runtime, no actor context
    console.log(`[StreamRuntime] Stream deactivated: ${subId}`);
    this._emit('stream.unsubscribed', { subscriptionId: subId });

    return true;
  }

  /**
   * Deactivate all streams
   */
  deactivateAll(): number {
    const count = this._activeSubscriptions.size;
    for (const subId of this._activeSubscriptions.keys()) {
      this.deactivateStream(subId);
    }
    return count;
  }

  /**
   * Get subscription info
   */
  getSubscription(subId: string): IActiveSubscription | undefined {
    return this._activeSubscriptions.get(subId);
  }

  /**
   * List all active subscriptions
   */
  listSubscriptions(): IActiveSubscription[] {
    return [...this._activeSubscriptions.values()];
  }
}
