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