1use turbovault_core::prelude::*;
4use petgraph::algo::kosaraju_scc;
5use petgraph::prelude::*;
6use std::collections::{HashMap, HashSet};
7use std::path::PathBuf;
8
9type NodeIndex = petgraph::graph::NodeIndex;
11
12pub struct LinkGraph {
14    graph: DiGraph<PathBuf, Link>,
16
17    file_index: HashMap<String, NodeIndex>,
19
20    alias_index: HashMap<String, NodeIndex>,
22
23    path_index: HashMap<PathBuf, NodeIndex>,
25}
26
27impl LinkGraph {
28    pub fn new() -> Self {
30        Self {
31            graph: DiGraph::new(),
32            file_index: HashMap::new(),
33            alias_index: HashMap::new(),
34            path_index: HashMap::new(),
35        }
36    }
37
38    pub fn add_file(&mut self, file: &VaultFile) -> Result<()> {
40        let path = file.path.clone();
41
42        let node_idx = if let Some(&idx) = self.path_index.get(&path) {
44            idx
45        } else {
46            let idx = self.graph.add_node(path.clone());
47            self.path_index.insert(path.clone(), idx);
48
49            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
51                self.file_index.insert(stem.to_string(), idx);
52            }
53
54            idx
55        };
56
57        if let Some(fm) = &file.frontmatter {
59            for alias in fm.aliases() {
60                self.alias_index.insert(alias, node_idx);
61            }
62        }
63
64        Ok(())
65    }
66
67    pub fn remove_file(&mut self, path: &PathBuf) -> Result<()> {
69        if let Some(&idx) = self.path_index.get(path) {
70            self.path_index.remove(path);
72
73            if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
74                self.file_index.remove(stem);
75            }
76
77            self.alias_index.retain(|_, &mut node_idx| node_idx != idx);
79
80            self.graph.remove_node(idx);
82        }
83
84        Ok(())
85    }
86
87    pub fn update_links(&mut self, file: &VaultFile) -> Result<()> {
89        let source_path = &file.path;
90
91        let source_idx = if let Some(&idx) = self.path_index.get(source_path) {
93            idx
94        } else {
95            let idx = self.graph.add_node(source_path.clone());
96            self.path_index.insert(source_path.clone(), idx);
97            idx
98        };
99
100        let outgoing: Vec<_> = self.graph.edges(source_idx).map(|e| e.id()).collect();
102        for edge_id in outgoing {
103            self.graph.remove_edge(edge_id);
104        }
105
106        for link in &file.links {
108            if matches!(link.type_, LinkType::WikiLink | LinkType::Embed)
109                && let Some(target_idx) = self.resolve_link(&link.target)
110            {
111                self.graph.add_edge(source_idx, target_idx, link.clone());
113            }
114        }
115
116        Ok(())
117    }
118
119    fn resolve_link(&self, target: &str) -> Option<NodeIndex> {
121        let clean_target = target.split('#').next()?.trim();
123
124        if let Some(&idx) = self.file_index.get(clean_target) {
126            return Some(idx);
127        }
128
129        if let Some(&idx) = self.alias_index.get(clean_target) {
131            return Some(idx);
132        }
133
134        let target_parts: Vec<&str> = clean_target.split('/').filter(|p| !p.is_empty()).collect();
136        if target_parts.is_empty() {
137            return None;
138        }
139
140        for (path, &idx) in self.path_index.iter() {
142            let path_parts: Vec<&str> = path.iter().filter_map(|p| p.to_str()).collect();
143
144            if path_parts.len() >= target_parts.len() {
145                let start = path_parts.len() - target_parts.len();
146                if path_parts[start..] == target_parts[..] {
147                    return Some(idx);
148                }
149            }
150        }
151
152        None
153    }
154
155    pub fn backlinks(&self, path: &PathBuf) -> Result<Vec<(PathBuf, Vec<Link>)>> {
157        if let Some(&target_idx) = self.path_index.get(path) {
158            let backlinks: Vec<_> = self
159                .graph
160                .edges_directed(target_idx, Incoming)
161                .map(|edge| {
162                    let source_idx = edge.source();
163                    let source_path = self.graph[source_idx].clone();
164                    (source_path, edge.weight().clone())
165                })
166                .fold(HashMap::new(), |mut acc, (path, link)| {
167                    acc.entry(path).or_insert_with(Vec::new).push(link);
168                    acc
169                })
170                .into_iter()
171                .collect();
172
173            Ok(backlinks)
174        } else {
175            Ok(vec![])
176        }
177    }
178
179    pub fn forward_links(&self, path: &PathBuf) -> Result<Vec<(PathBuf, Vec<Link>)>> {
181        if let Some(&source_idx) = self.path_index.get(path) {
182            let forward_links: Vec<_> = self
183                .graph
184                .edges(source_idx)
185                .map(|edge| {
186                    let target_idx = edge.target();
187                    let target_path = self.graph[target_idx].clone();
188                    (target_path, edge.weight().clone())
189                })
190                .fold(HashMap::new(), |mut acc, (path, link)| {
191                    acc.entry(path).or_insert_with(Vec::new).push(link);
192                    acc
193                })
194                .into_iter()
195                .collect();
196
197            Ok(forward_links)
198        } else {
199            Ok(vec![])
200        }
201    }
202
203    pub fn orphaned_notes(&self) -> Vec<PathBuf> {
205        self.graph
206            .node_indices()
207            .filter(|&idx| {
208                let in_degree = self.graph.edges_directed(idx, Incoming).count();
209                let out_degree = self.graph.edges(idx).count();
210                in_degree == 0 && out_degree == 0
211            })
212            .map(|idx| self.graph[idx].clone())
213            .collect()
214    }
215
216    pub fn related_notes(&self, path: &PathBuf, max_hops: usize) -> Result<Vec<PathBuf>> {
218        if let Some(&start_idx) = self.path_index.get(path) {
219            let mut visited = HashSet::new();
220            let mut queue = vec![(start_idx, 0)];
221            let mut related = Vec::new();
222
223            visited.insert(start_idx);
224
225            while let Some((idx, hops)) = queue.pop() {
226                if hops > 0 {
227                    related.push(self.graph[idx].clone());
228                }
229
230                if hops < max_hops {
231                    for neighbor_idx in self.graph.neighbors(idx) {
233                        if visited.insert(neighbor_idx) {
234                            queue.push((neighbor_idx, hops + 1));
235                        }
236                    }
237
238                    for neighbor_idx in self.graph.edges_directed(idx, Incoming).map(|e| e.source())
240                    {
241                        if visited.insert(neighbor_idx) {
242                            queue.push((neighbor_idx, hops + 1));
243                        }
244                    }
245                }
246            }
247
248            Ok(related)
249        } else {
250            Ok(vec![])
251        }
252    }
253
254    pub fn cycles(&self) -> Vec<Vec<PathBuf>> {
256        let sccs = kosaraju_scc(&self.graph);
257        sccs.into_iter()
258            .filter(|scc| scc.len() > 1) .map(|scc| scc.iter().map(|&idx| self.graph[idx].clone()).collect())
260            .collect()
261    }
262
263    pub fn stats(&self) -> GraphStats {
265        let node_count = self.graph.node_count();
266        let edge_count = self.graph.edge_count();
267
268        let orphaned_count = self.orphaned_notes().len();
269
270        let avg_links_per_file = if node_count > 0 {
271            edge_count as f64 / node_count as f64
272        } else {
273            0.0
274        };
275
276        GraphStats {
277            total_files: node_count,
278            total_links: edge_count,
279            orphaned_files: orphaned_count,
280            average_links_per_file: avg_links_per_file,
281        }
282    }
283
284    pub fn all_files(&self) -> Vec<PathBuf> {
286        self.graph
287            .node_indices()
288            .map(|idx| self.graph[idx].clone())
289            .collect()
290    }
291
292    pub fn node_count(&self) -> usize {
294        self.graph.node_count()
295    }
296
297    pub fn edge_count(&self) -> usize {
299        self.graph.edge_count()
300    }
301
302    pub fn incoming_links(&self, path: &PathBuf) -> Result<Vec<Link>> {
304        if let Some(&target_idx) = self.path_index.get(path) {
305            let links: Vec<Link> = self
306                .graph
307                .edges_directed(target_idx, Incoming)
308                .map(|edge| edge.weight().clone())
309                .collect();
310            Ok(links)
311        } else {
312            Ok(vec![])
313        }
314    }
315
316    pub fn outgoing_links(&self, path: &PathBuf) -> Result<Vec<Link>> {
318        if let Some(&source_idx) = self.path_index.get(path) {
319            let links: Vec<Link> = self
320                .graph
321                .edges(source_idx)
322                .map(|edge| edge.weight().clone())
323                .collect();
324            Ok(links)
325        } else {
326            Ok(vec![])
327        }
328    }
329
330    pub fn all_links(&self) -> HashMap<PathBuf, Vec<Link>> {
332        let mut result = HashMap::new();
333
334        for node_idx in self.graph.node_indices() {
335            let source_path = self.graph[node_idx].clone();
336            let links: Vec<Link> = self
337                .graph
338                .edges(node_idx)
339                .map(|edge| edge.weight().clone())
340                .collect();
341
342            if !links.is_empty() {
343                result.insert(source_path, links);
344            }
345        }
346
347        result
348    }
349
350    pub fn connected_components(&self) -> Result<Vec<Vec<PathBuf>>> {
352        use petgraph::algo::tarjan_scc;
353
354        let components = tarjan_scc(&self.graph);
356
357        let result: Vec<Vec<PathBuf>> = components
358            .into_iter()
359            .map(|component| {
360                component
361                    .iter()
362                    .map(|&idx| self.graph[idx].clone())
363                    .collect()
364            })
365            .collect();
366
367        Ok(result)
368    }
369}
370
371impl Default for LinkGraph {
372    fn default() -> Self {
373        Self::new()
374    }
375}
376
377#[derive(Debug, Clone)]
379pub struct GraphStats {
380    pub total_files: usize,
381    pub total_links: usize,
382    pub orphaned_files: usize,
383    pub average_links_per_file: f64,
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389
390    fn create_test_file(path: &str, links: Vec<&str>) -> VaultFile {
391        let parsed_links: Vec<Link> = links
392            .into_iter()
393            .enumerate()
394            .map(|(i, target)| Link {
395                type_: LinkType::WikiLink,
396                source_file: PathBuf::from(path),
397                target: target.to_string(),
398                display_text: None,
399                position: SourcePosition::new(0, 0, i * 10, 10),
400                resolved_target: None,
401                is_valid: true,
402            })
403            .collect();
404
405        let mut vault_file = VaultFile::new(
406            PathBuf::from(path),
407            String::new(),
408            FileMetadata {
409                path: PathBuf::from(path),
410                size: 0,
411                created_at: 0.0,
412                modified_at: 0.0,
413                checksum: String::new(),
414                is_attachment: false,
415            },
416        );
417        vault_file.links = parsed_links;
418        vault_file
419    }
420
421    #[test]
422    fn test_add_file() {
423        let mut graph = LinkGraph::new();
424        let file = create_test_file("note.md", vec![]);
425
426        assert!(graph.add_file(&file).is_ok());
427        assert_eq!(graph.node_count(), 1);
428    }
429
430    #[test]
431    fn test_add_multiple_files() {
432        let mut graph = LinkGraph::new();
433        let file1 = create_test_file("note1.md", vec![]);
434        let file2 = create_test_file("note2.md", vec![]);
435
436        graph.add_file(&file1).unwrap();
437        graph.add_file(&file2).unwrap();
438
439        assert_eq!(graph.node_count(), 2);
440    }
441
442    #[test]
443    fn test_update_links() {
444        let mut graph = LinkGraph::new();
445        let file1 = create_test_file("note1.md", vec![]);
446        let file2 = create_test_file("note2.md", vec!["note1"]);
447
448        graph.add_file(&file1).unwrap();
449        graph.add_file(&file2).unwrap();
450        graph.update_links(&file2).unwrap();
451
452        assert_eq!(graph.edge_count(), 1);
453    }
454
455    #[test]
456    fn test_orphaned_notes() {
457        let mut graph = LinkGraph::new();
458        let orphan = create_test_file("orphan.md", vec![]);
459        let linked1 = create_test_file("note1.md", vec![]);
460        let linked2 = create_test_file("note2.md", vec!["note1"]);
461
462        graph.add_file(&orphan).unwrap();
463        graph.add_file(&linked1).unwrap();
464        graph.add_file(&linked2).unwrap();
465        graph.update_links(&linked2).unwrap();
466
467        let orphans = graph.orphaned_notes();
468        assert_eq!(orphans.len(), 1);
469        assert_eq!(orphans[0], PathBuf::from("orphan.md"));
470    }
471
472    #[test]
473    fn test_graph_stats() {
474        let mut graph = LinkGraph::new();
475        let file1 = create_test_file("note1.md", vec![]);
476        let file2 = create_test_file("note2.md", vec!["note1"]);
477
478        graph.add_file(&file1).unwrap();
479        graph.add_file(&file2).unwrap();
480        graph.update_links(&file2).unwrap();
481
482        let stats = graph.stats();
483        assert_eq!(stats.total_files, 2);
484        assert_eq!(stats.total_links, 1);
485        assert_eq!(stats.orphaned_files, 0); }
487}