1use clap::{Args, Subcommand};
2use serde_json::json;
3
4use crate::client::BackpacClient;
5use crate::errors::CairnError;
6
7use super::{output_json, Cli};
8
9#[derive(Args, Debug)]
10pub struct IntentArgs {
11 #[command(subcommand)]
12 pub command: IntentCommands,
13}
14
15#[derive(Subcommand, Debug)]
16pub enum IntentCommands {
17 Send {
19 #[arg(long)]
21 method: String,
22
23 #[arg(long)]
25 params: Option<String>,
26
27 #[arg(long)]
29 params_file: Option<String>,
30
31 #[arg(long)]
33 poi_id: Option<String>,
34
35 #[arg(long)]
37 confidence: Option<f64>,
38
39 #[arg(long, default_value = "1")]
41 id: u64,
42
43 #[arg(long)]
45 x_backpac_hostname: Option<String>,
46 },
47
48 Status {
50 intent_id: String,
52 },
53
54 Verify {
56 intent_id: String,
58
59 #[arg(long)]
61 receiver_did: Option<String>,
62
63 #[arg(long)]
65 min_confidence: Option<f64>,
66 },
67
68 Wait {
70 intent_id: String,
72
73 #[arg(long, default_value = "2")]
75 interval: u64,
76
77 #[arg(long, default_value = "120")]
79 timeout: u64,
80 },
81
82 History {
84 intent_id: String,
86
87 #[arg(long, default_value = "50")]
89 limit: u32,
90
91 #[arg(long)]
93 status: Option<String>,
94 },
95
96 List {
98 #[arg(long)]
100 status: Option<String>,
101
102 #[arg(long)]
104 since: Option<String>,
105
106 #[arg(long, default_value = "20")]
108 limit: u32,
109 },
110}
111
112impl IntentArgs {
113 pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
114 let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref());
115
116 match &self.command {
117 IntentCommands::Send {
118 method,
119 params,
120 params_file,
121 poi_id,
122 confidence,
123 id,
124 x_backpac_hostname,
125 } => {
126 let params_str = match (params, params_file) {
128 (Some(p), _) => p.clone(),
129 (None, Some(f)) => {
130 std::fs::read_to_string(f)
131 .map_err(|e| CairnError::InvalidInput(format!("Cannot read params file: {}", e)))?
132 .trim()
133 .to_string()
134 }
135 (None, None) => {
136 return Err(CairnError::InvalidInput(
137 "Either --params or --params-file is required".to_string(),
138 ));
139 }
140 };
141
142 let params_val: serde_json::Value = serde_json::from_str(¶ms_str)
143 .map_err(|e| CairnError::InvalidInput(format!("Invalid JSON params: {}", e)))?;
144
145 let body = json!({
146 "jsonrpc": "2.0",
147 "method": method,
148 "params": params_val,
149 "id": id,
150 });
151
152 let result = client
153 .rpc_post(&body, poi_id.as_deref(), *confidence, x_backpac_hostname.as_deref())
154 .await?;
155
156 output_json(&result, &cli.output);
157 Ok(())
158 }
159
160 IntentCommands::Status { intent_id } => {
161 let path = format!("/v1/intents/{}", intent_id);
162 let result = client.get(&path).await?;
163 output_json(&result, &cli.output);
164 Ok(())
165 }
166
167 IntentCommands::Verify {
168 intent_id,
169 receiver_did,
170 min_confidence,
171 } => {
172 let mut path = format!("/v1/intents/{}/verify", intent_id);
173 let mut query_parts: Vec<String> = Vec::new();
174
175 if let Some(did) = receiver_did {
176 query_parts.push(format!("receiver_did={}", did));
177 }
178 if let Some(conf) = min_confidence {
179 query_parts.push(format!("min_confidence={}", conf));
180 }
181 if !query_parts.is_empty() {
182 path = format!("{}?{}", path, query_parts.join("&"));
183 }
184
185 let result = client.get(&path).await?;
186 output_json(&result, &cli.output);
187 Ok(())
188 }
189
190 IntentCommands::Wait {
191 intent_id,
192 interval,
193 timeout,
194 } => {
195 let start = std::time::Instant::now();
196 let max_duration = std::time::Duration::from_secs(*timeout);
197 let poll_interval = std::time::Duration::from_secs(*interval);
198 let terminal_states = ["FINALIZED", "ABORTED", "EXPIRED"];
199
200 loop {
201 if start.elapsed() > max_duration {
202 return Err(CairnError::Timeout);
203 }
204
205 let path = format!("/v1/intents/{}", intent_id);
206 let result = client.get(&path).await?;
207
208 let status = result
209 .get("status")
210 .and_then(|v| v.as_str())
211 .unwrap_or("UNKNOWN");
212
213 if terminal_states.contains(&status) {
214 output_json(&result, &cli.output);
215 return Ok(());
216 }
217
218 tokio::time::sleep(poll_interval).await;
219 }
220 }
221
222 IntentCommands::History { intent_id, limit, status } => {
223 let mut path = format!("/v1/intents/{}/history?limit={}", intent_id, limit);
224 if let Some(s) = status {
225 path = format!("{}&status={}", path, s);
226 }
227 let result = client.get(&path).await?;
228 output_json(&result, &cli.output);
229 Ok(())
230 }
231
232 IntentCommands::List {
233 status,
234 since,
235 limit,
236 } => {
237 let mut query_parts: Vec<String> = Vec::new();
238
239 if let Some(s) = status {
240 query_parts.push(format!("status={}", s));
241 }
242 if let Some(s) = since {
243 query_parts.push(format!("since={}", s));
244 }
245 query_parts.push(format!("limit={}", limit));
246
247 let path = format!("/v1/intents?{}", query_parts.join("&"));
248 let result = client.get(&path).await?;
249 output_json(&result, &cli.output);
250 Ok(())
251 }
252 }
253 }
254}