///|
/// A requested physical query. The runner must reset, execute each input, then
/// clean up successfully before committing. Arrays are defensive copies.
pub(all) struct QueryRequest {
  id : Int32
  inputs : Array[String]
} derive(Debug)

///|
pub(all) enum QueryStart {
  Cached(Array[String])
  Execute(QueryRequest)
} derive(Debug)

///|
pub extend QueryRequest with @debug.Debug::{to_repr}

///|
pub extend QueryStart with @debug.Debug::{to_repr}

///|
priv struct PendingQuery {
  id : Int32
  word : Array[String]
  cached : Array[String]
  outputs : Array[String]
  mut reset_started : Bool
  mut awaiting_output : Bool
}

///|
/// IO-independent cache, budgets and observation consistency. Exactly one
/// outstanding query is allowed. Failed/cancelled work must be aborted.
pub struct QuerySession {
  priv alphabet : Array[String]
  priv nodes : Array[TraceNode]
  priv max_queries : Int32
  priv max_steps : Int32
  priv max_word_length : Int32
  priv max_cache_nodes : Int32
  priv max_output_code_units : Int32
  priv use_cache : Bool
  priv recheck_every : Int32
  priv mut queries : Int32
  priv mut cache_hits : Int32
  priv mut resets : Int32
  priv mut steps : Int32
  priv mut completed_queries : Int32
  priv mut output_code_units : Int32
  priv mut serial : Int32
  priv mut pending : PendingQuery?
  priv mut conflict : (Array[String], Array[String], Array[String])?
}

///|
pub fn QuerySession::new(
  alphabet : Array[String],
  max_queries~ : Int32 = 100000,
  max_steps~ : Int32 = 2000000,
  max_word_length~ : Int32 = 4096,
  max_cache_nodes~ : Int32 = 1000000,
  max_output_code_units~ : Int32 = 16777216,
  use_cache~ : Bool = true,
  recheck_every~ : Int32 = 0,
) -> QuerySession raise LensError {
  validate_alphabet(alphabet)
  if max_queries < 0 ||
    max_steps < 0 ||
    max_word_length < 0 ||
    max_cache_nodes < 1 ||
    max_output_code_units < 0 ||
    recheck_every < 0 {
    raise InvalidArgument("invalid experiment budget")
  }
  {
    alphabet: alphabet.copy(),
    nodes: [{ children: Map([]), output: None, }],
    max_queries,
    max_steps,
    max_word_length,
    max_cache_nodes,
    max_output_code_units,
    use_cache,
    recheck_every,
    queries: 0,
    cache_hits: 0,
    resets: 0,
    steps: 0,
    completed_queries: 0,
    output_code_units: 0,
    serial: 0,
    pending: None,
    conflict: None,
  }
}

///|
pub fn QuerySession::alphabet(self : QuerySession) -> Array[String] {
  self.alphabet.copy()
}

///|
pub fn QuerySession::statistics(self : QuerySession) -> Statistics {
  {
    queries: self.queries,
    cache_hits: self.cache_hits,
    resets: self.resets,
    steps: self.steps,
    completed_queries: self.completed_queries,
    cache_nodes: self.nodes.length(),
    output_code_units: self.output_code_units,
  }
}

