///|
/// Validate a corpus before it is handed to an index.
///
/// The check is deliberately strict: vector databases should reject malformed
/// data at ingestion time instead of producing incomplete or misleading search
/// results later.
pub fn validate_documents(docs : Array[Document]) -> Unit raise VectorError {
  if docs.length() == 0 {
    raise IndexError("Document corpus must not be empty")
  }
  let expected_dim = docs[0].vector.length()
  if expected_dim == 0 {
    raise EmptyVector
  }
  let ids = Map([])
  for doc in docs {
    if doc.id.trim().length() == 0 {
      raise IndexError("Document id must not be empty")
    }
    if doc.vector.length() != expected_dim {
      raise DimensionMismatch(
        "Corpus contains dimensions " +
        expected_dim.to_string() +
        " and " +
        doc.vector.length().to_string(),
      )
    }
    if ids.contains(doc.id) {
      raise IndexError("Duplicate document id: " + doc.id)
    }
    ids.set(doc.id, true)
  }
}

///|
/// Return a copy of `vector` with unit L2 norm.
pub fn normalize_l2(vector : Array[Double]) -> Array[Double] raise VectorError {
  if vector.length() == 0 {
    raise EmptyVector
  }
  let mut squared = 0.0
  for value in vector {
    squared = squared + value * value
  }
  if squared == 0.0 {
    raise IndexError("Cannot normalize a zero vector")
  }
  let norm = squared.sqrt()
  let normalized = Array::make(vector.length(), 0.0)
  for i = 0; i < vector.length(); i = i + 1 {
    normalized[i] = vector[i] / norm
  }
  normalized
}

///|
/// Normalize every document while preserving ids and metadata.
pub fn normalize_documents(
  docs : Array[Document],
) -> Array[Document] raise VectorError {
  validate_documents(docs)
  let normalized = []
  for doc in docs {
    normalized.push(
      Document::new(doc.id, normalize_l2(doc.vector), doc.metadata),
    )
  }
  normalized
}

///|
/// Return the number of distinct ids in a corpus.
pub fn distinct_document_count(docs : Array[Document]) -> Int {
  let ids = Map([])
  for doc in docs {
    ids.set(doc.id, true)
  }
  ids.length()
}