///|
pub(all) enum BindingKind {
  LetBinding
  ConstBinding
  VarBinding
  // §9.1.1.1 CreateImmutableBinding(N, false): immutable but S=false means
  // TypeError only in strict mode; non-strict assignment is silently ignored.
  // Used for named function expression self-name bindings per §14.1.22.
  FunctionNameBinding
} derive(Debug, Eq)

///|
pub impl Show for BindingKind with fn output(self, logger) {
  Debug::to_repr(self).output(logger)
}

///|
pub(all) struct Binding {
  mut value : Value
  kind : BindingKind
  mut initialized : Bool
  // Annex B §B.3.3.3: set when this binding was created or marked eligible
  // by the Annex B block-level function extension. Runtime block-entry
  // reinit only updates bindings whose `annex_b_hoisted` is true — this
  // prevents overwriting genuine lex bindings while allowing updates to
  // params (LetBinding kind) reused by the extension.
  mut annex_b_hoisted : Bool
  // True if this binding was created as a function parameter (including
  // rest parameters). Parameters share BindingKind=LetBinding with real
  // `let` declarations, but §B.3.4 treats `var name` as non-conflicting
  // with params (so Annex B hoist may tag them), while real `let` must
  // suppress the extension. This flag is the only runtime-distinguishable
  // signal, since at tag-time both are `initialized=true`.
  is_parameter : Bool
}

///|
pub(all) struct Environment {
  bindings : Map[String, Binding]
  parent : Environment?
  mut is_var_scope : Bool // true for function/global scopes (where var is hoisted to)
  mut with_object : Value? // For 'with' statement: the live object to proxy lookups through
  mut realm_state : RealmState? // Realm that owns host-installed builtin functions.
  mut interpreter_context : Interpreter? // Interpreter explicitly threaded through this scope chain.
  /// Registry of class_names that carry [[ErrorData]] semantics.
  /// Populated by register_error_ctor / register_aggregate_error_ctor at
  /// stdlib init time; consumed by Error.isError.  Stored per-Environment so
  /// future Realm work isolates correctly.
  error_class_names : Map[String, Unit]
  /// Internal marker names used for scope metadata that must not collide with
  /// real JavaScript bindings. Static markers use a fixed key; eval-created
  /// deletable vars use `eval_deletable_var_marker(name)` per binding.
  markers : Map[String, Bool]
}

///|
const EVAL_DELETABLE_VAR_PREFIX = "[[EvalDeletableVar]]:"

///|
fn eval_deletable_var_marker(name : String) -> String {
  EVAL_DELETABLE_VAR_PREFIX + name
}

///|
const EVAL_FUNCTION_RECONCILE_MARKER = "[[EvalFunctionReconcile]]"

///|
const THIS_INITIALIZED_BY_SUPER_MARKER = "[[ThisInitializedBySuper]]"

///|
pub fn Environment::new(parent? : Environment? = None) -> Environment {
  let (realm_state, interpreter_context) = match parent {
    Some(parent) => (parent.realm_state, parent.interpreter_context)
    None => (None, None)
  }
  {
    bindings: Map([]),
    parent,
    is_var_scope: false,
    with_object: None,
    realm_state,
    interpreter_context,
    error_class_names: Map([]),
    markers: Map([]),
  }
}

///|
fn Environment::set_marker(self : Environment, name : String) -> Unit {
  self.markers[name] = true
}

///|
fn Environment::has_marker(self : Environment, name : String) -> Bool {
  self.markers.contains(name)
}

///|
fn Environment::has_marker_in_chain(self : Environment, name : String) -> Bool {
  if self.markers.contains(name) {
    true
  } else {
    match self.parent {
      Some(parent) => parent.has_marker_in_chain(name)
      None => false
    }
  }
}

///|
/// Walk up the scope chain to find the nearest variable environment
/// (function or global scope where var declarations are hoisted to).
pub fn Environment::find_var_env(self : Environment) -> Environment {
  guard !self.is_var_scope else { return self }
  match self.parent {
    Some(parent) => parent.find_var_env()
    None => self // Global scope (no parent) is always a variable scope
  }
}

///|
pub fn Environment::def(
  self : Environment,
  name : String,
  value : Value,
  kind : BindingKind,
) -> Unit raise Error {
  match self.bindings.get(name) {
    Some(existing) =>
      // var can redeclare var, but nothing else can redeclare
      if kind == VarBinding && existing.kind == VarBinding {
        // Allow var/var redeclaration - just update the value
        existing.value = value
      } else {
        raise @errors.SyntaxError(
          message="Identifier '\{name}' has already been declared",
        )
      }
    None =>
      self.bindings[name] = {
        value,
        kind,
        initialized: true,
        annex_b_hoisted: false,
        is_parameter: false,
      }
  }
}

