1use std::error::Error;
6use std::path::PathBuf;
7
8use chrono::{DateTime, Local};
9use icalendar::{Calendar, CalendarComponent, Component};
10use tokio::fs;
11use uuid::Uuid;
12
13use crate::io::{add_calendar, parse_ics};
14use crate::localdb::LocalDb;
15use crate::short_id::ShortIds;
16use crate::{
17 Config, Event, EventConditions, EventDraft, EventPatch, Id, Kind, Pager, Todo, TodoConditions,
18 TodoDraft, TodoPatch, TodoSort,
19};
20
21#[derive(Debug, Clone)]
23pub struct Aim {
24 now: DateTime<Local>,
25 config: Config,
26 db: LocalDb,
27 short_ids: ShortIds,
28 calendar_path: PathBuf,
29}
30
31impl Aim {
32 pub async fn new(mut config: Config) -> Result<Self, Box<dyn Error>> {
37 let now = Local::now();
38
39 config.normalize()?;
40 prepare(&config).await?;
41
42 let db = initialize_db(&config).await?;
43 let short_ids = ShortIds::new(db.clone());
44 let calendar_path = config.calendar_path.clone();
45 add_calendar(&db, &calendar_path)
46 .await
47 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
48
49 Ok(Self {
50 now,
51 config,
52 db,
53 short_ids,
54 calendar_path,
55 })
56 }
57
58 #[must_use]
60 pub fn now(&self) -> DateTime<Local> {
61 self.now
62 }
63
64 pub fn refresh_now(&mut self) {
66 self.now = Local::now();
67 }
68
69 #[must_use]
71 pub fn default_event_draft(&self) -> EventDraft {
72 EventDraft::default(&self.now)
73 }
74
75 pub async fn get_event(&self, id: &Id) -> Result<impl Event + 'static, Box<dyn Error>> {
80 let uid = self.short_ids.get_uid(id).await?;
81 match self.db.events.get(&uid).await {
82 Ok(Some(event)) => Ok(self.short_ids.event(event).await?),
83 Ok(None) => Err("Event not found".into()),
84 Err(e) => Err(e.into()),
85 }
86 }
87
88 pub async fn new_event(
93 &self,
94 draft: EventDraft,
95 ) -> Result<impl Event + 'static, Box<dyn Error>> {
96 let uid = self.generate_uid(Kind::Event).await?;
97 let event = draft.resolve(&self.now).into_ics(&uid);
98 let path = self.get_path(&uid);
99
100 let calendar = Calendar::new().push(event.clone()).done();
101 fs::write(&path, calendar.to_string())
102 .await
103 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
104
105 self.db.upsert_event(&path, &event).await?;
106
107 let todo = self.short_ids.event(event).await?;
108 Ok(todo)
109 }
110
111 pub async fn update_event(
116 &self,
117 id: &Id,
118 patch: EventPatch,
119 ) -> Result<impl Event + 'static, Box<dyn Error>> {
120 let uid = self.short_ids.get_uid(id).await?;
121 let Some(event) = self.db.events.get(&uid).await? else {
122 return Err("Event not found".into());
123 };
124
125 let path: PathBuf = event.path().into();
126 let mut calendar = parse_ics(&path).await?;
127 let e = calendar
128 .components
129 .iter_mut()
130 .filter_map(|a| match a {
131 CalendarComponent::Event(a) => Some(a),
132 _ => None,
133 })
134 .find(|a| a.get_uid() == Some(event.uid()))
135 .ok_or("Event not found in calendar")?;
136
137 patch.resolve(self.now).apply_to(e);
138 let event = e.clone();
139 fs::write(&path, calendar.done().to_string())
140 .await
141 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
142
143 self.db.upsert_event(&path, &event).await?;
144
145 let todo = self.short_ids.event(event).await?;
146 Ok(todo)
147 }
148
149 pub async fn get_kind(&self, id: &Id) -> Result<Kind, Box<dyn Error>> {
154 tracing::debug!(?id, "getting kind of id");
155 if let Some(data) = self.short_ids.get(id).await? {
156 return Ok(data.kind);
157 }
158
159 let uid = id.as_uid();
160
161 tracing::debug!(uid, "checking if id is an event");
162 if self.db.events.get(uid).await?.is_some() {
163 return Ok(Kind::Event);
164 }
165
166 tracing::debug!(uid, "checking if id is a todo");
167 if self.db.todos.get(uid).await?.is_some() {
168 return Ok(Kind::Todo);
169 }
170
171 Err("Id not found".into())
172 }
173
174 pub async fn list_events(
179 &self,
180 conds: &EventConditions,
181 pager: &Pager,
182 ) -> Result<Vec<impl Event + 'static>, Box<dyn Error>> {
183 let conds = conds.resolve(&self.now);
184 let events = self.db.events.list(&conds, pager).await?;
185 let events = self.short_ids.events(events).await?;
186 Ok(events)
187 }
188
189 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
194 let conds = conds.resolve(&self.now);
195 self.db.events.count(&conds).await
196 }
197
198 #[must_use]
200 pub fn default_todo_draft(&self) -> TodoDraft {
201 TodoDraft::default(&self.config, &self.now)
202 }
203
204 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo + 'static, Box<dyn Error>> {
209 let uid = self.generate_uid(Kind::Todo).await?;
210 let todo = draft.resolve(&self.config, &self.now).into_ics(&uid);
211 let path = self.get_path(&uid);
212
213 let calendar = Calendar::new().push(todo.clone()).done();
214 fs::write(&path, calendar.to_string())
215 .await
216 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
217
218 self.db.upsert_todo(&path, &todo).await?;
219
220 let todo = self.short_ids.todo(todo).await?;
221 Ok(todo)
222 }
223
224 pub async fn update_todo(
229 &self,
230 id: &Id,
231 patch: TodoPatch,
232 ) -> Result<impl Todo + 'static, Box<dyn Error>> {
233 let uid = self.short_ids.get_uid(id).await?;
234 let Some(todo) = self.db.todos.get(&uid).await? else {
235 return Err("Todo not found".into());
236 };
237
238 let path: PathBuf = todo.path().into();
239 let mut calendar = parse_ics(&path).await?;
240 let t = calendar
241 .components
242 .iter_mut()
243 .filter_map(|a| match a {
244 CalendarComponent::Todo(a) => Some(a),
245 _ => None,
246 })
247 .find(|a| a.get_uid() == Some(todo.uid()))
248 .ok_or("Todo not found in calendar")?;
249
250 patch.resolve(&self.now).apply_to(t);
251 let todo = t.clone();
252 fs::write(&path, calendar.done().to_string())
253 .await
254 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
255
256 self.db.upsert_todo(&path, &todo).await?;
257
258 let todo = self.short_ids.todo(todo).await?;
259 Ok(todo)
260 }
261
262 pub async fn get_todo(&self, id: &Id) -> Result<impl Todo + 'static, Box<dyn Error>> {
267 let uid = self.short_ids.get_uid(id).await?;
268 match self.db.todos.get(&uid).await {
269 Ok(Some(todo)) => Ok(self.short_ids.todo(todo).await?),
270 Ok(None) => Err("Event not found".into()),
271 Err(e) => Err(e.into()),
272 }
273 }
274
275 pub async fn list_todos(
280 &self,
281 conds: &TodoConditions,
282 sort: &[TodoSort],
283 pager: &Pager,
284 ) -> Result<Vec<impl Todo + 'static>, Box<dyn Error>> {
285 let conds = conds.resolve(&self.now);
286 let sort = TodoSort::resolve_vec(sort, &self.config);
287 let todos = self.db.todos.list(&conds, &sort, pager).await?;
288 let todos = self.short_ids.todos(todos).await?;
289 Ok(todos)
290 }
291
292 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
297 let conds = conds.resolve(&self.now);
298 self.db.todos.count(&conds).await
299 }
300
301 pub async fn flush_short_ids(&self) -> Result<(), Box<dyn Error>> {
306 self.short_ids.flush().await
307 }
308
309 pub async fn close(self) -> Result<(), Box<dyn Error>> {
314 self.db.close().await
315 }
316
317 async fn generate_uid(&self, kind: Kind) -> Result<String, Box<dyn Error>> {
318 for i in 0..16 {
319 let uid = Uuid::new_v4().to_string(); tracing::debug!(
321 ?uid,
322 attempt = i + 1,
323 "generated uid, checking for uniqueness"
324 );
325
326 let exists = match kind {
327 Kind::Event => self.db.events.get(&uid).await?.is_some(),
328 Kind::Todo => self.db.todos.get(&uid).await?.is_some(),
329 };
330 if exists {
331 tracing::debug!(uid, ?kind, "uid already exists in db");
332 continue;
333 }
334
335 let path = self.get_path(&uid);
336 if fs::try_exists(&path).await? {
337 tracing::debug!(uid, ?path, "uid already exists as a file");
338 continue;
339 }
340 return Ok(uid);
341 }
342
343 tracing::warn!("failed to generate a unique uid after multiple attempts");
344 Err("Failed to generate a unique UID after multiple attempts".into())
345 }
346
347 fn get_path(&self, uid: &str) -> PathBuf {
348 self.calendar_path.join(format!("{uid}.ics"))
349 }
350}
351
352async fn prepare(config: &Config) -> Result<(), Box<dyn Error>> {
353 if let Some(parent) = &config.state_dir {
354 tracing::debug!(path = %parent.display(), "ensuring state directory exists");
355 fs::create_dir_all(parent).await?;
356 }
357 Ok(())
358}
359
360async fn initialize_db(config: &Config) -> Result<LocalDb, Box<dyn Error>> {
361 const NAME: &str = "aim.db";
362 let db = if let Some(parent) = &config.state_dir {
363 LocalDb::open(Some(&parent.join(NAME))).await
364 } else {
365 LocalDb::open(None).await
366 }
367 .map_err(|e| format!("Failed to initialize db: {e}"))?;
368
369 Ok(db)
370}