// src/framework/shadowlisp/SlEffects.ts
//
// Vision 48: Algebraic Effects for ShadowLisp
//
// Andrej Bauer & Matija Pretnar (Effect handlers creators):
//   "Algebraic effects separate the description of a side effect from
//    its implementation. Perform describes what you need. Handle provides it."
//
// Daan Leijen (Koka language, effect types):
//   "Effects are like resumable exceptions. You can catch an effect,
//    handle it, and optionally resume the computation that raised it."
//
// This provides algebraic effects for ShadowLisp actors.

import type { SExpr } from '../../lisp/SExpr';

/**
 * Effect operation descriptor.
 */
export interface IEffectOp {
  name: string;
  params: string[];
}

/**
 * Effect handler function.
 * Takes effect payload and resumption continuation.
 */
export type EffectHandler = (
  payload: unknown,
  resume: (value: unknown) => unknown
) => unknown;

/**
 * Effect signature - describes what effects a computation might perform.
 */
export interface IEffectSignature {
  name: string;
  operations: IEffectOp[];
}

/**
 * Handler definition - maps effect operations to handlers.
 */
export interface IHandlerDef {
  effect: string;
  operations: Map<string, EffectHandler>;
  returnHandler?: (value: unknown) => unknown;
}

/**
 * Performed effect - raised when an effect is performed.
 */
export class PerformedEffect {
  constructor(
    public readonly effect: string,
    public readonly operation: string,
    public readonly payload: unknown
  ) {}
}

/**
 * SlEffectContext - Manages algebraic effects.
 *
 * Gordon Plotkin & Matija Pretnar (Effects theory):
 *   "Effect handlers are composable. Multiple handlers can be stacked,
 *    each handling different effects, forwarding what they don't handle."
 *
 * @example
 * ```lisp
 * ;; Define an async effect
 * (define-effect Async
 *   (await promise))
 *
 * ;; Use the effect
 * (handle
 *   (let ((data (perform Async.await (fetch url))))
 *     (process data))
 *   (Async.await (promise resume)
 *     (promise.then resume)))
 *
 * ;; Or in ShadowLisp actors:
 * (handle
 *   (let ((result (perform State.get :count)))
 *     (emit "changed" (hash "count" result)))
 *   (State.get (_ resume) (resume count)))
 * ```
 */
export class SlEffectContext {
  private readonly handlerStack: IHandlerDef[] = [];
  private readonly effects = new Map<string, IEffectSignature>();

  /**
   * Define a new effect signature.
   */
  defineEffect(name: string, operations: IEffectOp[]): void {
    this.effects.set(name, { name, operations });
  }

  /**
   * Check if an effect is defined.
   */
  hasEffect(name: string): boolean {
    return this.effects.has(name);
  }

  /**
   * Install a handler and execute computation.
   *
   * @param handler - Handler definition
   * @param computation - Thunk to execute with handler installed
   * @returns Result of computation (possibly modified by handler)
   */
  handle<T>(handler: IHandlerDef, computation: () => T): T | unknown {
    this.handlerStack.push(handler);
    try {
      const result = computation();
      // Apply return handler if present
      if (handler.returnHandler) {
        return handler.returnHandler(result);
      }
      return result;
    } catch (e) {
      if (e instanceof PerformedEffect) {
        return this._handlePerformedEffect(e);
      }
      throw e;
    } finally {
      this.handlerStack.pop();
    }
  }

  /**
   * Perform an effect operation.
   *
   * @param effect - Effect name (e.g., "State")
   * @param operation - Operation name (e.g., "get")
   * @param payload - Operation payload
   * @returns Result from handler
   */
  perform(effect: string, operation: string, payload?: unknown): unknown {
    // Find a handler for this effect
    for (let i = this.handlerStack.length - 1; i >= 0; i--) {
      const handler = this.handlerStack[i];
      if (handler.effect === effect && handler.operations.has(operation)) {
        const opHandler = handler.operations.get(operation)!;

        // Create resumption continuation
        // In full implementation, this would capture the continuation
        let resumed = false;
        let resumeValue: unknown;

        const resume = (value: unknown) => {
          if (resumed) {
            throw new Error('[SlEffects] Cannot resume more than once');
          }
          resumed = true;
          resumeValue = value;
          return value;
        };

        // Call the handler
        const result = opHandler(payload, resume);

        // If handler resumed, return the resume value
        if (resumed) {
          return resumeValue;
        }

        // Otherwise return handler result
        return result;
      }
    }

    // No handler found - raise effect
    throw new PerformedEffect(effect, operation, payload);
  }

