// src/framework/shadowlisp/SlRuntime.ts
//
// Vision 48: ShadowLisp Runtime Context Provider
//
// Alan Kay (Smalltalk creator, Turing Award 2003):
//   "The runtime provides the evaluation context. It connects syntax (DOM)
//    to semantics (Matrix actors). Live programming at last."
//
// Gerald Sussman (SICP co-author):
//   "SlRuntime is the metacircular evaluator's environment.
//    It binds names to values and evaluates expressions."
//
// <sl-runtime> provides the evaluation context for ShadowLisp forms.

import type { SExpr } from '../../lisp/SExpr';
import type { IMatrixContext } from '../../engine/core/IMatrixContext';
import type { ITransportAdapter } from '../../engine/remoting/ITransportAdapter';
import { SlForm } from './SlForm';
import { SlAtom } from './SlAtom';
import { SlMacroExpander } from './SlMacro';
import { SlEffectContext, createHandler, type EffectHandler } from './SlEffects';
import { SlContinuationContext, DEFAULT_PROMPT, makePromptTag } from './SlContinuation';
import { TopicRouter } from '../../engine/messaging/TopicRouter';

const SlRuntimeElementBase = HTMLElement;

/**
 * Simple environment for variable bindings.
 */
export class Environment {
  private bindings = new Map<string, unknown>();
  private parent?: Environment;

  constructor(parent?: Environment) {
    this.parent = parent;
  }

  /**
   * Define a new binding in this environment.
   */
  define(name: string, value: unknown): void {
    this.bindings.set(name, value);
  }

  /**
   * Look up a binding, searching parent scopes.
   */
  lookup(name: string): unknown {
    if (this.bindings.has(name)) {
      return this.bindings.get(name);
    }
    if (this.parent) {
      return this.parent.lookup(name);
    }
    throw new Error(`Undefined: ${name}`);
  }

  /**
   * Check if a binding exists.
   */
  has(name: string): boolean {
    if (this.bindings.has(name)) return true;
    if (this.parent) return this.parent.has(name);
    return false;
  }

  /**
   * Set an existing binding (mutate).
   */
  set(name: string, value: unknown): void {
    if (this.bindings.has(name)) {
      this.bindings.set(name, value);
    } else if (this.parent) {
      this.parent.set(name, value);
    } else {
      throw new Error(`Cannot set undefined: ${name}`);
    }
  }

  /**
   * Create a child environment (lexical scope).
   */
  extend(): Environment {
    return new Environment(this);
  }

  /**
   * Get all bindings as record.
   */
  toRecord(): Record<string, unknown> {
    const result: Record<string, unknown> = {};
    if (this.parent) {
      Object.assign(result, this.parent.toRecord());
    }
    for (const [k, v] of this.bindings) {
      result[k] = v;
    }
    return result;
  }
}

/**
 * SlRuntime - Evaluation context for ShadowLisp.
 *
 * Connects to Matrix transport, manages environment, evaluates forms.
 *
 * @example
 * <sl-runtime namespace="my-app">
 *   <sl-form op="define">
 *     <sl-atom kind="sym">x</sl-atom>
 *     <sl-atom kind="num">42</sl-atom>
 *   </sl-form>
 * </sl-runtime>
 */
export class SlRuntime extends SlRuntimeElementBase {
  static observedAttributes = ['namespace', 'mount'];

  private _env: Environment;
  private _context?: IMatrixContext;
  private _transport?: ITransportAdapter;
  private _initialized = false;
  private _macroExpander: SlMacroExpander;
  private _effectContext: SlEffectContext;
  private _continuationContext: SlContinuationContext;

  constructor() {
    super();
    this._env = this._createBaseEnvironment();
    this._macroExpander = SlMacroExpander.withStandardMacros();
    this._effectContext = new SlEffectContext();
    this._continuationContext = new SlContinuationContext();
    this.attachShadow({ mode: 'open' });
  }

  private _infoKernel(...args: unknown[]): void {
    // eslint-disable-next-line no-console -- kernel ShadowLisp runtime diagnostics have no actor context
    console.log(...args);
  }

  private _errorKernel(...args: unknown[]): void {
    // eslint-disable-next-line no-console -- kernel ShadowLisp runtime diagnostics have no actor context
    console.error(...args);
  }