///|
/// Define a function parameter binding. Parameters are stored as
/// `LetBinding` with `is_parameter: true` so the Annex B block-level
/// function extension can distinguish them from real `let` declarations.
pub fn Environment::def_parameter(
  self : Environment,
  name : String,
  value : Value,
) -> Unit raise Error {
  if self.bindings.contains(name) {
    raise @errors.SyntaxError(
      message="Identifier '\{name}' has already been declared",
    )
  }
  self.bindings[name] = {
    value,
    kind: LetBinding,
    initialized: true,
    annex_b_hoisted: false,
    is_parameter: true,
  }
}

///|
/// Define an uninitialized parameter TDZ binding. Used by the parameter
/// pre-pass (§10.2.11 step 21) so self/forward-referring defaults throw
/// ReferenceError rather than resolving the outer scope's binding.
pub fn Environment::def_param_tdz(
  self : Environment,
  name : String,
) -> Unit raise Error {
  if self.bindings.contains(name) {
    raise @errors.SyntaxError(
      message="Identifier '\{name}' has already been declared",
    )
  }
  self.bindings[name] = {
    value: Undefined,
    kind: LetBinding,
    initialized: false,
    annex_b_hoisted: false,
    is_parameter: true,
  }
}

///|
/// Define a TDZ binding (let/const before initialization)
pub fn Environment::def_tdz(
  self : Environment,
  name : String,
  kind : BindingKind,
) -> Unit raise Error {
  if self.bindings.contains(name) {
    raise @errors.SyntaxError(
      message="Identifier '\{name}' has already been declared",
    )
  }
  self.bindings[name] = {
    value: Undefined,
    kind,
    initialized: false,
    annex_b_hoisted: false,
    is_parameter: false,
  }
}

///|
/// Initialize a TDZ binding (when declaration is executed)
pub fn Environment::initialize(
  self : Environment,
  name : String,
  value : Value,
) -> Unit raise Error {
  match self.bindings.get(name) {
    Some(binding) => {
      binding.value = value
      binding.initialized = true
    }
    None =>
      raise @errors.InternalError(
        message="Cannot initialize non-existent binding '\{name}'",
      )
  }
}

///|
/// Walk the scope chain to find an existing binding for `name` and
/// initialize it with `value`. Unlike `initialize`, which only looks at
/// the local env, this climbs parents. Needed for `super()` to write
/// `this` back to the derived class's param env when body execution
/// happens in a separate body env (§10.2.11 split). For derived-constructor
/// TDZ `this`, the write follows BindThisValue and rejects a second
/// initialization.
pub fn Environment::initialize_in_chain(
  self : Environment,
  name : String,
  value : Value,
) -> Unit raise Error {
  match self.bindings.get(name) {
    Some(binding) => {
      if name == "this" {
        let initialized_by_super = self.has_marker(
          THIS_INITIALIZED_BY_SUPER_MARKER,
        )
        if binding.initialized &&
          (binding.kind == LetBinding || initialized_by_super) {
          raise @errors.ReferenceError(
            message="Super constructor may only be called once",
          )
        }
      }
      binding.value = value
      binding.initialized = true
      if name == "this" {
        self.set_marker(THIS_INITIALIZED_BY_SUPER_MARKER)
      }
    }
    None =>
      match self.parent {
        Some(parent) => parent.initialize_in_chain(name, value)
        None =>
          raise @errors.InternalError(
            message="Cannot initialize non-existent binding '\{name}' in chain",
          )
      }
  }
}

///|
pub fn Environment::def_builtin(
  self : Environment,
  name : String,
  value : Value,
) -> Unit {
  let bound_value = match self.realm_state {
    Some(realm_state) =>
      stamp_function_realm_from_state_if_unstamped(value, realm_state)
    None => value
  }
  self.bindings[name] = {
    value: bound_value,
    kind: VarBinding,
    initialized: true,
    annex_b_hoisted: false,
    is_parameter: false,
  }
}

///|
pub fn Environment::has(self : Environment, name : String) -> Bool raise Error {
  self.resolve_binding_env(name) is Some(_)
}

///|
fn Environment::resolve_binding_env(
  self : Environment,
  name : String,
) -> Environment? raise Error {
  // Check with_object first for 'with' statement environments
  match self.with_object {
    Some(obj) => {
      let has_with = with_object_has(self.interpreter_context, obj, name)
      if has_with {
        return Some(self)
      }
    }
    None => ()
  }
  match self.bindings.get(name) {
    Some(_) => Some(self)
    None =>
      match self.parent {
        Some(parent) => parent.resolve_binding_env(name)
        None => None
      }
  }
}

