use super::EventRecord;
use anyhow::{anyhow, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::warn;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventIndex {
pub job_id: String,
pub event_counts: HashMap<String, usize>,
pub time_range: (DateTime<Utc>, DateTime<Utc>),
pub file_offsets: Vec<FileOffset>,
pub total_events: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileOffset {
pub file_path: PathBuf,
pub byte_offset: u64,
pub line_number: usize,
pub event_id: Uuid,
pub timestamp: DateTime<Utc>,
}
pub type ParsedEventData = Vec<(EventRecord, u64, usize)>;
pub type FileEventsData = Vec<(PathBuf, ParsedEventData)>;
pub fn calculate_time_range(events: &[EventRecord]) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
if events.is_empty() {
return None;
}
let mut start = events[0].timestamp;
let mut end = events[0].timestamp;
for event in events {
if event.timestamp < start {
start = event.timestamp;
}
if event.timestamp > end {
end = event.timestamp;
}
}
Some((start, end))
}
pub fn build_index_from_events(job_id: &str, file_events: FileEventsData) -> EventIndex {
let mut index = EventIndex {
job_id: job_id.to_string(),
event_counts: HashMap::new(),
time_range: (Utc::now(), Utc::now()),
file_offsets: Vec::new(),
total_events: 0,
};
let mut all_timestamps = Vec::new();
for (file_path, events) in file_events {
for (event, byte_offset, line_number) in events {
index.total_events += 1;
let event_name = event.event.event_name().to_string();
increment_event_count(&mut index.event_counts, event_name);
let file_offset = FileOffset {
file_path: file_path.clone(),
byte_offset,
line_number,
event_id: event.id,
timestamp: event.timestamp,
};
index.file_offsets.push(file_offset);
all_timestamps.push(event.timestamp);
}
}
if !all_timestamps.is_empty() {
let min_time = all_timestamps
.iter()
.min()
.copied()
.unwrap_or_else(Utc::now);
let max_time = all_timestamps
.iter()
.max()
.copied()
.unwrap_or_else(Utc::now);
index.time_range = (min_time, max_time);
}
index
}
pub fn update_time_range(
start: Option<DateTime<Utc>>,
end: Option<DateTime<Utc>>,
event_time: DateTime<Utc>,
) -> (Option<DateTime<Utc>>, Option<DateTime<Utc>>) {
let new_start = match start {
None => Some(event_time),
Some(s) if event_time < s => Some(event_time),
Some(s) => Some(s),
};
let new_end = match end {
None => Some(event_time),
Some(e) if event_time > e => Some(event_time),
Some(e) => Some(e),
};
(new_start, new_end)
}
pub fn increment_event_count(counts: &mut HashMap<String, usize>, event_name: String) {
*counts.entry(event_name).or_insert(0) += 1;
}
pub fn validate_job_id(job_id: &str) -> Result<()> {
if job_id.is_empty() {
return Err(anyhow!("Job ID cannot be empty"));
}
if !job_id
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
return Err(anyhow!(
"Job ID contains invalid characters. Only alphanumeric, dash, and underscore allowed: {}",
job_id
));
}
if job_id.len() > 255 {
return Err(anyhow!(
"Job ID is too long (max 255 characters): {} characters",
job_id.len()
));
}
Ok(())
}
pub fn validate_index_consistency(index: &mut EventIndex) -> Result<()> {
let (start, end) = index.time_range;
if start > end {
warn!(
"Index time range was inverted (start: {}, end: {}). Correcting...",
start, end
);
index.time_range = (end, start);
}
let sum_of_counts: usize = index.event_counts.values().sum();
if sum_of_counts != index.total_events {
warn!(
"Event count mismatch: sum of individual counts ({}) != total_events ({}). Using sum.",
sum_of_counts, index.total_events
);
index.total_events = sum_of_counts;
}
if index.file_offsets.len() != index.total_events {
warn!(
"File offset count ({}) doesn't match total events ({}). This may indicate partial indexing.",
index.file_offsets.len(),
index.total_events
);
}
if index.job_id.is_empty() {
return Err(anyhow!("Index has empty job_id"));
}
Ok(())
}