/**
 * ShellEffectBackend — Ring 3 shell tool bindings for CESK programs.
 *
 * Maps agent tools to Lisp effects so LLM can compose multi-step
 * operations as single Lisp expressions via `evaluate_shell`.
 *
 * Effects:
 *   shell.invoke, shell.emit, shell.respond, shell.define-component,
 *   shell.load-component, shell.save-component, shell.list-components,
 *   shell.eval, shell.introspect
 *
 * Uses delegate pattern (same as MatrixEffectBackend) — the delegate
 * bridges to AgentActor's existing _executeXxx methods.
 */
import type { EffectBackend, State, Val, OpCall } from '@open-matrix/omega-core';

type ChildSummary = {
  mount?: string;
  id?: string;
};

// ─── Delegate interface ──────────────────────────────────────────────────────

export interface ShellDelegate {
  /** Security check: is target within the agent's authorized subtree? */
  isInSubtree(target: string): boolean;

  /** Security check: is this effect family allowed for this session? */
  isEffectAllowed?(effectOp: string): boolean;

  /** Send op to an actor and wait for reply. */
  executeInvoke(
    target: string,
    op: string,
    payload: Record<string, unknown>,
  ): Promise<{ ok: boolean; data: unknown }>;

  /** Publish fire-and-forget event. */
  executeEmit(
    event: string,
    payload: Record<string, unknown>,
  ): Promise<{ ok: boolean; data: unknown }>;

  /** Return text response to caller. */
  executeRespond(text: string): Promise<{ ok: boolean; data: unknown }>;

  /** Create a live component from code string. */
  executeDefineComponent(
    name: string,
    code: string,
  ): Promise<{ ok: boolean; data: unknown }>;

  /** Load a saved component by name. */
  executeLoadComponent(name: string): Promise<{ ok: boolean; data: unknown }>;

  /** Save a component to the store. */
  executeSaveComponent(
    name: string,
    code: string,
  ): Promise<{ ok: boolean; data: unknown }>;

  /** List available components. */
  executeListComponents(): Promise<{ ok: boolean; data: unknown }>;

  /** Evaluate Omega Lisp code in the REPL sandbox. */
  executeEvalLisp(code: string): Promise<{ ok: boolean; data: unknown }>;

  /** Search components by query string. */
  executeFindComponents(query: string): Promise<{ ok: boolean; data: unknown }>;

  /** Get source code of a named component. */
  executeGetComponentSource(name: string): Promise<{ ok: boolean; data: unknown }>;

  /** Publish a component to the registry. */
  executePublishComponent(name: string): Promise<{ ok: boolean; data: unknown }>;

  /** Install a component from the registry. */
  executeInstallComponent(name: string): Promise<{ ok: boolean; data: unknown }>;

  /** Resolve natural language intent to an op. */
  executeResolveIntent(text: string): Promise<{ ok: boolean; data: unknown }>;

  /** Query trace/observability data. */
  executeTraceQuery(query: string): Promise<{ ok: boolean; data: unknown }>;

  /** List REPL packages. */
  executeReplPackages(): Promise<{ ok: boolean; data: unknown }>;

  /** Get documentation for a symbol. */
  executeReplDoc(symbol: string): Promise<{ ok: boolean; data: unknown }>;

  /** Fork the current session (COW). Returns new session ID. */
  executeForkSession?(
    newSessionId: string,
    allowedEffects?: string,
  ): Promise<{ ok: boolean; data: unknown }>;
}

// ─── Backend implementation ──────────────────────────────────────────────────

export class ShellEffectBackend implements EffectBackend {
  readonly ops = [
    'shell.invoke',
    'shell.emit',
    'shell.respond',
    'shell.define-component',
    'shell.load-component',
    'shell.save-component',
    'shell.list-components',
    'shell.eval',
    'shell.introspect',
    'shell.find-components',
    'shell.get-component-source',
    'shell.publish-component',
    'shell.install-component',
    'shell.resolve-intent',
    'shell.trace-query',
    'shell.repl-packages',
    'shell.repl-doc',
    'shell.ls',
    'shell.spawn',
    'shell.kill',
    'shell.fork',
  ] as const;

