///|
/// PKL-106: sandbox configuration consulted by the evaluator and the
/// CLI loader. Apple Pkl exposes three CLI knobs that shape what a
/// module can `import` or `read`:
///
///   - `--allowed-modules `: a `|`-separated list of URI
///     prefixes that imports must match. When empty (the default)
///     imports are unrestricted.
///   - `--module-path `: extra directories searched when
///     resolving an unqualified import URI. Repeatable.
///   - `-p NAME=VALUE`: populate the `prop:` resolver so
///     `read("prop:NAME")` returns `VALUE`. Repeatable. Lifts `prop:`
///     into the `read` allow-list alongside `env:`.
///
/// The configuration is stored in module-level mutable state because
/// the CLI installs it once at startup and the evaluator threads it
/// through deep call paths that we don't want to rewrite to thread
/// an extra parameter. The shape mirrors how `env:` already reads
/// from process-global state (host environment variables); flipping
/// the storage to a `SandboxConfig` argument later is straightforward
/// once an embedded use case demands it.
priv struct SandboxConfig {
  mut props : Map[String, String]
  mut allowed_modules : Array[String]
  mut module_paths : Array[String]
  // PKL-150 (#258): package cache roots. The CLI can fill this cache
  // itself from registry metadata + zipballs; embedded callers may
  // still provide pre-extracted roots. A directory looks like
  // `//@/package/`, with metadata in
  // the package root sibling (`@.json`).
  mut package_caches : Array[String]
  mut env_override_enabled : Bool
  mut env_vars : Map[String, String]
  // PKL-148r: triple-dot import resolutions cached by the CLI's load
  // step (FS-based) so the IO-free analysis layer can map the same
  // `(from, ".../X")` pair back to the absolute path the CLI already
  // loaded into the session source store. Key shape is
  // `"\n"`.
  triple_dot_resolutions : Map[String, String]
  read_resources : Map[String, SandboxResource]
  read_globs : Map[String, Array[SandboxResource]]
  import_globs : Map[String, Array[String]]
  import_glob_errors : Map[String, Map[String, String]]
  // PKL-148bo: per-module import edges in source order, keyed by the
  // canonical `file:///$snippetsDir/...` or `package://...` URI.
  // Populated by the CLI's load step (which has the file IO surface)
  // so the IO-free `pkl:analyze.importGraph` intrinsic can rebuild the
  // graph by walking these adjacency lists.
  module_imports : Map[String, Array[String]]
  module_aliases : Map[String, String]
  // PKL-080: stdlib `base.pkl` source text, registered by the CLI
  // when the file is available on disk. The library-side reflect
  // intrinsics parse this on first use to build full stdlib Class
  // mirrors (location / docComment / methods / properties etc.) for
  // `reflect.Class(stdlibType)` calls in `api/reflectedDeclaration`.
  // `None` when the source isn't reachable (embedded callers, JS /
  // WASM target without FS access).
  mut stdlib_base_source : String?
  // Lazy `base.pkl` resolution: the CLI registers a loader closure that
  // reads from a candidate path list on first use. Fixtures that never
  // touch `reflect.*` (the common case) skip the ~140 KB file read
  // entirely. Resolution is one-shot: `stdlib_base_source` caches the
  // result (even None) so subsequent calls are O(1).
  mut stdlib_base_resolved : Bool
  mut stdlib_base_loader : (() -> String?)?
  // Host-specific URI canonicalization for pklbinary metadata. Embedded
  // sessions keep their logical path strings; the CLI installs a file-URI
  // normalizer so its bytes match Apple Pkl's command-line output.
  mut pklbinary_module_uri_normalizer : ((String) -> String)?
  // PKL-153: dynamic resource readers registered by embedded callers.
  // Keyed by scheme (no trailing `:`), the closure is invoked when
  // `read("scheme:path")` is evaluated and the static
  // `read_resources` map has no entry. Returning `None` falls back
  // to the existing "Cannot find / refusing resource" diagnostic.
  // Apple Pkl's `--external-resource-reader==` is the
  // CLI-side equivalent; this hook lets pkfire / pkspec embed mpkl
  // and serve `cmd:` / `http:` / arbitrary-scheme reads in-process.
  mut resource_readers : Map[String, (String) -> SandboxResource?]
}