  /**
   * Get the namespace for this runtime.
   */
  get namespace(): string {
    return this.getAttribute('namespace') ?? 'shadowlisp';
  }

  /**
   * Get the mount path.
   */
  get mount(): string {
    return this.getAttribute('mount') ?? `${this.namespace}`;
  }

  /**
   * Get the environment.
   */
  get env(): Environment {
    return this._env;
  }

  /**
   * Get the Matrix context (if connected).
   */
  get context(): IMatrixContext | undefined {
    return this._context;
  }

  /**
   * Set the Matrix context for transport integration.
   * Subscribes to {mount}.accepts.+ for incoming commands.
   */
  setContext(context: IMatrixContext): void {
    this._context = context;
    this._transport = context.transport;
    this._env.define('__context', context);
    this._env.define('__transport', context.transport);
    this._env.define('__mount', context.mount);

    // Subscribe to incoming commands: {mount}.accepts.{command}
    const acceptsTopic = `${this.mount}.accepts.+`;
    this._transport.subscribe(acceptsTopic, (payload: unknown, meta: { topic: string; ts: number }) => {
      // Extract command name from topic
      const parts = meta.topic.split('.');
      const command = parts[parts.length - 1];

      this._infoKernel(`[SlRuntime] Received command: ${command}`, payload);

      // Define payload in environment and evaluate command handler
      this._env.define('__payload', payload);
      this._env.define('__command', command);

      // Look for a handler defined as (define-handler command-name ...)
      // Or just dispatch to a handler function if defined
      const handlerName = `handle-${command}`;
      if (this._env.has(handlerName)) {
        const handler = this._env.lookup(handlerName);
        if (typeof handler === 'function') {
          handler(payload);
        }
      }

      // Emit event that command was received (for UI binding)
      this.dispatchEvent(new CustomEvent('sl-command-received', {
        bubbles: true,
        detail: { command, payload, mount: this.mount }
      }));
    });

    this._infoKernel(`[SlRuntime] Subscribed to ${acceptsTopic}`);

    // Re-evaluate forms now that we have context
    if (this._initialized) {
      this._evaluateChildren();
    }
  }

  /**
   * Create base environment with primitives.
   */
  private _createBaseEnvironment(): Environment {
    const env = new Environment();

    // McCarthy primitives
    env.define('+', (...args: number[]) => args.reduce((a, b) => a + b, 0));
    env.define('-', (a: number, b: number) => a - b);
    env.define('*', (...args: number[]) => args.reduce((a, b) => a * b, 1));
    env.define('/', (a: number, b: number) => a / b);
    env.define('=', (a: unknown, b: unknown) => a === b);
    env.define('<', (a: number, b: number) => a < b);
    env.define('>', (a: number, b: number) => a > b);
    env.define('<=', (a: number, b: number) => a <= b);
    env.define('>=', (a: number, b: number) => a >= b);

    // List operations
    env.define('list', (...args: unknown[]) => args);
    env.define('car', (list: unknown[]) => list[0]);
    env.define('cdr', (list: unknown[]) => list.slice(1));
    env.define('cons', (head: unknown, tail: unknown[]) => [head, ...tail]);
    env.define('null?', (x: unknown) => x === null || (Array.isArray(x) && x.length === 0));
    env.define('atom?', (x: unknown) => !Array.isArray(x));

    // String operations
    env.define('concat', (...args: string[]) => args.join(''));
    env.define('format', (template: string, ...args: unknown[]) => {
      let result = template;
      for (const arg of args) {
        result = result.replace(/~[asd]/, String(arg));
      }
      return result;
    });

    // Type predicates
    env.define('number?', (x: unknown) => typeof x === 'number');
    env.define('string?', (x: unknown) => typeof x === 'string');
    env.define('boolean?', (x: unknown) => typeof x === 'boolean');
    env.define('symbol?', (x: unknown) => typeof x === 'string');

    // Console/debug
    env.define('print', (...args: unknown[]) => this._infoKernel('[ShadowLisp]', ...args));
    env.define('display', (...args: unknown[]) => this._infoKernel(...args));

    // Runtime reference
    env.define('__runtime', this);

    return env;
  }

  /**
   * Get the macro expander.
   */
  get macroExpander(): SlMacroExpander {
    return this._macroExpander;
  }

