1use std::collections::{HashMap, HashSet, VecDeque};
4
5use thiserror::Error;
6
7use crate::{
8 db::{AimemDb, DbError},
9 types::{RoomNode, Tunnel},
10};
11
12#[derive(Debug, Error)]
13pub enum GraphError {
14 #[error("db: {0}")]
15 Db(#[from] DbError),
16 #[error("turso: {0}")]
17 Turso(#[from] turso::Error),
18}
19
20#[derive(Debug, Clone)]
22pub struct AimemGraph {
23 db: AimemDb,
24}
25
26impl AimemGraph {
27 pub fn new(db: AimemDb) -> Self {
28 Self { db }
29 }
30
31 pub async fn build(&self) -> Result<(Vec<RoomNode>, Vec<Tunnel>), GraphError> {
33 let conn = self.db.conn()?;
34 let mut room_wings: HashMap<String, (HashSet<String>, i64)> = HashMap::new();
35
36 let mut rows = conn
37 .query(
38 "SELECT room, wing, COUNT(*) as cnt \
39 FROM drawers \
40 WHERE room != 'general' \
41 GROUP BY room, wing",
42 (),
43 )
44 .await?;
45
46 while let Some(row) = rows.next().await? {
47 let room = val_str(&row, 0);
48 let wing = val_str(&row, 1);
49 let cnt = row
50 .get_value(2)
51 .ok()
52 .and_then(|v| v.as_integer().copied())
53 .unwrap_or(0);
54 if room.is_empty() || wing.is_empty() {
55 continue;
56 }
57 let entry = room_wings.entry(room).or_insert((HashSet::new(), 0));
58 entry.0.insert(wing);
59 entry.1 += cnt;
60 }
61
62 let mut nodes: Vec<RoomNode> = room_wings
63 .iter()
64 .map(|(room, (wings, cnt))| {
65 let mut ws: Vec<String> = wings.iter().cloned().collect();
66 ws.sort();
67 RoomNode {
68 room: room.clone(),
69 wings: ws,
70 drawer_count: *cnt,
71 }
72 })
73 .collect();
74 nodes.sort_by(|a, b| a.room.cmp(&b.room));
75
76 let tunnels: Vec<Tunnel> = nodes
77 .iter()
78 .filter(|n| n.wings.len() >= 2)
79 .map(|n| Tunnel {
80 room: n.room.clone(),
81 wings: n.wings.clone(),
82 drawer_count: n.drawer_count,
83 })
84 .collect();
85
86 Ok((nodes, tunnels))
87 }
88
89 pub async fn traverse(
91 &self,
92 start_room: &str,
93 max_hops: usize,
94 ) -> Result<Vec<TraversalNode>, GraphError> {
95 let (nodes, _) = self.build().await?;
96 let node_map: HashMap<&str, &RoomNode> =
97 nodes.iter().map(|n| (n.room.as_str(), n)).collect();
98
99 let Some(start) = node_map.get(start_room) else {
100 let suggestions: Vec<String> = nodes
101 .iter()
102 .filter(|n| n.room.contains(start_room) || start_room.contains(n.room.as_str()))
103 .take(5)
104 .map(|n| n.room.clone())
105 .collect();
106 return Ok(vec![TraversalNode {
107 room: format!("Room '{start_room}' not found. Suggestions: {suggestions:?}"),
108 wings: vec![],
109 drawer_count: 0,
110 hop: 0,
111 connected_via: vec![],
112 }]);
113 };
114
115 let mut visited: HashSet<&str> = HashSet::new();
116 visited.insert(start_room);
117
118 let mut results = vec![TraversalNode {
119 room: start.room.clone(),
120 wings: start.wings.clone(),
121 drawer_count: start.drawer_count,
122 hop: 0,
123 connected_via: vec![],
124 }];
125
126 let mut frontier: VecDeque<(&str, usize)> = VecDeque::new();
127 frontier.push_back((start_room, 0));
128
129 while let Some((current_room, depth)) = frontier.pop_front() {
130 if depth >= max_hops {
131 continue;
132 }
133 let current_wings: HashSet<&str> = node_map
134 .get(current_room)
135 .map(|n| n.wings.iter().map(|w| w.as_str()).collect())
136 .unwrap_or_default();
137
138 for (room, node) in &node_map {
139 if visited.contains(room) {
140 continue;
141 }
142 let shared: Vec<String> = node
143 .wings
144 .iter()
145 .filter(|w| current_wings.contains(w.as_str()))
146 .cloned()
147 .collect();
148 if !shared.is_empty() {
149 visited.insert(room);
150 results.push(TraversalNode {
151 room: node.room.clone(),
152 wings: node.wings.clone(),
153 drawer_count: node.drawer_count,
154 hop: depth + 1,
155 connected_via: shared,
156 });
157 if depth + 1 < max_hops {
158 frontier.push_back((room, depth + 1));
159 }
160 }
161 }
162 }
163
164 results.sort_by(|a, b| a.hop.cmp(&b.hop).then(b.drawer_count.cmp(&a.drawer_count)));
165 results.truncate(50);
166 Ok(results)
167 }
168
169 pub async fn find_tunnels(
171 &self,
172 wing_a: Option<&str>,
173 wing_b: Option<&str>,
174 ) -> Result<Vec<Tunnel>, GraphError> {
175 let (_, mut tunnels) = self.build().await?;
176 if let Some(wa) = wing_a {
177 tunnels.retain(|t| t.wings.contains(&wa.to_string()));
178 }
179 if let Some(wb) = wing_b {
180 tunnels.retain(|t| t.wings.contains(&wb.to_string()));
181 }
182 tunnels.sort_by(|a, b| b.drawer_count.cmp(&a.drawer_count));
183 tunnels.truncate(50);
184 Ok(tunnels)
185 }
186
187 pub async fn stats(&self) -> Result<serde_json::Value, GraphError> {
189 let (nodes, tunnels) = self.build().await?;
190 let mut rooms_per_wing: HashMap<String, usize> = HashMap::new();
191 for node in &nodes {
192 for wing in &node.wings {
193 *rooms_per_wing.entry(wing.clone()).or_default() += 1;
194 }
195 }
196 let mut rpw: Vec<(String, usize)> = rooms_per_wing.into_iter().collect();
197 rpw.sort_by(|a, b| b.1.cmp(&a.1));
198
199 Ok(serde_json::json!({
200 "total_rooms": nodes.len(),
201 "tunnel_rooms": tunnels.len(),
202 "rooms_per_wing": rpw.into_iter().map(|(w, c)| serde_json::json!({"wing": w, "count": c})).collect::<Vec<_>>(),
203 "top_tunnels": tunnels.iter().take(10).map(|t| serde_json::json!({
204 "room": t.room, "wings": t.wings, "count": t.drawer_count,
205 })).collect::<Vec<_>>(),
206 }))
207 }
208}
209
210#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
211pub struct TraversalNode {
212 pub room: String,
213 pub wings: Vec<String>,
214 pub drawer_count: i64,
215 pub hop: usize,
216 pub connected_via: Vec<String>,
217}
218
219fn val_str(row: &turso::Row, idx: usize) -> String {
220 match row.get_value(idx) {
221 Ok(turso::Value::Text(s)) => s,
222 Ok(turso::Value::Null) | Err(_) => String::new(),
223 Ok(v) => format!("{v:?}"),
224 }
225}