///|
pub(all) struct SandboxResource {
  uri : String
  resource_uri : String
  text : String
} derive(Eq, Debug)

///|
let sandbox_config : SandboxConfig = {
  props: Map([], capacity=8),
  allowed_modules: [],
  module_paths: [],
  package_caches: [],
  env_override_enabled: false,
  env_vars: Map([], capacity=8),
  triple_dot_resolutions: Map([], capacity=8),
  read_resources: Map([], capacity=8),
  read_globs: Map([], capacity=8),
  import_globs: Map([], capacity=8),
  import_glob_errors: Map([], capacity=8),
  module_imports: Map([], capacity=16),
  module_aliases: Map([], capacity=16),
  stdlib_base_source: None,
  stdlib_base_resolved: false,
  stdlib_base_loader: None,
  pklbinary_module_uri_normalizer: None,
  resource_readers: Map([], capacity=4),
}

///|
pub fn configure_pklbinary_module_uri_normalizer(
  normalizer : (String) -> String,
) -> Unit {
  sandbox_config.pklbinary_module_uri_normalizer = Some(normalizer)
}

///|
fn normalize_pklbinary_module_uri(uri : String) -> String {
  match sandbox_config.pklbinary_module_uri_normalizer {
    Some(normalizer) => normalizer(uri)
    None => uri
  }
}

///|
/// Replace the configured props map. Pass an empty map to clear.
pub fn configure_sandbox_props(props : Map[String, String]) -> Unit {
  sandbox_config.props = props
}

///|
pub fn configure_sandbox_env_vars(vars : Map[String, String]) -> Unit {
  sandbox_config.env_override_enabled = true
  sandbox_config.env_vars = vars
}

///|
pub fn clear_sandbox_env_vars() -> Unit {
  sandbox_config.env_override_enabled = false
  sandbox_config.env_vars = Map([], capacity=8)
}

///|
fn sandbox_lookup_env(name : String) -> String? {
  if sandbox_config.env_override_enabled {
    sandbox_config.env_vars.get(name)
  } else {
    @env.get_env_var(name)
  }
}

///|
fn sandbox_env_entries() -> Array[(String, String)] {
  if sandbox_config.env_override_enabled {
    sandbox_config.env_vars.to_array()
  } else {
    @env.get_env_vars().to_array()
  }
}

///|
/// Replace the configured allowed-modules pattern list. An empty
/// list disables the allow-list (everything is permitted).
pub fn configure_sandbox_allowed_modules(patterns : Array[String]) -> Unit {
  sandbox_config.allowed_modules = patterns
}

///|
/// Replace the configured module-path search list. Directories are
/// consulted in order when resolving an unqualified import.
pub fn configure_sandbox_module_paths(dirs : Array[String]) -> Unit {
  sandbox_config.module_paths = dirs
}

///|
/// Look up a `prop:` value installed via `-p NAME=VALUE`. Returns
/// `None` when the key is absent, which `eval_read_uri` translates
/// into a diagnostic mirroring the `env:` missing-variable path.
fn sandbox_lookup_prop(name : String) -> String? {
  sandbox_config.props.get(sandbox_percent_decode(name))
}

///|
fn sandbox_prop_entries() -> Array[(String, String)] {
  sandbox_config.props.to_array()
}

///|
fn sandbox_hex_value(c : UInt16) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - '0'.to_int())
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 'a'.to_int() + 10)
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 'A'.to_int() + 10)
  } else {
    None
  }
}