///|
/// Walk up the scope chain to find a var-compatible binding with the given
/// name. Formal parameters are included because sloppy function-body `var`
/// declarations may redeclare and update mapped parameter bindings.
pub fn Environment::has_var(self : Environment, name : String) -> Bool {
  match self.bindings.get(name) {
    Some(binding) =>
      if binding.kind == VarBinding || binding.is_parameter {
        true
      } else {
        // Name exists but is let/const — continue searching parent scopes
        match self.parent {
          Some(parent) => parent.has_var(name)
          None => false
        }
      }
    None =>
      match self.parent {
        Some(parent) => parent.has_var(name)
        None => false
      }
  }
}

///|
/// Assign to the nearest var-compatible binding with the given name, skipping
/// real let/const bindings in intervening scopes. This is needed for eval'd var
/// declarations that must target the function scope's var or parameter binding,
/// even when a block-scoped let/const shadows the name in between.
pub fn Environment::assign_var(
  self : Environment,
  name : String,
  value : Value,
) -> Unit raise Error {
  match self.bindings.get(name) {
    Some(binding) =>
      if binding.kind == VarBinding || binding.is_parameter {
        binding.value = value
      } else {
        // Skip real let/const binding, look in parent
        match self.parent {
          Some(parent) => parent.assign_var(name, value)
          None => raise @errors.ReferenceError(message="\{name} is not defined")
        }
      }
    None =>
      match self.parent {
        Some(parent) => parent.assign_var(name, value)
        None => raise @errors.ReferenceError(message="\{name} is not defined")
      }
  }
}

///|
pub fn Environment::get(self : Environment, name : String) -> Value raise Error {
  self.get_with_strict(name, false)
}

///|
fn Environment::get_with_strict(
  self : Environment,
  name : String,
  strict : Bool,
) -> Value raise Error {
  // Check with_object first for 'with' statement environments
  match self.with_object {
    Some(obj) => {
      let result = with_object_get(self.interpreter_context, obj, name, strict~)
      match result {
        Some(v) => return v
        None => ()
      }
    }
    None => ()
  }
  match self.bindings.get(name) {
    Some(binding) =>
      if !binding.initialized {
        raise @errors.ReferenceError(
          message="Cannot access '\{name}' before initialization",
        )
      } else {
        resolve_module_export_value(binding.value)
      }
    None =>
      match self.parent {
        Some(parent) => parent.get_with_strict(name, strict)
        None => raise @errors.ReferenceError(message="\{name} is not defined")
      }
  }
}

///|
pub fn Environment::assign(
  self : Environment,
  name : String,
  value : Value,
) -> Unit raise Error {
  self.assign_with_strict(name, value, false)
}

///|
fn Environment::assign_with_strict(
  self : Environment,
  name : String,
  value : Value,
  strict : Bool,
) -> Unit raise Error {
  // Check with_object first for 'with' statement environments
  match self.with_object {
    Some(obj) => {
      let had_own = with_object_has_own_direct(obj, name)
      if with_object_has(self.interpreter_context, obj, name) {
        match self.interpreter_context {
          Some(_) =>
            if strict && had_own && !with_object_has_own_direct(obj, name) {
              raise @errors.ReferenceError(message="\{name} is not defined")
            }
          None => ()
        }
        with_object_set(self.interpreter_context, obj, name, value, strict~)
        return
      }
    }
    None => ()
  }
  match self.bindings.get(name) {
    Some(binding) => assign_binding_value(binding, name, value, strict)
    None =>
      match self.parent {
        Some(parent) => parent.assign_with_strict(name, value, strict)
        None => raise @errors.ReferenceError(message="\{name} is not defined")
      }
  }
}

///|
fn Environment::assign_resolved(
  self : Environment,
  name : String,
  value : Value,
  strict : Bool,
) -> Unit raise Error {
  match self.with_object {
    Some(obj) => {
      match self.interpreter_context {
        Some(interp) =>
          if strict && !interp.has_property_key(obj, String_(name)) {
            raise @errors.ReferenceError(message="\{name} is not defined")
          }
        None => ()
      }
      with_object_set(self.interpreter_context, obj, name, value, strict~)
      return
    }
    None => ()
  }
  match self.bindings.get(name) {
    Some(binding) => assign_binding_value(binding, name, value, strict)
    None => raise @errors.ReferenceError(message="\{name} is not defined")
  }
}

