1use std::path::Path;
11
12use algocline_engine::card;
13use serde::{Deserialize, Serialize};
14
15use super::hub;
16use super::AppService;
17
18#[derive(Debug, Deserialize)]
21pub struct SinkBackfillParams {
22 pub sink: String,
23 #[serde(default)]
24 pub dry_run: bool,
25}
26
27#[derive(Debug, Serialize, Deserialize)]
39pub struct CardAnalyzeResult {
40 pub pattern: String,
42 pub suggested_change: String,
44 pub confidence: f64,
46 #[serde(default)]
48 pub failure_count: Option<u64>,
49 #[serde(default)]
51 pub sample_count: Option<u64>,
52}
53
54pub const DEFAULT_CARD_ANALYZE_PKG: &str = "card_analysis";
63
64impl AppService {
65 pub fn card_list(&self, pkg: Option<&str>) -> Result<String, String> {
67 let rows = self.card_store.list(pkg)?;
68 Ok(card::summaries_to_json(&rows).to_string())
69 }
70
71 pub fn card_get(&self, card_id: &str) -> Result<String, String> {
73 match self.card_store.get(card_id)? {
74 Some(v) => Ok(v.to_string()),
75 None => Err(format!("card '{card_id}' not found")),
76 }
77 }
78
79 pub fn card_find(
81 &self,
82 pkg: Option<String>,
83 where_: Option<serde_json::Value>,
84 order_by: Option<serde_json::Value>,
85 limit: Option<usize>,
86 offset: Option<usize>,
87 ) -> Result<String, String> {
88 let where_parsed = match where_ {
89 Some(v) => Some(card::parse_where(&v)?),
90 None => None,
91 };
92 let order_parsed = match order_by {
93 Some(v) => card::parse_order_by(&v)?,
94 None => Vec::new(),
95 };
96 let q = card::FindQuery {
97 pkg,
98 where_: where_parsed,
99 order_by: order_parsed,
100 limit,
101 offset,
102 };
103 let rows = self.card_store.find(q)?;
104 Ok(card::summaries_to_json(&rows).to_string())
105 }
106
107 pub fn card_get_by_alias(&self, name: &str) -> Result<String, String> {
109 match self.card_store.get_by_alias(name)? {
110 Some(v) => Ok(v.to_string()),
111 None => Err(format!("alias '{name}' not found")),
112 }
113 }
114
115 pub fn card_alias_list(&self, pkg: Option<&str>) -> Result<String, String> {
117 let rows = self.card_store.alias_list(pkg)?;
118 Ok(card::aliases_to_json(&rows).to_string())
119 }
120
121 pub fn card_alias_set(
123 &self,
124 name: &str,
125 card_id: &str,
126 pkg: Option<&str>,
127 note: Option<&str>,
128 ) -> Result<String, String> {
129 let alias = self.card_store.alias_set(name, card_id, pkg, note)?;
130 let arr = card::aliases_to_json(std::slice::from_ref(&alias));
131 let single = arr
132 .as_array()
133 .and_then(|a| a.first().cloned())
134 .unwrap_or(serde_json::Value::Null);
135 Ok(single.to_string())
136 }
137
138 pub fn card_append(&self, card_id: &str, fields: serde_json::Value) -> Result<String, String> {
140 let merged = self.card_store.append(card_id, fields)?;
141 Ok(merged.to_string())
142 }
143
144 pub async fn card_install(&self, url: String) -> Result<String, String> {
150 let local_path = Path::new(&url);
152 if local_path.is_absolute() && local_path.is_dir() {
153 return self.card_install_from_dir(local_path, &url);
154 }
155
156 let git_url = if url.starts_with("http://")
158 || url.starts_with("https://")
159 || url.starts_with("file://")
160 || url.starts_with("git@")
161 {
162 url.clone()
163 } else {
164 format!("https://{url}")
165 };
166
167 let staging = tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {e}"))?;
169
170 let output = tokio::process::Command::new("git")
171 .args([
172 "clone",
173 "--depth",
174 "1",
175 &git_url,
176 &staging.path().to_string_lossy(),
177 ])
178 .output()
179 .await
180 .map_err(|e| format!("Failed to run git: {e}"))?;
181
182 if !output.status.success() {
183 let stderr = String::from_utf8_lossy(&output.stderr);
184 return Err(format!("git clone failed: {stderr}"));
185 }
186
187 self.card_install_from_dir(staging.path(), &url)
188 }
189
190 fn card_install_from_dir(&self, root: &Path, source: &str) -> Result<String, String> {
192 let manifest_path = root.join("alc_cards.toml");
194 if !manifest_path.exists() {
195 return Err("Not a Card Collection: alc_cards.toml not found at root. \
196 Card Collections must have an alc_cards.toml manifest."
197 .into());
198 }
199
200 let mut all_imported: Vec<String> = Vec::new();
201 let mut all_skipped: Vec<String> = Vec::new();
202 let mut packages: Vec<String> = Vec::new();
203
204 let entries =
205 std::fs::read_dir(root).map_err(|e| format!("Failed to read source dir: {e}"))?;
206
207 for entry in entries.flatten() {
208 let path = entry.path();
209 if !path.is_dir() {
210 continue;
211 }
212 let pkg_name = match entry.file_name().to_str() {
213 Some(n) if !n.starts_with('_') && !n.starts_with('.') => n.to_string(),
214 _ => continue,
215 };
216
217 let has_toml = std::fs::read_dir(&path)
219 .map(|entries| {
220 entries
221 .flatten()
222 .any(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
223 })
224 .unwrap_or(false);
225
226 if !has_toml {
227 continue;
228 }
229
230 let (imported, skipped) =
231 card::import_from_dir_with_store(&*self.card_store, &path, &pkg_name)?;
232 if !imported.is_empty() || !skipped.is_empty() {
233 packages.push(pkg_name);
234 }
235 all_imported.extend(imported);
236 all_skipped.extend(skipped);
237 }
238
239 if all_imported.is_empty() && all_skipped.is_empty() {
240 return Err("No Card files found in any subdirectory.".into());
241 }
242
243 let mut storage_warnings: Vec<String> = Vec::new();
247 if let Err(e) = hub::register_source(&self.log_config.app_dir(), source, "card_install") {
248 storage_warnings.push(format!("hub register_source: {e}"));
249 }
250
251 let mut response = serde_json::json!({
252 "installed_cards": all_imported,
253 "skipped_cards": all_skipped,
254 "packages": packages,
255 "source": source,
256 "mode": "card_collection",
257 });
258 if !storage_warnings.is_empty() {
259 response["storage_warnings"] = serde_json::json!(storage_warnings);
260 }
261 Ok(response.to_string())
262 }
263
264 pub(crate) fn import_pkg_bundled_cards(&self, pkg_name: &str, cards_dir: &Path) -> Vec<String> {
269 match card::import_from_dir_with_store(&*self.card_store, cards_dir, pkg_name) {
270 Ok((imported, _)) => imported,
271 Err(e) => {
272 tracing::warn!("Failed to import bundled cards for '{pkg_name}': {e}");
273 Vec::new()
274 }
275 }
276 }
277
278 pub fn card_samples(
280 &self,
281 card_id: &str,
282 offset: usize,
283 limit: Option<usize>,
284 where_: Option<serde_json::Value>,
285 ) -> Result<String, String> {
286 let where_parsed = match where_ {
287 Some(v) => Some(card::parse_where(&v)?),
288 None => None,
289 };
290 let q = card::SamplesQuery {
291 offset,
292 limit,
293 where_: where_parsed,
294 };
295 let rows = self.card_store.read_samples(card_id, q)?;
296 Ok(serde_json::Value::Array(rows).to_string())
297 }
298
299 pub fn card_lineage(
301 &self,
302 card_id: &str,
303 direction: Option<&str>,
304 depth: Option<usize>,
305 include_stats: Option<bool>,
306 relation_filter: Option<Vec<String>>,
307 ) -> Result<String, String> {
308 let dir = match direction {
309 Some(s) => card::LineageDirection::parse(s)?,
310 None => card::LineageDirection::Up,
311 };
312 let q = card::LineageQuery {
313 card_id: card_id.to_string(),
314 direction: dir,
315 depth,
316 include_stats: include_stats.unwrap_or(true),
317 relation_filter,
318 };
319 match self.card_store.lineage(q)? {
320 Some(res) => Ok(card::lineage_to_json(&res).to_string()),
321 None => Err(format!("card '{card_id}' not found")),
322 }
323 }
324
325 pub fn card_sink_backfill(&self, params: SinkBackfillParams) -> Result<String, String> {
331 let report = self
332 .card_store
333 .card_sink_backfill(¶ms.sink, params.dry_run)?;
334 serde_json::to_string(&report)
335 .map_err(|e| format!("failed to serialize SinkBackfillReport: {e}"))
336 }
337
338 pub async fn card_analyze(&self, card_id: &str, pkg: Option<String>) -> Result<String, String> {
363 let card_value = match self.card_store.get(card_id)? {
365 Some(v) => v,
366 None => return Err(format!("card '{card_id}' not found")),
367 };
368
369 let samples = self
371 .card_store
372 .read_samples(card_id, card::SamplesQuery::default())?;
373
374 let mut opts = serde_json::Map::new();
375 opts.insert(
376 "card_id".into(),
377 serde_json::Value::String(card_id.to_string()),
378 );
379 opts.insert("card".into(), card_value);
380 opts.insert("samples".into(), serde_json::Value::Array(samples));
381
382 let pkg_name = pkg.as_deref().unwrap_or(DEFAULT_CARD_ANALYZE_PKG);
383 let raw = self
384 .advice(pkg_name, None, Some(serde_json::Value::Object(opts)), None)
385 .await?;
386
387 let mut envelope: serde_json::Value = serde_json::from_str(&raw)
391 .map_err(|e| format!("card_analyze: response is not valid JSON: {e}"))?;
392
393 if envelope.get("status").and_then(serde_json::Value::as_str) == Some("completed") {
394 let inner = envelope
397 .get_mut("result")
398 .ok_or_else(|| {
399 "card_analyze: completed response missing top-level 'result' field".to_string()
400 })?
401 .get_mut("result")
402 .ok_or_else(|| {
403 "card_analyze: pkg response missing 'result.result' field".to_string()
404 })?
405 .take();
406
407 let typed: CardAnalyzeResult = serde_json::from_value(inner)
408 .map_err(|e| format!("card_analyze: pkg returned invalid result shape: {e}"))?;
409
410 envelope["result"] = serde_json::to_value(&typed).map_err(|e| {
411 format!("card_analyze: failed to re-serialize CardAnalyzeResult: {e}")
412 })?;
413 }
414
415 serde_json::to_string(&envelope)
416 .map_err(|e| format!("card_analyze: failed to serialize response: {e}"))
417 }
418}