///|
/// Quality summary for a collection of parsed GenBank records.
pub struct BatchQualityReport {
  record_count : Int
  valid_count : Int
  invalid_count : Int
  total_bases : Int
  mean_length : Double
  mean_gc : Double
  duplicate_accessions : Array[String]
  records_with_remote_locations : Int
  records_with_fuzzy_locations : Int
  records_with_translation_errors : Int
} derive(Debug, Eq)

///|
/// Build a batch report without mutating or reparsing records.
pub fn batch_quality(records : Array[Record]) -> BatchQualityReport {
  let accessions : Array[String] = []
  let duplicates : Array[String] = []
  let mut total_bases = 0
  let mut total_gc = 0.0
  let mut valid = 0
  let mut remote_records = 0
  let mut fuzzy_records = 0
  let mut translation_errors = 0
  for record in records {
    if accessions.contains(record.accession) && record.accession != "" {
      if !duplicates.contains(record.accession) {
        duplicates.push(record.accession)
      }
    } else {
      accessions.push(record.accession)
    }
    let stats = record.sequence_stats()
    total_bases += stats.length
    total_gc += stats.gc_percent * stats.length.to_double()
    if record_is_valid(record) {
      valid += 1
    }
    if record_remote_accession_count(record) > 0 {
      remote_records += 1
    }
    if record_fuzzy_location_count(record) > 0 {
      fuzzy_records += 1
    }
    translation_errors += record
      .check_cds_translations()
      .fold(init=0, fn(total, item) { total + (if item.ok { 0 } else { 1 }) })
  }
  {
    record_count: records.length(),
    valid_count: valid,
    invalid_count: records.length() - valid,
    total_bases,
    mean_length: if records.length() == 0 {
      0.0
    } else {
      total_bases.to_double() / records.length().to_double()
    },
    mean_gc: if total_bases == 0 {
      0.0
    } else {
      total_gc / total_bases.to_double()
    },
    duplicate_accessions: duplicates,
    records_with_remote_locations: remote_records,
    records_with_fuzzy_locations: fuzzy_records,
    records_with_translation_errors: translation_errors,
  }
}

///|
/// Parse and assess a multi-record stream in one operation.
pub fn batch_quality_from_text(
  text : String,
) -> BatchQualityReport raise GenBankError {
  batch_quality(parse_records(text))
}

///|
/// Return records that pass structural validation and CDS translation checks.
pub fn clean_records(records : Array[Record]) -> Array[Record] {
  records.filter(fn(record) {
    record_is_valid(record) &&
    record.check_cds_translations().all(fn(item) { item.ok })
  })
}

///|
/// Return a stable report line useful for a CLI summary.
pub fn BatchQualityReport::to_text(self : BatchQualityReport) -> String {
  "records=" +
  self.record_count.to_string() +
  " valid=" +
  self.valid_count.to_string() +
  " invalid=" +
  self.invalid_count.to_string() +
  " bases=" +
  self.total_bases.to_string() +
  " mean_length=" +
  self.mean_length.to_string() +
  " mean_gc=" +
  self.mean_gc.to_string() +
  " duplicate_accessions=" +
  self.duplicate_accessions.length().to_string() +
  " remote_records=" +
  self.records_with_remote_locations.to_string() +
  " fuzzy_records=" +
  self.records_with_fuzzy_locations.to_string() +
  " translation_errors=" +
  self.records_with_translation_errors.to_string()
}

///|
pub fn BatchQualityReport::to_json_string(self : BatchQualityReport) -> String {
  "{\"record_count\":" +
  self.record_count.to_string() +
  ",\"valid_count\":" +
  self.valid_count.to_string() +
  ",\"invalid_count\":" +
  self.invalid_count.to_string() +
  ",\"total_bases\":" +
  self.total_bases.to_string() +
  ",\"mean_length\":" +
  self.mean_length.to_string() +
  ",\"mean_gc\":" +
  self.mean_gc.to_string() +
  ",\"duplicate_accessions\":[" +
  self.duplicate_accessions.map(json_string).join(",") +
  "],\"records_with_remote_locations\":" +
  self.records_with_remote_locations.to_string() +
  ",\"records_with_fuzzy_locations\":" +
  self.records_with_fuzzy_locations.to_string() +
  ",\"records_with_translation_errors\":" +
  self.records_with_translation_errors.to_string() +
  "}"
}