///|
fn assign_binding_value(
  binding : Binding,
  name : String,
  value : Value,
  strict : Bool,
) -> Unit raise Error {
  if !binding.initialized {
    raise @errors.ReferenceError(
      message="Cannot access '\{name}' before initialization",
    )
  } else if binding.kind == ConstBinding {
    raise @errors.TypeError(message="Assignment to constant variable '\{name}'")
  } else if binding.kind == FunctionNameBinding {
    if strict {
      raise @errors.TypeError(
        message="Assignment to constant variable '\{name}'",
      )
    }
    // Non-strict: silently ignore per §9.1.1.1 CreateImmutableBinding(N, false)
  } else {
    binding.value = value
  }
}

///|
/// Check if a property is blocked by @@unscopables on the binding object
fn is_blocked_by_unscopables(
  interp : Interpreter,
  obj : Value,
  name : String,
) -> Bool raise Error {
  let unscopables_sym = interp.realm_state.well_known_symbols.unscopables
  let unscopables = interp.get_computed_property(
    obj,
    Symbol(unscopables_sym),
    @token.Loc::default(),
  )
  if is_object_value(unscopables) {
    is_truthy(interp.get_property(unscopables, name, @token.Loc::default()))
  } else {
    false
  }
}

///|
/// Check if a with-object (or its prototype chain) has a property
fn with_object_has(
  interp_context : Interpreter?,
  obj : Value,
  name : String,
) -> Bool raise Error {
  match interp_context {
    Some(interp) =>
      if interp.has_property_key(obj, String_(name)) {
        return !is_blocked_by_unscopables(interp, obj, name)
      } else {
        return false
      }
    None => ()
  }
  let mut current = obj
  while true {
    match current {
      Object(data) => {
        if data.bag.properties.contains(name) {
          return true
        }
        // Check descriptors (for accessor-only properties)
        if data.bag.descriptors.contains(name) {
          return true
        }
        current = data.prototype
      }
      _ => break
    }
  }
  false
}

///|
/// Get a property from a with-object (walking prototype chain)
fn with_object_has_own_direct(obj : Value, name : String) -> Bool {
  match obj {
    Object(data) =>
      data.bag.properties.contains(name) || data.bag.descriptors.contains(name)
    _ => false
  }
}

///|
fn with_object_get(
  interp_context : Interpreter?,
  obj : Value,
  name : String,
  strict? : Bool = false,
) -> Value? raise Error {
  match interp_context {
    Some(interp) => {
      // Check if the property exists (spec's HasProperty). For proxies
      // this triggers the has trap exactly once.
      let has_prop = interp.has_property_key(obj, String_(name))
      if has_prop {
        // Record own-property status BEFORE the @@unscopables getter
        // runs, since the getter may delete own properties as a side
        // effect. Use non-trapping direct-bag check for this.
        let had_own = with_object_has_own_direct(obj, name)
        if !is_blocked_by_unscopables(interp, obj, name) {
          // The property is not blocked. If the unscopables getter
          // deleted an own property, check whether the prototype
          // chain still provides it (non-proxy path).
          let still_own = with_object_has_own_direct(obj, name)
          if had_own &&
            !still_own &&
            !interp.has_property_key(obj, String_(name)) {
            return if strict {
              raise @errors.ReferenceError(message="\{name} is not defined")
            } else {
              Some(Undefined)
            }
          }
          return Some(interp.get_property(obj, name, @token.Loc::default()))
        }
      }
      return None
    }
    None => ()
  }
  let mut current = obj
  while true {
    match current {
      Object(data) => {
        match data.bag.properties.get(name) {
          Some(v) => return Some(v)
          None => ()
        }
        current = data.prototype
      }
      _ => break
    }
  }
  None
}

///|
/// Set a property on a with-object (always sets on the own object)
fn with_object_set(
  interp_context : Interpreter?,
  obj : Value,
  name : String,
  value : Value,
  strict? : Bool = false,
) -> Unit raise Error {
  match interp_context {
    Some(interp) => {
      let _ = interp.set_property(
        obj,
        name,
        value,
        @token.Loc::default(),
        strict~,
      )
      return
    }
    None => ()
  }
  match obj {
    Object(data) => data.bag.properties[name] = value
    _ => ()
  }
}

///|
/// Find the with-object for a name (walking up env chain to find a with env containing this name)
pub fn Environment::find_with_object(
  self : Environment,
  name : String,
) -> Value? raise Error {
  match self.with_object {
    Some(obj) =>
      if with_object_has(self.interpreter_context, obj, name) {
        return Some(obj)
      }
    None => ()
  }
  match self.parent {
    Some(parent) => parent.find_with_object(name)
    None => None
  }
}