///|
priv struct ImportReader {
  sources : Map[(String, String), Source]
  binary_sources : Map[(String, String), BinarySource]
  resolver : ((String, String) -> Source raise)?
  binary_resolver : ((String, String) -> BinarySource raise)?
  limits : Limits
}

///|
// Runtime callbacks retain this snapshot, not the owning Evaluator. Capturing
// the Evaluator itself would create a reference-count cycle through state: Vm.
fn Evaluator::reader(self : Evaluator) -> ImportReader {
  {
    sources: self.sources,
    binary_sources: self.binary_sources,
    resolver: self.resolver,
    binary_resolver: self.binary_resolver,
    limits: self.limits,
  }
}

///|
fn[T] Evaluator::query(
  self : Evaluator,
  from : String,
  action : () -> T raise,
) -> T raise JsonnetError {
  self.idle()
  let loc : Location = { file: from, line: 1, column: 1, }
  check_limits(self.limits, loc)
  self.busy = true
  defer {
    self.busy = false
  }
  action() catch {
    Error(d) => raise Error(d)
    e => raise problem(Import, loc, e.to_string())
  }
}

///|
// Queries and execution use the same immutable source identities. A binary
// callback, when present, is authoritative; text-only callbacks are UTF-8 encoded.
fn ImportReader::load_data(
  self : ImportReader,
  from : String,
  path : String,
) -> BinarySource raise {
  let key = (from, path)
  let loc : Location = { file: from, line: 1, column: 1, }
  let source = match self.binary_sources.get(key) {
    Some(source) => source
    None =>
      if self.sources.get(key) is Some(source) {
        { id: source.id, bytes: @utf8.encode(source.text), }
      } else if self.binary_resolver is Some(resolve) {
        resolve(from, path)
      } else if self.resolver is Some(resolve) {
        let source = resolve(from, path)
        { id: source.id, bytes: @utf8.encode(source.text), }
      } else {
        raise problem(Import, loc, "imports disabled: no resolver supplied")
      }
  }
  if source.bytes.length() > self.limits.source_bytes ||
    source.bytes.length() > self.limits.import_bytes {
    raise problem(Limit, loc, "import byte limit exceeded")
  }
  self.binary_sources[key] = source
  source
}

///|
fn ImportReader::load_text(
  self : ImportReader,
  from : String,
  path : String,
) -> Source raise {
  let source = self.load_data(from, path)
  let text = @utf8.decode(source.bytes) catch {
    _ =>
      raise problem(
        Import,
        { file: source.id, line: 1, column: 1, },
        "invalid UTF-8",
      )
  }
  { id: source.id, text, }
}

///|
/// Resolve an imported identity without parsing or evaluating its contents.
/// Source bytes are cached immediately and shared with later queries/evaluation.
pub fn Evaluator::resolve_import(
  self : Evaluator,
  imported_from : String,
  imported_path : String,
) -> String raise JsonnetError {
  self.query(imported_from, () => {
    self.reader().load_data(imported_from, imported_path).id
  })
}

///|
/// Fetch raw imported bytes and their resolved identity without UTF-8 decoding.
/// Unlike a String result, BinarySource can preserve invalid UTF-8 and NUL bytes.
pub fn Evaluator::import_data(
  self : Evaluator,
  imported_from : String,
  imported_path : String,
) -> BinarySource raise JsonnetError {
  self.query(imported_from, () => {
    self.reader().load_data(imported_from, imported_path)
  })
}

///|
/// Parse and validate an imported program, returning its AST wrapper and identity.
/// This neither executes the program nor applies TLA/ext/native configuration.
///
/// ```mbt check
/// test {
///   let evaluator = @moonjsonnet.Evaluator::new()
///   evaluator.set_importer(binary_resolver=fn(_, _) {
///     { id: "lib/answer", bytes: b"42", }
///   })
///   let (program, identity) = evaluator.import_program("", "answer")
///   assert_eq(identity, "lib/answer")
///   assert_eq(evaluator.evaluate(program), 42)
/// }
/// ```
pub fn Evaluator::import_program(
  self : Evaluator,
  imported_from : String,
  imported_path : String,
) -> (Program, String) raise JsonnetError {
  self.query(imported_from, () => {
    let source = self.reader().load_data(imported_from, imported_path)
    (self.imported_program(source), source.id)
  })
}

///|
fn Evaluator::imported_program(
  self : Evaluator,
  source : BinarySource,
) -> Program raise {
  let expression = if self.parsed_imports.get(source.id) is Some(expression) {
    check_ast_depth(expression, self.limits.depth)
    expression
  } else {
    let text = @utf8.decode(source.bytes) catch {
      _ =>
        raise problem(
          Import,
          { file: source.id, line: 1, column: 1, },
          "invalid UTF-8",
        )
    }
    let expression = parse(text, source.id, self.limits.depth)
    self.parsed_imports[source.id] = expression
    expression
  }
  {
    expression,
    filename: source.id,
    anonymous: false,
    source_bytes: source.bytes.length(),
  }
}

///|
/// Query an imported AST using Go ImportAST's nullable failure-cache behavior.
/// The first Syntax/Static failure raises and records a nil AST; later calls to
/// this method return None and the same identity. Importer replacement clears
/// that history. Valid ASTs and source bytes share the ordinary import caches.
/// Import/UTF-8/resource errors are never cached as successful nil results.
/// This does not execute code. import_program remains strict, including retries,
/// and its failures do not seed this method's optional failure history.
///
/// ```mbt check
/// test {
///   let evaluator = @moonjsonnet.Evaluator::new()
///   evaluator.set_importer(resolver=fn(_, path) { { id: path, text: "missing", } })
///   try evaluator.import_ast("", "main") catch {
///     @moonjsonnet.Error(d) => assert_eq(d.kind, @moonjsonnet.Static)
///   } noraise {
///     _ => fail("expected first parse error")
///   }
///   let (program, id) = evaluator.import_ast("", "main")
///   assert_true(program is None)
///   assert_eq(id, "main")
/// }
/// ```
pub fn Evaluator::import_ast(
  self : Evaluator,
  imported_from : String,
  imported_path : String,
) -> (Program?, String) raise JsonnetError {
  self.query(imported_from, () => {
    let source = self.reader().load_data(imported_from, imported_path)
    if self.failed_import_asts.contains(source.id) {
      return (None, source.id)
    }
    let program = self.imported_program(source) catch {
      Error(d) => {
        if d.kind == Syntax || d.kind == Static {
          self.failed_import_asts[source.id] = ()
        }
        raise Error(d)
      }
      e => raise e
    }
    (Some(program), source.id)
  })
}

///|
/// Scan dependencies without evaluation, sharing source/AST caches with this
/// evaluator. Runtime values, ext/TLA configuration and callbacks are not forced.
pub fn Evaluator::find_dependencies(
  self : Evaluator,
  imported_from : String,
  imported_paths : Array[String],
) -> Array[String] raise JsonnetError {
  let reader = self.reader()
  self.query(imported_from, () => {
    find_dependencies_with_cache(
      imported_from,
      imported_paths,
      resolver=(from, path) => reader.load_data(from, path),
      limits=self.limits,
      parsed=self.parsed_imports,
    )
  })
}