// src/flow/FlowBuilder.ts

import type { IMatrixContext } from '../engine/core/IMatrixContext';
import {
  createFlowContext,
  FlowContext,
  type IFlowContextData,
} from './FlowContext';
import type {
  AnyStage,
  ILocalMapStage,
  ILocalFilterStage,
  IRemoteRRStage,
  IRemoteSinkStage,
} from './FlowTypes';
import type { IFlowPlan } from './FlowPlan';
import { FlowEngine } from './FlowEngine';
import type { IMatrixComponent } from './IMatrixComponent';
import { createComponent } from './MatrixComponentImpl';
import { ShellRegistry } from './ShellRegistry';

/**
 * Helper type for objects that may have shell information.
 */
interface IShellableData {
  componentClass?: string;
  shellTag?: string;
  moduleUrl?: string;
  [key: string]: unknown;
}

function newStageId(): string {
  return `stage-${Date.now().toString(36)}-${Math.random()
    .toString(36)
    .slice(2)}`;
}

/**
 * Fluent builder for distributed flows over Matrix.
 *
 * Example:
 *
 *   const result = await FlowBuilder
 *     .start<string>(context)
 *     .fromValue("SELECT * FROM Items")
 *     .viaRemote("domain.db", "Query")
 *     .mapLocal(rows => rows.filter(x => x.score > 0.9))
 *     .toRemoteSink("domain.ui", "Render")
 *     .run();
 */
export class FlowBuilder<TIn, TCurrent> {
  private readonly context: IMatrixContext;
  private readonly ctx: FlowContext;
  private readonly id: string;
  private readonly stages: AnyStage[];
  private readonly seedValue?: TIn;

  private constructor(
    context: IMatrixContext,
    ctx: FlowContext,
    id: string,
    stages: AnyStage[] = [],
    seedValue?: TIn
  ) {
    this.context = context;
    this.ctx = ctx;
    this.id = id;
    this.stages = stages;
    this.seedValue = seedValue;
  }

  /**
   * Entry point: create a builder tied to a context.
   */
  static start<TIn = unknown>(
    context: IMatrixContext,
    ctxSeed?: FlowContext | Partial<IFlowContextData>
  ): FlowBuilder<TIn, TIn> {
    const ctx =
      ctxSeed instanceof FlowContext
        ? ctxSeed
        : createFlowContext(ctxSeed as Partial<IFlowContextData> | undefined);

    const id = `flow-${Date.now().toString(36)}-${Math.random()
      .toString(36)
      .slice(2)}`;

    return new FlowBuilder<TIn, TIn>(context, ctx, id, [], undefined);
  }

  /**
   * Attach an initial value to the flow (similar to a LINQ source).
   */
  fromValue(value: TIn): FlowBuilder<TIn, TCurrent> {
    return new FlowBuilder<TIn, TCurrent>(
      this.context,
      this.ctx,
      this.id,
      this.stages,
      value
    );
  }

  /**
   * Local synchronous/async projection.
   */
  mapLocal<TNext>(
    fn: (input: TCurrent) => TNext | Promise<TNext>
  ): FlowBuilder<TIn, TNext> {
    const stage: ILocalMapStage<TCurrent, TNext> = {
      id: newStageId(),
      kind: 'local-map',
      fn,
    };

    return new FlowBuilder<TIn, TNext>(
      this.context,
      this.ctx,
      this.id,
      [...this.stages, stage],
      this.seedValue
    );
  }

  /**
   * Local synchronous/async gate (whole-value filter).
   */
  filterLocal(
    predicate: (input: TCurrent) => boolean | Promise<boolean>
  ): FlowBuilder<TIn, TCurrent> {
    const stage: ILocalFilterStage<TCurrent> = {
      id: newStageId(),
      kind: 'local-filter',
      fn: predicate,
    };

    return new FlowBuilder<TIn, TCurrent>(
      this.context,
      this.ctx,
      this.id,
      [...this.stages, stage],
      this.seedValue
    );
  }

