///|
/// Lightweight lineage graph for tracing production transformations.
pub struct LineageNode {
  id : String
  operation : String
  input_count : Int
  output_count : Int
  score : Double
}

///|
pub struct LineageGraph {
  name : String
  nodes : Array[LineageNode]
  edges : Array[Array[Int]]
}

///|
pub fn lineage_graph(name : String) -> LineageGraph {
  { name, nodes: [], edges: [] }
}

///|
pub fn lineage_add_node(
  graph : LineageGraph,
  id : String,
  operation : String,
  input_count : Int,
  output_count : Int,
  score : Double,
) -> Int {
  let index = graph.nodes.length()
  graph.nodes.push({ id, operation, input_count, output_count, score })
  index
}

///|
pub fn lineage_add_edge(graph : LineageGraph, from : Int, to : Int) -> Unit {
  graph.edges.push([from, to])
}

///|
pub fn lineage_node_count(graph : LineageGraph) -> Int {
  graph.nodes.length()
}

///|
pub fn lineage_edge_count(graph : LineageGraph) -> Int {
  graph.edges.length()
}

///|
pub fn lineage_operations(graph : LineageGraph) -> Array[String] {
  let result = []
  for node in graph.nodes {
    result.push(node.operation)
  }
  result
}

///|
pub fn lineage_scores(graph : LineageGraph) -> Array[Double] {
  let result = []
  for node in graph.nodes {
    result.push(node.score)
  }
  result
}

///|
pub fn lineage_quality(graph : LineageGraph) -> Double {
  if graph.nodes.length() == 0 {
    1.0
  } else {
    mean(lineage_scores(graph))
  }
}

///|
pub fn lineage_valid(graph : LineageGraph) -> Bool {
  for edge in graph.edges {
    if edge.length() != 2 ||
      edge[0] < 0 ||
      edge[1] < 0 ||
      edge[0] >= graph.nodes.length() ||
      edge[1] >= graph.nodes.length() {
      return false
    }
  }
  true
}

///|
pub fn lineage_terminal(graph : LineageGraph) -> Int {
  if graph.nodes.length() == 0 {
    -1
  } else {
    graph.nodes.length() - 1
  }
}

///|
pub fn lineage_lines(graph : LineageGraph) -> Array[String] {
  let lines = [
    "name=" + graph.name,
    "nodes=" + graph.nodes.length().to_string(),
    "edges=" + graph.edges.length().to_string(),
    "quality=" + lineage_quality(graph).to_string(),
  ]
  for node in graph.nodes {
    lines.push(
      node.id +
      "|" +
      node.operation +
      "|" +
      node.input_count.to_string() +
      "|" +
      node.output_count.to_string() +
      "|" +
      node.score.to_string(),
    )
  }
  lines
}

///|
pub fn lineage_string(graph : LineageGraph) -> String {
  lineage_lines(graph).join("\n")
}

///|
pub fn lineage_summary(graph : LineageGraph) -> Array[Double] {
  [
    graph.nodes.length().to_double(),
    graph.edges.length().to_double(),
    lineage_quality(graph),
    if lineage_valid(graph) {
      1.0
    } else {
      0.0
    },
    lineage_terminal(graph).to_double(),
  ]
}