///|
/// A user-facing catalog entry for a waveform artifact.
pub struct TraceCatalogItem {
  name : String
  category : String
  description : String
  signal_count : Int
  change_count : Int
  duration : Int64
  tags : Array[String]
  quality : Int
} derive(Debug)

///|
/// A catalog assembled from one or more analyzed traces.
pub struct TraceCatalogV2 {
  items : Array[TraceCatalogItem]
  total_signals : Int
  total_changes : Int
  total_duration : Int64
} derive(Debug)

///|
/// Build a catalog entry from a trace and descriptive metadata.
pub fn trace_catalog_item(
  name : String,
  category : String,
  description : String,
  trace : Trace,
  tags : Array[String],
) -> TraceCatalogItem {
  let quality = if analysis_safe(trace) {
    trace_quality_score(trace)
  } else {
    0
  }
  {
    name,
    category,
    description,
    signal_count: trace.signals.length(),
    change_count: trace.changes.length(),
    duration: trace.end_time,
    tags,
    quality,
  }
}

///|
/// Create a catalog from named traces.
pub fn trace_catalog(items : Array[TraceCatalogItem]) -> TraceCatalogV2 {
  {
    items,
    total_signals: items.fold(init=0, fn(sum, item) { sum + item.signal_count }),
    total_changes: items.fold(init=0, fn(sum, item) { sum + item.change_count }),
    total_duration: items.fold(init=0L, fn(sum, item) { sum + item.duration }),
  }
}

///|
/// Add an item to a catalog.
pub fn TraceCatalogV2::add(
  self : TraceCatalogV2,
  item : TraceCatalogItem,
) -> TraceCatalogV2 {
  let items = self.items.copy()
  items.push(item)
  trace_catalog(items)
}

///|
/// Find catalog items by category.
pub fn TraceCatalogV2::category(
  self : TraceCatalogV2,
  category : String,
) -> Array[TraceCatalogItem] {
  self.items.filter(fn(item) { item.category == category })
}

///|
/// Find catalog items containing a tag.
pub fn TraceCatalogV2::tagged(
  self : TraceCatalogV2,
  tag : String,
) -> Array[TraceCatalogItem] {
  self.items.filter(fn(item) { item.tags.contains(tag) })
}

///|
/// Return the highest-quality artifact.
pub fn TraceCatalogV2::best(self : TraceCatalogV2) -> TraceCatalogItem? {
  self.items.fold(init=None, fn(current, item) {
    match current {
      Some(previous) if item.quality > previous.quality => Some(item)
      Some(previous) => Some(previous)
      None => Some(item)
    }
  })
}

///|
/// Export catalog entries as CSV.
pub fn trace_catalog_csv(items : Array[TraceCatalogItem]) -> String {
  let out = StringBuilder()
  out.write_string(
    "name,category,signals,changes,duration,quality,tags,description\n",
  )
  for item in items {
    out.write_string(
      escape_text(item.name) +
      "," +
      escape_text(item.category) +
      "," +
      item.signal_count.to_string() +
      "," +
      item.change_count.to_string() +
      "," +
      item.duration.to_string() +
      "," +
      item.quality.to_string() +
      "," +
      item.tags.join("|") +
      "," +
      escape_text(item.description) +
      "\n",
    )
  }
  out.to_string()
}

///|
/// Render catalog metadata as JSON.
pub fn TraceCatalogV2::to_json(self : TraceCatalogV2) -> String {
  "{\"items\":[" +
  self.items.map(trace_catalog_item_json).join(",") +
  "],\"total_signals\":" +
  self.total_signals.to_string() +
  ",\"total_changes\":" +
  self.total_changes.to_string() +
  ",\"total_duration\":" +
  self.total_duration.to_string() +
  "}"
}

///|
/// Render catalog totals for terminal output.
pub fn TraceCatalogV2::to_text(self : TraceCatalogV2) -> String {
  "items=" +
  self.items.length().to_string() +
  " signals=" +
  self.total_signals.to_string() +
  " changes=" +
  self.total_changes.to_string() +
  " duration=" +
  self.total_duration.to_string()
}

///|
/// Encode one catalog item as JSON.
fn trace_catalog_item_json(item : TraceCatalogItem) -> String {
  "{\"name\":\"" +
  escape_text(item.name) +
  "\",\"category\":\"" +
  escape_text(item.category) +
  "\",\"description\":\"" +
  escape_text(item.description) +
  "\",\"signals\":" +
  item.signal_count.to_string() +
  ",\"changes\":" +
  item.change_count.to_string() +
  ",\"duration\":" +
  item.duration.to_string() +
  ",\"quality\":" +
  item.quality.to_string() +
  ",\"tags\":[" +
  item.tags.map(fn(tag) { "\"" + escape_text(tag) + "\"" }).join(",") +
  "]}"
}