1use chrono::{DateTime, Utc};
4use ngdp_cache::generic::GenericCache;
5use serde::{Deserialize, Serialize};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8const WAGO_API_BASE: &str = "https://wago.tools/api";
10
11const WAGO_CACHE_TTL_SECS: u64 = 30 * 60;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct WagoBuild {
17 pub product: String,
19
20 pub version: String,
22
23 pub created_at: String,
25
26 pub build_config: String,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub product_config: Option<String>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub cdn_config: Option<String>,
36
37 pub is_bgdl: bool,
39}
40
41#[derive(Debug, Serialize, Deserialize)]
43#[serde(untagged)]
44pub enum WagoBuildsResponse {
45 Map(std::collections::HashMap<String, Vec<WagoBuild>>),
47 Array(Vec<WagoBuild>),
49}
50
51async fn fetch_builds_uncached() -> Result<WagoBuildsResponse, Box<dyn std::error::Error>> {
53 let client = reqwest::Client::new();
54 let url = format!("{WAGO_API_BASE}/builds");
55
56 let response = client
57 .get(&url)
58 .header("User-Agent", "ngdp-client")
59 .send()
60 .await?;
61
62 if !response.status().is_success() {
63 return Err(format!("Wago API returned status: {}", response.status()).into());
64 }
65
66 let builds = response.json::<WagoBuildsResponse>().await?;
67 Ok(builds)
68}
69
70pub async fn fetch_builds() -> Result<WagoBuildsResponse, Box<dyn std::error::Error>> {
72 let cache_enabled = crate::cached_client::is_caching_enabled();
74
75 if !cache_enabled {
76 return fetch_builds_uncached().await;
77 }
78
79 let cache = match GenericCache::with_subdirectory("wago").await {
81 Ok(cache) => cache,
82 Err(_) => {
83 return fetch_builds_uncached().await;
85 }
86 };
87
88 let cache_key = "builds.json";
89 let meta_key = "builds.meta";
90
91 let cache_path = cache.get_path(cache_key);
93 let meta_path = cache.get_path(meta_key);
94
95 if let Ok(metadata_content) = tokio::fs::read_to_string(&meta_path).await {
97 if let Ok(timestamp) = metadata_content.trim().parse::<u64>() {
98 let now = SystemTime::now()
99 .duration_since(UNIX_EPOCH)
100 .unwrap_or_default()
101 .as_secs();
102
103 if now < timestamp + WAGO_CACHE_TTL_SECS {
104 if let Ok(cached_data) = tokio::fs::read(&cache_path).await {
106 if let Ok(builds) = serde_json::from_slice(&cached_data) {
107 tracing::debug!("Using cached Wago builds data");
108 return Ok(builds);
109 }
110 }
111 }
112 }
113 }
114
115 tracing::debug!("Fetching fresh Wago builds data");
117 let builds = fetch_builds_uncached().await?;
118
119 if let Ok(json_data) = serde_json::to_vec(&builds) {
121 let _ = cache.write(cache_key, &json_data).await;
123
124 let timestamp = SystemTime::now()
126 .duration_since(UNIX_EPOCH)
127 .unwrap_or_default()
128 .as_secs();
129 let _ = cache
130 .write(meta_key, timestamp.to_string().as_bytes())
131 .await;
132 }
133
134 Ok(builds)
135}
136
137pub fn filter_builds_by_product(builds: WagoBuildsResponse, product: &str) -> Vec<WagoBuild> {
139 match builds {
140 WagoBuildsResponse::Map(map) => map.get(product).cloned().unwrap_or_default(),
141 WagoBuildsResponse::Array(builds) => builds
142 .into_iter()
143 .filter(|b| b.product == product)
144 .collect(),
145 }
146}
147
148pub fn extract_build_id(version: &str) -> Option<String> {
150 version.split('.').next_back().map(|s| s.to_string())
151}
152
153pub fn find_build_by_id<'a>(builds: &'a [WagoBuild], build_id: &str) -> Option<&'a WagoBuild> {
155 builds.iter().find(|build| {
156 if let Some(extracted_id) = extract_build_id(&build.version) {
158 extracted_id == build_id
159 } else {
160 build.version == build_id
162 }
163 })
164}
165
166pub fn parse_wago_date(date_str: &str) -> Option<DateTime<Utc>> {
168 DateTime::parse_from_str(&format!("{date_str} +00:00"), "%Y-%m-%d %H:%M:%S %z")
170 .ok()
171 .map(|dt| dt.with_timezone(&Utc))
172}