///|
/// A compact result for a feature extraction workflow.
pub struct FeatureExtraction {
  index : Int
  key : String
  location : String
  sequence : String
  sequence_length : Int
  translation : String
} derive(Debug, Eq)

///|
/// Extract feature sequences while retaining enough context for audit logs.
pub fn extract_features(
  record : Record,
  key : String,
) -> Array[FeatureExtraction] {
  let result : Array[FeatureExtraction] = []
  for index, feature in record.features {
    if feature.key == key {
      let sequence = record.sequence_for(feature)
      result.push({
        index,
        key: feature.key,
        location: feature.location.raw,
        sequence,
        sequence_length: sequence.length(),
        translation: match feature.qualifier("translation") {
          Some(value) => value
          None => ""
        },
      })
    }
  }
  result
}

///|
/// Extract all CDS sequences and orient joined/complement locations correctly.
pub fn extract_cds(record : Record) -> Array[FeatureExtraction] {
  extract_features(record, "CDS")
}

///|
/// Find features whose qualifier contains a case-insensitive search term.
pub fn features_with_qualifier_text(
  record : Record,
  name : String,
  needle : String,
) -> Array[Feature] {
  let wanted = needle.to_lower()
  record.features.filter(fn(feature) {
    match feature.qualifier(name) {
      Some(value) => value.to_lower().contains(wanted)
      None => false
    }
  })
}

///|
/// Return feature rows with a location length in the inclusive range.
pub fn features_in_length_range(
  record : Record,
  minimum : Int,
  maximum : Int,
) -> Array[FeatureTableRow] {
  if minimum < 0 || maximum < minimum {
    return []
  }
  feature_table(record).filter(fn(row) {
    minimum <= row.length && row.length <= maximum
  })
}

///|
/// Return the normalized ORIGIN split into fixed-size chunks for streaming UIs.
pub fn record_origin_chunks(record : Record, chunk_size : Int) -> Array[String] {
  sequence_chunks(normalize_sequence(record.origin), chunk_size)
}

///|
/// Count features that cannot be materialized from the local ORIGIN.
pub fn unresolved_feature_count(record : Record) -> Int {
  record.features.fold(init=0, fn(total, feature) {
    total + (if location_is_remote(feature.location) { 1 } else { 0 })
  })
}

///|
/// Produce stable feature labels in source order for list and search widgets.
pub fn record_feature_labels(record : Record) -> Array[String] {
  feature_table(record).map(fn(row) {
    row.key +
    ":" +
    row.label +
    " [" +
    row.start.to_string() +
    ".." +
    row.end.to_string() +
    "]"
  })
}

///|
/// Return an extraction as JSON without requiring a JSON dependency.
pub fn FeatureExtraction::to_json_string(self : FeatureExtraction) -> String {
  "{\"index\":" +
  self.index.to_string() +
  ",\"key\":" +
  json_string(self.key) +
  ",\"location\":" +
  json_string(self.location) +
  ",\"sequence\":" +
  json_string(self.sequence) +
  ",\"sequence_length\":" +
  self.sequence_length.to_string() +
  ",\"translation\":" +
  json_string(self.translation) +
  "}"
}

///|
/// Export extracted features as a line-oriented JSON document.
pub fn feature_extractions_json(record : Record, key : String) -> String {
  "[" +
  extract_features(record, key)
  .map(fn(item) { item.to_json_string() })
  .join(",") +
  "]"
}

///|
/// Return all features with their local sequence length, preserving order.
pub fn feature_sequence_lengths(record : Record) -> Array[(String, Int)] {
  record.features.map(fn(feature) {
    (feature.key, record.sequence_for(feature).length())
  })
}

///|
/// Check whether every local feature lies inside the declared ORIGIN length.
pub fn local_features_fit_origin(record : Record) -> Bool {
  record.features.all(fn(feature) {
    if location_is_remote(feature.location) {
      true
    } else {
      location_maximum(feature.location) <= record.origin.length()
    }
  })
}

///|
fn location_maximum(location : Location) -> Int {
  location.ranges.fold(init=0, fn(current, range) {
    Int::max(current, range.end)
  })
}