///|
/// Auditable metadata for model updates, data lineage, and privacy budgets.
pub(all) enum AuditAction {
  Train
  Predict
  Promote
  Rollback
  Reject
  Snapshot
} derive(Debug, Eq)

///|
pub fn audit_action_catalog() -> Array[AuditAction] {
  [Train, Predict, Promote, Rollback, Reject, Snapshot]
}

///|
pub struct AuditRecord {
  event_id : String
  actor : String
  action : AuditAction
  model : String
  version : String
  timestamp : Int64
  detail : String
  success : Bool
}

///|
pub fn AuditRecord::new(
  event_id : String,
  actor : String,
  action : AuditAction,
  model : String,
  version : String,
  timestamp : Int64,
  detail? : String = "",
  success? : Bool = true,
) -> AuditRecord {
  { event_id, actor, action, model, version, timestamp, detail, success }
}

///|
pub fn AuditRecord::event_id(self : AuditRecord) -> String {
  self.event_id
}

///|
pub fn AuditRecord::actor(self : AuditRecord) -> String {
  self.actor
}

///|
pub fn AuditRecord::action(self : AuditRecord) -> AuditAction {
  self.action
}

///|
pub fn AuditRecord::model(self : AuditRecord) -> String {
  self.model
}

///|
pub fn AuditRecord::version(self : AuditRecord) -> String {
  self.version
}

///|
pub fn AuditRecord::timestamp(self : AuditRecord) -> Int64 {
  self.timestamp
}

///|
pub fn AuditRecord::detail(self : AuditRecord) -> String {
  self.detail
}

///|
pub fn AuditRecord::success(self : AuditRecord) -> Bool {
  self.success
}

///|
pub struct AuditTrail {
  capacity : Int
  records : Array[AuditRecord]
  ids : Map[String, Bool]
  mut accepted : Int
  mut duplicates : Int
  mut evicted : Int
}

///|
pub fn AuditTrail::new(capacity? : Int = 2048) -> AuditTrail {
  {
    capacity: if capacity < 1 {
      1
    } else {
      capacity
    },
    records: [],
    ids: {},
    accepted: 0,
    duplicates: 0,
    evicted: 0,
  }
}

///|
pub fn AuditTrail::append(self : AuditTrail, record : AuditRecord) -> Bool {
  if self.ids.contains(record.event_id()) {
    self.duplicates += 1
    false
  } else {
    self.records.push(record)
    self.ids[record.event_id()] = true
    self.accepted += 1
    if self.records.length() > self.capacity {
      let removed = self.records.remove(0)
      self.ids.remove(removed.event_id())
      self.evicted += 1
    }
    true
  }
}

///|
pub fn AuditTrail::size(self : AuditTrail) -> Int {
  self.records.length()
}

///|
pub fn AuditTrail::records(self : AuditTrail) -> Array[AuditRecord] {
  self.records.map(record => record)
}

///|
pub fn AuditTrail::find(self : AuditTrail, event_id : String) -> AuditRecord? {
  for record in self.records {
    if record.event_id() == event_id {
      return Some(record)
    }
  }
  None
}

///|
pub fn AuditTrail::count_action(self : AuditTrail, action : AuditAction) -> Int {
  self.records.count_if(record => record.action() == action)
}

///|
pub fn AuditTrail::failed(self : AuditTrail) -> Int {
  self.records.count_if(record => !record.success())
}

///|
pub fn AuditTrail::accepted(self : AuditTrail) -> Int {
  self.accepted
}

///|
pub fn AuditTrail::duplicates(self : AuditTrail) -> Int {
  self.duplicates
}

///|
pub fn AuditTrail::evicted(self : AuditTrail) -> Int {
  self.evicted
}

///|
pub fn AuditTrail::clear(self : AuditTrail) -> Unit {
  self.records.clear()
  self.ids.clear()
  self.accepted = 0
  self.duplicates = 0
  self.evicted = 0
}

///|
pub struct LineageNode {
  dataset : String
  source : String
  schema : String
  row_count : Int
  checksum : String
  timestamp : Int64
}

///|
pub fn LineageNode::new(
  dataset : String,
  source : String,
  schema : String,
  row_count : Int,
  checksum : String,
  timestamp : Int64,
) -> LineageNode {
  {
    dataset,
    source,
    schema,
    row_count: if row_count < 0 {
      0
    } else {
      row_count
    },
    checksum,
    timestamp,
  }
}

///|
pub fn LineageNode::dataset(self : LineageNode) -> String {
  self.dataset
}

///|
pub fn LineageNode::source(self : LineageNode) -> String {
  self.source
}

///|
pub fn LineageNode::schema(self : LineageNode) -> String {
  self.schema
}

///|
pub fn LineageNode::row_count(self : LineageNode) -> Int {
  self.row_count
}

///|
pub fn LineageNode::checksum(self : LineageNode) -> String {
  self.checksum
}

///|
pub fn LineageNode::timestamp(self : LineageNode) -> Int64 {
  self.timestamp
}

///|
pub struct DataLineage {
  nodes : Map[String, LineageNode]
  edges : Map[String, Array[String]]
  mut registrations : Int
}

///|
pub fn DataLineage::new() -> DataLineage {
  { nodes: {}, edges: {}, registrations: 0 }
}

