///|
/// In-memory feature registry with schema checks, versioning, and bounded history.
pub struct FeatureSchema {
  name : String
  dimension : Int
  minimum : Array[Double]
  maximum : Array[Double]
  required : Bool
}

///|
pub fn FeatureSchema::new(
  name : String,
  dimension : Int,
  minimum? : Array[Double] = [],
  maximum? : Array[Double] = [],
  required? : Bool = true,
) -> FeatureSchema {
  {
    name,
    dimension: if dimension < 0 {
      0
    } else {
      dimension
    },
    minimum: copy_vector(minimum),
    maximum: copy_vector(maximum),
    required,
  }
}

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

///|
pub fn FeatureSchema::dimension(self : FeatureSchema) -> Int {
  self.dimension
}

///|
pub fn FeatureSchema::required(self : FeatureSchema) -> Bool {
  self.required
}

///|
pub fn FeatureSchema::validate(
  self : FeatureSchema,
  values : Array[Double],
) -> ValidationReport {
  if self.required && values.is_empty() {
    ValidationReport::error("required feature is empty")
  } else if values.length() != self.dimension {
    ValidationReport::error("feature dimension mismatch")
  } else {
    let mut message = ""
    for i in 0.. self.maximum[i] {
        message = "feature above schema maximum"
      }
    }
    if message == "" {
      ValidationReport::ok()
    } else {
      ValidationReport::error(message)
    }
  }
}

///|
pub struct FeatureRecord {
  key : String
  version : Int
  values : Array[Double]
  timestamp : Int64
  source : String
}

///|
pub fn FeatureRecord::new(
  key : String,
  values : Array[Double],
  timestamp : Int64,
  source? : String = "unknown",
  version? : Int = 1,
) -> FeatureRecord {
  {
    key,
    version: if version < 1 {
      1
    } else {
      version
    },
    values: copy_vector(values),
    timestamp,
    source,
  }
}

///|
pub fn FeatureRecord::key(self : FeatureRecord) -> String {
  self.key
}

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

///|
pub fn FeatureRecord::values(self : FeatureRecord) -> Array[Double] {
  copy_vector(self.values)
}

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

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

///|
pub fn FeatureRecord::checksum(self : FeatureRecord) -> String {
  snapshot_checksum(self.values.map(value => "\{value}").join(","))
}

///|
pub struct FeatureStore {
  schemas : Map[String, FeatureSchema]
  records : Map[String, Array[FeatureRecord]]
  capacity : Int
  mut writes : Int
  mut reads : Int
  mut misses : Int
  mut rejected : Int
}

///|
pub fn FeatureStore::new(capacity? : Int = 4) -> FeatureStore {
  {
    schemas: {},
    records: {},
    capacity: if capacity < 1 {
      1
    } else {
      capacity
    },
    writes: 0,
    reads: 0,
    misses: 0,
    rejected: 0,
  }
}

///|
pub fn FeatureStore::register(
  self : FeatureStore,
  schema : FeatureSchema,
) -> Bool {
  if self.schemas.contains(schema.name()) {
    false
  } else {
    self.schemas[schema.name()] = schema
    true
  }
}

///|
pub fn FeatureStore::put(
  self : FeatureStore,
  name : String,
  record : FeatureRecord,
) -> Bool {
  match self.schemas.get(name) {
    None => {
      self.rejected += 1
      false
    }
    Some(schema) => {
      let report = schema.validate(record.values())
      if !report.is_valid() {
        self.rejected += 1
        false
      } else {
        let history = self.records.get(name).unwrap_or([])
        history.push(record)
        if history.length() > self.capacity {
          let _ = history.remove(0)
        }
        self.records[name] = history
        self.writes += 1
        true
      }
    }
  }
}

///|
pub fn FeatureStore::latest(
  self : FeatureStore,
  name : String,
) -> FeatureRecord? {
  self.reads += 1
  match self.records.get(name) {
    None => {
      self.misses += 1
      None
    }
    Some(history) =>
      match history.last() {
        None => {
          self.misses += 1
          None
        }
        Some(record) => Some(record)
      }
  }
}

///|
pub fn FeatureStore::at_version(
  self : FeatureStore,
  name : String,
  version : Int,
) -> FeatureRecord? {
  self.reads += 1
  match self.records.get(name) {
    None => {
      self.misses += 1
      None
    }
    Some(history) => {
      let mut result : FeatureRecord? = None
      for record in history {
        if record.version() == version {
          result = Some(record)
        }
      }
      match result {
        None => self.misses += 1
        Some(_) => ()
      }
      result
    }
  }
}

///|
pub fn FeatureStore::history(
  self : FeatureStore,
  name : String,
) -> Array[FeatureRecord] {
  self.records.get(name).unwrap_or([]).map(record => record)
}

///|
pub fn FeatureStore::schema(
  self : FeatureStore,
  name : String,
) -> FeatureSchema? {
  self.schemas.get(name)
}

///|
pub fn FeatureStore::writes(self : FeatureStore) -> Int {
  self.writes
}

///|
pub fn FeatureStore::reads(self : FeatureStore) -> Int {
  self.reads
}

///|
pub fn FeatureStore::misses(self : FeatureStore) -> Int {
  self.misses
}

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

///|
pub fn FeatureStore::hit_rate(self : FeatureStore) -> Double {
  if self.reads == 0 {
    1.0
  } else {
    (self.reads - self.misses).to_double() / self.reads.to_double()
  }
}

///|
pub fn FeatureStore::clear(self : FeatureStore) -> Unit {
  self.records.clear()
  self.writes = 0
  self.reads = 0
  self.misses = 0
  self.rejected = 0
}

///|
pub struct FeatureMaterializer {
  schema : FeatureSchema
  defaults : Array[Double]
  mut materialized : Int
  mut fallback : Int
}

///|
pub fn FeatureMaterializer::new(
  schema : FeatureSchema,
  defaults? : Array[Double] = [],
) -> FeatureMaterializer {
  { schema, defaults: copy_vector(defaults), materialized: 0, fallback: 0 }
}

///|
pub fn FeatureMaterializer::materialize(
  self : FeatureMaterializer,
  values : Array[Double],
) -> Array[Double] {
  if self.schema.validate(values).is_valid() {
    self.materialized += 1
    copy_vector(values)
  } else {
    self.fallback += 1
    Array::makei(self.schema.dimension(), i => {
      if i < self.defaults.length() {
        self.defaults[i]
      } else {
        0.0
      }
    })
  }
}

///|
pub fn FeatureMaterializer::materialized(self : FeatureMaterializer) -> Int {
  self.materialized
}

///|
pub fn FeatureMaterializer::fallback(self : FeatureMaterializer) -> Int {
  self.fallback
}

///|
pub fn FeatureMaterializer::reset(self : FeatureMaterializer) -> Unit {
  self.materialized = 0
  self.fallback = 0
}