///|
/// Reusable evaluation settings. Evaluation never replaces session bindings.
pub struct Session {
  env : Bindings
  mode : Mode
  zone : ZoneOffset
  clock : Clock?
}

///|
/// Build reusable evaluation settings. Default to empty globals, permissive
/// mode and UTC. Without `clock`, clock functions raise an evaluation error.
/// Each evaluated statement samples the clock at most once.
///
/// ```mbt check
/// test {
///   let session = @eval.Session()
///   @expect.expect(session.evaluate_script("1 + 2; 3 * 4")).to_equal([
///     @value.Value(3),
///     @value.Value(12),
///   ])
/// }
/// ```
#alias(new, deprecated="Use Session(...) instead")
pub fn Session::Session(
  env~ : Bindings = Bindings::empty(),
  mode~ : Mode = Permissive,
  zone~ : ZoneOffset = ZoneOffset::utc(),
  clock? : Clock,
) -> Session {
  { env, mode, zone, clock, }
}

///|
/// Parse and evaluate one statement with this session's settings.
/// Raise `EvalError::Syntax` for invalid text or an evaluation error on failure.
/// Session bindings remain unchanged.
pub fn Session::evaluate(
  self : Session,
  source : String,
) -> @value.Value raise EvalError {
  evaluate_text(
    source,
    env=self.env,
    mode=self.mode,
    zone=self.zone,
    clock?=self.clock,
  )
}

///|
/// Parse the complete script before evaluation, then return results in order.
pub fn Session::evaluate_script(
  self : Session,
  source : String,
) -> Array[@value.Value] raise EvalError {
  let script = @syntax.parse_script(source) catch { e => raise Syntax(e) }
  script.statements.map(statement => {
    evaluate(
      statement,
      env=self.env,
      mode=self.mode,
      zone=self.zone,
      clock?=self.clock,
    )
  })
}

///|
/// Parse once, bind each top-level element to `name`, and evaluate in order.
/// Empty input yields no results. A failure stops evaluation at that element.
pub fn Session::evaluate_each(
  self : Session,
  source : String,
  name~ : String,
  values~ : Array[@value.Value],
) -> Array[@value.Value] raise EvalError {
  let statement = @syntax.parse(source) catch { e => raise Syntax(e) }
  values.map(value => {
    evaluate(
      statement,
      env=self.env.with_value(name, value),
      mode=self.mode,
      zone=self.zone,
      clock?=self.clock,
    )
  })
}

///|
/// Either evaluation or output encoding failed at the session boundary.
pub(all) suberror SessionError {
  Evaluation(EvalError)
  Ion(@partiql_ion.IonDataError)
} derive(Debug)

///|
/// Evaluate a complete script and encode its results as PartiQL Ion text,
/// one value per line. `pretty` controls each value's layout.
/// Raise `SessionError` on evaluation or encoding failure; no partial output
/// is returned. Empty scripts produce an empty string.
pub fn Session::evaluate_ion_text(
  self : Session,
  source : String,
  pretty~ : Bool = false,
) -> String raise SessionError {
  let values = self.evaluate_script(source) catch { e => raise Evaluation(e) }
  @partiql_ion.write_text(values, pretty~) catch {
    e => raise Ion(e)
  }
}

///|
/// Evaluate a complete script and encode its results as one binary Ion
/// datagram. Raise `SessionError` on evaluation or encoding failure; no
/// partial output is returned.
pub fn Session::evaluate_ion_bytes(
  self : Session,
  source : String,
) -> Bytes raise SessionError {
  let values = self.evaluate_script(source) catch { e => raise Evaluation(e) }
  @partiql_ion.write_bytes(values) catch {
    e => raise Ion(e)
  }
}