use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::trajectory::branch::{BranchStateMachine, BranchError};
use crate::trajectory::graph::{TrajectoryGraph, NodeId};
pub type ChainId = String;
#[inline]
fn current_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
fn generate_chain_id() -> ChainId {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let timestamp = current_timestamp();
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("chain_{:x}_{:04x}", timestamp, counter)
}
#[derive(Debug, Clone)]
pub struct ChainMetadata {
pub id: ChainId,
pub title: Option<String>,
pub created_at: i64,
pub last_accessed_at: i64,
pub node_count: usize,
pub branch_count: usize,
pub is_active: bool,
pub tags: Vec<String>,
}
impl ChainMetadata {
pub fn new(id: ChainId) -> Self {
let now = current_timestamp();
Self {
id,
title: None,
created_at: now,
last_accessed_at: now,
node_count: 0,
branch_count: 0,
is_active: true,
tags: Vec::new(),
}
}
pub fn touch(&mut self) {
self.last_accessed_at = current_timestamp();
}
pub fn update_stats(&mut self, machine: &BranchStateMachine) {
self.node_count = machine.graph().node_count();
self.branch_count = machine.branch_count();
}
}
#[derive(Debug, Clone)]
pub struct ChainManagerConfig {
pub max_chains: usize,
pub inactivity_threshold_secs: u64,
pub auto_cleanup: bool,
}
impl Default for ChainManagerConfig {
fn default() -> Self {
Self {
max_chains: 1000,
inactivity_threshold_secs: 3600, auto_cleanup: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ChainManagerStats {
pub total_chains: usize,
pub active_chains: usize,
pub total_nodes: usize,
pub total_branches: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChainManagerError {
ChainNotFound(ChainId),
ChainAlreadyExists(ChainId),
MaxChainsReached,
BranchError(String),
InvalidOperation(String),
}
impl std::fmt::Display for ChainManagerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ChainNotFound(id) => write!(f, "Chain not found: {}", id),
Self::ChainAlreadyExists(id) => write!(f, "Chain already exists: {}", id),
Self::MaxChainsReached => write!(f, "Maximum number of chains reached"),
Self::BranchError(msg) => write!(f, "Branch error: {}", msg),
Self::InvalidOperation(msg) => write!(f, "Invalid operation: {}", msg),
}
}
}
impl std::error::Error for ChainManagerError {}
impl From<BranchError> for ChainManagerError {
fn from(err: BranchError) -> Self {
Self::BranchError(err.to_string())
}
}
pub struct ChainManager {
chains: HashMap<ChainId, BranchStateMachine>,
metadata: HashMap<ChainId, ChainMetadata>,
active_chain: Option<ChainId>,
config: ChainManagerConfig,
}
impl Default for ChainManager {
fn default() -> Self {
Self::new()
}
}
impl ChainManager {
pub fn new() -> Self {
Self::with_config(ChainManagerConfig::default())
}
pub fn with_config(config: ChainManagerConfig) -> Self {
Self {
chains: HashMap::new(),
metadata: HashMap::new(),
active_chain: None,
config,
}
}
pub fn create_chain(&mut self, chain_id: Option<ChainId>) -> Result<ChainId, ChainManagerError> {
if self.chains.len() >= self.config.max_chains {
if self.config.auto_cleanup {
self.cleanup_inactive(Duration::from_secs(self.config.inactivity_threshold_secs));
}
if self.chains.len() >= self.config.max_chains {
return Err(ChainManagerError::MaxChainsReached);
}
}
let id = chain_id.unwrap_or_else(generate_chain_id);
if self.chains.contains_key(&id) {
return Err(ChainManagerError::ChainAlreadyExists(id));
}
let graph = TrajectoryGraph::new();
let machine = BranchStateMachine::from_graph(graph);
let metadata = ChainMetadata::new(id.clone());
self.chains.insert(id.clone(), machine);
self.metadata.insert(id.clone(), metadata);
if self.active_chain.is_none() {
self.active_chain = Some(id.clone());
}
Ok(id)
}
pub fn create_chain_from_graph(
&mut self,
chain_id: Option<ChainId>,
graph: TrajectoryGraph,
) -> Result<ChainId, ChainManagerError> {
let id = chain_id.unwrap_or_else(generate_chain_id);
if self.chains.contains_key(&id) {
return Err(ChainManagerError::ChainAlreadyExists(id));
}
let machine = BranchStateMachine::from_graph(graph);
let mut metadata = ChainMetadata::new(id.clone());
metadata.update_stats(&machine);
self.chains.insert(id.clone(), machine);
self.metadata.insert(id.clone(), metadata);
if self.active_chain.is_none() {
self.active_chain = Some(id.clone());
}
Ok(id)
}
pub fn get_chain(&self, chain_id: &ChainId) -> Option<&BranchStateMachine> {
self.chains.get(chain_id)
}
pub fn get_chain_mut(&mut self, chain_id: &ChainId) -> Option<&mut BranchStateMachine> {
if let Some(meta) = self.metadata.get_mut(chain_id) {
meta.touch();
}
self.chains.get_mut(chain_id)
}
pub fn get_metadata(&self, chain_id: &ChainId) -> Option<&ChainMetadata> {
self.metadata.get(chain_id)
}
pub fn get_metadata_mut(&mut self, chain_id: &ChainId) -> Option<&mut ChainMetadata> {
self.metadata.get_mut(chain_id)
}
pub fn contains(&self, chain_id: &ChainId) -> bool {
self.chains.contains_key(chain_id)
}
pub fn chain_ids(&self) -> impl Iterator<Item = &ChainId> {
self.chains.keys()
}
pub fn chain_count(&self) -> usize {
self.chains.len()
}
pub fn active_chain(&self) -> Option<&ChainId> {
self.active_chain.as_ref()
}
pub fn set_active_chain(&mut self, chain_id: ChainId) -> Result<(), ChainManagerError> {
if !self.chains.contains_key(&chain_id) {
return Err(ChainManagerError::ChainNotFound(chain_id));
}
self.active_chain = Some(chain_id);
Ok(())
}
pub fn get_active_chain(&self) -> Option<&BranchStateMachine> {
self.active_chain.as_ref().and_then(|id| self.chains.get(id))
}
pub fn get_active_chain_mut(&mut self) -> Option<&mut BranchStateMachine> {
if let Some(id) = &self.active_chain {
if let Some(meta) = self.metadata.get_mut(id) {
meta.touch();
}
self.chains.get_mut(id)
} else {
None
}
}
pub fn delete_chain(&mut self, chain_id: &ChainId) -> bool {
let removed = self.chains.remove(chain_id).is_some();
self.metadata.remove(chain_id);
if self.active_chain.as_ref() == Some(chain_id) {
self.active_chain = self.chains.keys().next().cloned();
}
removed
}
pub fn merge_chains(
&mut self,
from: &ChainId,
into: &ChainId,
) -> Result<(), ChainManagerError> {
if from == into {
return Err(ChainManagerError::InvalidOperation(
"Cannot merge chain with itself".to_string(),
));
}
let from_chain = self.chains.remove(from)
.ok_or_else(|| ChainManagerError::ChainNotFound(from.clone()))?;
let into_chain = self.chains.get_mut(into)
.ok_or_else(|| ChainManagerError::ChainNotFound(into.clone()))?;
for branch in from_chain.all_branches() {
let _ = branch; }
self.metadata.remove(from);
if let Some(meta) = self.metadata.get_mut(into) {
meta.update_stats(into_chain);
meta.touch();
}
if self.active_chain.as_ref() == Some(from) {
self.active_chain = Some(into.clone());
}
Ok(())
}
pub fn split_chain(
&mut self,
chain_id: &ChainId,
node_id: NodeId,
) -> Result<ChainId, ChainManagerError> {
let chain = self.chains.get_mut(chain_id)
.ok_or_else(|| ChainManagerError::ChainNotFound(chain_id.clone()))?;
let split_result = chain.split(node_id)?;
let new_chain_id = generate_chain_id();
let mut metadata = ChainMetadata::new(new_chain_id.clone());
metadata.title = Some(format!("Split from {} at node {}", chain_id, node_id));
if let Some(meta) = self.metadata.get_mut(chain_id) {
meta.update_stats(chain);
meta.touch();
}
self.metadata.insert(new_chain_id.clone(), metadata);
let _ = split_result;
Ok(new_chain_id)
}
pub fn cleanup_inactive(&mut self, threshold: Duration) {
let now = current_timestamp();
let threshold_secs = threshold.as_secs() as i64;
let inactive: Vec<ChainId> = self.metadata
.iter()
.filter(|(_, meta)| {
now - meta.last_accessed_at > threshold_secs
})
.map(|(id, _)| id.clone())
.collect();
for chain_id in inactive {
self.delete_chain(&chain_id);
}
}
pub fn deactivate_chain(&mut self, chain_id: &ChainId) -> Result<(), ChainManagerError> {
let meta = self.metadata.get_mut(chain_id)
.ok_or_else(|| ChainManagerError::ChainNotFound(chain_id.clone()))?;
meta.is_active = false;
Ok(())
}
pub fn reactivate_chain(&mut self, chain_id: &ChainId) -> Result<(), ChainManagerError> {
let meta = self.metadata.get_mut(chain_id)
.ok_or_else(|| ChainManagerError::ChainNotFound(chain_id.clone()))?;
meta.is_active = true;
meta.touch();
Ok(())
}
pub fn stats(&self) -> ChainManagerStats {
let mut stats = ChainManagerStats::default();
stats.total_chains = self.chains.len();
for (id, chain) in &self.chains {
stats.total_nodes += chain.graph().node_count();
stats.total_branches += chain.branch_count();
if let Some(meta) = self.metadata.get(id) {
if meta.is_active {
stats.active_chains += 1;
}
}
}
stats
}
pub fn all_metadata(&self) -> impl Iterator<Item = &ChainMetadata> {
self.metadata.values()
}
pub fn find_by_tag(&self, tag: &str) -> Vec<&ChainId> {
self.metadata
.iter()
.filter(|(_, meta)| meta.tags.contains(&tag.to_string()))
.map(|(id, _)| id)
.collect()
}
pub fn find_by_title(&self, query: &str) -> Vec<&ChainId> {
let query_lower = query.to_lowercase();
self.metadata
.iter()
.filter(|(_, meta)| {
meta.title
.as_ref()
.map_or(false, |t| t.to_lowercase().contains(&query_lower))
})
.map(|(id, _)| id)
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_chain() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(None).unwrap();
assert!(manager.contains(&chain_id));
assert_eq!(manager.chain_count(), 1);
}
#[test]
fn test_create_chain_with_id() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(Some("my-chain".to_string())).unwrap();
assert_eq!(chain_id, "my-chain");
assert!(manager.contains(&"my-chain".to_string()));
}
#[test]
fn test_duplicate_chain_error() {
let mut manager = ChainManager::new();
manager.create_chain(Some("test".to_string())).unwrap();
let result = manager.create_chain(Some("test".to_string()));
assert!(matches!(result, Err(ChainManagerError::ChainAlreadyExists(_))));
}
#[test]
fn test_delete_chain() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(None).unwrap();
assert!(manager.delete_chain(&chain_id));
assert!(!manager.contains(&chain_id));
assert_eq!(manager.chain_count(), 0);
}
#[test]
fn test_active_chain() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(None).unwrap();
assert_eq!(manager.active_chain(), Some(&chain_id));
let chain_id2 = manager.create_chain(None).unwrap();
assert_eq!(manager.active_chain(), Some(&chain_id));
manager.set_active_chain(chain_id2.clone()).unwrap();
assert_eq!(manager.active_chain(), Some(&chain_id2));
}
#[test]
fn test_get_chain() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(None).unwrap();
assert!(manager.get_chain(&chain_id).is_some());
assert!(manager.get_chain(&"nonexistent".to_string()).is_none());
}
#[test]
fn test_stats() {
let mut manager = ChainManager::new();
manager.create_chain(None).unwrap();
manager.create_chain(None).unwrap();
let stats = manager.stats();
assert_eq!(stats.total_chains, 2);
assert_eq!(stats.active_chains, 2);
}
#[test]
fn test_metadata() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(Some("test".to_string())).unwrap();
let meta = manager.get_metadata(&chain_id).unwrap();
assert!(meta.is_active);
assert!(meta.created_at > 0);
let meta = manager.get_metadata_mut(&chain_id).unwrap();
meta.title = Some("My Chain".to_string());
meta.tags.push("important".to_string());
let meta = manager.get_metadata(&chain_id).unwrap();
assert_eq!(meta.title, Some("My Chain".to_string()));
assert!(meta.tags.contains(&"important".to_string()));
}
#[test]
fn test_find_by_tag() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(Some("test".to_string())).unwrap();
let meta = manager.get_metadata_mut(&chain_id).unwrap();
meta.tags.push("project".to_string());
let found = manager.find_by_tag("project");
assert_eq!(found.len(), 1);
assert_eq!(found[0], &chain_id);
let not_found = manager.find_by_tag("nonexistent");
assert!(not_found.is_empty());
}
#[test]
fn test_deactivate_reactivate() {
let mut manager = ChainManager::new();
let chain_id = manager.create_chain(None).unwrap();
assert!(manager.get_metadata(&chain_id).unwrap().is_active);
manager.deactivate_chain(&chain_id).unwrap();
assert!(!manager.get_metadata(&chain_id).unwrap().is_active);
manager.reactivate_chain(&chain_id).unwrap();
assert!(manager.get_metadata(&chain_id).unwrap().is_active);
}
}