1use clap::{Args, Subcommand};
2use serde_json::json;
3
4use crate::client::BackpacClient;
5use crate::config::CairnConfig;
6use crate::errors::CairnError;
7
8use super::{output_json, Cli};
9
10#[derive(Args, Debug)]
11pub struct PoiArgs {
12 #[command(subcommand)]
13 pub command: PoiCommands,
14}
15
16#[derive(Subcommand, Debug)]
17pub enum PoiCommands {
18 Create {
20 #[arg(long)]
22 chain: Option<String>,
23
24 #[arg(long)]
26 network: Option<String>,
27
28 #[arg(long)]
30 parent: Option<String>,
31
32 #[arg(long, default_value = "5")]
34 max_depth: u32,
35
36 #[arg(long, default_value = "600")]
38 ttl: u64,
39
40 #[arg(long)]
42 metadata: Option<String>,
43
44 #[arg(long)]
46 key_file: Option<String>,
47 },
48
49 Get {
51 poi_id: String,
53 },
54
55 List {
57 #[arg(long)]
59 status: Option<String>,
60
61 #[arg(long)]
63 since: Option<String>,
64
65 #[arg(long, default_value = "20")]
67 limit: u32,
68 },
69}
70
71impl PoiArgs {
72 pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
73 let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref(), cli.worker_url.as_deref());
74 let config = CairnConfig::load();
75
76 match &self.command {
77 PoiCommands::Create {
78 chain: _,
79 network,
80 parent,
81 max_depth,
82 ttl,
83 metadata,
84 key_file,
85 } => {
86 let _n = network.as_deref().or(cli.network.as_deref()).unwrap_or(&config.network);
87
88 let claims = client.claims().ok_or_else(|| {
90 CairnError::Auth("Not logged in. Run 'cairn auth connect' first.".to_string())
91 })?;
92
93 let agent_id = claims["sub"].as_str().ok_or_else(|| CairnError::Auth("Missing 'sub' in JWT".to_string()))?;
94 let account_id = claims["account_id"].as_str().ok_or_else(|| CairnError::Auth("Missing 'account_id' in JWT".to_string()))?;
95 let claim_chain_raw = claims["chain"].as_str().ok_or_else(|| CairnError::Auth("Missing 'chain' in JWT".to_string()))?;
96
97 let (auth_chain, auth_network) = if let Some(pos) = claim_chain_raw.find(':') {
99 (&claim_chain_raw[..pos], Some(&claim_chain_raw[pos+1..]))
100 } else {
101 (claim_chain_raw, None)
102 };
103
104 let final_chain = auth_chain;
105 let final_network = network.as_deref()
106 .or(auth_network)
107 .or(cli.network.as_deref())
108 .unwrap_or(&config.network);
109
110 let env_key = std::env::var("CAIRN_KEY_FILE").ok();
112 let key_path = key_file
113 .as_deref()
114 .or(config.key_file.as_deref())
115 .or(env_key.as_deref())
116 .ok_or_else(|| CairnError::InvalidInput("Missing --key-file. Required for PoI signing.".to_string()))?;
117
118 let key_bytes = crate::crypto::read_key_file(key_path)?;
119 let secret_bytes = if key_bytes.len() >= 64 { &key_bytes[0..32] } else { &key_bytes };
121
122 let expires_at = chrono::Utc::now() + chrono::Duration::seconds(*ttl as i64);
124
125 let poi_data = json!({
126 "account_id": account_id,
127 "agent_id": agent_id,
128 "chain": final_chain,
129 "network": final_network,
130 "metadata": metadata.as_ref().and_then(|m| serde_json::from_str(m).ok()).unwrap_or(json!({})),
131 "parent_poi_id": parent.as_deref().map(|p| json!(p)).unwrap_or(serde_json::Value::Null),
132 "chain_depth": 0, "max_chain_depth": max_depth,
134 "expires_at": expires_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
135 });
136
137 let sorted_payload: std::collections::BTreeMap<String, serde_json::Value> =
141 serde_json::from_value(poi_data.clone()).unwrap();
142
143 let message = serde_json::to_string(&sorted_payload).unwrap();
144 let signature_bytes = crate::crypto::ed25519_sign(message.as_bytes(), secret_bytes)?;
145 let signature_hex = hex::encode(signature_bytes);
146
147 let mut body = poi_data;
149 body["poi_signature"] = json!(signature_hex);
150
151 let result = client.post("/v1/pois", &body).await?;
152 output_json(&result, &cli.output);
153 Ok(())
154 }
155
156 PoiCommands::Get { poi_id } => {
157 let path = format!("/v1/pois/{}", poi_id);
158 let result = client.get(&path).await?;
159 output_json(&result, &cli.output);
160 Ok(())
161 }
162
163 PoiCommands::List { status, since, limit } => {
164 let mut query_parts: Vec<String> = Vec::new();
165 if let Some(s) = status {
166 query_parts.push(format!("status={}", s));
167 }
168 if let Some(s) = since {
169 query_parts.push(format!("since={}", s));
170 }
171 query_parts.push(format!("limit={}", limit));
172
173 let path = format!("/v1/pois?{}", query_parts.join("&"));
174 let result = client.get(&path).await?;
175 output_json(&result, &cli.output);
176 Ok(())
177 }
178 }
179 }
180}