use std::path::Path;
use std::sync::Arc;
use anyhow::Result;
use futures::stream::{self, StreamExt};
use serde_json::Value;
use tokio::sync::Semaphore;
use tracing::info;
use rigg_client::AzureSearchClient;
use rigg_core::config::ResolvedEnvironment;
use rigg_core::resources::ResourceKind;
use rigg_core::resources::managed::{self, ManagedMap};
use rigg_core::service::ServiceDomain;
use rigg_core::state::Checksums;
use crate::commands::common::ResourceSelection;
use crate::commands::confirm::prompt_yes_no;
use super::discover;
use super::output;
use super::write;
#[allow(clippy::too_many_arguments)]
pub async fn execute_pull(
project_root: &Path,
files_root: &Path,
env: &ResolvedEnvironment,
selection: &ResourceSelection,
filter: Option<&str>,
force: bool,
use_explain: bool,
) -> Result<()> {
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 checksums = Checksums::load_env(project_root, &env.name)?;
let mut new_resources = Vec::new();
let mut updated_resources = Vec::new();
let mut deleted_resources: Vec<(ResourceKind, String, std::path::PathBuf)> = Vec::new();
let mut total_unchanged: usize = 0;
let mut managed_map = ManagedMap::new();
let mut checksum_backfill: Vec<(ResourceKind, String, String)> = Vec::new();
if !search_kinds.is_empty() {
let search_svc = env
.primary_search_service()
.ok_or_else(|| anyhow::anyhow!("No search service in environment '{}'", env.name))?;
eprintln!("Fetching resources from {}...", search_svc.name);
let client = AzureSearchClient::from_service_config(search_svc)?;
info!(
"Connected to {} using {}",
search_svc.name,
client.auth_method()
);
let mut fetch_kinds = search_kinds.clone();
if fetch_kinds.contains(&ResourceKind::KnowledgeSource) {
for managed_kind in managed::MANAGED_SUB_RESOURCE_KINDS {
if !fetch_kinds.contains(managed_kind) {
fetch_kinds.push(*managed_kind);
}
}
}
let has_ks = fetch_kinds.contains(&ResourceKind::KnowledgeSource);
let needs_managed_map = has_ks || env.sync.include_preview;
if needs_managed_map {
let ks_results = client.list(ResourceKind::KnowledgeSource).await;
if let Ok(ks_list) = &ks_results {
let ks_pairs: Vec<(String, Value)> = ks_list
.iter()
.filter_map(|r| {
r.get("name")
.and_then(|n| n.as_str())
.map(|n| (n.to_string(), r.clone()))
})
.collect();
managed_map = managed::build_managed_map(&ks_pairs);
}
}
let remaining_kinds: Vec<ResourceKind> = if has_ks {
fetch_kinds
.iter()
.filter(|k| **k != ResourceKind::KnowledgeSource)
.copied()
.collect()
} else {
fetch_kinds.clone()
};
let semaphore = Arc::new(Semaphore::new(5));
let mut fetched_results: Vec<(ResourceKind, Result<Vec<Value>, _>)> =
stream::iter(remaining_kinds.iter())
.map(|kind| {
let client = &client;
let sem = Arc::clone(&semaphore);
async move {
let _permit = sem.acquire().await.expect("semaphore closed unexpectedly");
let result = client.list(*kind).await;
(*kind, result)
}
})
.buffer_unordered(5)
.collect()
.await;
if has_ks {
let ks_result = client.list(ResourceKind::KnowledgeSource).await;
fetched_results.push((ResourceKind::KnowledgeSource, ks_result));
}
let service_dir = env.search_service_dir(files_root, search_svc);
discover::discover_search_resources(
&service_dir,
&fetched_results,
selection,
filter,
&checksums,
&managed_map,
&mut new_resources,
&mut updated_resources,
&mut deleted_resources,
&mut total_unchanged,
&mut checksum_backfill,
)?;
}
if !foundry_kinds.is_empty() && env.has_foundry() {
for foundry_config in &env.foundry {
eprintln!(
"Fetching agents from {}/{}...",
foundry_config.name, foundry_config.project
);
let agents_dir = write::foundry_agents_dir(env, files_root, foundry_config);
discover::discover_foundry_agents(
&agents_dir,
foundry_config,
selection,
filter,
&checksums,
&mut new_resources,
&mut updated_resources,
&mut deleted_resources,
&mut total_unchanged,
&mut checksum_backfill,
)
.await?;
}
}
write::backfill_checksums(project_root, &env.name, &checksum_backfill, &managed_map)?;
let total_changes = new_resources.len() + updated_resources.len() + deleted_resources.len();
if total_changes == 0 {
println!(
"All {} resource(s) are up to date, nothing to pull.",
total_unchanged
);
return Ok(());
}
let used_ai_narrative = if use_explain {
match output::generate_pull_narrative(
&new_resources,
&updated_resources,
&deleted_resources,
total_unchanged,
)
.await
{
Some(narrative) => {
println!("{}", narrative);
true
}
None => false,
}
} else {
false
};
if !used_ai_narrative {
output::print_pull_summary(
&new_resources,
&updated_resources,
&deleted_resources,
total_unchanged,
);
}
println!();
output::print_local_modification_warnings(&updated_resources);
if !force && !prompt_yes_no("Proceed with pull?")? {
println!("Aborted.");
return Ok(());
}
let (upsert_count, delete_count) = write::write_resources(
project_root,
files_root,
env,
new_resources,
updated_resources,
&deleted_resources,
&managed_map,
total_unchanged,
)?;
output::print_pull_result(upsert_count, delete_count, total_unchanged);
Ok(())
}