  /**
   * Remote request/response invocation as a pipeline stage.
   */
  viaRemote<TRes = unknown>(
    mount: string,
    op: string,
    timeoutMs?: number
  ): FlowBuilder<TIn, TRes> {
    const stage: IRemoteRRStage<TCurrent, TRes> = {
      id: newStageId(),
      kind: 'remote-rr',
      mount,
      op,
      timeoutMs,
    };

    return new FlowBuilder<TIn, TRes>(
      this.context,
      this.ctx,
      this.id,
      [...this.stages, stage],
      this.seedValue
    );
  }

  /**
   * Remote sink: RR side-effect that must succeed; value flows through unchanged.
   */
  toRemoteSink(
    mount: string,
    op: string,
    timeoutMs?: number
  ): FlowBuilder<TIn, TCurrent> {
    const stage: IRemoteSinkStage<TCurrent> = {
      id: newStageId(),
      kind: 'remote-sink',
      mount,
      op,
      timeoutMs,
    };

    return new FlowBuilder<TIn, TCurrent>(
      this.context,
      this.ctx,
      this.id,
      [...this.stages, stage],
      this.seedValue
    );
  }

  /**
   * Materialize a IFlowPlan for inspection or deferred execution.
   */
  build(): IFlowPlan<TIn, TCurrent> {
    return {
      id: this.id,
      context: this.ctx,
      stages: [...this.stages],
    };
  }

  /**
   * Execute the flow immediately via FlowEngine.
   */
  async run(input?: TIn): Promise<TCurrent> {
    const engine = new FlowEngine(this.context);
    const effectiveInput =
      input !== undefined ? input : (this.seedValue as TIn | undefined);

    if (effectiveInput === undefined) {
      throw new Error('[FlowBuilder] No input supplied; call fromValue(...) or pass input to run(...)');
    }

    return engine.run(this.build(), effectiveInput as TIn);
  }

  /**
   * Execute the flow and wrap the result as IMatrixComponent.
   *
   * This is the strongly-typed way to get a component from a flow.
   * The return type tells you it's a component - no attribute checking needed.
   * Like LINQ returns IEnumerable<T>, this returns IMatrixComponent.
   *
   * If the result doesn't have shell info, it looks up the ShellRegistry
   * by componentClass to find rendering info.
   *
   * @example
   * const tree: IMatrixComponent = await flow()
   *   .fromValue({})
   *   .viaRemote("app.tree", "introspect")
   *   .runAsComponent();
   *
   * // Now you have strongly-typed component access
   * const child = await tree.child("root-1");
   * await child.send("setLabel", { label: "New" });
   * await child.render(container);
   */
  async runAsComponent(input?: TIn): Promise<IMatrixComponent> {
    const result = await this.run(input);

    // If result doesn't have shellTag, try to look it up by type
    if (result && typeof result === 'object') {
      const data = result as IShellableData;
      if (!data.shellTag && data.componentClass) {
        const info = ShellRegistry.lookup(data.componentClass);
        if (info) {
          data.shellTag = info.shellTag;
          data.moduleUrl = data.moduleUrl ?? info.moduleUrl;
        }
      }
    }

    return createComponent(this.context, result);
  }

  /**
   * Transform the current value into an IMatrixComponent.
   *
   * Use this when you want to continue the flow but need component access.
   * The returned builder has IMatrixComponent as its current type.
   *
   * @example
   * const flow = FlowBuilder
   *   .start(context)
   *   .fromValue({})
   *   .viaRemote("app.tree", "introspect")
   *   .asComponent()  // TCurrent is now IMatrixComponent
   *   .mapLocal(comp => comp.child("root-1"));  // Returns Promise<IMatrixComponent>
   */
  asComponent(): FlowBuilder<TIn, IMatrixComponent> {
    return this.mapLocal((value: TCurrent) => {
      const data = value as IShellableData;
      // Look up shell info by type if not present
      if (!data.shellTag && data.componentClass) {
        const info = ShellRegistry.lookup(data.componentClass);
        if (info) {
          data.shellTag = info.shellTag;
          data.moduleUrl = data.moduleUrl ?? info.moduleUrl;
        }
      }
      return createComponent(this.context, data);
    });
  }
}