  constructor(private delegate: ShellDelegate) {}

  async handle(_state: State, opcall: OpCall): Promise<State> {
    // Per-operation capability check: if delegate provides isEffectAllowed, gate every effect
    if (this.delegate.isEffectAllowed && !this.delegate.isEffectAllowed(opcall.op)) {
      return opcall.resumption.invoke(
        this.errorMap(`Effect "${opcall.op}" not allowed by session capabilities`),
      );
    }
    switch (opcall.op) {
      case 'shell.invoke':
        return this.handleInvoke(opcall);
      case 'shell.emit':
        return this.handleEmit(opcall);
      case 'shell.respond':
        return this.handleRespond(opcall);
      case 'shell.define-component':
        return this.handleDefineComponent(opcall);
      case 'shell.load-component':
        return this.handleLoadComponent(opcall);
      case 'shell.save-component':
        return this.handleSaveComponent(opcall);
      case 'shell.list-components':
        return this.handleListComponents(opcall);
      case 'shell.eval':
        return this.handleEval(opcall);
      case 'shell.introspect':
        return this.handleIntrospect(opcall);
      case 'shell.find-components':
        return this.handleFindComponents(opcall);
      case 'shell.get-component-source':
        return this.handleGetComponentSource(opcall);
      case 'shell.publish-component':
        return this.handlePublishComponent(opcall);
      case 'shell.install-component':
        return this.handleInstallComponent(opcall);
      case 'shell.resolve-intent':
        return this.handleResolveIntent(opcall);
      case 'shell.trace-query':
        return this.handleTraceQuery(opcall);
      case 'shell.repl-packages':
        return this.handleReplPackages(opcall);
      case 'shell.repl-doc':
        return this.handleReplDoc(opcall);
      case 'shell.ls':
        return this.handleLs(opcall);
      case 'shell.spawn':
        return this.handleSpawn(opcall);
      case 'shell.kill':
        return this.handleKill(opcall);
      case 'shell.fork':
        return this.handleFork(opcall);
      default:
        return opcall.resumption.invoke(this.errorMap(`Unknown shell effect: ${opcall.op}`));
    }
  }

  // ─── Effect handlers ─────────────────────────────────────────────────────