///|
fn sandbox_percent_decode(s : String) -> String {
  let buf = StringBuilder::new()
  let mut i = 0
  while i < s.length() {
    if s[i] == '%' && i + 2 < s.length() {
      match (sandbox_hex_value(s[i + 1]), sandbox_hex_value(s[i + 2])) {
        (Some(hi), Some(lo)) => {
          buf.write_char((hi * 16 + lo).unsafe_to_char())
          i = i + 3
          continue
        }
        _ => ()
      }
    }
    buf.write_char(s[i].to_int().unsafe_to_char())
    i = i + 1
  }
  buf.to_string()
}

///|
fn sandbox_percent_encode_resource_key(s : String) -> String {
  let buf = StringBuilder::new()
  for c in s.iter() {
    match c {
      ' ' => buf.write_string("%20")
      '[' => buf.write_string("%5B")
      ']' => buf.write_string("%5D")
      '\\' => buf.write_string("%5C")
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
fn sandbox_glob_matches(pattern : String, text : String) -> Bool {
  sandbox_glob_matches_at(pattern, 0, text, 0)
}

///|
fn sandbox_glob_matches_at(
  pattern : String,
  pi : Int,
  text : String,
  ti : Int,
) -> Bool {
  if pi >= pattern.length() {
    return ti >= text.length()
  }
  if pattern[pi] == '*' {
    if pi + 1 < pattern.length() && pattern[pi + 1] == '*' {
      let mut j = ti
      while j <= text.length() {
        if sandbox_glob_matches_at(pattern, pi + 2, text, j) {
          return true
        }
        j = j + 1
      }
      return false
    }
    let mut j = ti
    while j <= text.length() {
      if sandbox_glob_matches_at(pattern, pi + 1, text, j) {
        return true
      }
      if j >= text.length() || text[j] == '/' {
        return false
      }
      j = j + 1
    }
    false
  } else if pattern[pi] == '[' {
    match sandbox_match_char_class(pattern, pi, text, ti) {
      Some(next_pi) => sandbox_glob_matches_at(pattern, next_pi, text, ti + 1)
      None => false
    }
  } else if ti < text.length() && pattern[pi] == text[ti] {
    sandbox_glob_matches_at(pattern, pi + 1, text, ti + 1)
  } else {
    false
  }
}

///|
fn sandbox_match_char_class(
  pattern : String,
  pi : Int,
  text : String,
  ti : Int,
) -> Int? {
  if ti >= text.length() {
    return None
  }
  let mut i = pi + 1
  let c = text[ti]
  let mut matched = false
  while i < pattern.length() && pattern[i] != ']' {
    if i + 2 < pattern.length() &&
      pattern[i + 1] == '-' &&
      pattern[i + 2] != ']' {
      if c >= pattern[i] && c <= pattern[i + 2] {
        matched = true
      }
      i = i + 3
    } else {
      if c == pattern[i] {
        matched = true
      }
      i = i + 1
    }
  }
  if i < pattern.length() && pattern[i] == ']' && matched {
    Some(i + 1)
  } else {
    None
  }
}

///|
/// True when `uri` is permitted by the current allow-list. An empty
/// allow-list means "no restriction" so the default CLI behaviour is
/// unchanged from before the slice. Each pattern is treated as a
/// literal URI prefix — the format matches Apple Pkl's
/// `|`-separated prefix lookup (e.g. `pkl:|file:|https:`).
///
/// Bare filesystem paths (no `scheme:` prefix) bypass the allow-list
/// because the check is intended for sandbox-relevant URIs that
/// cross trust boundaries (`https:`, `package:`, the stdlib `pkl:`,
/// etc.). The entrypoint and locally-rooted imports are already
/// gated by filesystem permissions and the user typing the path on
/// the command line, so re-checking them against the same allow-list
/// would force users to spell out every directory.
pub fn sandbox_is_module_allowed(uri : String) -> Bool {
  if sandbox_config.allowed_modules.length() == 0 {
    return true
  }
  match uri.find(":") {
    None => true
    Some(_) =>
      for pattern in sandbox_config.allowed_modules {
        if uri.has_prefix(pattern) {
          break true
        }
      } nobreak {
        false
      }
  }
}

///|
/// Iterate the configured `--module-path` directories in CLI order.
/// The loader prepends each directory to an unqualified import URI
/// and tries the resulting path; the first hit wins. Returning the
/// array directly keeps the loader free of an iterator wrapper.
pub fn sandbox_module_paths() -> Array[String] {
  sandbox_config.module_paths
}

///|
/// Replace the configured package-cache search list. See
/// `SandboxConfig.package_caches` for the layout each directory must
/// follow.
pub fn configure_sandbox_package_caches(dirs : Array[String]) -> Unit {
  sandbox_config.package_caches = dirs
}

///|
/// Iterate the configured `--package-cache` directories in CLI order.
pub fn sandbox_package_caches() -> Array[String] {
  sandbox_config.package_caches
}

///|
/// PKL-148r: register a triple-dot import resolution. Called by the
/// CLI loader after `@fs.path_exists` finds the matching ancestor
/// candidate, so the analysis-layer resolver can map the same
/// `(from, uri)` pair back to the same absolute path without
/// re-doing FS access.
pub fn register_triple_dot_resolution(
  from : String,
  uri : String,
  resolved : String,
) -> Unit {
  sandbox_config.triple_dot_resolutions[from + "\n" + uri] = resolved
}

///|
/// PKL-148r: look up a previously-registered triple-dot resolution.
/// Returns the absolute path the CLI loaded the module under, or
/// `None` if the CLI never saw this `(from, uri)` pair.
pub fn lookup_triple_dot_resolution(from : String, uri : String) -> String? {
  sandbox_config.triple_dot_resolutions.get(from + "\n" + uri)
}

///|
fn sandbox_io_key(from : String, uri : String) -> String {
  from + "\n" + uri
}

///|
pub fn reset_sandbox_io_cache() -> Unit {
  sandbox_config.read_resources.clear()
  sandbox_config.read_globs.clear()
  sandbox_config.import_globs.clear()
  sandbox_config.import_glob_errors.clear()
  sandbox_config.module_imports.clear()
  sandbox_config.module_aliases.clear()
}

///|
pub fn register_read_resource(
  from : String,
  uri : String,
  visible_uri : String,
  resource_uri : String,
  text : String,
) -> Unit {
  sandbox_config.read_resources[sandbox_io_key(from, uri)] = {
    uri: visible_uri,
    resource_uri,
    text,
  }
}

///|
fn sandbox_lookup_read_resource(
  from : String,
  uri : String,
) -> SandboxResource? {
  sandbox_config.read_resources.get(sandbox_io_key(from, uri))
}

///|
pub fn register_read_glob(
  from : String,
  pattern : String,
  resources : Array[SandboxResource],
) -> Unit {
  sandbox_config.read_globs[sandbox_io_key(from, pattern)] = resources
}

///|
fn sandbox_lookup_read_glob(
  from : String,
  pattern : String,
) -> Array[SandboxResource]? {
  sandbox_config.read_globs.get(sandbox_io_key(from, pattern))
}

///|
pub fn register_import_glob(
  from : String,
  pattern : String,
  uris : Array[String],
) -> Unit {
  sandbox_config.import_globs[sandbox_io_key(from, pattern)] = uris
}

///|
pub fn register_import_glob_errors(
  from : String,
  pattern : String,
  errors : Map[String, String],
) -> Unit {
  sandbox_config.import_glob_errors[sandbox_io_key(from, pattern)] = errors
}

///|
/// PKL-148bo: record the resolved import edges for a single module.
/// `module_uri` is the canonical `file:///$snippetsDir/...` or
/// `package://...` URI of the module; `import_uris` is the (ordered)
/// list of resolved URIs the module declares via `import` / `import*`
/// / `amends` / `extends`. The list preserves source order because
/// `pkl:analyze.importGraph` is order-sensitive in its rendering.
pub fn register_module_imports(
  module_uri : String,
  import_uris : Array[String],
) -> Unit {
  sandbox_config.module_imports[module_uri] = import_uris
}

///|
/// PKL-148bo: lookup helper used by the `_pkl_analyze_import_graph`
/// intrinsic to walk a recorded module's adjacency list.
pub fn sandbox_module_imports(module_uri : String) -> Array[String]? {
  sandbox_config.module_imports.get(module_uri)
}

///|
/// PKL-148bo: every module URI we've registered. The intrinsic uses
/// this to break out of cycles cleanly.
pub fn sandbox_all_module_uris() -> Array[String] {
  let out : Array[String] = []
  for entry in sandbox_config.module_imports {
    out.push(entry.0)
  }
  out
}

///|
/// PKL-148bo: register a canonical alias from one URI form to another.
/// Used for `import "@dep/foo.pkl"` → `package://...#/foo.pkl` so the
/// `resolvedImports` table sees the right shape even when callers ask
/// for the raw form.
pub fn register_module_alias(raw : String, resolved : String) -> Unit {
  sandbox_config.module_aliases[raw] = resolved
}

///|
pub fn sandbox_module_alias(raw : String) -> String? {
  sandbox_config.module_aliases.get(raw)
}

///|
/// Register a dynamic resource reader for `scheme:` URIs. The reader
/// is invoked the first time `read(":")` misses the
/// static `read_resources` map. Returning `None` falls back to the
/// "Cannot find / refusing" diagnostic. Re-registering for the same
/// scheme replaces the prior reader. Pass the bare scheme (no
/// trailing colon) — `configure_sandbox_resource_reader("cmd", fn)`
/// services `read("cmd:ls")` etc.
pub fn configure_sandbox_resource_reader(
  scheme : String,
  reader : (String) -> SandboxResource?,
) -> Unit {
  sandbox_config.resource_readers[scheme] = reader
}

///|
/// Drop all dynamic resource readers. Useful for embedded callers that
/// share a sandbox across runs.
pub fn clear_sandbox_resource_readers() -> Unit {
  sandbox_config.resource_readers = Map([], capacity=4)
}

///|
fn sandbox_dynamic_resource_reader(
  scheme : String,
) -> ((String) -> SandboxResource?)? {
  sandbox_config.resource_readers.get(scheme)
}

///|
/// Eagerly register the `base.pkl` source text. Use this when the
/// caller already holds the source in memory (tests, embedded callers).
/// CLI callers should prefer `configure_stdlib_base_paths` so the file
/// is only read on first reflect use.
pub fn configure_stdlib_base_source(source : String) -> Unit {
  sandbox_config.stdlib_base_source = Some(source)
  sandbox_config.stdlib_base_resolved = true
}

///|
/// Register candidate filesystem paths for `base.pkl`. The first
/// readable path is loaded lazily on the first `sandbox_stdlib_base_source()`
/// call. Reads go through `stdlib_base_loader` so this module avoids a
/// direct dependency on `@fs`.
pub fn configure_stdlib_base_paths(loader : () -> String?) -> Unit {
  sandbox_config.stdlib_base_loader = Some(loader)
}

///|
pub fn sandbox_stdlib_base_source() -> String? {
  if sandbox_config.stdlib_base_resolved {
    return sandbox_config.stdlib_base_source
  }
  sandbox_config.stdlib_base_resolved = true
  match sandbox_config.stdlib_base_loader {
    Some(loader) =>
      match loader() {
        Some(src) => {
          sandbox_config.stdlib_base_source = Some(src)
          Some(src)
        }
        None => None
      }
    None => None
  }
}

///|
fn sandbox_lookup_import_glob(
  from : String,
  pattern : String,
) -> Array[String]? {
  sandbox_config.import_globs.get(sandbox_io_key(from, pattern))
}

///|
fn sandbox_lookup_import_glob_error(
  from : String,
  pattern : String,
  uri : String,
) -> String? {
  match sandbox_config.import_glob_errors.get(sandbox_io_key(from, pattern)) {
    Some(errors) => errors.get(uri)
    None => None
  }
}