///|
/// Validated source record. IDs are preserved; descriptions are not echoed in errors.
pub(all) struct Sample {
  id : String
  dna : Dna
} derive(Eq, Debug, ToJson)

///|
/// Strict wrapper: upstream recoverable parse issues become atomic failures.
pub fn read_fasta(
  text : String,
  topology? : Topology = Linear,
) -> Array[Sample] raise RestrictError {
  if text.length() > 1000000 {
    raise LimitExceeded("FASTA_BYTES")
  }
  let mut headers = 0
  let mut lines = 0
  for line in text.split("\n") {
    lines = lines + 1
    if lines > 10000 {
      raise LimitExceeded("FASTA_LINES")
    }
    if line.trim().has_prefix(">") {
      headers = headers + 1
    }
    if headers > 64 {
      raise LimitExceeded("FASTA_RECORDS")
    }
  }
  let parsed = @fasta.parse_fasta(text)
  if !parsed.issues.is_empty() {
    raise InvalidInput("FASTA_ISSUE_LINE_" + parsed.issues[0].line.to_string())
  }
  if parsed.records.is_empty() {
    raise InvalidInput("FASTA_EMPTY")
  }
  let samples : Array[Sample] = []
  let mut total = 0
  for record in parsed.records {
    if record.identifier.length() > 128 {
      raise LimitExceeded("FASTA_ID")
    }
    let dna = Dna::new(record.sequence, topology~)
    total = total + dna.length()
    if total > 100000 {
      raise LimitExceeded("FASTA_TOTAL_BASES")
    }
    samples.push({ id: record.identifier, dna })
  }
  samples
}

///|
/// Serialize the strict subset with wrapped sequence lines and no descriptions.
pub fn write_fasta(samples : Array[Sample]) -> String raise RestrictError {
  if samples.is_empty() || samples.length() > 64 {
    raise InvalidInput("FASTA_RECORDS")
  }
  let out = StringBuilder::new()
  for sample in samples {
    if sample.id.is_empty() {
      raise InvalidInput("FASTA_ID")
    }
    for c in sample.id.iter() {
      if c <= ' ' || c == '>' || c.to_int() > 126 {
        raise InvalidInput("FASTA_ID")
      }
    }
    out.write_string(">" + sample.id + "\n")
    for i = 0; i < sample.dna.length(); i = i + 60 {
      let end = (i + 60).min(sample.dna.length())
      out.write_string(sample.dna.bases[i:end].to_owned() + "\n")
    }
  }
  let text = out.to_string()
  let _ = read_fasta(text)
  text
}