///|
/// Exact source set tracker: idempotent changes, immutable snapshot exports.
pub struct Inventory {
  priv scope : UInt64
  priv mut epoch : UInt64
  priv keys : Map[UInt64, Bool]
  priv mut fingerprint : Fingerprint
}

///|
pub fn Inventory::new(
  scope~ : UInt64 = 0UL,
  epoch~ : UInt64 = 0UL,
) -> Inventory {
  { scope, epoch, keys: Map([]), fingerprint: empty_fingerprint(), }
}

///|
/// Initial duplicates are discarded. All UInt64 values, including zero, are valid IDs.
pub fn Inventory::from_ids(
  ids : Array[UInt64],
  scope~ : UInt64 = 0UL,
  epoch~ : UInt64 = 0UL,
) -> Inventory {
  let inventory = Inventory::new(scope~, epoch~)
  for id in ids {
    if !inventory.keys.contains(id) {
      inventory.keys[id] = true
      inventory.fingerprint = fingerprint_add(inventory.fingerprint, id, 1)
    }
  }
  inventory
}

///|
pub fn Inventory::contains(self : Inventory, id : UInt64) -> Bool {
  self.keys.contains(id)
}

///|
pub fn Inventory::epoch(self : Inventory) -> UInt64 {
  self.epoch
}

///|
pub fn Inventory::fingerprint(self : Inventory) -> Fingerprint {
  self.fingerprint
}

///|
pub fn Inventory::ids(self : Inventory) -> Array[UInt64] {
  let ids = self.keys.keys().to_array()
  ids.sort()
  ids
}

///|
pub fn Inventory::insert(
  self : Inventory,
  id : UInt64,
) -> Bool raise ReconcileError {
  if self.keys.contains(id) {
    return false
  }
  if self.epoch == 0xffffffffffffffffUL || self.fingerprint.count == 2147483647 {
    raise LimitExceeded("inventory version or size overflow")
  }
  self.keys[id] = true
  self.fingerprint = fingerprint_add(self.fingerprint, id, 1)
  self.epoch += 1UL
  true
}

///|
pub fn Inventory::remove(
  self : Inventory,
  id : UInt64,
) -> Bool raise ReconcileError {
  if !self.keys.contains(id) {
    return false
  }
  if self.epoch == 0xffffffffffffffffUL {
    raise LimitExceeded("inventory version overflow")
  }
  self.keys.remove(id)
  self.fingerprint = fingerprint_add(self.fingerprint, id, -1)
  self.epoch += 1UL
  true
}

///|
pub fn Inventory::snapshot(
  self : Inventory,
  config : Config,
) -> Snapshot raise ReconcileError {
  let sketch = Sketch::new(config)
  for id in self.keys.keys() {
    sketch.insert(id)
  }
  {
    scope: self.scope,
    epoch: self.epoch,
    fingerprint: self.fingerprint,
    sketch,
  }
}

///|
/// Atomically applies a plan to a copy, so failures never alter the source.
/// This transfers IDs only. Applications fetch/authenticate payloads separately.
pub fn Inventory::apply(
  self : Inventory,
  plan : Plan,
) -> Inventory raise ReconcileError {
  if self.scope != plan.scope ||
    self.epoch != plan.source_epoch ||
    self.fingerprint != plan.source {
    raise StaleSnapshot
  }
  let result = Inventory::from_ids(
    self.ids(),
    scope=self.scope,
    epoch=plan.target_epoch,
  )
  for id in plan.remove {
    if !result.keys.contains(id) {
      raise InvalidPlan("removal missing from source")
    }
    result.keys.remove(id)
    result.fingerprint = fingerprint_add(result.fingerprint, id, -1)
  }
  for id in plan.add {
    if result.keys.contains(id) {
      raise InvalidPlan("addition already exists")
    }
    result.keys[id] = true
    result.fingerprint = fingerprint_add(result.fingerprint, id, 1)
  }
  if result.fingerprint != plan.target {
    raise VerificationFailed
  }
  result
}