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};
17use crate::{
18 Config, Event, EventConditions, EventDraft, EventPatch, Id, Pager, Todo, TodoConditions,
19 TodoDraft, TodoPatch, TodoSort,
20};
21
22#[derive(Debug, Clone)]
24pub struct Aim {
25 now: DateTime<Local>,
26 config: Config,
27 db: LocalDb,
28 short_ids: ShortIds,
29 calendar_path: PathBuf,
30}
31
32impl Aim {
33 pub async fn new(mut config: Config) -> Result<Self, Box<dyn Error>> {
35 let now = Local::now();
36
37 config.normalize()?;
38 prepare(&config).await?;
39
40 let db = LocalDb::open(&config.state_dir)
41 .await
42 .map_err(|e| format!("Failed to initialize db: {e}"))?;
43
44 let short_ids = ShortIds::new(db.clone());
45 let calendar_path = config.calendar_path.clone();
46 let that = Self {
47 now,
48 config,
49 db,
50 short_ids,
51 calendar_path,
52 };
53 that.add_calendar(&that.calendar_path)
54 .await
55 .map_err(|e| format!("Failed to add calendar files: {e}"))?;
56
57 Ok(that)
58 }
59
60 pub fn now(&self) -> DateTime<Local> {
62 self.now
63 }
64
65 pub fn refresh_now(&mut self) {
67 self.now = Local::now();
68 }
69
70 pub fn default_event_draft(&self) -> EventDraft {
72 EventDraft::default()
73 }
74
75 pub async fn new_event(&self, draft: EventDraft) -> Result<impl Event, Box<dyn Error>> {
77 let uid = self.generate_uid().await?;
78 let event = draft.into_ics(&uid);
79 let path = self.get_path(&uid);
80
81 let calendar = Calendar::new().push(event.clone()).done();
82 fs::write(&path, calendar.to_string())
83 .await
84 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
85
86 self.db.upsert_event(&path, &event).await?;
87
88 let todo = self.short_ids.event(event).await?;
89 Ok(todo)
90 }
91
92 pub async fn update_event(
94 &self,
95 id: &Id,
96 patch: EventPatch,
97 ) -> Result<impl Event, Box<dyn Error>> {
98 let uid = self.short_ids.get_uid(id).await?;
99 let event = match self.db.events.get(&uid).await? {
100 Some(todo) => todo,
101 None => return Err("Todo not found".into()),
102 };
103
104 let path: PathBuf = event.path().into();
105 let mut calendar = parse_ics(&path).await?;
106 let t = calendar
107 .components
108 .iter_mut()
109 .filter_map(|a| match a {
110 CalendarComponent::Event(a) => Some(a),
111 _ => None,
112 })
113 .find(|a| a.get_uid() == Some(event.uid()))
114 .ok_or("Event not found in calendar")?;
115
116 patch.apply_to(t);
117 let todo = t.clone();
118 fs::write(&path, calendar.done().to_string())
119 .await
120 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
121
122 self.db.upsert_event(&path, &todo).await?;
123
124 let todo = self.short_ids.event(todo).await?;
125 Ok(todo)
126 }
127
128 pub async fn get_event(&self, id: &Id) -> Result<Option<impl Event + 'static>, Box<dyn Error>> {
130 let uid = self.short_ids.get_uid(id).await?;
131 match self.db.events.get(&uid).await {
132 Ok(Some(event)) => Ok(Some(self.short_ids.event(event).await?)),
133 Ok(None) => Ok(None),
134 Err(e) => Err(e.into()),
135 }
136 }
137
138 pub async fn list_events(
140 &self,
141 conds: &EventConditions,
142 pager: &Pager,
143 ) -> Result<Vec<impl Event>, Box<dyn Error>> {
144 let conds = ParsedEventConditions::parse(&self.now, conds);
145 let events = self.db.events.list(&conds, pager).await?;
146 let events = self.short_ids.events(events).await?;
147 Ok(events)
148 }
149
150 pub async fn count_events(&self, conds: &EventConditions) -> Result<i64, sqlx::Error> {
152 let conds = ParsedEventConditions::parse(&self.now, conds);
153 self.db.events.count(&conds).await
154 }
155
156 pub fn default_todo_draft(&self) -> TodoDraft {
158 TodoDraft::default(&self.config, self.now)
159 }
160
161 pub async fn new_todo(&self, draft: TodoDraft) -> Result<impl Todo, Box<dyn Error>> {
163 let uid = self.generate_uid().await?;
164 let todo = draft.into_ics(&self.config, self.now, &uid);
165 let path = self.get_path(&uid);
166
167 let calendar = Calendar::new().push(todo.clone()).done();
168 fs::write(&path, calendar.to_string())
169 .await
170 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
171
172 self.db.upsert_todo(&path, &todo).await?;
173
174 let todo = self.short_ids.todo(todo).await?;
175 Ok(todo)
176 }
177
178 pub async fn update_todo(
180 &self,
181 id: &Id,
182 patch: TodoPatch,
183 ) -> Result<impl Todo, Box<dyn Error>> {
184 let uid = self.short_ids.get_uid(id).await?;
185 let todo = match self.db.todos.get(&uid).await? {
186 Some(todo) => todo,
187 None => return Err("Todo not found".into()),
188 };
189
190 let path: PathBuf = todo.path().into();
191 let mut calendar = parse_ics(&path).await?;
192 let t = calendar
193 .components
194 .iter_mut()
195 .filter_map(|a| match a {
196 CalendarComponent::Todo(a) => Some(a),
197 _ => None,
198 })
199 .find(|a| a.get_uid() == Some(todo.uid()))
200 .ok_or("Todo not found in calendar")?;
201
202 patch.apply_to(t);
203 let todo = t.clone();
204 fs::write(&path, calendar.done().to_string())
205 .await
206 .map_err(|e| format!("Failed to write calendar file: {e}"))?;
207
208 self.db.upsert_todo(&path, &todo).await?;
209
210 let todo = self.short_ids.todo(todo).await?;
211 Ok(todo)
212 }
213
214 pub async fn get_todo(&self, id: &Id) -> Result<Option<impl Todo + 'static>, Box<dyn Error>> {
216 let uid = self.short_ids.get_uid(id).await?;
217 match self.db.todos.get(&uid).await {
218 Ok(Some(todo)) => Ok(Some(self.short_ids.todo(todo).await?)),
219 Ok(None) => Ok(None),
220 Err(e) => Err(e.into()),
221 }
222 }
223
224 pub async fn list_todos(
226 &self,
227 conds: &TodoConditions,
228 sort: &[TodoSort],
229 pager: &Pager,
230 ) -> Result<Vec<impl Todo>, Box<dyn Error>> {
231 let conds = ParsedTodoConditions::parse(&self.now, conds);
232 let sort = ParsedTodoSort::parse_vec(&self.config, sort);
233 let todos = self.db.todos.list(&conds, &sort, pager).await?;
234 let todos = self.short_ids.todos(todos).await?;
235 Ok(todos)
236 }
237
238 pub async fn count_todos(&self, conds: &TodoConditions) -> Result<i64, sqlx::Error> {
240 let conds = ParsedTodoConditions::parse(&self.now, conds);
241 self.db.todos.count(&conds).await
242 }
243
244 pub async fn close(self) -> Result<(), Box<dyn Error>> {
246 self.db.close().await?;
247 Ok(())
248 }
249
250 async fn add_calendar(&self, calendar_path: &PathBuf) -> Result<(), Box<dyn Error>> {
251 let mut reader = fs::read_dir(calendar_path)
252 .await
253 .map_err(|e| format!("Failed to read directory: {e}"))?;
254
255 let mut handles = vec![];
256 let mut count_ics = 0;
257
258 while let Some(entry) = reader.next_entry().await? {
259 let path = entry.path();
260 match path.extension() {
261 Some(ext) if ext == "ics" => {
262 count_ics += 1;
263 let that = self.clone();
264 handles.push(tokio::spawn(async move {
265 if let Err(e) = that.add_ics(&path).await {
266 log::error!("Failed to process file {}: {}", path.display(), e);
267 }
268 }));
269 }
270 _ => {}
271 }
272 }
273
274 for handle in handles {
275 handle.await?;
276 }
277
278 log::debug!("Total .ics files processed: {count_ics}");
279 Ok(())
280 }
281
282 async fn add_ics(self, path: &Path) -> Result<(), Box<dyn Error>> {
283 log::debug!("Parsing file: {}", path.display());
284 let calendar = parse_ics(path).await?;
285 log::debug!(
286 "Found {} components in {}.",
287 calendar.components.len(),
288 path.display()
289 );
290
291 for component in calendar.components {
292 log::debug!("Processing component: {component:?}");
293 match component {
294 CalendarComponent::Event(event) => self.db.upsert_event(path, &event).await?,
295 CalendarComponent::Todo(todo) => self.db.upsert_todo(path, &todo).await?,
296 _ => log::warn!("Ignoring unsupported component type: {component:?}"),
297 }
298 }
299
300 Ok(())
301 }
302
303 async fn generate_uid(&self) -> Result<String, Box<dyn Error>> {
304 for _ in 0..16 {
305 let uid = Uuid::new_v4().to_string(); if self.db.todos.get(&uid).await?.is_some()
307 || fs::try_exists(&self.get_path(&uid)).await?
308 {
309 continue;
310 }
311 return Ok(uid);
312 }
313
314 Err("Failed to generate a unique UID after multiple attempts".into())
315 }
316
317 fn get_path(&self, uid: &str) -> PathBuf {
318 self.calendar_path.join(format!("{uid}.ics"))
319 }
320}
321
322async fn prepare(config: &Config) -> Result<(), Box<dyn Error>> {
323 if let Some(parent) = &config.state_dir {
324 log::info!("Ensuring state directory exists: {}", parent.display());
325 fs::create_dir_all(parent).await?;
326 }
327 Ok(())
328}
329
330async fn parse_ics(path: &Path) -> Result<Calendar, Box<dyn Error>> {
331 fs::read_to_string(path)
332 .await
333 .map_err(|e| format!("Failed to read file {}: {}", path.display(), e))?
334 .parse()
335 .map_err(|e| format!("Failed to parse calendar: {e}").into())
336}