// src/core/eval/strategy/chain.ts
// MiddlewareChain: wraps a RunStrategy with security + observation middleware.
//
// The chain is TRANSPARENT to the strategy — it wraps the Runtime that the
// strategy receives, so from the strategy's perspective it just calls
// runtime.dispatch() as normal.

import type { State, StepOutcome } from "../machine";
import type { Val } from "../values";
import type { Runtime, DispatchResult } from "../runtime";
import type { OpCall } from "../../effects/opcall";
import type {
  RunStrategy,
  RunResult,
  RunOptions,
  StepFn,
  StrategyOptions,
} from "./contract";
import { resolveStrategyOptions } from "./contract";
import type {
  SecurityMiddleware,
  ObservationMiddleware,
} from "./middleware";

/**
 * MiddlewareChain wraps a RunStrategy, injecting security and observation
 * middleware transparently. The strategy never knows middleware exists.
 *
 * Execution order for each Op dispatch:
 *   1. SecurityMiddleware[0..N].checkOp() — any false → deny (throw)
 *   2. ObservationMiddleware[0..N].onOp() — fire-and-forget
 *   3. Real runtime.dispatch() — actual backend handling
 *   4. ObservationMiddleware[N..0].afterOp() — reverse order
 */
export class MiddlewareChain implements RunStrategy {
  readonly name: string;
  readonly description: string;
  readonly interceptedOps: readonly string[];

  constructor(
    private readonly inner: RunStrategy,
    private readonly security: Array<{
      middleware: SecurityMiddleware;
      config: Record<string, unknown>;
    }>,
    private readonly observation: Array<{
      middleware: ObservationMiddleware;
      config: Record<string, unknown>;
    }>,
  ) {
    this.name = `${inner.name}+middleware`;
    this.description = `${inner.description} wrapped with security and observation middleware`;
    this.interceptedOps = inner.interceptedOps;
  }

  async run(
    runtime: Runtime,
    initial: State,
    stepFnOrOptions?: StepFn | StrategyOptions,
    maybeOptions?: RunOptions,
  ): Promise<RunResult> {
    const options = resolveStrategyOptions(stepFnOrOptions, maybeOptions);
    const maybeStepFn = typeof stepFnOrOptions === "function" ? stepFnOrOptions : undefined;

    // Wrap the runtime with middleware interception
    const wrappedRuntime = this.wrapRuntime(runtime);

    // Wrap the stepFn to fire onStep observations when a step function is provided.
    const wrappedStepFn: StepFn | undefined = maybeStepFn
      ? (state: State) => {
          const outcome = maybeStepFn(state);
          for (const { middleware } of this.observation) {
            try {
              middleware.onStep?.(outcome);
            } catch {
              // Observation errors are swallowed — they must not affect execution
            }
          }
          return outcome;
        }
      : undefined;

    try {
      const result = wrappedStepFn
        ? await this.inner.run(wrappedRuntime, initial, wrappedStepFn, options)
        : await this.inner.run(wrappedRuntime, initial, options);

      // Fire onResult observations
      for (const { middleware } of this.observation) {
        try {
          middleware.onResult?.(result);
        } catch {
          // Swallowed
        }
      }

      return result;
    } catch (error) {
      // Fire onError observations
      const err = error instanceof Error ? error : new Error(String(error));
      for (const { middleware } of this.observation) {
        try {
          middleware.onError?.(err);
        } catch {
          // Swallowed
        }
      }
      throw error;
    }
  }

  async dispose(): Promise<void> {
    // Dispose inner strategy
    await this.inner.dispose?.();

    // Dispose all middleware
    for (const { middleware } of this.security) {
      await middleware.dispose?.();
    }
    for (const { middleware } of this.observation) {
      await middleware.dispose?.();
    }
  }

  /**
   * Wrap a Runtime to inject middleware checks on dispatch.
   */
  private wrapRuntime(runtime: Runtime): Runtime {
    const security = this.security;
    const observation = this.observation;

    return {
      async dispatch(state: State, opcall: OpCall): Promise<DispatchResult> {
        // 1. Security checks (all must pass)
        for (const { middleware, config } of security) {
          const allowed = await middleware.checkOp(state, opcall, config);
          if (!allowed) {
            throw new MiddlewareDenied(middleware.name, opcall.op);
          }
        }

        // 2. Fire onOp observations
        for (const { middleware } of observation) {
          try {
            middleware.onOp?.(state, opcall);
          } catch {
            // Swallowed
          }
        }

        // 3. Real dispatch
        const result = await runtime.dispatch(state, opcall);

        // 4. Fire afterOp observations (reverse order)
        if (result !== "Uncaught") {
          for (let i = observation.length - 1; i >= 0; i--) {
            try {
              observation[i].middleware.afterOp?.(state, opcall, result);
            } catch {
              // Swallowed
            }
          }
        }

        return result;
      },
    };
  }
}

/**
 * Error thrown when a SecurityMiddleware denies an operation.
 */
export class MiddlewareDenied extends Error {
  constructor(
    public readonly middlewareName: string,
    public readonly opName: string,
  ) {
    super(`SecurityMiddleware "${middlewareName}" denied operation "${opName}"`);
    this.name = "MiddlewareDenied";
  }
}

/**
 * Create a MiddlewareChain from a strategy and middleware configs.
 * If no middleware is configured, returns the strategy directly (no wrapping).
 */
export function withMiddleware(
  strategy: RunStrategy,
  security: Array<{
    middleware: SecurityMiddleware;
    config: Record<string, unknown>;
  }>,
  observation: Array<{
    middleware: ObservationMiddleware;
    config: Record<string, unknown>;
  }>,
): RunStrategy {
  if (security.length === 0 && observation.length === 0) {
    return strategy; // No wrapping needed
  }
  return new MiddlewareChain(strategy, security, observation);
}
