1use std::error::Error;
6use std::path::{Path, PathBuf};
7
8use chrono::{DateTime, Local};
9use icalendar::{Calendar, CalendarComponent, Component};
10use tokio::fs;
11use uuid::Uuid;
12
13use crate::event::ParsedEventConditions;
14use crate::localdb::LocalDb;
15use crate::short_id::ShortIds;
16use crate::todo::{ParsedTodoConditions, ParsedTodoSort, TodoPatch};
17use crate::{Config, Event, EventConditions, Id, Pager, Todo, TodoConditions, TodoDraft, TodoSort};
18
19#[derive(Debug, Clone)]
21pub struct Aim {
22 now: DateTime<Local>,
23 config: Config,
24 db: LocalDb,
25 short_ids: ShortIds,
26 calendar_path: PathBuf,
27}
28
29impl Aim {
30 pub async fn new(mut config: Config) -> Result<Self, Box<dyn Error>> {
32 let now = Local::now();
33
34 config.normalize()?;
35 prepare(&config).await?;
36
37 let db = LocalDb::open(&config.state_dir)
38 .await
39 .map_err(|e| format!("Failed to initialize db: {e}"))?;
40
41 let short_ids = ShortIds::new(db.clone());
42 let calendar_path = config.calendar_path.clone();
43 let that = Self {
44 now,
45 config,
46 db,
47 short_ids,
48 calendar_path,
49 };
50 that.add_calendar(&that.calendar_path)
51 .await
52 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
53
54 Ok(that)
55 }
56
57 pub fn now(&self) -> DateTime<Local> {
59 self.now
60 }
61
62 pub fn refresh_now(&mut self) {
64 self.now = Local::now();
65 }
66
67 pub async fn list_events(
69 &self,
70 conds: &EventConditions,
71 pager: &Pager,
72 ) -> Result<Vec<impl Event>, Box<dyn Error>> {
73 let conds = ParsedEventConditions::parse(&self.now, conds);
74 let events = self.db.events.list(&conds, pager).await?;
75 let events = self.short_ids.events(events).await?;
76 Ok(events)
77 }
78
79 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
81 let conds = ParsedEventConditions::parse(&self.now, conds);
82 self.db.events.count(&conds).await
83 }
84
85 pub fn default_todo_draft(&self) -> TodoDraft {
87 TodoDraft::default(&self.config, self.now)
88 }
89
90 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo, Box<dyn Error>> {
92 let uid = self.generate_uid().await?;
93 let todo = draft.into_todo(&self.config, self.now, &uid);
94 let path = self.get_path(&uid);
95
96 let calendar = Calendar::new().push(todo.clone()).done();
97 fs::write(&path, calendar.to_string())
98 .await
99 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
100
101 self.db.upsert_todo(&path, &todo).await?;
102
103 let todo = self.short_ids.todo(todo).await?;
104 Ok(todo)
105 }
106
107 pub async fn update_todo(
109 &self,
110 id: &Id,
111 patch: TodoPatch,
112 ) -> Result<impl Todo, Box<dyn Error>> {
113 let uid = self.short_ids.get_uid(id).await?;
114 let todo = match self.db.todos.get(&uid).await? {
115 Some(todo) => todo,
116 None => return Err("Todo not found".into()),
117 };
118
119 let path: PathBuf = todo.path().into();
120 let mut calendar = parse_ics(&path).await?;
121 let t = calendar
122 .components
123 .iter_mut()
124 .filter_map(|a| match a {
125 CalendarComponent::Todo(a) => Some(a),
126 _ => None,
127 })
128 .find(|a| a.get_uid() == Some(todo.uid()))
129 .ok_or("Todo not found in calendar")?;
130
131 patch.apply_to(t);
132 let todo = t.clone();
133 fs::write(&path, calendar.done().to_string())
134 .await
135 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
136
137 self.db.upsert_todo(&path, &todo).await?;
138
139 let todo = self.short_ids.todo(todo).await?;
140 Ok(todo)
141 }
142
143 pub async fn get_todo(&self, id: &Id) -> Result<Option<impl Todo>, Box<dyn Error>> {
145 let uid = self.short_ids.get_uid(id).await?;
146 match self.db.todos.get(&uid).await {
147 Ok(Some(todo)) => {
148 let todo = self.short_ids.todo(todo).await?;
149 Ok(Some(todo))
150 }
151 Ok(None) => Ok(None),
152 Err(e) => Err(e.into()),
153 }
154 }
155
156 pub async fn list_todos(
158 &self,
159 conds: &TodoConditions,
160 sort: &[TodoSort],
161 pager: &Pager,
162 ) -> Result<Vec<impl Todo>, Box<dyn Error>> {
163 let conds = ParsedTodoConditions::parse(&self.now, conds);
164 let sort = ParsedTodoSort::parse_vec(&self.config, sort);
165 let todos = self.db.todos.list(&conds, &sort, pager).await?;
166 let todos = self.short_ids.todos(todos).await?;
167 Ok(todos)
168 }
169
170 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
172 let conds = ParsedTodoConditions::parse(&self.now, conds);
173 self.db.todos.count(&conds).await
174 }
175
176 pub async fn close(self) -> Result<(), Box<dyn Error>> {
178 self.db.close().await?;
179 Ok(())
180 }
181
182 async fn add_calendar(&self, calendar_path: &PathBuf) -> Result<(), Box<dyn Error>> {
183 let mut reader = fs::read_dir(calendar_path)
184 .await
185 .map_err(|e| format!("Failed to read directory: {e}"))?;
186
187 let mut handles = vec![];
188 let mut count_ics = 0;
189
190 while let Some(entry) = reader.next_entry().await? {
191 let path = entry.path();
192 match path.extension() {
193 Some(ext) if ext == "ics" => {
194 count_ics += 1;
195 let that = self.clone();
196 handles.push(tokio::spawn(async move {
197 if let Err(e) = that.add_ics(&path).await {
198 log::error!("Failed to process file {}: {}", path.display(), e);
199 }
200 }));
201 }
202 _ => {}
203 }
204 }
205
206 for handle in handles {
207 handle.await?;
208 }
209
210 log::debug!("Total .ics files processed: {count_ics}");
211 Ok(())
212 }
213
214 async fn add_ics(self, path: &Path) -> Result<(), Box<dyn Error>> {
215 log::debug!("Parsing file: {}", path.display());
216 let calendar = parse_ics(path).await?;
217 log::debug!(
218 "Found {} components in {}.",
219 calendar.components.len(),
220 path.display()
221 );
222
223 for component in calendar.components {
224 log::debug!("Processing component: {component:?}");
225 match component {
226 CalendarComponent::Event(event) => self.db.upsert_event(path, &event).await?,
227 CalendarComponent::Todo(todo) => self.db.upsert_todo(path, &todo).await?,
228 _ => log::warn!("Ignoring unsupported component type: {component:?}"),
229 }
230 }
231
232 Ok(())
233 }
234
235 async fn generate_uid(&self) -> Result<String, Box<dyn Error>> {
236 for _ in 0..16 {
237 let uid = Uuid::new_v4().to_string(); if self.db.todos.get(&uid).await?.is_some()
239 || fs::try_exists(&self.get_path(&uid)).await?
240 {
241 continue;
242 }
243 return Ok(uid);
244 }
245
246 Err("Failed to generate a unique UID after multiple attempts".into())
247 }
248
249 fn get_path(&self, uid: &str) -> PathBuf {
250 self.calendar_path.join(format!("{uid}.ics"))
251 }
252}
253
254async fn prepare(config: &Config) -> Result<(), Box<dyn Error>> {
255 if let Some(parent) = &config.state_dir {
256 log::info!("Ensuring state directory exists: {}", parent.display());
257 fs::create_dir_all(parent).await?;
258 }
259 Ok(())
260}
261
262async fn parse_ics(path: &Path) -> Result<Calendar, Box<dyn Error>> {
263 fs::read_to_string(path)
264 .await
265 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
266 .parse()
267 .map_err(|e| format!("Failed to parse calendar: {e}").into())
268}