///|
/// Reproducible model registry and deployment governance primitives.
pub enum ModelStage {
  Draft
  Validating
  Staging
  Production
  Archived
  Rejected
} derive(Debug, Eq, ToJson, FromJson)

///|
pub enum ApprovalStatus {
  Pending
  Approved
  Blocked
  Expired
} derive(Debug, Eq, ToJson, FromJson)

///|
pub enum DeploymentStrategy {
  Shadow
  Canary
  BlueGreen
  Rolling
  Batch
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct FeatureSignature {
  name : String
  data_type : String
  required : Bool
  default_value : String
  minimum : Double
  maximum : Double
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn FeatureSignature::accepts(
  self : FeatureSignature,
  value : Double,
) -> Bool {
  value == value && value >= self.minimum && value <= self.maximum
}

///|
pub fn FeatureSignature::is_bounded(self : FeatureSignature) -> Bool {
  self.minimum < self.maximum
}

///|
pub struct ModelMetric {
  name : String
  value : Double
  threshold : Double
  higher_is_better : Bool
  validation_set : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ModelMetric::passes(self : ModelMetric) -> Bool {
  if self.higher_is_better {
    self.value >= self.threshold
  } else {
    self.value <= self.threshold
  }
}

///|
pub fn ModelMetric::margin(self : ModelMetric) -> Double {
  if self.higher_is_better {
    self.value - self.threshold
  } else {
    self.threshold - self.value
  }
}

///|
pub struct ModelVersion {
  name : String
  version : String
  artifact_uri : String
  source_revision : String
  created_at : Int
  stage : ModelStage
  metrics : Array[ModelMetric]
  features : Array[FeatureSignature]
  tags : Array[String]
  owner : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ModelVersion::new(
  name : String,
  version : String,
  artifact_uri : String,
) -> ModelVersion {
  if name == "" || version == "" || artifact_uri == "" {
    abort("ModelVersion: identifying fields cannot be empty")
  }
  {
    name,
    version,
    artifact_uri,
    source_revision: "",
    created_at: 0,
    stage: ModelStage::Draft,
    metrics: [],
    features: [],
    tags: [],
    owner: "",
  }
}

///|
pub fn ModelVersion::with_revision(
  self : ModelVersion,
  revision : String,
  created_at : Int,
) -> ModelVersion {
  { ..self, source_revision: revision, created_at }
}

///|
pub fn ModelVersion::with_owner(
  self : ModelVersion,
  owner : String,
) -> ModelVersion {
  { ..self, owner, }
}

///|
pub fn ModelVersion::add_metric(
  self : ModelVersion,
  metric : ModelMetric,
) -> ModelVersion {
  let metrics = self.metrics.copy()
  metrics.push(metric)
  { ..self, metrics, }
}

///|
pub fn ModelVersion::add_feature(
  self : ModelVersion,
  feature : FeatureSignature,
) -> ModelVersion {
  let features = self.features.copy()
  features.push(feature)
  { ..self, features, }
}

///|
pub fn ModelVersion::add_tag(self : ModelVersion, tag : String) -> ModelVersion {
  let tags = self.tags.copy()
  if !tags.contains(tag) {
    tags.push(tag)
  }
  { ..self, tags, }
}

///|
pub fn ModelVersion::all_metrics_pass(self : ModelVersion) -> Bool {
  self.metrics.all(fn(metric) { metric.passes() })
}

///|
pub fn ModelVersion::metric(self : ModelVersion, name : String) -> Double {
  for metric in self.metrics.iter() {
    if metric.name == name {
      return metric.value
    }
  }
  abort("ModelVersion: metric not found")
}

///|
pub fn ModelVersion::feature_count(self : ModelVersion) -> Int {
  self.features.length()
}

///|
pub fn ModelVersion::required_features(self : ModelVersion) -> Array[String] {
  self.features
  .filter(fn(feature) { feature.required })
  .map(fn(feature) { feature.name })
}

///|
pub fn ModelVersion::is_deployable(self : ModelVersion) -> Bool {
  self.stage != ModelStage::Rejected &&
  self.stage != ModelStage::Archived &&
  self.all_metrics_pass() &&
  self.artifact_uri != "" &&
  self.source_revision != ""
}

///|
pub struct ValidationGate {
  name : String
  required_metrics : Array[String]
  minimum_score : Double
  maximum_error_rate : Double
  maximum_drift : Double
  require_revision : Bool
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn ValidationGate::production() -> ValidationGate {
  {
    name: "production",
    required_metrics: [],
    minimum_score: 0.8,
    maximum_error_rate: 0.05,
    maximum_drift: 0.2,
    require_revision: true,
  }
}

///|
pub fn ValidationGate::accepts(
  self : ValidationGate,
  model : ModelVersion,
  score : Double,
  error_rate : Double,
  drift : Double,
) -> Bool {
  let required_ok = self.required_metrics.all(fn(name) {
    model.metrics.any(fn(metric) { metric.name == name && metric.passes() })
  })
  required_ok &&
  score >= self.minimum_score &&
  error_rate <= self.maximum_error_rate &&
  drift <= self.maximum_drift &&
  (!self.require_revision || model.source_revision != "")
}

///|
pub struct GateResult {
  gate : String
  accepted : Bool
  failed_checks : Array[String]
  score : Double
  error_rate : Double
  drift : Double
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn evaluate_gate(
  gate : ValidationGate,
  model : ModelVersion,
  score : Double,
  error_rate : Double,
  drift : Double,
) -> GateResult {
  let failures : Array[String] = []
  if score < gate.minimum_score {
    failures.push("score")
  }
  if error_rate > gate.maximum_error_rate {
    failures.push("error_rate")
  }
  if drift > gate.maximum_drift {
    failures.push("drift")
  }
  if gate.require_revision && model.source_revision == "" {
    failures.push("source_revision")
  }
  for metric in gate.required_metrics.iter() {
    if !model.metrics.any(fn(item) { item.name == metric && item.passes() }) {
      failures.push(metric)
    }
  }
  {
    gate: gate.name,
    accepted: failures.length() == 0,
    failed_checks: failures,
    score,
    error_rate,
    drift,
  }
}

///|
pub struct Approval {
  reviewer : String
  status : ApprovalStatus
  comment : String
  timestamp : Int
  expires_at : Int
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn Approval::is_active(self : Approval, now : Int) -> Bool {
  self.status == ApprovalStatus::Approved &&
  (self.expires_at == 0 || now <= self.expires_at)
}

///|
pub fn Approval::approve(
  reviewer : String,
  comment : String,
  timestamp : Int,
  expires_at : Int,
) -> Approval {
  if reviewer == "" {
    abort("Approval: reviewer cannot be empty")
  }
  { reviewer, status: ApprovalStatus::Approved, comment, timestamp, expires_at }
}

///|
pub struct DeploymentRecord {
  model_name : String
  model_version : String
  strategy : DeploymentStrategy
  stage : ModelStage
  traffic_percent : Double
  replicas : Int
  started_at : Int
  ended_at : Int
  rollback_version : String
  approvals : Array[Approval]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn DeploymentRecord::new(
  model_name : String,
  model_version : String,
  strategy : DeploymentStrategy,
) -> DeploymentRecord {
  {
    model_name,
    model_version,
    strategy,
    stage: ModelStage::Staging,
    traffic_percent: 0.0,
    replicas: 1,
    started_at: 0,
    ended_at: 0,
    rollback_version: "",
    approvals: [],
  }
}

///|
pub fn DeploymentRecord::set_traffic(
  self : DeploymentRecord,
  percent : Double,
) -> DeploymentRecord {
  if percent < 0.0 || percent > 100.0 {
    abort("DeploymentRecord: traffic must be in [0, 100]")
  }
  { ..self, traffic_percent: percent }
}

///|
pub fn DeploymentRecord::set_replicas(
  self : DeploymentRecord,
  replicas : Int,
) -> DeploymentRecord {
  if replicas <= 0 {
    abort("DeploymentRecord: replicas must be positive")
  }
  { ..self, replicas, }
}

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

///|
pub fn DeploymentRecord::add_approval(
  self : DeploymentRecord,
  approval : Approval,
) -> DeploymentRecord {
  let approvals = self.approvals.copy()
  approvals.push(approval)
  { ..self, approvals, }
}

///|
pub fn DeploymentRecord::approved(self : DeploymentRecord, now : Int) -> Bool {
  self.approvals.any(fn(approval) { approval.is_active(now) })
}

///|
pub fn DeploymentRecord::is_complete(self : DeploymentRecord) -> Bool {
  self.ended_at > self.started_at && self.traffic_percent >= 100.0
}

///|
pub enum AuditAction {
  Registered
  MetricUpdated
  GateEvaluated
  Approved
  Deployed
  Promoted
  RolledBack
  Archived
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct AuditEvent {
  model_name : String
  version : String
  action : AuditAction
  actor : String
  timestamp : Int
  details : String
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn AuditEvent::new(
  model_name : String,
  version : String,
  action : AuditAction,
  actor : String,
  timestamp : Int,
  details : String,
) -> AuditEvent {
  { model_name, version, action, actor, timestamp, details }
}

///|
pub struct Registry {
  models : Array[ModelVersion]
  deployments : Array[DeploymentRecord]
  audit : Array[AuditEvent]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn Registry::new() -> Registry {
  { models: [], deployments: [], audit: [] }
}

///|
pub fn Registry::register(
  self : Registry,
  model : ModelVersion,
  actor : String,
  timestamp : Int,
) -> Registry {
  if self.models.any(fn(item) {
      item.name == model.name && item.version == model.version
    }) {
    abort("Registry: model version already exists")
  }
  let models = self.models.copy()
  models.push(model)
  let audit = self.audit.copy()
  audit.push(
    AuditEvent::new(
      models[models.length() - 1].name,
      models[models.length() - 1].version,
      AuditAction::Registered,
      actor,
      timestamp,
      "registered model version",
    ),
  )
  { ..self, models, audit }
}

///|
pub fn Registry::find(
  self : Registry,
  name : String,
  version : String,
) -> ModelVersion {
  for model in self.models.iter() {
    if model.name == name && model.version == version {
      return model
    }
  }
  abort("Registry: model version not found")
}

///|
pub fn Registry::versions(
  self : Registry,
  name : String,
) -> Array[ModelVersion] {
  self.models.filter(fn(model) { model.name == name })
}

///|
pub fn Registry::latest(self : Registry, name : String) -> ModelVersion {
  let models = self.versions(name)
  if models.length() == 0 {
    abort("Registry: model has no versions")
  }
  models[models.length() - 1]
}

///|
pub fn Registry::promote(
  self : Registry,
  name : String,
  version : String,
  target : ModelStage,
  gate : ValidationGate,
  score : Double,
  error_rate : Double,
  drift : Double,
  actor : String,
  timestamp : Int,
) -> (Registry, GateResult) {
  let model = self.find(name, version)
  let result = evaluate_gate(gate, model, score, error_rate, drift)
  if !result.accepted {
    return (self, result)
  }
  let models = self.models.map(fn(item) {
    if item.name == name && item.version == version {
      { ..item, stage: target }
    } else {
      item
    }
  })
  let audit = self.audit.copy()
  audit.push(
    AuditEvent::new(
      name,
      version,
      AuditAction::Promoted,
      actor,
      timestamp,
      "passed validation gate",
    ),
  )
  ({ ..self, models, audit }, result)
}

///|
pub fn Registry::deploy(
  self : Registry,
  record : DeploymentRecord,
  actor : String,
  timestamp : Int,
) -> Registry {
  let model = self.find(record.model_name, record.model_version)
  if !model.is_deployable() {
    abort("Registry: model is not deployable")
  }
  let deployments = self.deployments.copy()
  deployments.push({
    ..record,
    stage: ModelStage::Production,
    started_at: timestamp,
  })
  let audit = self.audit.copy()
  audit.push(
    AuditEvent::new(
      record.model_name,
      record.model_version,
      AuditAction::Deployed,
      actor,
      timestamp,
      "deployment started",
    ),
  )
  { ..self, deployments, audit }
}

///|
pub fn Registry::rollback(
  self : Registry,
  name : String,
  version : String,
  rollback_version : String,
  actor : String,
  timestamp : Int,
) -> Registry {
  let model = self.find(name, version)
  let target = self.find(name, rollback_version)
  if target.stage == ModelStage::Rejected ||
    target.stage == ModelStage::Archived {
    abort("Registry: rollback target is unavailable")
  }
  let deployments = self.deployments.map(fn(item) {
    if item.model_name == name && item.model_version == version {
      {
        ..item,
        rollback_version,
        stage: ModelStage::Archived,
        ended_at: timestamp,
      }
    } else {
      item
    }
  })
  let audit = self.audit.copy()
  audit.push(
    AuditEvent::new(
      model.name,
      model.version,
      AuditAction::RolledBack,
      actor,
      timestamp,
      "rolled back deployment",
    ),
  )
  { ..self, deployments, audit }
}

///|
pub fn Registry::production(
  self : Registry,
  name : String,
) -> Array[ModelVersion] {
  self.models.filter(fn(model) {
    model.name == name && model.stage == ModelStage::Production
  })
}

///|
pub fn Registry::audit_for(self : Registry, name : String) -> Array[AuditEvent] {
  self.audit.filter(fn(event) { event.model_name == name })
}

///|
pub struct CompatibilityReport {
  compatible : Bool
  missing : Array[String]
  type_changes : Array[String]
  newly_required : Array[String]
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn compare_feature_signatures(
  old : ModelVersion,
  newer : ModelVersion,
) -> CompatibilityReport {
  let missing : Array[String] = []
  let changes : Array[String] = []
  let newly_required : Array[String] = []
  for feature in old.features.iter() {
    let found = newer.features.filter(fn(item) { item.name == feature.name })
    if found.length() == 0 {
      missing.push(feature.name)
    } else if found[0].data_type != feature.data_type {
      changes.push(feature.name)
    }
  }
  for feature in newer.features.iter() {
    if feature.required &&
      !old.features.any(fn(item) { item.name == feature.name }) {
      newly_required.push(feature.name)
    }
  }
  {
    compatible: missing.length() == 0 &&
    changes.length() == 0 &&
    newly_required.length() == 0,
    missing,
    type_changes: changes,
    newly_required,
  }
}

///|
pub fn registry_health(registry : Registry, now : Int) -> Double {
  if registry.models.length() == 0 {
    return 1.0
  }
  let deployable = registry.models
    .filter(fn(model) { model.is_deployable() })
    .length()
  let active_approvals = registry.deployments
    .filter(fn(item) { item.approved(now) })
    .length()
  let coverage = deployable.to_double() / registry.models.length().to_double()
  let deployment = if registry.deployments.length() == 0 {
    1.0
  } else {
    active_approvals.to_double() / registry.deployments.length().to_double()
  }
  (0.7 * coverage + 0.3 * deployment).min(1.0)
}