///|
/// A compact index over observations used by audit and export layers.
pub(all) struct EvidenceIndex {
  observation_count : Int
  unique_feature_count : Int
  duplicate_feature_count : Int
  sample_count : Int
  feature_ids : Array[String]
}

///|
fn evidence_contains(ids : Array[String], value : String) -> Bool {
  for id in ids {
    if id == value {
      return true
    }
  }
  false
}

///|
/// Build a deterministic feature-to-observation index.
pub fn build_evidence_index(observations : Array[Observation]) -> EvidenceIndex {
  let feature_ids = []
  let mut sample_count = 0
  let mut duplicate_feature_count = 0
  for observation in observations {
    sample_count += if observation.sample_count > 0 {
      observation.sample_count
    } else {
      0
    }
    if evidence_contains(feature_ids, observation.feature_id) {
      duplicate_feature_count += 1
    } else {
      feature_ids.push(observation.feature_id)
    }
  }
  {
    observation_count: observations.length(),
    unique_feature_count: feature_ids.length(),
    duplicate_feature_count,
    sample_count,
    feature_ids,
  }
}

///|
/// Return expected feature IDs that have no observation in the index.
pub fn missing_feature_ids(
  index : EvidenceIndex,
  expected : Array[String],
) -> Array[String] {
  let result = []
  for id in expected {
    if !evidence_contains(index.feature_ids, id) {
      result.push(id)
    }
  }
  result
}

///|
/// Return whether the index contains useful, non-empty evidence.
pub fn evidence_index_is_usable(index : EvidenceIndex) -> Bool {
  index.observation_count > 0 &&
  index.unique_feature_count > 0 &&
  index.sample_count > 0
}

///|
/// Calculate the accepted-observation ratio against indexed samples.
pub fn evidence_index_pass_rate(
  index : EvidenceIndex,
  passed_observation_count : Int,
) -> Double {
  if index.sample_count <= 0 || passed_observation_count <= 0 {
    0.0
  } else {
    let bounded = if passed_observation_count > index.sample_count {
      index.sample_count
    } else {
      passed_observation_count
    }
    bounded.to_double() / index.sample_count.to_double()
  }
}

///|
/// Return the number of indexed features represented in expected IDs.
pub fn matched_feature_count(
  index : EvidenceIndex,
  expected : Array[String],
) -> Int {
  expected.length() - missing_feature_ids(index, expected).length()
}

///|
/// Return whether every expected feature has evidence.
pub fn evidence_is_complete(
  index : EvidenceIndex,
  expected : Array[String],
) -> Bool {
  missing_feature_ids(index, expected).length() == 0
}

///|
/// Return the number of observations with non-positive sample counts.
pub fn invalid_sample_observation_count(
  observations : Array[Observation],
) -> Int {
  let mut result = 0
  for observation in observations {
    if observation.sample_count <= 0 {
      result += 1
    }
  }
  result
}

///|
/// Return a stable evidence-density ratio per unique feature.
pub fn evidence_density(index : EvidenceIndex) -> Double {
  if index.unique_feature_count == 0 {
    0.0
  } else {
    index.observation_count.to_double() / index.unique_feature_count.to_double()
  }
}

///|
/// Return a concise evidence index label for reports.
pub fn render_evidence_index(index : EvidenceIndex) -> String {
  "features=" +
  index.unique_feature_count.to_string() +
  " observations=" +
  index.observation_count.to_string() +
  " samples=" +
  index.sample_count.to_string()
}