///|
pub(all) enum CacheFormat {
  AppInfo
  PackageInfo
} derive(Eq, Debug)

///|
pub extend CacheFormat with Eq::{equal, not_equal}

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

///|
pub(all) struct CacheRecord {
  id : UInt
  change_number : UInt
  token : UInt64
  sha1 : Bytes
  binary_sha1 : Bytes?
  info_state : UInt?
  last_updated : UInt?
  offset : Int
  length : Int
} derive(Eq, Debug)

///|
pub extend CacheRecord with Eq::{equal, not_equal}

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

///|
/// An index owns a snapshot of the input. AppInfo payloads are decoded only when
/// requested. PackageInfo has no record sizes, so indexing validates each tree.
struct SteamCache {
  bytes : Bytes
  kind : CacheFormat
  revision : Int
  universe_id : UInt
  rows : Array[CacheRecord]
  strings : Array[String]?
  options : BinaryOptions
  indices : Map[UInt, Int]
}

///|
pub fn SteamCache::format(self : SteamCache) -> CacheFormat {
  self.kind
}

///|
pub fn SteamCache::version(self : SteamCache) -> Int {
  self.revision
}

///|
pub fn SteamCache::universe(self : SteamCache) -> UInt {
  self.universe_id
}

///|
pub fn SteamCache::records(self : SteamCache) -> Array[CacheRecord] {
  self.rows.map(r => r)
}

///|
pub(all) struct CachePage {
  records : Array[CacheRecord]
  total : Int
  offset : Int
}

///|
/// Metadata-only pagination never decodes payloads. Query matches decimal IDs.
pub fn SteamCache::page(
  self : SteamCache,
  query? : String = "",
  offset? : Int = 0,
  limit? : Int = 100,
) -> CachePage raise BinaryError {
  if offset < 0 || limit < 1 || limit > 200 {
    raise InvalidBinary(0, "Invalid page range")
  }
  let records : Array[CacheRecord] = []
  let mut total = 0
  for row in self.rows {
    if query == "" || row.id.to_string().contains(query) {
      if total >= offset && records.length() < limit {
        records.push(row)
      }
      total += 1
    }
  }
  { records, total, offset, }
}

///|
pub fn SteamCache::read(
  self : SteamCache,
  id : UInt,
) -> Array[BinaryEntry]? raise BinaryError {
  if self.indices.get(id) is Some(index) {
    let row = self.rows[index]
    return Some(
      decode_binary(
        self.bytes[row.offset:row.offset + row.length].to_owned(),
        options=self.options,
        string_table=self.strings,
      ),
    )
  }
  None
}

///|
fn BinaryReader::take(
  self : BinaryReader,
  length : Int,
) -> Bytes raise BinaryError {
  if length < 0 || length > self.bytes.length() - self.pos {
    raise InvalidBinary(self.pos, "Truncated cache field")
  }
  let result = self.bytes[self.pos:self.pos + length].to_owned()
  self.pos += length
  result
}

///|
/// Supported cache revisions: appinfo 39/40/41, packageinfo 39/40. Record bounds,
/// table offsets, identifiers, sentinels and trailing data are checked strictly.
pub fn open_steam_cache(
  input : Bytes,
  max_bytes? : Int = 536870912,
  max_records? : Int = 1000000,
) -> SteamCache raise BinaryError {
  if max_bytes < 8 ||
    max_records < 0 ||
    max_records > 1000000 ||
    input.length() > max_bytes {
    raise InvalidBinary(0, "Invalid cache limits or oversized input")
  }
  let options = { ..BinaryOptions::default(), max_bytes, max_entries: 1000000, }
  let bytes = input
  let r = BinaryReader::{ bytes, options, pos: 0, count: 0, table: None, }
  let magic = r.number(4).to_uint()
  let revision = (magic & 255U).reinterpret_as_int()
  let kind = if magic >> 8 == 0x075644U && revision >= 39 && revision <= 41 {
    CacheFormat::AppInfo
  } else if magic >> 8 == 0x065655U && revision >= 39 && revision <= 40 {
    PackageInfo
  } else {
    raise InvalidBinary(0, "Unsupported Steam cache header or revision")
  }
  let universe_id = r.number(4).to_uint()
  let mut table_start = bytes.length()
  let mut strings : Array[String]? = None
  if kind == AppInfo && revision == 41 {
    let offset = r.number(8)
    if offset < 20UL || offset > bytes.length().to_uint64() {
      raise InvalidBinary(8, "Invalid string table offset")
    }
    table_start = offset.to_int()
    let table = BinaryReader::{
      bytes,
      options,
      pos: table_start,
      count: 0,
      table: None,
    }
    let count = table.number(4)
    if count > 1000000UL {
      raise InvalidBinary(table_start, "String table count exceeds limit")
    }
    let pool : Array[String] = []
    for _ in 0.. ignore
  }
  let rows : Array[CacheRecord] = []
  let ids : Map[UInt, Int] = Map([])
  while true {
    if r.pos + 4 > table_start {
      raise InvalidBinary(r.pos, "Missing cache sentinel")
    }
    let id = r.number(4).to_uint()
    if (kind == AppInfo && id == 0U) ||
      (kind == PackageInfo && id == 0xffffffffU) {
      break
    }
    if rows.length() >= max_records || ids.contains(id) {
      raise InvalidBinary(r.pos - 4, "Record limit or duplicate identifier")
    }
    ids[id] = rows.length()
    let mut end = table_start
    let mut info_state = None
    let mut last_updated = None
    let mut token = 0UL
    if kind == AppInfo {
      let size = r.number(4)
      if r.pos > table_start ||
        size < (if revision >= 40 { 61UL } else { 41UL }) ||
        size > (table_start - r.pos).to_uint64() {
        raise InvalidBinary(r.pos - 4, "Invalid appinfo record size")
      }
      end = r.pos + size.to_int()
      info_state = Some(r.number(4).to_uint())
      last_updated = Some(r.number(4).to_uint())
      token = r.number(8)
    }
    let sha1 = r.take(20)
    let change_number = r.number(4).to_uint()
    let binary_sha1 = if kind == AppInfo && revision >= 40 {
      Some(r.take(20))
    } else {
      None
    }
    if kind == PackageInfo && revision >= 40 {
      token = r.number(8)
    }
    let offset = r.pos
    if kind == PackageInfo {
      r.read_binary_entries(0) |> ignore
      end = r.pos
    } else {
      r.pos = end
    }
    rows.push({
      id,
      change_number,
      token,
      sha1,
      binary_sha1,
      info_state,
      last_updated,
      offset,
      length: end - offset,
    })
  }
  if r.pos != table_start {
    raise InvalidBinary(r.pos, "Trailing cache bytes before string table")
  }
  { bytes, kind, revision, universe_id, rows, strings, options, indices: ids, }
}