  /**
   * Evaluate an S-expression in this runtime's environment.
   * Macros are expanded before evaluation.
   */
  async eval(sexp: SExpr): Promise<unknown> {
    // Phase 4: Expand macros before evaluation
    const expanded = this._macroExpander.expand(sexp);
    return this._evalSExpr(expanded, this._env);
  }

  /**
   * Internal S-expression evaluator.
   *
   * Rich Hickey (Clojure creator):
   *   "The evaluator is small because S-expressions are simple.
   *    Atoms self-evaluate. Forms dispatch on operator."
   */
  private async _evalSExpr(sexp: SExpr, env: Environment): Promise<unknown> {
    // Atoms self-evaluate
    if (!Array.isArray(sexp)) {
      if (sexp === null) return null;
      if (typeof sexp === 'number') return sexp;
      if (typeof sexp === 'boolean') return sexp;
      if (typeof sexp === 'string') {
        // Quoted strings are literals - strip quotes and return
        if (sexp.startsWith('"') && sexp.endsWith('"')) {
          return sexp.slice(1, -1);
        }
        // String starting with : is keyword (self-evaluating)
        if (sexp.startsWith(':')) return sexp;
        // Otherwise look up in environment
        if (env.has(sexp)) return env.lookup(sexp);
        // Unbound symbol returns as-is
        return sexp;
      }
      return sexp;
    }

    // Empty list
    if (sexp.length === 0) return [];

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

    // Special forms
    switch (op) {
      case 'quote':
        return args[0];

      case 'if': {
        const test = await this._evalSExpr(args[0], env);
        if (test) {
          return this._evalSExpr(args[1], env);
        } else if (args.length > 2) {
          return this._evalSExpr(args[2], env);
        }
        return null;
      }

      case 'begin': {
        let result: unknown = null;
        for (const expr of args) {
          result = await this._evalSExpr(expr, env);
        }
        return result;
      }

      case 'define': {
        const name = args[0] as string;
        const value = await this._evalSExpr(args[1], env);
        env.define(name, value);
        return value;
      }

      case 'set!': {
        const name = args[0] as string;
        const value = await this._evalSExpr(args[1], env);
        env.set(name, value);
        return value;
      }

      case 'lambda': {
        const params = args[0] as string[];
        const body = args.slice(1);
        return { type: 'closure', params, body, env };
      }

      case 'let': {
        const bindings = args[0] as [string, SExpr][];
        const body = args.slice(1);
        const letEnv = env.extend();

        for (const [name, expr] of bindings) {
          const value = await this._evalSExpr(expr, env);
          letEnv.define(name, value);
        }

        let result: unknown = null;
        for (const expr of body) {
          result = await this._evalSExpr(expr, letEnv);
        }
        return result;
      }

      // Matrix integration forms
      case 'emit': {
        if (!this._context) throw new Error('emit requires Matrix context');
        const [event, payload] = args;
        const eventName = await this._evalSExpr(event, env) as string;
        const eventPayload = await this._evalSExpr(payload, env);
        const eventsTopic = TopicRouter.events(this._context.mount);
        this._transport?.publish(eventsTopic, {
          op: eventName,
          payload: eventPayload ?? {},
          from: this._context.mount,
        });
        return null;
      }

      case 'send': {
        if (!this._context) throw new Error('send requires Matrix context');
        const [target, command, payload] = args;
        const targetMount = await this._evalSExpr(target, env) as string;
        const cmd = await this._evalSExpr(command, env) as string;
        const cmdPayload = payload ? await this._evalSExpr(payload, env) : undefined;
        const inboxTopic = TopicRouter.inbox(targetMount);
        this._transport?.publish(inboxTopic, {
          op: cmd,
          payload: cmdPayload ?? {},
          from: this._context.mount,
        });
        return null;
      }

      // ═══════════════════════════════════════════════════════════════════
      // Vision 52: CROSS-DOMAIN EVAL - eval-at primitive
      // ═══════════════════════════════════════════════════════════════════
      // (eval-at domain mount expr)
      // Routes expression to the specified LISP domain via Matrix transport.
      //
      // Domains:
      //   "vlm" - Route to VLM for parallel distributed eval
      //   "shadowlisp" - Route to ShadowLisp for DOM operations
      //   "fold" - Route to FoldVLM for filesystem operations
      //
      // Example: (eval-at "vlm" "vlm.eval" '(+ 1 2 3))
      case 'eval-at': {
        if (!this._context) throw new Error('eval-at requires Matrix context');
        const [domain, mount, expr] = args;
        const domainName = await this._evalSExpr(domain, env) as string;
        const targetMount = await this._evalSExpr(mount, env) as string;
        const evalExpr = await this._evalSExpr(expr, env);

        // Validate domain
        const validDomains = ['vlm', 'shadowlisp', 'fold'];
        if (!validDomains.includes(domainName)) {
          throw new Error(
            `[SlRuntime eval-at] Invalid domain '${domainName}'. Valid domains: ${validDomains.join(', ')}`
          );
        }

        this._infoKernel(
          `%c[SlRuntime eval-at] ${domainName} → ${targetMount}`,
          'color: #00bcd4; font-weight: bold',
          evalExpr
        );

        // Import RequestReply for synchronous result
        const { RequestReply } = await import('../../engine/remoting/RequestReply');
        return RequestReply.execute(
          this._context,
          targetMount,
          'eval',
          { expr: evalExpr, type: domainName }
        );
      }

      // ═══════════════════════════════════════════════════════════════════
      // PHASE 4: DELIMITED CONTINUATIONS (reset/shift)
      // ═══════════════════════════════════════════════════════════════════

      case 'reset': {
        // (reset body) or (reset tag body)
        let tag = DEFAULT_PROMPT;
        let body = args[0];

        if (args.length > 1 && typeof args[0] === 'string') {
          tag = makePromptTag(args[0] as string);
          body = args[1];
        }

        // Execute body within reset boundary
        return this._continuationContext.reset(tag, async () => {
          return await this._evalSExpr(body, env);
        });
      }

      case 'shift': {
        // (shift k body) or (shift tag k body)
        let tag = DEFAULT_PROMPT;
        let kName: string;
        let body: SExpr;

        if (args.length === 2) {
          kName = args[0] as string;
          body = args[1];
        } else if (args.length === 3) {
          tag = makePromptTag(args[0] as string);
          kName = args[1] as string;
          body = args[2];
        } else {
          throw new Error('[SlRuntime] shift requires 2 or 3 arguments');
        }

        // Capture continuation up to reset
        return this._continuationContext.shift(tag, async (capturedK) => {
          const shiftEnv = env.extend();
          shiftEnv.define(kName, capturedK);
          return await this._evalSExpr(body, shiftEnv);
        });
      }

      // ═══════════════════════════════════════════════════════════════════
      // PHASE 4: ALGEBRAIC EFFECTS (perform/handle)
      // ═══════════════════════════════════════════════════════════════════

      case 'perform': {
        // (perform effect-name operation payload?)
        const effectName = await this._evalSExpr(args[0], env) as string;
        const operation = await this._evalSExpr(args[1], env) as string;
        const payload = args[2] ? await this._evalSExpr(args[2], env) : undefined;

        return this._effectContext.perform(effectName, operation, payload);
      }

      case 'handle': {
        // (handle handler-spec body)
        // handler-spec: (effect-name (op1 (params) body1) (op2 (params) body2) ...)
        const handlerSpec = args[0] as SExpr[];
        const body = args[1];

        if (!Array.isArray(handlerSpec) || handlerSpec.length < 1) {
          throw new Error('[SlRuntime] handle requires handler specification');
        }

        const effectName = handlerSpec[0] as string;
        const opSpecs = handlerSpec.slice(1) as SExpr[][];

        // Build operations map
        const operationsMap: Record<string, EffectHandler> = {};

        for (const opDef of opSpecs) {
          if (!Array.isArray(opDef) || opDef.length < 3) continue;
          const [opName, opParams, ...opBody] = opDef;

          operationsMap[opName as string] = (payload: unknown, resume: (v: unknown) => unknown) => {
            const opEnv = env.extend();
            opEnv.define('payload', payload);
            opEnv.define('resume', resume);

            // Bind named params if provided
            if (Array.isArray(opParams)) {
              for (const paramName of opParams) {
                if (paramName === 'payload') opEnv.define(paramName as string, payload);
                else if (paramName === 'resume') opEnv.define(paramName as string, resume);
              }
            }

            // Evaluate handler body synchronously
            let result: unknown = null;
            for (const expr of opBody) {
              // Simplified sync eval for effects
              result = this._evalSExprSync(expr as SExpr, opEnv);
            }
            return result;
          };
        }

        // Create handler and execute body
        const handlerDef = createHandler(effectName, operationsMap);
        return this._effectContext.handle(handlerDef, () => {
          return this._evalSExprSync(body, env);
        });
      }

      default: {
        // Application: evaluate operator and arguments
        const proc = await this._evalSExpr(op, env);
        const evaledArgs = await Promise.all(
          args.map(arg => this._evalSExpr(arg, env))
        );

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

        // Closure application
        if (proc && typeof proc === 'object' && (proc as Record<string, unknown>).type === 'closure') {
          const closure = proc as { params: string[]; body: SExpr[]; env: Environment };
          const callEnv = closure.env.extend();

          // Bind parameters
          for (let i = 0; i < closure.params.length; i++) {
            callEnv.define(closure.params[i], evaledArgs[i]);
          }

          // Evaluate body
          let result: unknown = null;
          for (const expr of closure.body) {
            result = await this._evalSExpr(expr, callEnv);
          }
          return result;
        }

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

  /**
   * Synchronous S-expression evaluator for effect handlers.
   * Effects require synchronous execution for proper stack capture.
   */
  private _evalSExprSync(sexp: SExpr, env: Environment): unknown {
    // Atoms
    if (!Array.isArray(sexp)) {
      if (sexp === null) return null;
      if (typeof sexp === 'number') return sexp;
      if (typeof sexp === 'boolean') return sexp;
      if (typeof sexp === 'string') {
        if (sexp.startsWith('"') && sexp.endsWith('"')) {
          return sexp.slice(1, -1);
        }
        if (sexp.startsWith(':')) return sexp;
        if (env.has(sexp)) return env.lookup(sexp);
        return sexp;
      }
      return sexp;
    }

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

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

    // Special forms (subset for effects)
    switch (op) {
      case 'quote':
        return args[0];

      case 'if': {
        const test = this._evalSExprSync(args[0], env);
        if (test) {
          return this._evalSExprSync(args[1], env);
        } else if (args.length > 2) {
          return this._evalSExprSync(args[2], env);
        }
        return null;
      }

      case 'begin': {
        let result: unknown = null;
        for (const expr of args) {
          result = this._evalSExprSync(expr, env);
        }
        return result;
      }

      case 'perform': {
        const effectName = this._evalSExprSync(args[0], env) as string;
        const operation = this._evalSExprSync(args[1], env) as string;
        const payload = args[2] ? this._evalSExprSync(args[2], env) : undefined;
        return this._effectContext.perform(effectName, operation, payload);
      }

      default: {
        const proc = this._evalSExprSync(op, env);
        const evaledArgs = args.map(arg => this._evalSExprSync(arg, env));

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

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

  /**
   * Evaluate all child sl-form elements.
   */
  private async _evaluateChildren(): Promise<void> {
    for (const child of Array.from(this.children)) {
      if (child instanceof SlForm || child.tagName === 'SL-FORM') {
        const form = child as SlForm;
        try {
          const sexp = form.toSExpr();
          await this.eval(sexp);
        } catch (err) {
          this._errorKernel('[SlRuntime] Error evaluating form:', err, form);
        }
      }
    }
  }

  connectedCallback(): void {
    // Render shadow DOM
    if (this.shadowRoot) {
      this.shadowRoot.innerHTML = `
        <style>
          :host { display: block; }
          .sl-runtime-info { font-size: 10px; color: #666; margin-bottom: 8px; }
        </style>
        <div class="sl-runtime-info">
          <span>ShadowLisp Runtime: ${this.namespace}</span>
        </div>
        <slot></slot>
      `;
    }

    // Evaluate children on next tick (allow DOM to settle)
    this._initialized = true;
    setTimeout(() => this._evaluateChildren(), 0);
  }

  attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void {
    if (name === 'namespace' || name === 'mount') {
      this.dispatchEvent(new CustomEvent('sl-runtime-changed', {
        bubbles: true,
        detail: { namespace: this.namespace, mount: this.mount }
      }));
    }
  }
}

// Define the custom element
if (!customElements.get('sl-runtime')) {
  customElements.define('sl-runtime', SlRuntime);
}
