///|
/// Lightweight in-process model registry for reproducible experiments.
pub struct ModelArtifact {
  name : String
  version : String
  checksum : String
  created_at : String
  mut stage : String
  metrics : Map[String, Double]
} derive(ToJson, FromJson, Debug)

///|
pub fn ModelArtifact::new(
  name : String,
  version : String,
  checksum : String,
  created_at : String,
) -> ModelArtifact {
  { name, version, checksum, created_at, stage: "candidate", metrics: {} }
}

///|
pub fn ModelArtifact::set_stage(self : ModelArtifact, stage : String) -> Unit {
  self.stage = stage
}

///|
pub fn ModelArtifact::add_metric(
  self : ModelArtifact,
  name : String,
  value : Double,
) -> Unit {
  self.metrics[name] = value
}

///|
pub fn ModelArtifact::name(self : ModelArtifact) -> String {
  self.name
}

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

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

///|
pub fn ModelArtifact::stage(self : ModelArtifact) -> String {
  self.stage
}

///|
pub fn ModelArtifact::metric(self : ModelArtifact, name : String) -> Double? {
  self.metrics.get(name)
}

///|
pub fn ModelArtifact::metrics(self : ModelArtifact) -> Map[String, Double] {
  self.metrics.copy()
}

///|
pub struct ModelRegistry {
  artifacts : Map[String, ModelArtifact]
  mut registrations : Int
}

///|
pub fn ModelRegistry::new() -> ModelRegistry {
  { artifacts: {}, registrations: 0 }
}

///|
pub fn ModelRegistry::register(
  self : ModelRegistry,
  artifact : ModelArtifact,
) -> Bool {
  let key = "\{artifact.name()}@\{artifact.version()}"
  if self.artifacts.contains(key) {
    false
  } else {
    self.artifacts[key] = artifact
    self.registrations += 1
    true
  }
}

///|
pub fn ModelRegistry::get(
  self : ModelRegistry,
  name : String,
  version : String,
) -> ModelArtifact? {
  self.artifacts.get("\{name}@\{version}")
}

///|
pub fn ModelRegistry::promote(
  self : ModelRegistry,
  name : String,
  version : String,
  stage : String,
) -> Bool {
  match self.get(name, version) {
    None => false
    Some(artifact) => {
      artifact.set_stage(stage)
      true
    }
  }
}

///|
pub fn ModelRegistry::versions(
  self : ModelRegistry,
  name : String,
) -> Array[String] {
  self.artifacts
  .keys()
  .filter(key => key.has_prefix("\{name}@"))
  .map(key => key.after("@").unwrap_or("").to_owned())
  .to_array()
}

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

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

///|
pub fn ModelRegistry::clear(self : ModelRegistry) -> Unit {
  self.artifacts.clear()
  self.registrations = 0
}

///|
pub struct PromotionGate {
  minimum_auc : Double
  maximum_log_loss : Double
  minimum_samples : Int
}

///|
pub fn PromotionGate::new(
  minimum_auc? : Double = 0.5,
  maximum_log_loss? : Double = 1.0,
  minimum_samples? : Int = 100,
) -> PromotionGate {
  {
    minimum_auc,
    maximum_log_loss,
    minimum_samples: if minimum_samples < 0 {
      0
    } else {
      minimum_samples
    },
  }
}

///|
pub fn PromotionGate::accept(
  self : PromotionGate,
  summary : EvaluationSummary,
) -> Bool {
  summary.samples() >= self.minimum_samples.to_double() &&
  summary.auc() >= self.minimum_auc &&
  summary.log_loss() <= self.maximum_log_loss
}