1use std::collections::HashMap;
2
3use chrono::{Datelike, Local, Timelike};
4
5use crate::data::models::{GlobalDataQuality, SessionData};
6use crate::pricing::calculator::PricingCalculator;
7
8use super::{
9 AggregatedTokens, CacheSavings, CostByCategory, OverviewResult, SessionSummary,
10 SubscriptionValue,
11};
12
13pub fn analyze_overview(
14 sessions: &[SessionData],
15 quality: GlobalDataQuality,
16 calc: &PricingCalculator,
17 subscription_price: Option<f64>,
18) -> OverviewResult {
19 let mut tokens_by_model: HashMap<String, AggregatedTokens> = HashMap::new();
20 let mut cost_by_model: HashMap<String, f64> = HashMap::new();
21 let mut total_cost = 0.0;
22 let mut hourly_distribution = [0usize; 24];
23 let mut weekday_hour_matrix = [[0usize; 24]; 7];
24 let mut total_turns = 0usize;
25 let mut total_agent_turns = 0usize;
26 let mut cost_by_category = CostByCategory::default();
27 let mut tool_count_map: HashMap<String, usize> = HashMap::new();
28
29 for session in sessions {
30 for turn in session.all_responses() {
31 process_turn(
32 turn,
33 calc,
34 &mut tokens_by_model,
35 &mut cost_by_model,
36 &mut total_cost,
37 &mut hourly_distribution,
38 &mut weekday_hour_matrix,
39 &mut cost_by_category,
40 );
41 total_turns += 1;
42 if turn.is_agent { total_agent_turns += 1; }
43
44 for name in &turn.tool_names {
46 *tool_count_map.entry(name.clone()).or_insert(0) += 1;
47 }
48 }
49 }
50
51 let mut tool_counts: Vec<(String, usize)> = tool_count_map.into_iter().collect();
52 tool_counts.sort_by(|a, b| b.1.cmp(&a.1));
53
54 let mut total_output_tokens: u64 = 0;
56 let mut total_context_tokens: u64 = 0;
57 for agg in tokens_by_model.values() {
58 total_output_tokens += agg.output_tokens;
59 total_context_tokens += agg.context_tokens();
60 }
61
62 let total_cache_read: u64 = tokens_by_model.values().map(|a| a.cache_read_tokens).sum();
64 let avg_cache_hit_rate = if total_context_tokens > 0 {
65 (total_cache_read as f64 / total_context_tokens as f64) * 100.0
66 } else {
67 0.0
68 };
69
70 let session_summaries: Vec<SessionSummary> = sessions
72 .iter()
73 .map(|s| build_session_summary(s, calc))
74 .collect();
75
76 let cache_savings = {
83 let mut total_saved = 0.0f64;
84 let mut without_cache = 0.0f64;
85 let mut with_cache = 0.0f64;
86 let mut by_model: Vec<(String, f64)> = Vec::new();
87
88 for (model, tokens) in &tokens_by_model {
89 if let Some((price, _)) = calc.get_price(model) {
90 let cache_read_mtok = tokens.cache_read_tokens as f64 / 1_000_000.0;
91 let hypothetical = cache_read_mtok * price.base_input;
92 let actual = cache_read_mtok * price.cache_read;
93 let saved = hypothetical - actual;
94 without_cache += hypothetical;
95 with_cache += actual;
96 total_saved += saved;
97 if saved > 0.01 {
98 by_model.push((model.clone(), saved));
99 }
100 }
101 }
102 by_model.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
103
104 let savings_pct = if without_cache > 0.0 {
105 total_saved / without_cache * 100.0
106 } else {
107 0.0
108 };
109
110 CacheSavings {
111 total_saved,
112 without_cache_cost: without_cache,
113 with_cache_cost: with_cache,
114 savings_pct,
115 by_model,
116 }
117 };
118
119 let subscription_value = subscription_price.map(|monthly_price| {
120 let value_multiplier = if total_cost > 0.0 {
121 total_cost / monthly_price
122 } else {
123 0.0
124 };
125 SubscriptionValue {
126 monthly_price,
127 api_equivalent: total_cost,
128 value_multiplier,
129 }
130 });
131
132 OverviewResult {
133 total_sessions: sessions.len(),
134 total_turns,
135 total_agent_turns,
136 tokens_by_model,
137 cost_by_model,
138 total_cost,
139 hourly_distribution,
140 quality,
141 subscription_value,
142 weekday_hour_matrix,
143 tool_counts,
144 cost_by_category,
145 session_summaries,
146 total_output_tokens,
147 total_context_tokens,
148 avg_cache_hit_rate,
149 cache_savings,
150 }
151}
152
153#[allow(clippy::too_many_arguments)]
154fn process_turn(
155 turn: &crate::data::models::ValidatedTurn,
156 calc: &PricingCalculator,
157 tokens_by_model: &mut HashMap<String, AggregatedTokens>,
158 cost_by_model: &mut HashMap<String, f64>,
159 total_cost: &mut f64,
160 hourly_distribution: &mut [usize; 24],
161 weekday_hour_matrix: &mut [[usize; 24]; 7],
162 cost_by_category: &mut CostByCategory,
163) {
164 tokens_by_model
166 .entry(turn.model.clone())
167 .or_default()
168 .add_usage(&turn.usage);
169
170 let cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
172 *cost_by_model.entry(turn.model.clone()).or_insert(0.0) += cost.total;
173 *total_cost += cost.total;
174
175 cost_by_category.input_cost += cost.input_cost;
177 cost_by_category.output_cost += cost.output_cost;
178 cost_by_category.cache_write_5m_cost += cost.cache_write_5m_cost;
179 cost_by_category.cache_write_1h_cost += cost.cache_write_1h_cost;
180 cost_by_category.cache_read_cost += cost.cache_read_cost;
181
182 let local_ts = turn.timestamp.with_timezone(&Local);
184 let hour = local_ts.hour() as usize;
185 hourly_distribution[hour] += 1;
186
187 let weekday = local_ts.weekday().num_days_from_monday() as usize; weekday_hour_matrix[weekday][hour] += 1;
190}
191
192fn build_session_summary(session: &SessionData, calc: &PricingCalculator) -> SessionSummary {
194 let session_id = if session.session_id.len() > 8 {
195 session.session_id[..8].to_string()
196 } else {
197 session.session_id.clone()
198 };
199
200 let project_display_name = session
201 .project
202 .as_deref()
203 .map(crate::analysis::project::project_display_name)
204 .unwrap_or_else(|| "(unknown)".to_string());
205
206 let all_turns = session.all_responses();
207 let turn_count = all_turns.len();
208
209 let duration_minutes = match (session.first_timestamp, session.last_timestamp) {
211 (Some(first), Some(last)) => (last - first).num_seconds() as f64 / 60.0,
212 _ => 0.0,
213 };
214
215 let mut model_counts: HashMap<&str, usize> = HashMap::new();
217 let mut output_tokens: u64 = 0;
218 let mut context_tokens: u64 = 0;
219 let mut max_context: u64 = 0;
220 let mut total_cache_read: u64 = 0;
221 let mut total_context: u64 = 0;
222 let mut total_5m: u64 = 0;
223 let mut total_1h: u64 = 0;
224 let mut compaction_count: usize = 0;
225 let mut agent_turn_count: usize = 0;
226 let mut tool_use_count: usize = 0;
227 let mut total_cost: f64 = 0.0;
228 let mut prev_context_size: Option<u64> = None;
229 let mut tool_map: HashMap<String, usize> = HashMap::new();
230
231 for turn in &all_turns {
232 *model_counts.entry(&turn.model).or_insert(0) += 1;
233
234 let input = turn.usage.input_tokens.unwrap_or(0);
235 let cache_create = turn.usage.cache_creation_input_tokens.unwrap_or(0);
236 let cache_read = turn.usage.cache_read_input_tokens.unwrap_or(0);
237 let out = turn.usage.output_tokens.unwrap_or(0);
238
239 output_tokens += out;
240 let ctx = input + cache_create + cache_read;
241 context_tokens += ctx;
242 total_context += ctx;
243 total_cache_read += cache_read;
244
245 if ctx > max_context {
246 max_context = ctx;
247 }
248
249 if let Some(ref detail) = turn.usage.cache_creation {
251 total_5m += detail.ephemeral_5m_input_tokens.unwrap_or(0);
252 total_1h += detail.ephemeral_1h_input_tokens.unwrap_or(0);
253 }
254
255 if let Some(prev) = prev_context_size {
257 if prev > 0 && (ctx as f64) < (prev as f64 * 0.9) {
258 compaction_count += 1;
259 }
260 }
261 prev_context_size = Some(ctx);
262
263 if turn.is_agent {
265 agent_turn_count += 1;
266 }
267
268 if turn.stop_reason.as_deref() == Some("tool_use") {
270 tool_use_count += 1;
271 }
272 for name in &turn.tool_names {
273 *tool_map.entry(name.clone()).or_insert(0) += 1;
274 }
275
276 let cost = calc.calculate_turn_cost(&turn.model, &turn.usage);
278 total_cost += cost.total;
279 }
280
281 let model = model_counts
283 .into_iter()
284 .max_by_key(|(_, count)| *count)
285 .map(|(m, _)| m.to_string())
286 .unwrap_or_default();
287
288 let cache_hit_rate = if total_context > 0 {
290 (total_cache_read as f64 / total_context as f64) * 100.0
291 } else {
292 0.0
293 };
294
295 let total_cache_write = total_5m + total_1h;
297 let cache_write_5m_pct = if total_cache_write > 0 {
298 (total_5m as f64 / total_cache_write as f64) * 100.0
299 } else {
300 0.0
301 };
302
303 SessionSummary {
304 session_id,
305 project_display_name,
306 first_timestamp: session.first_timestamp,
307 duration_minutes,
308 model,
309 turn_count,
310 agent_turn_count,
311 output_tokens,
312 context_tokens,
313 max_context,
314 cache_hit_rate,
315 cache_write_5m_pct,
316 compaction_count,
317 cost: total_cost,
318 tool_use_count,
319 top_tools: {
320 let mut tools: Vec<(String, usize)> = tool_map.into_iter().collect();
321 tools.sort_by(|a, b| b.1.cmp(&a.1));
322 tools.truncate(5);
323 tools
324 },
325 turn_details: None,
326 }
327}
328