use std::fs;
use std::path::PathBuf;
use anyhow::{Result, anyhow};
use directories::BaseDirs;
use serde::{Deserialize, Serialize};
use crate::geo::GeoLocation;
use crate::recommendations::{get_recommended_method, get_recommended_school};
const DEFAULT_METHOD: i64 = 2;
const DEFAULT_SCHOOL: i64 = 0;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct RamadanConfigStore {
latitude: Option<f64>,
longitude: Option<f64>,
city: Option<String>,
country: Option<String>,
method: Option<i64>,
school: Option<i64>,
timezone: Option<String>,
first_roza_date: Option<String>,
format24h: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct StoredLocation {
pub city: Option<String>,
pub country: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct StoredPrayerSettings {
pub method: i64,
pub school: i64,
pub timezone: Option<String>,
}
fn config_base_dir() -> PathBuf {
if let Ok(path) = std::env::var("RAMADAN_CLI_CONFIG_DIR") {
return PathBuf::from(path);
}
let is_test = std::env::var("VITEST").as_deref() == Ok("true")
|| std::env::var("NODE_ENV").as_deref() == Ok("test");
if is_test {
return PathBuf::from("/tmp");
}
if let Some(base) = BaseDirs::new() {
return base.config_dir().to_path_buf();
}
PathBuf::from(".")
}
fn config_dir() -> PathBuf {
config_base_dir().join("ramadan-cli")
}
fn config_file_path() -> PathBuf {
config_dir().join("config.json")
}
fn default_store() -> RamadanConfigStore {
RamadanConfigStore {
method: Some(DEFAULT_METHOD),
school: Some(DEFAULT_SCHOOL),
format24h: Some(false),
..RamadanConfigStore::default()
}
}
fn load_store() -> RamadanConfigStore {
let path = config_file_path();
let raw = fs::read_to_string(path);
match raw {
Ok(contents) => serde_json::from_str::<RamadanConfigStore>(&contents)
.unwrap_or_else(|_| default_store()),
Err(_) => default_store(),
}
}
fn save_store(store: &RamadanConfigStore) -> Result<()> {
let dir = config_dir();
fs::create_dir_all(&dir)?;
let json = serde_json::to_string_pretty(store)?;
fs::write(config_file_path(), json)?;
Ok(())
}
pub fn should_apply_recommended_method(current_method: i64, recommended_method: i64) -> bool {
current_method == DEFAULT_METHOD || current_method == recommended_method
}
pub fn should_apply_recommended_school(current_school: i64, recommended_school: i64) -> bool {
current_school == DEFAULT_SCHOOL || current_school == recommended_school
}
pub fn get_stored_location() -> StoredLocation {
let store = load_store();
StoredLocation {
city: store.city,
country: store.country,
latitude: store.latitude,
longitude: store.longitude,
}
}
pub fn has_stored_location() -> bool {
let location = get_stored_location();
let has_city_country = location.city.is_some() && location.country.is_some();
let has_coords = location.latitude.is_some() && location.longitude.is_some();
has_city_country || has_coords
}
pub fn get_stored_prayer_settings() -> StoredPrayerSettings {
let store = load_store();
StoredPrayerSettings {
method: store.method.unwrap_or(DEFAULT_METHOD),
school: store.school.unwrap_or(DEFAULT_SCHOOL),
timezone: store.timezone,
}
}
pub fn set_stored_location(location: &StoredLocation) -> Result<()> {
let mut store = load_store();
if let Some(city) = &location.city {
store.city = Some(city.clone());
}
if let Some(country) = &location.country {
store.country = Some(country.clone());
}
if let Some(latitude) = location.latitude {
store.latitude = Some(latitude);
}
if let Some(longitude) = location.longitude {
store.longitude = Some(longitude);
}
save_store(&store)
}
pub fn set_stored_timezone(timezone: Option<&str>) -> Result<()> {
let mut store = load_store();
if let Some(timezone) = timezone {
if !timezone.trim().is_empty() {
store.timezone = Some(timezone.trim().to_string());
}
}
save_store(&store)
}
pub fn set_stored_method(method: i64) -> Result<()> {
let mut store = load_store();
store.method = Some(method);
save_store(&store)
}
pub fn set_stored_school(school: i64) -> Result<()> {
let mut store = load_store();
store.school = Some(school);
save_store(&store)
}
pub fn get_stored_first_roza_date() -> Option<String> {
let store = load_store();
store.first_roza_date
}
fn is_valid_iso_date(value: &str) -> bool {
let parts: Vec<&str> = value.split('-').collect();
if parts.len() != 3 {
return false;
}
let year = parts[0].parse::<i32>().ok();
let month = parts[1].parse::<u32>().ok();
let day = parts[2].parse::<u32>().ok();
match (year, month, day) {
(Some(y), Some(m), Some(d)) => chrono::NaiveDate::from_ymd_opt(y, m, d).is_some(),
_ => false,
}
}
pub fn set_stored_first_roza_date(first_roza_date: &str) -> Result<()> {
if !is_valid_iso_date(first_roza_date) {
return Err(anyhow!("Invalid first roza date. Use YYYY-MM-DD."));
}
let mut store = load_store();
store.first_roza_date = Some(first_roza_date.to_string());
save_store(&store)
}
pub fn clear_stored_first_roza_date() -> Result<()> {
let mut store = load_store();
store.first_roza_date = None;
save_store(&store)
}
pub fn clear_ramadan_config() -> Result<()> {
let config_path = config_file_path();
if config_path.exists() {
fs::remove_file(config_path)?;
}
let legacy = config_base_dir().join("azaan").join("config.json");
if legacy.exists() {
let _ = fs::remove_file(legacy);
}
Ok(())
}
fn maybe_set_recommended_method(store: &mut RamadanConfigStore, country: &str) {
let Some(recommended_method) = get_recommended_method(country) else {
return;
};
let current_method = store.method.unwrap_or(DEFAULT_METHOD);
if !should_apply_recommended_method(current_method, recommended_method) {
return;
}
store.method = Some(recommended_method);
}
fn maybe_set_recommended_school(store: &mut RamadanConfigStore, country: &str) {
let recommended_school = get_recommended_school(country);
let current_school = store.school.unwrap_or(DEFAULT_SCHOOL);
if !should_apply_recommended_school(current_school, recommended_school) {
return;
}
store.school = Some(recommended_school);
}
pub fn save_auto_detected_setup(location: &GeoLocation) -> Result<()> {
let mut store = load_store();
store.city = Some(location.city.clone());
store.country = Some(location.country.clone());
store.latitude = Some(location.latitude);
store.longitude = Some(location.longitude);
if !location.timezone.trim().is_empty() {
store.timezone = Some(location.timezone.clone());
}
maybe_set_recommended_method(&mut store, &location.country);
maybe_set_recommended_school(&mut store, &location.country);
save_store(&store)
}
pub fn apply_recommended_settings_if_unset(country: &str) -> Result<()> {
let mut store = load_store();
maybe_set_recommended_method(&mut store, country);
maybe_set_recommended_school(&mut store, country);
save_store(&store)
}
#[cfg(test)]
mod tests {
use super::{should_apply_recommended_method, should_apply_recommended_school};
#[test]
fn recommendation_guards_match_expected() {
assert!(should_apply_recommended_method(2, 1));
assert!(!should_apply_recommended_method(3, 1));
assert!(should_apply_recommended_school(0, 1));
assert!(!should_apply_recommended_school(1, 0));
}
}