  /**
   * Handle a performed effect that wasn't caught in-line.
   */
  private _handlePerformedEffect(effect: PerformedEffect): unknown {
    // Search through remaining handlers
    for (let i = this.handlerStack.length - 1; i >= 0; i--) {
      const handler = this.handlerStack[i];
      if (handler.effect === effect.effect && handler.operations.has(effect.operation)) {
        const opHandler = handler.operations.get(effect.operation)!;

        // Create resumption (simplified - would need full continuation)
        const resume = (value: unknown) => value;

        return opHandler(effect.payload, resume);
      }
    }

    // No handler - re-throw
    throw new Error(
      `[SlEffects] Unhandled effect: ${effect.effect}.${effect.operation}`
    );
  }

  /**
   * Get list of defined effects.
   */
  listEffects(): string[] {
    return [...this.effects.keys()];
  }
}

/**
 * Standard effects for ShadowLisp actors.
 */
export const StandardEffects = {
  /**
   * State effect - read/write actor state.
   */
  State: {
    name: 'State',
    operations: [
      { name: 'get', params: ['key'] },
      { name: 'set', params: ['key', 'value'] },
      { name: 'update', params: ['key', 'fn'] }
    ]
  },

  /**
   * Emit effect - emit events to the actor system.
   */
  Emit: {
    name: 'Emit',
    operations: [
      { name: 'emit', params: ['event', 'payload'] }
    ]
  },

  /**
   * Send effect - send messages to other actors.
   */
  Send: {
    name: 'Send',
    operations: [
      { name: 'send', params: ['target', 'command', 'payload'] },
      { name: 'ask', params: ['target', 'command', 'payload'] }
    ]
  },

  /**
   * Async effect - asynchronous operations.
   */
  Async: {
    name: 'Async',
    operations: [
      { name: 'await', params: ['promise'] },
      { name: 'delay', params: ['ms'] },
      { name: 'race', params: ['promises'] }
    ]
  },

  /**
   * Log effect - logging/tracing.
   */
  Log: {
    name: 'Log',
    operations: [
      { name: 'info', params: ['message'] },
      { name: 'warn', params: ['message'] },
      { name: 'error', params: ['message'] },
      { name: 'debug', params: ['message'] }
    ]
  },

  /**
   * Random effect - non-deterministic operations.
   */
  Random: {
    name: 'Random',
    operations: [
      { name: 'int', params: ['min', 'max'] },
      { name: 'float', params: [] },
      { name: 'choice', params: ['list'] }
    ]
  }
};

/**
 * Create a handler definition from a simple object.
 *
 * @example
 * ```typescript
 * const stateHandler = createHandler('State', {
 *   get: (key, resume) => resume(state[key]),
 *   set: ([key, value], resume) => {
 *     state[key] = value;
 *     return resume(undefined);
 *   }
 * });
 * ```
 */
export function createHandler(
  effect: string,
  operations: Record<string, EffectHandler>,
  returnHandler?: (value: unknown) => unknown
): IHandlerDef {
  return {
    effect,
    operations: new Map(Object.entries(operations)),
    returnHandler
  };
}

/**
 * SlEffectEvaluator - Evaluate S-expressions with effect support.
 *
 * Provides handle and perform forms for effect handling.
 */
export class SlEffectEvaluator {
  private readonly context = new SlEffectContext();
  private readonly env = new Map<string, unknown>();

  /**
   * Register standard effects.
   */
  registerStandardEffects(): void {
    for (const effect of Object.values(StandardEffects)) {
      this.context.defineEffect(effect.name, effect.operations);
    }
  }

  /**
   * Evaluate an expression with effect support.
   */
  eval(expr: SExpr): unknown {
    if (!Array.isArray(expr)) {
      return this._evalAtom(expr);
    }

    if (expr.length === 0) return [];

    const [op, ...args] = expr;

    if (typeof op === 'string') {
      switch (op) {
        case 'handle':
          return this._evalHandle(args);

        case 'perform':
          return this._evalPerform(args);

        case 'define-effect':
          return this._evalDefineEffect(args);

        default:
          // Regular evaluation
          return this._evalApplication(op, args);
      }
    }

    return this._evalApplication(op, args);
  }

