use std::collections::HashMap;
use petgraph::graph::NodeIndex;
use super::super::ast::YieldItem;
use super::super::result::ResultRow;
use super::helpers::yield_alias;
use crate::datatypes::values::Value;
use crate::graph::dir_graph::DirGraph;
use crate::graph::storage::GraphRead;
const PROC: &str = "rev_diff";
pub(super) fn execute_rev_diff(
graph: &DirGraph,
params: &HashMap<String, Value>,
yield_items: &[YieldItem],
) -> Result<Vec<ResultRow>, String> {
let from_rev = require_string_param(params, "from")?;
let to_rev = require_string_param(params, "to")?;
let node_types = string_list_param(params, "node_type");
let bucket_var = yield_alias(yield_items, "bucket");
let type_var = yield_alias(yield_items, "type");
let qn_var = yield_alias(yield_items, "qualified_name");
let name_var = yield_alias(yield_items, "name");
let file_var = yield_alias(yield_items, "file");
let line_var = yield_alias(yield_items, "line");
let candidates: Vec<NodeIndex> = match &node_types {
Some(types) => {
let mut v = Vec::new();
for t in types {
if let Some(idxs) = graph.type_indices.get(t.as_str()) {
v.extend(idxs.iter());
}
}
v
}
None => graph.graph.node_indices().collect(),
};
let mut available: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
let mut saw_revs_prop = false;
let mut hits: Vec<(NodeIndex, &'static str)> = Vec::new();
for &nidx in &candidates {
let Some(node) = graph.graph.node_weight(nidx) else {
continue;
};
let Some(Value::List(revs)) = node.get_property_value("revs") else {
continue;
};
saw_revs_prop = true;
for r in &revs {
if let Some(s) = r.as_string() {
available.insert(s);
}
}
let pos_from = list_position(&revs, &from_rev);
let pos_to = list_position(&revs, &to_rev);
let bucket = match (pos_from, pos_to) {
(Some(_), None) => "removed",
(None, Some(_)) => "added",
(Some(i), Some(j)) => {
let fp = match node.get_property_value("rev_fp") {
Some(Value::List(items)) => items,
_ => Vec::new(),
};
match (fp.get(i), fp.get(j)) {
(Some(a), Some(b)) if a != b => "changed",
_ => continue,
}
}
(None, None) => continue,
};
hits.push((nidx, bucket));
}
if !saw_revs_prop {
return Err(format!(
"CALL {PROC}: this graph has no `revs` property — it is not a multi-rev graph. \
Build one with code_tree.build(path, revs=['v1', 'v2', ...])."
));
}
for rev in [&from_rev, &to_rev] {
if !available.contains(rev) {
let avail: Vec<&str> = available.iter().map(|s| s.as_str()).collect();
return Err(format!(
"CALL {PROC}: revision {rev:?} is not present in this graph. Available revs: [{}].",
avail.join(", ")
));
}
}
let mut rows: Vec<(String, String, ResultRow)> = Vec::with_capacity(hits.len());
for (nidx, bucket) in hits {
let Some(node) = graph.graph.node_weight(nidx) else {
continue;
};
let node_type = node.node_type_str(&graph.interner).to_string();
let qualified_name = value_to_string(&node.id());
let name = value_to_string(&node.title());
let mut row = ResultRow::new();
if let Some(a) = &bucket_var {
row.projected
.insert(a.clone(), Value::String(bucket.to_string()));
}
if let Some(a) = &type_var {
row.projected
.insert(a.clone(), Value::String(node_type.clone()));
}
if let Some(a) = &qn_var {
row.projected
.insert(a.clone(), Value::String(qualified_name.clone()));
}
if let Some(a) = &name_var {
row.projected.insert(a.clone(), Value::String(name));
}
if let Some(a) = &file_var {
let file = node.get_property_value("file_path").unwrap_or(Value::Null);
row.projected.insert(a.clone(), file);
}
if let Some(a) = &line_var {
let line = node
.get_property_value("line_number")
.unwrap_or(Value::Null);
row.projected.insert(a.clone(), line);
}
rows.push((bucket.to_string(), qualified_name, row));
}
rows.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
Ok(rows.into_iter().map(|(_, _, row)| row).collect())
}
fn list_position(list: &[Value], needle: &str) -> Option<usize> {
list.iter().position(|v| match v.as_string() {
Some(s) => s == needle,
None => false,
})
}
fn value_to_string(v: &Value) -> String {
v.as_string().unwrap_or_else(|| v.to_string())
}
fn require_string_param(params: &HashMap<String, Value>, key: &str) -> Result<String, String> {
match params.get(key) {
Some(Value::String(s)) => Ok(s.clone()),
Some(other) => Err(format!(
"CALL {PROC}: parameter '{key}' must be a string, got {other:?}"
)),
None => Err(format!(
"CALL {PROC}: missing required parameter '{key}'. \
Use map syntax — e.g. CALL {PROC}({{from: 'v1', to: 'v2'}})."
)),
}
}
fn string_list_param(params: &HashMap<String, Value>, key: &str) -> Option<Vec<String>> {
match params.get(key) {
Some(Value::String(s)) => Some(vec![s.clone()]),
Some(Value::List(items)) => {
let v: Vec<String> = items.iter().filter_map(|x| x.as_string()).collect();
if v.is_empty() {
None
} else {
Some(v)
}
}
_ => None,
}
}