use anyhow::Result;
use rigg_client::AzureSearchClient;
use rigg_core::normalize::normalize;
use rigg_core::resources::ResourceKind;
use rigg_core::resources::agent::{agent_volatile_fields, strip_agent_empty_fields};
use rigg_core::service::ServiceDomain;
use crate::cli::{DiffFormat, ResourceTypeFlags};
use crate::commands::common::{
get_read_only_fields, get_volatile_fields, resolve_resource_selection_from_flags,
};
use super::ResourceDiff;
use super::format::{format_diff_json, format_diff_text};
pub(super) async fn run_cross_env_diff(
flags: &ResourceTypeFlags,
format: DiffFormat,
exit_code: bool,
left_env: &rigg_core::config::ResolvedEnvironment,
right_env: &rigg_core::config::ResolvedEnvironment,
) -> Result<()> {
eprintln!(
"Comparing environments '{}' and '{}'...",
left_env.name, right_env.name
);
let include_preview = left_env.sync.include_preview || right_env.sync.include_preview;
let selection = resolve_resource_selection_from_flags(flags, include_preview, true);
if selection.is_empty() {
println!("No resource types specified. Use --all or specify types (e.g., --indexes)");
return Ok(());
}
let kinds = selection.kinds();
let search_kinds: Vec<ResourceKind> = kinds
.iter()
.filter(|k| k.domain() == ServiceDomain::Search)
.copied()
.collect();
let foundry_kinds: Vec<ResourceKind> = kinds
.iter()
.filter(|k| k.domain() == ServiceDomain::Foundry)
.copied()
.collect();
let mut all_diffs: Vec<ResourceDiff> = Vec::new();
let mut has_changes = false;
if let (Some(left_svc), Some(right_svc)) = (
left_env.primary_search_service(),
right_env.primary_search_service(),
) {
if !search_kinds.is_empty() {
diff_search_cross_env(
&search_kinds,
left_svc,
right_svc,
&selection,
&mut all_diffs,
&mut has_changes,
)
.await?;
}
}
if !foundry_kinds.is_empty() {
diff_foundry_cross_env(
left_env,
right_env,
&selection,
&mut all_diffs,
&mut has_changes,
)
.await?;
}
let left_label = format!("on {}", left_env.name);
let right_label = format!("on {}", right_env.name);
let no_ai = std::collections::HashMap::new();
match format {
DiffFormat::Text => {
format_diff_text(&all_diffs, Some((&left_label, &right_label)), &no_ai);
}
DiffFormat::Json => {
let json = format_diff_json(&mut all_diffs, (&left_label, &right_label), &no_ai);
print!("{}", json);
}
}
if exit_code && has_changes {
std::process::exit(5);
}
Ok(())
}
async fn diff_search_cross_env(
search_kinds: &[ResourceKind],
left_svc: &rigg_core::config::SearchServiceConfig,
right_svc: &rigg_core::config::SearchServiceConfig,
selection: &crate::commands::common::ResourceSelection,
all_diffs: &mut Vec<ResourceDiff>,
has_changes: &mut bool,
) -> Result<()> {
let left_client = AzureSearchClient::from_service_config(left_svc)?;
let right_client = AzureSearchClient::from_service_config(right_svc)?;
for kind in search_kinds {
let volatile = get_volatile_fields(*kind);
let read_only = get_read_only_fields(*kind);
let strip_fields: Vec<&str> = volatile.iter().chain(read_only.iter()).copied().collect();
let exact_name = selection.name_filter(*kind);
let left_resources = left_client.list(*kind).await?;
let right_resources = right_client.list(*kind).await?;
let left_map: std::collections::HashMap<String, serde_json::Value> = left_resources
.into_iter()
.filter_map(|r| {
let name = r.get("name")?.as_str()?.to_string();
Some((name, r))
})
.collect();
let right_map: std::collections::HashMap<String, serde_json::Value> = right_resources
.into_iter()
.filter_map(|r| {
let name = r.get("name")?.as_str()?.to_string();
Some((name, r))
})
.collect();
let mut all_names: Vec<String> = left_map.keys().cloned().collect();
for name in right_map.keys() {
if !all_names.contains(name) {
all_names.push(name.clone());
}
}
all_names.sort();
for name in &all_names {
if let Some(exact) = exact_name {
if name != exact {
continue;
}
}
let resource_id = format!("{}/{}", kind.directory_name(), name);
match (left_map.get(name), right_map.get(name)) {
(Some(left), Some(right)) => {
let left_norm = normalize(left, &strip_fields);
let right_norm = normalize(right, &strip_fields);
let diff_result = rigg_diff::diff(&left_norm, &right_norm, "name");
if !diff_result.is_equal {
*has_changes = true;
}
all_diffs.push(ResourceDiff {
kind: *kind,
resource_name: name.clone(),
display_id: resource_id,
result: diff_result,
local_content: None,
remote_content: None,
});
}
(Some(left), None) => {
*has_changes = true;
let left_norm = normalize(left, &strip_fields);
all_diffs.push(ResourceDiff {
kind: *kind,
resource_name: name.clone(),
display_id: resource_id,
result: rigg_diff::DiffResult {
is_equal: false,
changes: vec![rigg_diff::Change {
path: ".".to_string(),
kind: rigg_diff::ChangeKind::Added,
old_value: None,
new_value: Some(left_norm),
description: None,
}],
},
local_content: None,
remote_content: None,
});
}
(None, Some(right)) => {
*has_changes = true;
let right_norm = normalize(right, &strip_fields);
all_diffs.push(ResourceDiff {
kind: *kind,
resource_name: name.clone(),
display_id: resource_id,
result: rigg_diff::DiffResult {
is_equal: false,
changes: vec![rigg_diff::Change {
path: ".".to_string(),
kind: rigg_diff::ChangeKind::Added,
old_value: None,
new_value: Some(right_norm),
description: None,
}],
},
local_content: None,
remote_content: None,
});
}
(None, None) => {}
}
}
}
Ok(())
}
async fn diff_foundry_cross_env(
left_env: &rigg_core::config::ResolvedEnvironment,
right_env: &rigg_core::config::ResolvedEnvironment,
selection: &crate::commands::common::ResourceSelection,
all_diffs: &mut Vec<ResourceDiff>,
has_changes: &mut bool,
) -> Result<()> {
let volatile = agent_volatile_fields();
let exact_name = selection.name_filter(ResourceKind::Agent);
let left_agents = if left_env.has_foundry() {
let mut agents = Vec::new();
for fc in &left_env.foundry {
let client = rigg_client::FoundryClient::new(fc)?;
agents.extend(client.list_agents().await?);
}
agents
} else {
vec![]
};
let right_agents = if right_env.has_foundry() {
let mut agents = Vec::new();
for fc in &right_env.foundry {
let client = rigg_client::FoundryClient::new(fc)?;
agents.extend(client.list_agents().await?);
}
agents
} else {
vec![]
};
let left_map: std::collections::HashMap<String, serde_json::Value> = left_agents
.into_iter()
.filter_map(|mut a| {
strip_agent_empty_fields(&mut a);
let name = a.get("name")?.as_str()?.to_string();
Some((name, a))
})
.collect();
let right_map: std::collections::HashMap<String, serde_json::Value> = right_agents
.into_iter()
.filter_map(|mut a| {
strip_agent_empty_fields(&mut a);
let name = a.get("name")?.as_str()?.to_string();
Some((name, a))
})
.collect();
let mut all_names: Vec<String> = left_map.keys().cloned().collect();
for name in right_map.keys() {
if !all_names.contains(name) {
all_names.push(name.clone());
}
}
all_names.sort();
for name in &all_names {
if let Some(exact) = exact_name {
if name != exact {
continue;
}
}
let resource_id = format!("agents/{}", name);
match (left_map.get(name), right_map.get(name)) {
(Some(left), Some(right)) => {
let left_norm = normalize(left, volatile);
let right_norm = normalize(right, volatile);
let diff_result = rigg_diff::diff(&left_norm, &right_norm, "name");
if !diff_result.is_equal {
*has_changes = true;
}
all_diffs.push(ResourceDiff {
kind: ResourceKind::Agent,
resource_name: name.clone(),
display_id: resource_id,
result: diff_result,
local_content: None,
remote_content: None,
});
}
(Some(left), None) => {
*has_changes = true;
let left_norm = normalize(left, volatile);
all_diffs.push(ResourceDiff {
kind: ResourceKind::Agent,
resource_name: name.clone(),
display_id: resource_id,
result: rigg_diff::DiffResult {
is_equal: false,
changes: vec![rigg_diff::Change {
path: ".".to_string(),
kind: rigg_diff::ChangeKind::Added,
old_value: None,
new_value: Some(left_norm),
description: None,
}],
},
local_content: None,
remote_content: None,
});
}
(None, Some(right)) => {
*has_changes = true;
let right_norm = normalize(right, volatile);
all_diffs.push(ResourceDiff {
kind: ResourceKind::Agent,
resource_name: name.clone(),
display_id: resource_id,
result: rigg_diff::DiffResult {
is_equal: false,
changes: vec![rigg_diff::Change {
path: ".".to_string(),
kind: rigg_diff::ChangeKind::Added,
old_value: None,
new_value: Some(right_norm),
description: None,
}],
},
local_content: None,
remote_content: None,
});
}
(None, None) => {}
}
}
Ok(())
}