  /**
   * Evaluate handle form.
   *
   * (handle body (Effect.op (params resume) handler-body) ...)
   */
  private _evalHandle(args: SExpr[]): unknown {
    const [body, ...handlerClauses] = args;

    // Parse handler clauses
    const handlers = new Map<string, IHandlerDef>();

    for (const clause of handlerClauses) {
      if (!Array.isArray(clause) || clause.length < 2) continue;

      const [effectOp, paramsAndBody] = clause as [string, SExpr[]];
      const [effect, operation] = effectOp.split('.');

      if (!handlers.has(effect)) {
        handlers.set(effect, {
          effect,
          operations: new Map()
        });
      }

      const handler = handlers.get(effect)!;
      const [params, ...handlerBody] = paramsAndBody as [string[], ...SExpr[]];

      // Create handler function
      const handlerFn: EffectHandler = (payload, resume) => {
        // Bind params
        const handlerEnv = new Map(this.env);
        if (Array.isArray(params)) {
          if (params[0]) handlerEnv.set(params[0], payload);
          if (params[1]) handlerEnv.set(params[1], resume);
        }

        // Evaluate handler body
        let result: unknown = null;
        for (const expr of handlerBody) {
          result = this._evalInEnv(expr, handlerEnv);
        }
        return result;
      };

      handler.operations.set(operation, handlerFn);
    }

    // Install handlers and evaluate body
    let result: unknown = null;
    const handlerList = [...handlers.values()];

    const runWithHandlers = (idx: number): unknown => {
      if (idx >= handlerList.length) {
        return this.eval(body);
      }
      return this.context.handle(handlerList[idx], () => {
        return runWithHandlers(idx + 1);
      });
    };

    result = runWithHandlers(0);
    return result;
  }

  /**
   * Evaluate perform form.
   *
   * (perform Effect.operation payload)
   */
  private _evalPerform(args: SExpr[]): unknown {
    const [effectOp, payload] = args;

    if (typeof effectOp !== 'string') {
      throw new Error('[SlEffects] perform requires Effect.operation string');
    }

    const [effect, operation] = effectOp.split('.');
    const evaluatedPayload = payload !== undefined ? this.eval(payload) : undefined;

    return this.context.perform(effect, operation, evaluatedPayload);
  }

  /**
   * Evaluate define-effect form.
   *
   * (define-effect Name (op1 params) (op2 params) ...)
   */
  private _evalDefineEffect(args: SExpr[]): unknown {
    const [name, ...opDefs] = args;

    if (typeof name !== 'string') {
      throw new Error('[SlEffects] define-effect requires name');
    }

    const operations: IEffectOp[] = opDefs.map(def => {
      if (Array.isArray(def)) {
        const [opName, ...params] = def as [string, ...string[]];
        return { name: opName, params };
      }
      return { name: String(def), params: [] };
    });

    this.context.defineEffect(name, operations);
    return name;
  }

  /**
   * Evaluate an atom.
   */
  private _evalAtom(atom: SExpr): unknown {
    if (atom === null) return null;
    if (typeof atom === 'number') return atom;
    if (typeof atom === 'boolean') return atom;
    if (typeof atom === 'string') {
      if (atom.startsWith('"') && atom.endsWith('"')) {
        return atom.slice(1, -1);
      }
      if (atom.startsWith(':')) return atom;
      if (this.env.has(atom)) return this.env.get(atom);
      return atom;
    }
    return atom;
  }

  /**
   * Evaluate in a specific environment.
   */
  private _evalInEnv(expr: SExpr, env: Map<string, unknown>): unknown {
    const savedEnv = new Map(this.env);
    for (const [k, v] of env) {
      this.env.set(k, v);
    }
    try {
      return this.eval(expr);
    } finally {
      this.env.clear();
      for (const [k, v] of savedEnv) {
        this.env.set(k, v);
      }
    }
  }

  /**
   * Evaluate application.
   */
  private _evalApplication(op: SExpr, args: SExpr[]): unknown {
    const proc = this.eval(op);
    const evaledArgs = args.map(a => this.eval(a));

    if (typeof proc === 'function') {
      return proc(...evaledArgs);
    }

    throw new Error(`[SlEffects] Cannot apply: ${op}`);
  }
}
