///|
/// Ensure a function body starts with "use strict" directive.
/// Returns the body unchanged if it already has one, or prepends the directive.
fn ensure_strict_body(body : Array[@ast.Stmt]) -> Array[@ast.Stmt] {
  if @static_semantics.has_use_strict(body) {
    body
  } else {
    let strict_body : Array[@ast.Stmt] = [
      ExprStmt(
        StringLit(
          "use strict",
          false,
          @token.LexForm::LexNormal,
          @token.Loc::default(),
        ),
        @token.Loc::default(),
      ),
    ]
    for s in body {
      strict_body.push(s)
    }
    strict_body
  }
}

///|
fn Interpreter::create_class(
  self : Interpreter,
  ctx : ExecContext,
  name : String,
  superclass_expr : @ast.Expr?,
  members : Array[@ast.ClassMember],
  env : Environment,
  source_text : String?,
  class_binding? : String? = None,
) -> Value raise Error {
  // Class bodies are always strict per spec
  let class_ctx : ExecContext = {
    strict: true,
    current_generator: ctx.current_generator,
  }
  // ClassDefinitionEvaluation always creates a dedicated lexical environment.
  // A named class gets an immutable, initially-uninitialized binding there so
  // heritage/computed-name evaluation observes the TDZ and methods retain the
  // inner binding independently of the outer declaration/assignment binding.
  let class_env = Environment::new(parent=Some(env))
  match class_binding {
    Some(binding) => class_env.def_tdz(binding, ConstBinding)
    None => ()
  }
  // Evaluate superclass if present
  let (super_ctor, super_proto) : (Value?, Value) = match superclass_expr {
    Some(expr) => {
      let super_val = self.eval_expr(ctx, expr, class_env)
      match super_val {
        Null =>
          // `class C extends null {}` — legal per spec; the class is derived
          // (ConstructorKind "derived") so super() is required, but calling it
          // tries Construct(null, ...) which throws TypeError.  Represent this
          // as Some(Null) so construct_value follows the derived-class path.
          (Some(Null), Null)
        _ => {
          if !is_constructor_value(super_val) {
            raise @errors.TypeError(
              message="Class extends value is not a constructor",
            )
          }
          // Class heritage requires prototype lookup from [[Get]] semantics:
          // evaluate prototype via property access (including proxies/getters)
          // and require it to be Object or Null.
          let super_proto = self.get_computed_property(
            super_val,
            String_("prototype"),
            @token.Loc::default(),
          )
          match super_proto {
            Null => (Some(super_val), super_proto)
            _ =>
              if is_object_value(super_proto) {
                (Some(super_val), super_proto)
              } else {
                raise @errors.TypeError(
                  message="Class extends value's prototype must be an object or null",
                )
              }
          }
        }
      }
    }
    None => (None, self.global.get("[[ObjectPrototype]]") catch { _ => Null })
  }
  // Create the prototype object
  // Pre-insert constructor so it appears first in property enumeration order
  let proto_props : Map[String, Value] = { "constructor": Undefined }
  let proto_descriptors : Map[String, PropDescriptor] = Map([])
  let proto_symbol_props : Map[Int, Value] = Map([])
  let proto_symbol_descriptors : Map[Int, PropDescriptor] = Map([])
  // Create the constructor's intrinsic own properties before evaluating class
  // elements. Static elements may then redefine the configurable `name`
  // property while preserving the spec-mandated insertion order.
  let non_enum_configurable : PropDescriptor = {
    writable: false,
    enumerable: false,
    configurable: true,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let static_props : Map[String, Value] = Map([])
  static_props["length"] = Undefined
  static_props["name"] = String_(name)
  static_props["prototype"] = Undefined
  let static_descriptors : Map[String, PropDescriptor] = Map([])
  static_descriptors["length"] = non_enum_configurable
  static_descriptors["name"] = non_enum_configurable
  static_descriptors["prototype"] = {
    writable: false,
    enumerable: false,
    configurable: false,
    getter: None,
    setter: None,
    is_accessor: false,
  }
  let static_symbol_props : Map[Int, Value] = Map([])
  let static_symbol_descriptors : Map[Int, PropDescriptor] = Map([])
  // Track constructor method
  let mut ctor_fn : (Array[@ast.Param], String?, Array[@ast.Stmt])? = None
  let mut ctor_param_count : Int = 0
  let mut constructor_overridden : Bool = false
  // Collect instance field initializers (evaluated during construction)
  let instance_fields : Array[ClassFieldInit] = []
  // Collect static field initializers (evaluated after class object is created)
  let static_fields : Array[ClassFieldInit] = []
  // Collect private instance field initializers (evaluated during construction)
  let private_instance_fields : Array[ClassFieldInit] = []
  // Collect private static field initializers (evaluated after class object is created)
  let private_static_fields : Array[ClassFieldInit] = []
  // Collect private methods for this class (shared across all instances)
  let private_methods : Map[String, Value] = Map([])
  // Create shared method environments before their [[HomeObject]] values exist.
  // The snapshot bindings remain as legacy/direct-eval markers and as fallbacks
  // for constructor-call environments; normal class methods use [[HomeObject]].
  let instance_method_env : Environment = {
    let menv = Environment::new(parent=Some(class_env))
    let instance_super : Value = match super_ctor {
      Some(_) => super_proto
      None => self.global.get("[[ObjectPrototype]]") catch { _ => Null }
    }
    menv.def_builtin("[[SuperPrototype]]", instance_super)
    menv
  }
  let static_method_env : Environment = {
    let menv = Environment::new(parent=Some(class_env))
    let static_super : Value = match super_ctor {
      Some(sc) => sc
      None => self.global.get("[[FunctionPrototype]]") catch { _ => Null }
    }
    menv.def_builtin("[[SuperPrototype]]", static_super)
    menv
  }
  // Generate private brand symbol for this class declaration
  let private_brand : Value = if name != "" {
    Value::Symbol(self.realm_state.symbols.new_symbol(Some("[\{name}]")))
  } else {
    Value::Symbol(self.realm_state.symbols.new_symbol(Some("[anonymous]")))
  }
  // Bind private brand in method environments so PrivateMember/PrivateIdent
  // can resolve it at eval time via the environment chain.
  instance_method_env.def_builtin("[[PrivateBrand]]", private_brand)
  static_method_env.def_builtin("[[PrivateBrand]]", private_brand)
  // Process all members (methods and fields)
  // Class bodies are always strict — computed property keys and method keys
  // must be evaluated as strict code per ES2022 §15.7.
  for mbr in members {
    match mbr {
      @ast.ClassMember::Field(f) => {
        // Evaluate key now (computed field keys are evaluated in order at class definition)
        // ToPropertyKey: keep Symbol as-is; coerce everything else to String once,
        // so side-effectful toString/valueOf don't re-run per-construction.
        let key_val : Value = match f.key {
          StringLit(s, _, _, _) => String_(s)
          PrivateIdent(name, _) => String_(name) // private fields: use name directly
          _ => {
            let v = self.eval_expr(class_ctx, f.key, class_env)
            to_property_key(v, interp=Some(self))
          }
        }
        let field_init : ClassFieldInit = {
          key: key_val,
          initializer: f.initializer,
          closure: if f.is_static {
            static_method_env
          } else {
            instance_method_env
          },
        }
        if f.is_private {
          if f.is_static {
            private_static_fields.push(field_init)
          } else {
            private_instance_fields.push(field_init)
          }
        } else if f.is_static {
          static_fields.push(field_init)
        } else {
          instance_fields.push(field_init)
        }
      }
      @ast.ClassMember::StaticBlock(_) => ()
      @ast.ClassMember::Method(m) => { // method branch
        // Get the method key - could be string or symbol
        let key_val : Value = match m.key {
          StringLit(s, _, _, _) => String_(s)
          PrivateIdent(name, _) => String_(name) // private methods: use name directly
          _ =>
            to_property_key(
              self.eval_expr(class_ctx, m.key, class_env),
              interp=Some(self),
            )
        }
        let method_key_str : String = match key_val {
          Symbol(_) => key_val.to_string()
          _ => self.to_js_string(key_val)
        }
        // ES2022 §15.7.10 ClassElementEvaluation: a static MethodDefinition
        // whose property key is the String "prototype" throws TypeError.
        // Symbol keys are exempt (key_val is Symbol(_), method_key_str is the
        // Symbol description, not the literal "prototype").
        if m.is_static && key_val is String_("prototype") {
          raise @errors.TypeError(
            message="Class static method or accessor cannot be named 'prototype'",
          )
        }
        // Per ES2022 §15.7.13, only a non-computed `constructor` method
        // becomes the class's [[Construct]]. Computed `["constructor"]` is
        // a regular prototype method.
        if method_key_str == "constructor" &&
          !m.is_static &&
          !m.is_private &&
          !m.computed {
          // Extract constructor params and body
          // Class bodies are always strict, so ensure constructor body has "use strict"
          match m.value {
            FuncExpr(_, param_names, body, _, _) => {
              let strict_body = ensure_strict_body(body)
              validate_function_signature(true, None, param_names, strict_body)
              // Wrap plain names as Param objects (no defaults/rest/patterns)
              let params : Array[@ast.Param] = param_names.map(fn(n) {
                (
                  {
                    name: n,
                    default_val: None,
                    pattern: None,
                    is_rest_pattern: false,
                  } : @ast.Param)
              })
              ctor_fn = Some((params, None, strict_body))
              ctor_param_count = param_names.length()
            }
            FuncExprExt(_, params, rest, body, _, _) => {
              let strict_body = ensure_strict_body(body)
              validate_function_signature_ext(
                true,
                None,
                params,
                rest,
                strict_body,
              )
              ctor_fn = Some((params, rest, strict_body))
              ctor_param_count = expected_argument_count_ext(params)
            }
            _ => ()
          }
          continue
        }
        // A method that reaches here with key "constructor" on the prototype
        // (computed `["constructor"]`) overrides the spec back-reference write.
        // Static "constructor" doesn't touch the prototype slot, so gate on
        // !m.is_static.
        if method_key_str == "constructor" && !m.is_static {
          constructor_overridden = true
        }
        // Create the method function value
        // Class methods are always strict — ensure body has "use strict"
        // Use the appropriate method environment for super.method() support
        let method_closure = if m.is_static {
          static_method_env
        } else {
          instance_method_env
        }
        let method_val : Value = match m.value {
          FuncExpr(fn_name, params, body, _, method_source_text) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature(true, fn_name, params, strict_body)
            let func_data : FuncData = {
              name: fn_name,
              params,
              body: strict_body,
              closure: method_closure,
              strict: true,
              // Class methods don't get a §15.2.5 self-name binding.
              has_name_binding: false,
              // Class MethodDefinitions do not go through MakeConstructor.
              is_method: true,
              source_text: method_source_text,
            }
            make_func(func_data)
          }
          FuncExprExt(fn_name, params, rest, body, _, method_source_text) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature_ext(
              true, fn_name, params, rest, strict_body,
            )
            let func_data : FuncDataExt = {
              name: fn_name,
              params,
              rest_param: rest,
              body: strict_body,
              closure: method_closure,
              strict: true,
              has_name_binding: false,
              is_method: true,
              source_text: method_source_text,
            }
            make_func_ext(func_data)
          }
          // Generator/async methods: fast path with has_name_binding=false.
          // Class MethodDefinitionEvaluation must not create a FunctionExpressionName
          // binding; generator/async can't be stripped post-hoc so suppress at creation.
          GeneratorExpr(fn_name, params, body, _, method_source_text) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature(true, fn_name, params, strict_body)
            self.make_generator_function(
              fn_name,
              params,
              None,
              strict_body,
              true,
              method_closure,
              has_name_binding=false,
              is_method=true,
              source_text=method_source_text,
            )
          }
          GeneratorExprExt(fn_name, params, rest, body, _, method_source_text) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature_ext(
              true, fn_name, params, rest, strict_body,
            )
            self.make_generator_function_ext(
              fn_name,
              params,
              rest,
              strict_body,
              true,
              method_closure,
              has_name_binding=false,
              is_method=true,
              source_text=method_source_text,
            )
          }
          AsyncFuncExpr(fn_name, params, body, _, method_source_text) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature(true, fn_name, params, strict_body)
            self.make_async_function(
              fn_name,
              params,
              None,
              strict_body,
              true,
              method_closure,
              has_name_binding=false,
              is_method=true,
              source_text=method_source_text,
            )
          }
          AsyncFuncExprExt(fn_name, params, rest, body, _, method_source_text) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature_ext(
              true, fn_name, params, rest, strict_body,
            )
            self.make_async_function_ext(
              fn_name,
              params,
              rest,
              strict_body,
              true,
              method_closure,
              has_name_binding=false,
              is_method=true,
              source_text=method_source_text,
            )
          }
          AsyncGeneratorExpr(fn_name, params, body, _, method_source_text) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature(true, fn_name, params, strict_body)
            self.make_async_generator_function(
              fn_name,
              params,
              None,
              strict_body,
              true,
              method_closure,
              has_name_binding=false,
              is_method=true,
              source_text=method_source_text,
            )
          }
          AsyncGeneratorExprExt(
            fn_name,
            params,
            rest,
            body,
            _,
            method_source_text
          ) => {
            let strict_body = ensure_strict_body(body)
            validate_function_signature_ext(
              true, fn_name, params, rest, strict_body,
            )
            self.make_async_generator_function_ext(
              fn_name,
              params,
              rest,
              strict_body,
              true,
              method_closure,
              has_name_binding=false,
              is_method=true,
              source_text=method_source_text,
            )
          }
          _ => self.eval_expr(class_ctx, m.value, method_closure)
        }
        // Handle getter/setter vs regular method
        // Class methods are non-enumerable per spec
        // Set appropriate function name with get/set prefix
        match m.kind {
          Get => set_function_name(method_val, "get " + method_key_str)
          Set => set_function_name(method_val, "set " + method_key_str)
          _ => set_function_name(method_val, method_key_str)
        }
        // Private methods: skip public installation, store in private_methods map
        if m.is_private {
          let priv_name : String = match m.key {
            PrivateIdent(n, _) => n
            _ => method_key_str
          }
          private_methods[priv_name] = method_val
          continue
        }
        match m.kind {
          Get =>
            // Getter method - store as accessor descriptor
            match key_val {
              Symbol(sym) => {
                let (props, descs) = if m.is_static {
                  (static_symbol_props, static_symbol_descriptors)
                } else {
                  (proto_symbol_props, proto_symbol_descriptors)
                }
                props[sym.id] = Undefined
                let existing = descs.get(sym.id)
                descs[sym.id] = {
                  writable: false,
                  enumerable: false,
                  configurable: true,
                  getter: Some(method_val),
                  setter: match existing {
                    Some(d) => d.setter
                    None => None
                  },
                  is_accessor: true,
                }
              }
              _ => {
                let (props, descs) = if m.is_static {
                  (static_props, static_descriptors)
                } else {
                  (proto_props, proto_descriptors)
                }
                props[method_key_str] = Undefined
                let existing = descs.get(method_key_str)
                descs[method_key_str] = {
                  writable: false,
                  enumerable: false,
                  configurable: true,
                  getter: Some(method_val),
                  setter: match existing {
                    Some(d) => d.setter
                    None => None
                  },
                  is_accessor: true,
                }
              }
            }
          Set =>
            // Setter method - store as accessor descriptor
            match key_val {
              Symbol(sym) => {
                let (props, descs) = if m.is_static {
                  (static_symbol_props, static_symbol_descriptors)
                } else {
                  (proto_symbol_props, proto_symbol_descriptors)
                }
                props[sym.id] = Undefined
                let existing = descs.get(sym.id)
                descs[sym.id] = {
                  writable: false,
                  enumerable: false,
                  configurable: true,
                  getter: match existing {
                    Some(d) => d.getter
                    None => None
                  },
                  setter: Some(method_val),
                  is_accessor: true,
                }
              }
              _ => {
                let (props, descs) = if m.is_static {
                  (static_props, static_descriptors)
                } else {
                  (proto_props, proto_descriptors)
                }
                props[method_key_str] = Undefined
                let existing = descs.get(method_key_str)
                descs[method_key_str] = {
                  writable: false,
                  enumerable: false,
                  configurable: true,
                  getter: match existing {
                    Some(d) => d.getter
                    None => None
                  },
                  setter: Some(method_val),
                  is_accessor: true,
                }
              }
            }
          _ => {
            // Regular method or Init
            let method_descriptor : PropDescriptor = {
              writable: true,
              enumerable: false,
              configurable: true,
              getter: None,
              setter: None,
              is_accessor: false,
            }
            // Store method in appropriate map based on key type (string vs symbol)
            match key_val {
              Symbol(sym) =>
                // Symbol key - store in symbol_properties
                if m.is_static {
                  static_symbol_props[sym.id] = method_val
                  static_symbol_descriptors[sym.id] = method_descriptor
                } else {
                  proto_symbol_props[sym.id] = method_val
                  proto_symbol_descriptors[sym.id] = method_descriptor
                }
              _ =>
                // String key - store in regular properties
                if m.is_static {
                  static_props[method_key_str] = method_val
                  static_descriptors[method_key_str] = method_descriptor
                } else {
                  proto_props[method_key_str] = method_val
                  proto_descriptors[method_key_str] = method_descriptor
                }
            }
          }
        }
      } // end Method arm
    } // end match member
  } // end for member in members
  // Bind private methods in method environments for PrivateMember eval
  let private_methods_obj : Value = Object({
    bag: {
      properties: private_methods,
      symbol_properties: Map([]),
      descriptors: Map([]),
      symbol_descriptors: Map([]),
      internal_slots: Map([]),
      host_slots: Map([]),
    },
    prototype: Null,
    callable: None,
    class_name: "Object",
    extensible: true,
    arraybuffer_state: None,
  })
  instance_method_env.def_builtin("[[PrivateMethods]]", private_methods_obj)
  static_method_env.def_builtin("[[PrivateMethods]]", private_methods_obj)
  // Create prototype object
  let proto = Object({
    bag: {
      properties: proto_props,
      symbol_properties: proto_symbol_props,
      descriptors: proto_descriptors,
      symbol_descriptors: proto_symbol_descriptors,
      internal_slots: Map([]),
      host_slots: Map([]),
    },
    prototype: super_proto,
    callable: None,
    class_name: name,
    extensible: true,
    arraybuffer_state: None,
  })
  // Add constructor back-reference to prototype
  // (will be set after class object is created)
  // The class function's [[Prototype]] is always the super class constructor
  // so that static inheritance works.  For `extends null` (super_ctor = Some(Null))
  // and for base classes (None), the class function inherits from Function.prototype.
  let class_proto = match super_ctor {
    Some(Null) | None =>
      self.global.get("[[FunctionPrototype]]") catch {
        _ => Null
      }
    Some(super_class) => super_class
  }

  // Create the class constructor function
  let class_obj = stamp_function_realm(
    Object({
      bag: {
        properties: static_props,
        symbol_properties: static_symbol_props,
        descriptors: static_descriptors,
        symbol_descriptors: static_symbol_descriptors,
        internal_slots: Map([]),
        host_slots: Map([]),
      },
      prototype: class_proto,
      callable: Some(
        ClassConstructor({
          name,
          proto,
          super_ctor,
          ctor_fn,
          closure: instance_method_env,
          super_proto,
          instance_fields,
          private_instance_fields,
          source_text,
          private_brand,
          private_methods,
        }),
      ),
      class_name: "Function",
      extensible: true,
      arraybuffer_state: None,
    }),
    realm_state=Some(self.realm_state),
  )
  // Method closures share these mutable environments, so binding the completed
  // home objects here makes GetSuperBase observe their live [[Prototype]] values
  // on every evaluation, including after Object.setPrototypeOf.
  instance_method_env.def_builtin("[[HomeObject]]", proto)
  static_method_env.def_builtin("[[HomeObject]]", class_obj)
  // Set prototype.constructor to point to the class (non-enumerable per spec)
  if !constructor_overridden {
    match proto {
      Object(proto_data) => {
        proto_data.bag.properties["constructor"] = class_obj
        proto_data.bag.descriptors["constructor"] = {
          writable: true,
          enumerable: false,
          configurable: true,
          getter: None,
          setter: None,
          is_accessor: false,
        }
      }
      _ => ()
    }
  }
  // Add length, name, prototype properties to the class constructor per spec
  match class_obj {
    Object(class_data) => {
      // Add length (formal parameter count) - non-writable, non-enumerable, configurable
      class_data.bag.properties["length"] = Number(ctor_param_count.to_double())
      class_data.bag.descriptors["length"] = non_enum_configurable
      // Add prototype - non-writable, non-enumerable, non-configurable
      class_data.bag.properties["prototype"] = proto
      class_data.bag.descriptors["prototype"] = {
        writable: false,
        enumerable: false,
        configurable: false,
        getter: None,
        setter: None,
        is_accessor: false,
      }
    }
    _ => ()
  }
  // ClassDefinitionEvaluation initializes the inner immutable binding only
  // after evaluating class elements, and before executing static initializers.
  match class_binding {
    Some(binding) => class_env.initialize(binding, class_obj)
    None => ()
  }
  // Evaluate static field initializers (run once at class definition time).
  // Class bodies are strict, but field initializers are not generator bodies.
  let static_field_ctx : ExecContext = { strict: true, current_generator: None }
  for sf in static_fields {
    let key_str : String = match sf.key {
      String_(s) => s
      Symbol(sym) => sym.id.to_string() // use id for symbol-keyed static fields
      _ => self.to_js_string(sf.key)
    }
    let value : Value = match sf.initializer {
      Some(expr) => {
        // Evaluate initializer with `this` = class_obj and class name in scope
        let field_env = Environment::new(parent=Some(static_method_env))
        field_env.def_builtin("this", class_obj)
        self.eval_expr(static_field_ctx, expr, field_env)
      }
      None => Undefined
    }
    match class_obj {
      Object(class_data) =>
        match sf.key {
          Symbol(sym) => {
            if !class_data.bag.symbol_properties.contains(sym.id) &&
              !class_data.extensible {
              raise @errors.TypeError(
                message="Cannot define static field on non-extensible object",
              )
            }
            match class_data.bag.symbol_descriptors.get(sym.id) {
              Some(d) =>
                if !d.configurable {
                  raise @errors.TypeError(
                    message="Cannot redefine non-configurable static class field",
                  )
                }
              None => ()
            }
            class_data.bag.symbol_properties[sym.id] = value
            class_data.bag.symbol_descriptors[sym.id] = {
              writable: true,
              enumerable: true,
              configurable: true,
              getter: None,
              setter: None,
              is_accessor: false,
            }
          }
          _ => {
            if !class_data.bag.properties.contains(key_str) &&
              !class_data.extensible {
              raise @errors.TypeError(
                message="Cannot define static field on non-extensible object",
              )
            }
            match class_data.bag.descriptors.get(key_str) {
              Some(d) =>
                if !d.configurable {
                  raise @errors.TypeError(
                    message="Cannot redefine non-configurable static class field",
                  )
                }
              None => ()
            }
            class_data.bag.properties[key_str] = value
            class_data.bag.descriptors[key_str] = {
              writable: true,
              enumerable: true,
              configurable: true,
              getter: None,
              setter: None,
              is_accessor: false,
            }
          }
        }
      _ => ()
    }
  }
  // Execute static blocks in order (after static field initialization)
  // Each StaticBlock has `this` = class_obj and the class name in scope.
  for mbr in members {
    match mbr {
      @ast.ClassMember::StaticBlock(stmts, _) => {
        let block_env = Environment::new(parent=Some(static_method_env))
        block_env.def_builtin("this", class_obj)
        let _ = self.exec_stmts(static_field_ctx, stmts, block_env)
      }
      _ => ()
    }
  }
  class_obj
}