// Copyright 2026 Noverberrain.
// SPDX-License-Identifier: Apache-2.0

///|
priv struct ListBudget {
  max_nodes : Int32
  max_items : Int32
  mut nodes : Int32
  mut items : Int32
}

///|
fn ListBudget::check_items(
  self : ListBudget,
  count : UInt64,
  offset : Int32,
) -> Unit raise ParseError {
  // Bound the request before adding to the global count (avoids UInt64 wrap).
  check_resource_limit("list-items", offset, count, self.max_items)
  check_resource_limit(
    "list-items",
    offset,
    self.items.to_uint64() + count,
    self.max_items,
  )
}

///|
fn ListBudget::charge_items(
  self : ListBudget,
  count : UInt64,
  offset : Int32,
) -> Unit raise ParseError {
  self.check_items(count, offset)
  self.items += count.to_int()
}

///|
fn ListBudget::charge_nodes(
  self : ListBudget,
  count : UInt64,
  offset : Int32,
) -> Unit raise ParseError {
  check_resource_limit("list-nodes", offset, count, self.max_nodes)
  check_resource_limit(
    "list-nodes",
    offset,
    self.nodes.to_uint64() + count,
    self.max_nodes,
  )
  self.nodes += count.to_int()
}

///|
fn decode_list_items(
  input : Bytes,
  base : Int32,
  max_string_bytes : Int32,
  budget : DecodeBudget,
  lists : ListBudget,
) -> Array[Bytes] raise ParseError {
  let (reader, declared) = open_listpack(input, base)
  if declared != 65535 {
    lists.check_items(declared.to_uint64(), base)
  }
  let items = []
  while reader.pos < input.length() - 1 {
    lists.charge_items(1UL, base)
    items.push(reader.read_listpack_entry(base, max_string_bytes, budget))
  }
  if declared != 65535 && declared != items.length() {
    raise InvalidListpack(
      offset=base,
      inner_offset=4,
      reason="element count mismatch",
    )
  }
  if items.is_empty() {
    raise InvalidList(offset=base, reason="empty packed node")
  }
  items
}

///|
fn Reader::read_list_record(
  self : Reader,
  prefix : KeyPrefix,
  database : UInt64,
  max_string_bytes : Int32,
  max_packed_bytes : Int32,
  budget : DecodeBudget,
  lists : ListBudget,
) -> ListRecord raise ParseError {
  let key = self.read_string(
    max_string_bytes,
    budget,
    allow_lzf=true,
    resource="string-bytes",
  )
  let count_offset = self.pos
  let node_count = self.read_count()
  lists.charge_nodes(node_count, count_offset)
  if node_count == 0UL {
    raise InvalidList(offset=count_offset, reason="empty quicklist")
  }
  let items = []
  let nodes = []
  for _ in 0.. {
        lists.charge_items(1UL, base)
        let value = self.read_string(
          max_string_bytes,
          budget,
          allow_lzf=true,
          resource="string-bytes",
        )
        if value.value.is_empty() {
          raise InvalidList(offset=base, reason="empty plain node")
        }
        items.push(value.value)
        (Plain, value.storage)
      }
      2UL => {
        let scratch : DecodeBudget = { limit: max_packed_bytes, used: 0, }
        let packed = self.read_string(
          max_packed_bytes,
          scratch,
          allow_lzf=true,
          resource="packed-bytes",
        )
        for
          value in decode_list_items(
            packed.value,
            base,
            max_string_bytes,
            budget,
            lists,
          ) {
          items.push(value)
        }
        (Packed, packed.storage)
      }
      _ =>
        raise InvalidList(
          offset=node_start,
          reason="unknown quicklist container",
        )
    }
    nodes.push({
      container: kind,
      storage,
      item_start,
      item_end: items.length(),
      node_start,
      node_end: self.pos,
    })
  }
  {
    database,
    key: key.value,
    key_storage: key.storage,
    items,
    nodes,
    expires_at_ms: prefix.expires_at_ms,
    idle_seconds: prefix.idle_seconds,
    frequency: prefix.frequency,
    record_start: prefix.start,
    record_end: self.pos,
    serialized_bytes: (self.pos - prefix.start).to_uint64(),
  }
}