use crate::connector::{ConnectorRegistry, ConnectorStatus, ImportOptions};
use crate::model::{Session, SessionId};
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct SessionService {
registry: ConnectorRegistry,
cache: Arc<RwLock<HashMap<SessionId, Session>>>,
}
impl SessionService {
#[must_use]
pub fn new() -> Self {
Self {
registry: ConnectorRegistry::new(),
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
#[must_use]
pub fn with_registry(registry: ConnectorRegistry) -> Self {
Self {
registry,
cache: Arc::new(RwLock::new(HashMap::new())),
}
}
#[must_use]
pub fn registry(&self) -> &ConnectorRegistry {
&self.registry
}
pub fn detect_sources(&self) -> Vec<SourceInfo> {
self.registry
.detect_all()
.into_iter()
.map(|(id, status)| {
let connector = self.registry.get(id);
SourceInfo {
id: id.to_string(),
name: connector.map(|c| c.display_name().to_string()),
status,
}
})
.collect()
}
pub async fn import_from(
&self,
source_id: &str,
options: &ImportOptions,
) -> Result<Vec<Session>> {
let connector = self
.registry
.get(source_id)
.ok_or_else(|| anyhow::anyhow!("Unknown source: {}", source_id))?;
let sessions = connector.import(options).await?;
let mut cache = self.cache.write().await;
for session in &sessions {
cache.insert(session.id.clone(), session.clone());
}
Ok(sessions)
}
pub async fn import_all(&self, options: &ImportOptions) -> Result<Vec<Session>> {
let sessions = self.registry.import_all(options).await?;
let mut cache = self.cache.write().await;
for session in &sessions {
cache.insert(session.id.clone(), session.clone());
}
Ok(sessions)
}
pub async fn list_sessions(&self) -> Vec<Session> {
let cache = self.cache.read().await;
cache.values().cloned().collect()
}
pub async fn get_session(&self, id: &SessionId) -> Option<Session> {
let cache = self.cache.read().await;
cache.get(id).cloned()
}
pub async fn search(&self, query: &str) -> Vec<Session> {
let cache = self.cache.read().await;
let query_lower = query.to_lowercase();
cache
.values()
.filter(|session| {
if let Some(title) = &session.title {
if title.to_lowercase().contains(&query_lower) {
return true;
}
}
if let Some(path) = &session.metadata.project_path {
if path.to_lowercase().contains(&query_lower) {
return true;
}
}
for msg in &session.messages {
if msg.content.to_lowercase().contains(&query_lower) {
return true;
}
}
false
})
.cloned()
.collect()
}
pub async fn sessions_by_source(&self, source: &str) -> Vec<Session> {
let cache = self.cache.read().await;
cache
.values()
.filter(|s| s.source == source)
.cloned()
.collect()
}
pub async fn session_count(&self) -> usize {
let cache = self.cache.read().await;
cache.len()
}
pub async fn clear_cache(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
pub async fn statistics(&self) -> SessionStatistics {
let cache = self.cache.read().await;
let mut total_messages = 0;
let mut total_user_messages = 0;
let mut total_assistant_messages = 0;
let mut sources: HashMap<String, usize> = HashMap::new();
for session in cache.values() {
total_messages += session.message_count();
total_user_messages += session.user_message_count();
total_assistant_messages += session.assistant_message_count();
*sources.entry(session.source.clone()).or_default() += 1;
}
SessionStatistics {
total_sessions: cache.len(),
total_messages,
total_user_messages,
total_assistant_messages,
sessions_by_source: sources,
}
}
}
impl Default for SessionService {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct SourceInfo {
pub id: String,
pub name: Option<String>,
pub status: ConnectorStatus,
}
impl SourceInfo {
pub fn is_available(&self) -> bool {
self.status.is_available()
}
}
#[derive(Debug, Clone, Default)]
pub struct SessionStatistics {
pub total_sessions: usize,
pub total_messages: usize,
pub total_user_messages: usize,
pub total_assistant_messages: usize,
pub sessions_by_source: HashMap<String, usize>,
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_session_service_creation() {
let service = SessionService::new();
assert_eq!(service.session_count().await, 0);
}
#[tokio::test]
async fn test_detect_sources() {
let service = SessionService::new();
let sources = service.detect_sources();
assert!(!sources.is_empty());
assert!(sources.iter().any(|s| s.id == "claude-code-native"));
}
#[tokio::test]
async fn test_statistics_empty() {
let service = SessionService::new();
let stats = service.statistics().await;
assert_eq!(stats.total_sessions, 0);
assert_eq!(stats.total_messages, 0);
}
}