1use anyhow::{Result, anyhow};
2
3use crate::ramadan_config::{
4 StoredLocation, clear_ramadan_config, get_stored_first_roza_date, get_stored_location,
5 get_stored_prayer_settings, set_stored_location, set_stored_method, set_stored_school,
6 set_stored_timezone,
7};
8
9#[derive(Debug, Clone, Default)]
10pub struct ConfigCommandOptions {
11 pub city: Option<String>,
12 pub country: Option<String>,
13 pub latitude: Option<String>,
14 pub longitude: Option<String>,
15 pub method: Option<String>,
16 pub school: Option<String>,
17 pub timezone: Option<String>,
18 pub show: bool,
19 pub clear: bool,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct ParsedConfigUpdates {
24 pub city: Option<String>,
25 pub country: Option<String>,
26 pub latitude: Option<f64>,
27 pub longitude: Option<f64>,
28 pub method: Option<i64>,
29 pub school: Option<i64>,
30 pub timezone: Option<String>,
31}
32
33fn parse_optional_i64_in_range(
34 value: Option<&str>,
35 min: i64,
36 max: i64,
37 label: &str,
38) -> Result<Option<i64>> {
39 let Some(raw) = value else {
40 return Ok(None);
41 };
42
43 let parsed = raw
44 .parse::<i64>()
45 .map_err(|_| anyhow!("Invalid {label}."))?;
46 if parsed < min || parsed > max {
47 return Err(anyhow!("Invalid {label}."));
48 }
49
50 Ok(Some(parsed))
51}
52
53fn parse_optional_f64_in_range(
54 value: Option<&str>,
55 min: f64,
56 max: f64,
57 label: &str,
58) -> Result<Option<f64>> {
59 let Some(raw) = value else {
60 return Ok(None);
61 };
62
63 let parsed = raw
64 .parse::<f64>()
65 .map_err(|_| anyhow!("Invalid {label}."))?;
66 if parsed < min || parsed > max {
67 return Err(anyhow!("Invalid {label}."));
68 }
69
70 Ok(Some(parsed))
71}
72
73pub fn parse_config_updates(options: &ConfigCommandOptions) -> Result<ParsedConfigUpdates> {
74 Ok(ParsedConfigUpdates {
75 city: options
76 .city
77 .as_ref()
78 .map(|v| v.trim().to_string())
79 .filter(|v| !v.is_empty()),
80 country: options
81 .country
82 .as_ref()
83 .map(|v| v.trim().to_string())
84 .filter(|v| !v.is_empty()),
85 latitude: parse_optional_f64_in_range(
86 options.latitude.as_deref(),
87 -90.0,
88 90.0,
89 "latitude",
90 )?,
91 longitude: parse_optional_f64_in_range(
92 options.longitude.as_deref(),
93 -180.0,
94 180.0,
95 "longitude",
96 )?,
97 method: parse_optional_i64_in_range(options.method.as_deref(), 0, 23, "method")?,
98 school: parse_optional_i64_in_range(options.school.as_deref(), 0, 1, "school")?,
99 timezone: options
100 .timezone
101 .as_ref()
102 .map(|v| v.trim().to_string())
103 .filter(|v| !v.is_empty()),
104 })
105}
106
107pub fn merge_location_updates(
108 current: &StoredLocation,
109 updates: &ParsedConfigUpdates,
110) -> StoredLocation {
111 StoredLocation {
112 city: updates.city.clone().or_else(|| current.city.clone()),
113 country: updates.country.clone().or_else(|| current.country.clone()),
114 latitude: updates.latitude.or(current.latitude),
115 longitude: updates.longitude.or(current.longitude),
116 }
117}
118
119fn print_current_config() {
120 let location = get_stored_location();
121 let settings = get_stored_prayer_settings();
122 let first_roza_date = get_stored_first_roza_date();
123
124 println!("Current configuration:");
125 if let Some(city) = location.city {
126 println!(" City: {city}");
127 }
128 if let Some(country) = location.country {
129 println!(" Country: {country}");
130 }
131 if let Some(latitude) = location.latitude {
132 println!(" Latitude: {latitude}");
133 }
134 if let Some(longitude) = location.longitude {
135 println!(" Longitude: {longitude}");
136 }
137 println!(" Method: {}", settings.method);
138 println!(" School: {}", settings.school);
139 if let Some(timezone) = settings.timezone {
140 println!(" Timezone: {timezone}");
141 }
142 if let Some(date) = first_roza_date {
143 println!(" First Roza Date: {date}");
144 }
145}
146
147fn has_config_update_flags(options: &ConfigCommandOptions) -> bool {
148 options.city.is_some()
149 || options.country.is_some()
150 || options.latitude.is_some()
151 || options.longitude.is_some()
152 || options.method.is_some()
153 || options.school.is_some()
154 || options.timezone.is_some()
155}
156
157pub fn config_command(options: &ConfigCommandOptions) -> Result<()> {
158 if options.clear {
159 clear_ramadan_config()?;
160 println!("Configuration cleared.");
161 return Ok(());
162 }
163
164 if options.show {
165 print_current_config();
166 return Ok(());
167 }
168
169 if !has_config_update_flags(options) {
170 println!("No config updates provided. Use `ramadan-cli config --show` to inspect.");
171 return Ok(());
172 }
173
174 let updates = parse_config_updates(options)?;
175 let current_location = get_stored_location();
176 let next_location = merge_location_updates(¤t_location, &updates);
177
178 set_stored_location(&next_location)?;
179
180 if let Some(method) = updates.method {
181 set_stored_method(method)?;
182 }
183 if let Some(school) = updates.school {
184 set_stored_school(school)?;
185 }
186 if let Some(timezone) = updates.timezone {
187 set_stored_timezone(Some(&timezone))?;
188 }
189
190 println!("Configuration updated.");
191 Ok(())
192}
193
194#[cfg(test)]
195mod tests {
196 use super::{ConfigCommandOptions, merge_location_updates, parse_config_updates};
197
198 #[test]
199 fn parse_config_updates_validates_ranges() {
200 let parsed = parse_config_updates(&ConfigCommandOptions {
201 method: Some("2".to_string()),
202 school: Some("1".to_string()),
203 latitude: Some("37.7749".to_string()),
204 longitude: Some("-122.4194".to_string()),
205 ..ConfigCommandOptions::default()
206 })
207 .expect("valid updates");
208
209 assert_eq!(parsed.method, Some(2));
210 assert_eq!(parsed.school, Some(1));
211 assert_eq!(parsed.latitude, Some(37.7749));
212 assert_eq!(parsed.longitude, Some(-122.4194));
213 }
214
215 #[test]
216 fn merge_location_updates_only_overrides_provided_fields() {
217 let current = crate::ramadan_config::StoredLocation {
218 city: Some("Lahore".to_string()),
219 country: Some("Pakistan".to_string()),
220 latitude: Some(31.5204),
221 longitude: Some(74.3587),
222 };
223
224 let merged = merge_location_updates(
225 ¤t,
226 &super::ParsedConfigUpdates {
227 city: Some("San Francisco".to_string()),
228 ..super::ParsedConfigUpdates::default()
229 },
230 );
231
232 assert_eq!(merged.city.as_deref(), Some("San Francisco"));
233 assert_eq!(merged.country.as_deref(), Some("Pakistan"));
234 assert_eq!(merged.latitude, Some(31.5204));
235 assert_eq!(merged.longitude, Some(74.3587));
236 }
237}