  private async handleInvoke(opcall: OpCall): Promise<State> {
    const target = this.strArg(opcall.args[0], '');
    const op = this.strArg(opcall.args[1], '');
    const payloadRaw = this.strArg(opcall.args[2], '{}');

    let payload: Record<string, unknown>;
    try {
      payload = JSON.parse(payloadRaw);
    } catch {
      return opcall.resumption.invoke(this.errorMap(`Invalid JSON payload: ${payloadRaw}`));
    }

    // Subtree guard — always check, including cross-realm targets
    if (!this.delegate.isInSubtree(target)) {
      return opcall.resumption.invoke(
        this.errorMap(`Target "${target}" outside authorized subtree`),
      );
    }

    const result = await this.delegate.executeInvoke(target, op, payload);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleEmit(opcall: OpCall): Promise<State> {
    const event = this.strArg(opcall.args[0], '');
    const payloadRaw = this.strArg(opcall.args[1], '{}');

    let payload: Record<string, unknown>;
    try {
      payload = JSON.parse(payloadRaw);
    } catch {
      return opcall.resumption.invoke(this.errorMap(`Invalid JSON payload: ${payloadRaw}`));
    }

    // Subtree guard for events too
    if (!this.delegate.isInSubtree(event)) {
      return opcall.resumption.invoke(
        this.errorMap(`Event "${event}" outside authorized subtree`),
      );
    }

    const result = await this.delegate.executeEmit(event, payload);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleRespond(opcall: OpCall): Promise<State> {
    const text = this.strArg(opcall.args[0], '');
    await this.delegate.executeRespond(text);
    return opcall.resumption.invoke({ tag: 'Unit' } as Val);
  }

  private async handleDefineComponent(opcall: OpCall): Promise<State> {
    const name = this.strArg(opcall.args[0], '');
    const code = this.strArg(opcall.args[1], '');
    const result = await this.delegate.executeDefineComponent(name, code);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleLoadComponent(opcall: OpCall): Promise<State> {
    const name = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeLoadComponent(name);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleSaveComponent(opcall: OpCall): Promise<State> {
    const name = this.strArg(opcall.args[0], '');
    const code = this.strArg(opcall.args[1], '');
    const result = await this.delegate.executeSaveComponent(name, code);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleListComponents(opcall: OpCall): Promise<State> {
    const result = await this.delegate.executeListComponents();
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleEval(opcall: OpCall): Promise<State> {
    const code = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeEvalLisp(code);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleIntrospect(opcall: OpCall): Promise<State> {
    const mount = this.strArg(opcall.args[0], '');

    // Subtree guard — always check, including cross-realm targets
    if (!this.delegate.isInSubtree(mount)) {
      return opcall.resumption.invoke(
        this.errorMap(`Target "${mount}" outside authorized subtree`),
      );
    }

    const result = await this.delegate.executeInvoke(mount, '$introspect', {});
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleFindComponents(opcall: OpCall): Promise<State> {
    const query = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeFindComponents(query);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleGetComponentSource(opcall: OpCall): Promise<State> {
    const name = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeGetComponentSource(name);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handlePublishComponent(opcall: OpCall): Promise<State> {
    const name = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executePublishComponent(name);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleInstallComponent(opcall: OpCall): Promise<State> {
    const name = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeInstallComponent(name);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleResolveIntent(opcall: OpCall): Promise<State> {
    const text = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeResolveIntent(text);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleTraceQuery(opcall: OpCall): Promise<State> {
    const query = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeTraceQuery(query);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleReplPackages(opcall: OpCall): Promise<State> {
    const result = await this.delegate.executeReplPackages();
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  private async handleReplDoc(opcall: OpCall): Promise<State> {
    const symbol = this.strArg(opcall.args[0], '');
    const result = await this.delegate.executeReplDoc(symbol);
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  /**
   * shell.ls — list children of an actor as a proper Scheme list of mount strings.
   * Calls $introspect, extracts children array, returns (mount1 mount2 ...).
   */
  private async handleLs(opcall: OpCall): Promise<State> {
    const mount = this.strArg(opcall.args[0], '');

    // Subtree guard
    if (!this.delegate.isInSubtree(mount)) {
      return opcall.resumption.invoke(
        this.errorMap(`Target "${mount}" outside authorized subtree`),
      );
    }

    const result = await this.delegate.executeInvoke(mount, '$introspect', {});

    // Extract children mount strings and build a proper Scheme list (nested Vectors)
    if (result.ok && result.data && typeof result.data === 'object') {
      const data = result.data as Record<string, unknown>;
      const children = Array.isArray(data.children) ? data.children : [];
      const mounts: string[] = children.map((child) => {
        const summary = child as ChildSummary;
        return summary.mount ?? summary.id ?? '';
      });

      // Build proper list: { tag: "Vector", items: [head, tail] } ... { tag: "Unit" }
      let list: Val = { tag: 'Unit' } as Val;
      for (let i = mounts.length - 1; i >= 0; i--) {
        list = { tag: 'Vector', items: [{ tag: 'Str', s: mounts[i] } as Val, list] } as Val;
      }
      return opcall.resumption.invoke(list);
    }

    // No children or error — return empty list
    return opcall.resumption.invoke({ tag: 'Unit' } as Val);
  }

  // ── shell.spawn ──
  // Lisp: (effect shell.spawn "parentMount" "childId")
  // or:   (effect shell.spawn "parentMount" "childId" "componentClass")

  private async handleSpawn(opcall: OpCall): Promise<State> {
    const parent = this.strArg(opcall.args[0], '');
    const childId = this.strArg(opcall.args[1], '');
    const componentClass = this.strArg(opcall.args[2], 'DynamicActor');

    if (!childId) {
      return opcall.resumption.invoke(this.errorMap('shell.spawn: childId is required'));
    }

    if (!this.delegate.isInSubtree(parent)) {
      return opcall.resumption.invoke(
        this.errorMap(`Target "${parent}" outside authorized subtree`),
      );
    }

    const result = await this.delegate.executeInvoke(parent, 'factory.create', {
      id: childId,
      componentClass,
    });
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  // ── shell.kill ──
  // Lisp: (effect shell.kill "mount")

  private async handleKill(opcall: OpCall): Promise<State> {
    const target = this.strArg(opcall.args[0], '');

    if (!target) {
      return opcall.resumption.invoke(this.errorMap('shell.kill: target mount is required'));
    }

    if (!this.delegate.isInSubtree(target)) {
      return opcall.resumption.invoke(
        this.errorMap(`Target "${target}" outside authorized subtree`),
      );
    }

    const result = await this.delegate.executeInvoke(target, '$dispose', {});
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  // ── shell.fork ──
  // Lisp: (effect shell.fork "new-session-id" "allowed-effects")
  // Forks the current session (COW). Returns { ok, sessionId }.

  private async handleFork(opcall: OpCall): Promise<State> {
    const newSessionId = this.strArg(opcall.args[0], '');
    const allowedEffects = this.strArg(opcall.args[1], '');

    if (!newSessionId) {
      return opcall.resumption.invoke(this.errorMap('shell.fork: newSessionId is required'));
    }

    if (!this.delegate.executeForkSession) {
      return opcall.resumption.invoke(
        this.errorMap('shell.fork: delegate does not support fork sessions'),
      );
    }

    const result = await this.delegate.executeForkSession(
      newSessionId,
      allowedEffects || undefined,
    );
    return opcall.resumption.invoke(this.jsonToVal(result));
  }

  // ─── Value conversion helpers ────────────────────────────────────────────

  private strArg(v: Val | undefined, fallback: string): string {
    if (!v) return fallback;
    if (v.tag === 'Str') return (v as { tag: 'Str'; s: string }).s;
    if (v.tag === 'Num') return String((v as { tag: 'Num'; n: number }).n);
    if (v.tag === 'Bool') return String((v as { tag: 'Bool'; b: boolean }).b);
    return fallback;
  }

  private errorMap(message: string): Val {
    return {
      tag: 'Map',
      entries: [
        [{ tag: 'Str', s: 'ok' } as Val, { tag: 'Bool', b: false } as Val],
        [{ tag: 'Str', s: 'error' } as Val, { tag: 'Str', s: message } as Val],
      ],
    } as Val;
  }

  private jsonToVal(v: unknown, depth = 0): Val {
    if (v === null || v === undefined) return { tag: 'Unit' } as Val;
    if (typeof v === 'string') return { tag: 'Str', s: v } as Val;
    if (typeof v === 'number') return { tag: 'Num', n: v } as Val;
    if (typeof v === 'boolean') return { tag: 'Bool', b: v } as Val;
    // Depth limit prevents stack overflow on deep actor trees / introspect results
    if (depth > 20) return { tag: 'Str', s: '[depth limit exceeded]' } as Val;
    if (Array.isArray(v)) {
      return {
        tag: 'Vector',
        items: v.map(item => this.jsonToVal(item, depth + 1)),
      } as Val;
    }
    if (typeof v === 'object') {
      const entries: [Val, Val][] = Object.entries(v as Record<string, unknown>).map(
        ([k, val]) => [{ tag: 'Str', s: k } as Val, this.jsonToVal(val, depth + 1)] as [Val, Val],
      );
      return { tag: 'Map', entries } as Val;
    }
    return { tag: 'Str', s: String(v) } as Val;
  }
}