///|
pub fn QuerySession::start(
  self : QuerySession,
  requested : Array[String],
  bypass_cache~ : Bool = false,
) -> QueryStart raise LensError {
  if self.pending is Some(_) {
    raise InvalidArgument("reentrant experiment query")
  }
  if self.conflict is Some((word, expected, actual)) {
    raise InconsistentObservation(
      word=word.copy(),
      expected=expected.copy(),
      actual=actual.copy(),
    )
  }
  if requested.length() > self.max_word_length {
    raise LimitExceeded("word-length")
  }
  let word = requested.copy()
  for input in word {
    if !self.alphabet.contains(input) {
      raise UnknownInput(input)
    }
  }
  if self.queries == 2147483647 {
    raise LimitExceeded("logical-queries")
  }
  self.queries += 1
  let cached = []
  let mut node = 0
  for input in word {
    match self.nodes[node].children.get(input) {
      Some(next) => {
        node = next
        cached.push(self.nodes[node].output.unwrap())
      }
      None => break
    }
  }
  let force = bypass_cache ||
    (self.recheck_every > 0 && self.queries % self.recheck_every == 0)
  if self.use_cache && !force && cached.length() == word.length() {
    self.cache_hits += 1
    return Cached(cached)
  }
  if self.resets >= self.max_queries {
    raise LimitExceeded("physical-queries")
  }
  if word.length() > self.max_steps - self.steps {
    raise LimitExceeded("target-steps")
  }
  if word.length() - cached.length() >
    self.max_cache_nodes - self.nodes.length() {
    raise LimitExceeded("cache-nodes")
  }
  if self.serial == 2147483647 {
    raise LimitExceeded("query-identifiers")
  }
  self.serial += 1
  self.pending = Some({
    id: self.serial,
    word,
    cached,
    outputs: [],
    reset_started: false,
    awaiting_output: false,
  })
  Execute({ id: self.serial, inputs: word.copy(), })
}

///|
fn QuerySession::require_pending(
  self : QuerySession,
  id : Int32,
) -> PendingQuery raise LensError {
  guard self.pending is Some(pending) && pending.id == id else {
    raise InvalidArgument("stale or missing query identifier")
  }
  pending
}

///|
/// Record the attempted reset immediately before invoking the target.
pub fn QuerySession::reset_started(
  self : QuerySession,
  id : Int32,
) -> Unit raise LensError {
  let pending = self.require_pending(id)
  if pending.reset_started {
    raise InvalidArgument("duplicate reset")
  }
  self.resets += 1
  pending.reset_started = true
}

///|
/// Record an attempted step before invoking it, including attempts that fail.
pub fn QuerySession::step_started(
  self : QuerySession,
  id : Int32,
) -> Unit raise LensError {
  let pending = self.require_pending(id)
  if !pending.reset_started ||
    pending.awaiting_output ||
    pending.outputs.length() >= pending.word.length() {
    raise InvalidArgument("invalid step lifecycle")
  }
  self.steps += 1
  pending.awaiting_output = true
}

///|
pub fn QuerySession::accept_output(
  self : QuerySession,
  id : Int32,
  output : String,
) -> Unit raise LensError {
  let pending = self.require_pending(id)
  if !pending.awaiting_output {
    raise InvalidArgument("unsolicited step output")
  }
  if output.length() > self.max_output_code_units - self.output_code_units {
    raise LimitExceeded("output-code-units")
  }
  self.output_code_units += output.length()
  pending.outputs.push(output)
  pending.awaiting_output = false
}

///|
/// Commit only AFTER successful target cleanup. Conflicts poison the session.
pub fn QuerySession::commit(
  self : QuerySession,
  id : Int32,
) -> Array[String] raise LensError {
  let pending = self.require_pending(id)
  if !pending.reset_started ||
    pending.awaiting_output ||
    pending.outputs.length() != pending.word.length() {
    raise InvalidArgument("incomplete query")
  }
  self.pending = None
  self.completed_queries += 1
  let word = pending.word
  let outputs = pending.outputs
  for index in 0.. node = next
      None => {
        let next = self.nodes.length()
        self.nodes.push({ children: Map([]), output: Some(outputs[index]), })
        self.nodes[node].children[input] = next
        node = next
      }
    }
  }
  outputs.copy()
}

///|
/// Discard an unfinished query. Idempotent after commit/abort for safe defers.
/// A different outstanding request cannot be aborted by an old identifier.
pub fn QuerySession::abort(self : QuerySession, id : Int32) -> Unit {
  if self.pending is Some(pending) && pending.id == id {
    self.pending = None
  }
}