///|
/// Summary statistics for a batch of GenBank records.
pub struct BatchSummary {
  record_count : Int
  total_bases : Int
  total_features : Int
  total_cds : Int
  total_genes : Int
  valid_records : Int
  invalid_records : Int
  accessions : Array[String]
} derive(Debug, Eq)

///|
/// A reusable collection wrapper for multi-record workflows.
pub struct RecordCollection {
  records : Array[Record]
  source_name : String
} derive(Debug, Eq)

///|
pub fn parse_record_collection(
  text : String,
  source_name? : String = "input",
) -> RecordCollection raise GenBankError {
  { records: parse_records(text), source_name }
}

///|
pub fn collection_from_records(
  records : Array[Record],
  source_name? : String = "memory",
) -> RecordCollection {
  { records, source_name }
}

///|
pub fn RecordCollection::count(self : RecordCollection) -> Int {
  self.records.length()
}

///|
pub fn RecordCollection::records(self : RecordCollection) -> Array[Record] {
  self.records
}

///|
pub fn RecordCollection::source_name(self : RecordCollection) -> String {
  self.source_name
}

///|
pub fn RecordCollection::at(self : RecordCollection, index : Int) -> Record? {
  self.records.get(index)
}

///|
pub fn RecordCollection::accessions(self : RecordCollection) -> Array[String] {
  self.records.map(fn(record) { record.accession })
}

///|
pub fn RecordCollection::definitions(self : RecordCollection) -> Array[String] {
  self.records.map(fn(record) { record.definition })
}

///|
pub fn RecordCollection::total_bases(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) { total + record.origin.length() })
}

///|
pub fn RecordCollection::total_features(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) {
    total + record.features.length()
  })
}

///|
pub fn RecordCollection::total_cds(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) {
    total + record_cds_count(record)
  })
}

///|
pub fn RecordCollection::total_genes(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) {
    total + record_gene_count(record)
  })
}

///|
pub fn RecordCollection::valid_count(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) {
    total + (if record_is_valid(record) { 1 } else { 0 })
  })
}

///|
pub fn RecordCollection::invalid_count(self : RecordCollection) -> Int {
  self.count() - self.valid_count()
}

///|
pub fn RecordCollection::summary(self : RecordCollection) -> BatchSummary {
  {
    record_count: self.count(),
    total_bases: self.total_bases(),
    total_features: self.total_features(),
    total_cds: self.total_cds(),
    total_genes: self.total_genes(),
    valid_records: self.valid_count(),
    invalid_records: self.invalid_count(),
    accessions: self.accessions(),
  }
}

///|
pub fn RecordCollection::filter_accession(
  self : RecordCollection,
  prefix : String,
) -> RecordCollection {
  {
    records: self.records.filter(fn(record) {
      record.accession.has_prefix(prefix)
    }),
    source_name: self.source_name,
  }
}

///|
pub fn RecordCollection::filter_definition(
  self : RecordCollection,
  text : String,
) -> RecordCollection {
  let needle = text.to_lower()
  {
    records: self.records.filter(fn(record) {
      record.definition.to_lower().contains(needle)
    }),
    source_name: self.source_name,
  }
}

///|
pub fn RecordCollection::filter_valid(
  self : RecordCollection,
) -> RecordCollection {
  {
    records: self.records.filter(record_is_valid),
    source_name: self.source_name,
  }
}

///|
pub fn RecordCollection::map_normalized(
  self : RecordCollection,
) -> RecordCollection {
  { records: self.records.map(normalize_record), source_name: self.source_name }
}

///|
pub fn RecordCollection::all_feature_keys(
  self : RecordCollection,
) -> Array[String] {
  let result : Array[String] = []
  for record in self.records {
    for key in build_feature_index(record).keys() {
      if !result.contains(key) {
        result.push(key)
      }
    }
  }
  result
}

///|
pub fn RecordCollection::records_with_feature(
  self : RecordCollection,
  key : String,
) -> RecordCollection {
  {
    records: self.records.filter(fn(record) {
      record.features_by_key(key).length() > 0
    }),
    source_name: self.source_name,
  }
}