///|
pub fn DataLineage::register(self : DataLineage, node : LineageNode) -> Bool {
  if self.nodes.contains(node.dataset()) {
    false
  } else {
    self.nodes[node.dataset()] = node
    self.registrations += 1
    true
  }
}

///|
pub fn DataLineage::connect(
  self : DataLineage,
  input : String,
  output : String,
) -> Bool {
  if !self.nodes.contains(input) || !self.nodes.contains(output) {
    false
  } else {
    let dependencies = self.edges.get(output).unwrap_or([])
    if dependencies.contains(input) {
      false
    } else {
      dependencies.push(input)
      self.edges[output] = dependencies
      true
    }
  }
}

///|
pub fn DataLineage::node(self : DataLineage, dataset : String) -> LineageNode? {
  self.nodes.get(dataset)
}

///|
pub fn DataLineage::inputs(
  self : DataLineage,
  dataset : String,
) -> Array[String] {
  self.edges.get(dataset).unwrap_or([]).map(value => value)
}

///|
pub fn DataLineage::upstream(
  self : DataLineage,
  dataset : String,
) -> Array[String] {
  let visited : Map[String, Bool] = Map([])
  let output : Array[String] = []
  fn walk(
    lineage : DataLineage,
    current : String,
    visited : Map[String, Bool],
    output : Array[String],
  ) -> Unit {
    for input in lineage.inputs(current) {
      if !visited.contains(input) {
        visited[input] = true
        output.push(input)
        walk(lineage, input, visited, output)
      }
    }
  }
  walk(self, dataset, visited, output)
  output
}

///|
pub fn DataLineage::registrations(self : DataLineage) -> Int {
  self.registrations
}

///|
pub fn DataLineage::clear(self : DataLineage) -> Unit {
  self.nodes.clear()
  self.edges.clear()
  self.registrations = 0
}

///|
pub struct PrivacyBudget {
  total : Double
  mut remaining_budget : Double
  mut queries : Int
  mut rejected : Int
}

///|
pub fn PrivacyBudget::new(epsilon : Double) -> PrivacyBudget {
  let safe = if epsilon < 0.0 { 0.0 } else { epsilon }
  { total: safe, remaining_budget: safe, queries: 0, rejected: 0 }
}

///|
pub fn PrivacyBudget::consume(self : PrivacyBudget, cost : Double) -> Bool {
  if cost < 0.0 || cost > self.remaining_budget {
    self.rejected += 1
    false
  } else {
    self.remaining_budget -= cost
    self.queries += 1
    true
  }
}

///|
pub fn PrivacyBudget::total(self : PrivacyBudget) -> Double {
  self.total
}

///|
pub fn PrivacyBudget::remaining(self : PrivacyBudget) -> Double {
  self.remaining_budget
}

///|
pub fn PrivacyBudget::spent(self : PrivacyBudget) -> Double {
  self.total - self.remaining_budget
}

///|
pub fn PrivacyBudget::queries(self : PrivacyBudget) -> Int {
  self.queries
}

///|
pub fn PrivacyBudget::rejected(self : PrivacyBudget) -> Int {
  self.rejected
}

///|
pub fn PrivacyBudget::exhausted(self : PrivacyBudget) -> Bool {
  self.remaining_budget <= 0.0
}

///|
pub fn PrivacyBudget::reset(self : PrivacyBudget) -> Unit {
  self.remaining_budget = self.total
  self.queries = 0
  self.rejected = 0
}

///|
pub struct ReproducibilityManifest {
  model : String
  version : String
  source_checksum : String
  data_checksum : String
  seed : Int
  parameters : Map[String, String]
}

///|
pub fn ReproducibilityManifest::new(
  model : String,
  version : String,
  source_checksum : String,
  data_checksum : String,
  seed : Int,
) -> ReproducibilityManifest {
  { model, version, source_checksum, data_checksum, seed, parameters: {} }
}

///|
pub fn ReproducibilityManifest::set(
  self : ReproducibilityManifest,
  key : String,
  value : String,
) -> Unit {
  self.parameters[key] = value
}

///|
pub fn ReproducibilityManifest::model(self : ReproducibilityManifest) -> String {
  self.model
}

///|
pub fn ReproducibilityManifest::version(
  self : ReproducibilityManifest,
) -> String {
  self.version
}

///|
pub fn ReproducibilityManifest::source_checksum(
  self : ReproducibilityManifest,
) -> String {
  self.source_checksum
}

///|
pub fn ReproducibilityManifest::data_checksum(
  self : ReproducibilityManifest,
) -> String {
  self.data_checksum
}

///|
pub fn ReproducibilityManifest::seed(self : ReproducibilityManifest) -> Int {
  self.seed
}

///|
pub fn ReproducibilityManifest::parameter(
  self : ReproducibilityManifest,
  key : String,
) -> String {
  self.parameters.get(key).unwrap_or("")
}

///|
pub fn ReproducibilityManifest::parameter_count(
  self : ReproducibilityManifest,
) -> Int {
  self.parameters.length()
}

///|
pub fn ReproducibilityManifest::fingerprint(
  self : ReproducibilityManifest,
) -> String {
  let text = "\{self.model}:\{self.version}:\{self.source_checksum}:\{self.data_checksum}:\{self.seed}:\{self.parameters.length()}"
  snapshot_checksum(text)
}