///|
pub(all) struct ContextRule {
  topic : String
  depth : Int
  max_age : UInt64
  required : Bool
}

///|
pub fn context_rule(topic : String) -> ContextRule {
  { topic, depth: 1, max_age: 18446744073709551615UL, required: true, }
}

///|
pub(all) struct ContextClip {
  log : Log
  report : Json
}

///|
/// Export a bounded, independent message clip. Explicit context rules select
/// the last N messages per topic (not semantic state merging). Aux data is not
/// fetched by QuerySource; the manifest states this instead of implying retention.
pub fn extract_context_clip(
  source : QuerySource,
  start : UInt64,
  end : UInt64,
  topics? : Array[String] = [],
  context? : Array[ContextRule] = [],
  message_limit? : Int = 100000,
  byte_limit? : Int = 64 * 1024 * 1024,
  pending_bytes? : Int = 64 * 1024 * 1024,
  cancelled? : () -> Bool = fn() { false },
) -> ContextClip raise McapError {
  if start > end ||
    context.length() > 128 ||
    message_limit < 1 ||
    byte_limit < 1 ||
    pending_bytes < 1 {
    raise Invalid("invalid context clip interval or budgets")
  }
  let template = source.declaration_log()
  let messages : Array[Message] = []
  let mut bytes = 0
  let names : Map[String, Bool] = {}
  let evidence : Array[Json] = []
  fn retain(message : Message) -> Unit raise McapError {
    if messages.length() >= message_limit ||
      message.data.length() > byte_limit - bytes - 31 {
      raise Invalid("clip message count or payload budget exceeded")
    }
    messages.push(message)
    bytes += message.data.length() + 31
  }
  for rule in context {
    if rule.topic == "" ||
      rule.depth < 1 ||
      rule.depth > 10000 ||
      names.contains(rule.topic) {
      raise Invalid("invalid or duplicate context rule")
    }
    names[rule.topic] = true
    let retained : Array[Message] = []
    let mut retained_bytes = 0
    if start > 0UL {
      let stream = (source.open)(
        {
          ..all_query(),
          topics: [rule.topic],
          start_time: time_before(start, rule.max_age),
          end_time: start - 1UL,
          descending: true,
          limit: rule.depth,
        },
        pending_bytes,
      )
      defer (stream.stop)()
      while (stream.next_message)() is Some(message) {
        if cancelled() {
          raise Invalid("context extraction cancelled")
        }
        if retained.length() >= rule.depth ||
          message.log_time >= start ||
          message.log_time < time_before(start, rule.max_age) ||
          message.data.length() > byte_limit - bytes - retained_bytes - 31 {
          raise Invalid("context budget or provider interval violated")
        }
        retained.push(message)
        retained_bytes += message.data.length() + 31
      }
    }
    if retained.is_empty() && rule.required {
      raise Invalid("required context absent: " + rule.topic)
    }
    // Restore source-stable ascending order, including equal-time messages.
    for i = retained.length() - 1; i >= 0; i = i - 1 {
      retain(retained[i])
    }
    evidence.push({
      "topic": rule.topic,
      "requested_depth": rule.depth,
      "retained": retained.length(),
      "missing": retained.is_empty(),
      "messages": retained.map(fn(m) { event_context_message_json(m) }),
    })
  }
  let context_count = messages.length()
  let stream = (source.open)(
    { ..all_query(), topics, start_time: start, end_time: end, },
    pending_bytes,
  )
  defer (stream.stop)()
  let mut previous : UInt64? = None
  while (stream.next_message)() is Some(message) {
    if cancelled() {
      raise Invalid("clip extraction cancelled")
    }
    if message.log_time < start ||
      message.log_time > end ||
      (previous is Some(t) && message.log_time < t) {
      raise Invalid("clip provider interval/order violated")
    }
    previous = Some(message.log_time)
    retain(message)
  }
  let report : Json = {
    "start": start.to_string(),
    "end": end.to_string(),
    "window_messages": messages.length() - context_count,
    "context_messages": context_count,
    "payload_bytes": bytes,
    "context": evidence,
    "auxiliary_policy": "message clip; source attachments and metadata excluded",
  }
  let log = {
    ..template,
    messages,
    metadata: [
      { name: "moonmcap.context", entries: [("manifest", report.stringify())], },
    ],
  }
  // slice_log prunes declarations and stably orders all retained messages.
  { log: slice_log(log, all_query()), report, }
}

///|
fn event_context_message_json(message : Message) -> Json {
  {
    "channel_id": message.channel_id,
    "sequence": message.sequence.to_string(),
    "log_time": message.log_time.to_string(),
  }
}