///|
pub fn RecordCollection::sequence_signatures(
  self : RecordCollection,
) -> Array[String] {
  self.records.map(fn(record) { sequence_signature(record.origin) })
}

///|
pub fn RecordCollection::reports(self : RecordCollection) -> Array[String] {
  self.records.map(record_report)
}

///|
pub fn RecordCollection::validation_reports(
  self : RecordCollection,
) -> Array[ValidationReport] {
  self.records.map(build_validation_report)
}

///|
pub fn RecordCollection::to_genbank_string(self : RecordCollection) -> String {
  self.records.map(fn(record) { record.to_genbank_string() }).join("")
}

///|
pub fn RecordCollection::to_fasta_string(self : RecordCollection) -> String {
  let out = StringBuilder()
  for record in self.records {
    let id = if record.accession == "" {
      record.locus.name
    } else {
      record.accession
    }
    out.write_string(">" + id + " " + record.definition + "\n")
    out.write_string(format_sequence_lines(record.origin, 60))
  }
  out.to_string()
}

///|
pub fn BatchSummary::to_json_string(self : BatchSummary) -> String {
  "{\"record_count\":" +
  self.record_count.to_string() +
  ",\"total_bases\":" +
  self.total_bases.to_string() +
  ",\"total_features\":" +
  self.total_features.to_string() +
  ",\"total_cds\":" +
  self.total_cds.to_string() +
  ",\"total_genes\":" +
  self.total_genes.to_string() +
  ",\"valid_records\":" +
  self.valid_records.to_string() +
  ",\"invalid_records\":" +
  self.invalid_records.to_string() +
  ",\"accessions\":" +
  array_json(self.accessions.map(fn(item) { json_string(item) })) +
  "}"
}

///|
pub fn BatchSummary::to_text(self : BatchSummary) -> String {
  "records: " +
  self.record_count.to_string() +
  "\n" +
  "bases: " +
  self.total_bases.to_string() +
  "\n" +
  "features: " +
  self.total_features.to_string() +
  "\n" +
  "CDS: " +
  self.total_cds.to_string() +
  "\n" +
  "genes: " +
  self.total_genes.to_string() +
  "\n" +
  "valid: " +
  self.valid_records.to_string() +
  "\n" +
  "invalid: " +
  self.invalid_records.to_string() +
  "\n"
}

///|
pub fn summarize_records(text : String) -> BatchSummary raise GenBankError {
  parse_record_collection(text).summary()
}

///|
pub fn collection_json(text : String) -> String raise GenBankError {
  summarize_records(text).to_json_string()
}

///|
pub fn collection_fasta(text : String) -> String raise GenBankError {
  parse_record_collection(text).to_fasta_string()
}

///|
fn format_sequence_lines(sequence : String, width : Int) -> String {
  let out = StringBuilder()
  let normalized = normalize_sequence(sequence)
  let safe_width = Int::max(1, width)
  let mut start = 0
  while start < normalized.length() {
    let end = Int::min(start + safe_width, normalized.length())
    out.write_string(normalized.unsafe_substring(start~, end~))
    out.write_char('\n')
    start = end
  }
  out.to_string()
}

///|
pub fn RecordCollection::gc(self : RecordCollection) -> Double {
  if self.total_bases() == 0 {
    0.0
  } else {
    self.records.fold(init=0.0, fn(total, record) {
      total +
      record.sequence_stats().gc_percent * record.origin.length().to_double()
    }) /
    self.total_bases().to_double()
  }
}

///|
pub fn RecordCollection::feature_density(self : RecordCollection) -> Double {
  if self.total_bases() == 0 {
    0.0
  } else {
    self.total_features().to_double() / self.total_bases().to_double()
  }
}

///|
pub fn RecordCollection::remote_locations(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) {
    total + record_remote_accession_count(record)
  })
}

///|
pub fn RecordCollection::fuzzy_locations(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) {
    total + record_fuzzy_location_count(record)
  })
}

///|
pub fn RecordCollection::between_locations(self : RecordCollection) -> Int {
  self.records.fold(init=0, fn(total, record) {
    total + record_between_location_count(record)
  })
}