use std::collections::BTreeMap;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::cache::GoalCache;
use super::error::{SmGoalError, SmGoalResult};
use super::memory::{GOAL_TAG, GoalMemory};
use super::model::{Goal, GoalStatus, SessionLink, SessionTaskState};
#[derive(Debug, Clone, Default)]
pub struct SessionUpdate {
pub session_id: String,
pub state: Option<SessionTaskState>,
pub evidence: Option<String>,
pub note: Option<String>,
}
pub struct SmGoalStore {
goals: BTreeMap<String, Goal>,
memory: Arc<dyn GoalMemory>,
cache: GoalCache,
clock: fn() -> DateTime<Utc>,
}
impl SmGoalStore {
pub fn new(memory: Arc<dyn GoalMemory>, data_root: impl Into<std::path::PathBuf>) -> Self {
Self {
goals: BTreeMap::new(),
memory,
cache: GoalCache::new(data_root),
clock: Utc::now,
}
}
pub fn with_clock(
memory: Arc<dyn GoalMemory>,
data_root: impl Into<std::path::PathBuf>,
clock: fn() -> DateTime<Utc>,
) -> Self {
Self {
goals: BTreeMap::new(),
memory,
cache: GoalCache::new(data_root),
clock,
}
}
pub async fn load(
memory: Arc<dyn GoalMemory>,
data_root: impl Into<std::path::PathBuf>,
) -> SmGoalResult<Self> {
let mut store = Self::new(memory, data_root);
match store.memory.list_goals(GOAL_TAG).await {
Ok(entries) => {
for json in entries {
if let Ok(goal) = serde_json::from_str::<Goal>(&json) {
store.goals.insert(goal.id.clone(), goal);
}
}
store.persist_cache()?;
}
Err(_palace_err) => {
for goal in store.cache.load()? {
store.goals.insert(goal.id.clone(), goal);
}
}
}
Ok(store)
}
pub async fn create(
&mut self,
description: impl Into<String>,
acceptance: Vec<String>,
) -> SmGoalResult<Goal> {
let id = new_goal_id();
let goal = Goal::new(id.clone(), description, acceptance, (self.clock)());
self.goals.insert(id.clone(), goal);
if let Err(e) = self.persist(&id).await {
self.goals.remove(&id);
return Err(e);
}
Ok(self.goals.get(&id).cloned().expect("just inserted"))
}
pub async fn link(&mut self, goal_id: &str, link: SessionLink) -> SmGoalResult<Goal> {
let prior = self
.goals
.get(goal_id)
.cloned()
.ok_or_else(|| SmGoalError::NotFound(goal_id.to_string()))?;
{
let goal = self.goals.get_mut(goal_id).expect("present");
goal.sessions.push(link);
if goal.status == GoalStatus::Pending {
goal.status = GoalStatus::InProgress;
}
goal.recompute_progress();
goal.updated = (self.clock)();
}
self.persist_or_restore(goal_id, prior).await?;
Ok(self.goals.get(goal_id).cloned().expect("present"))
}
pub async fn update(&mut self, goal_id: &str, upd: SessionUpdate) -> SmGoalResult<Goal> {
let prior = self
.goals
.get(goal_id)
.cloned()
.ok_or_else(|| SmGoalError::NotFound(goal_id.to_string()))?;
{
let goal = self.goals.get_mut(goal_id).expect("present");
let link = goal
.sessions
.iter_mut()
.find(|s| s.session_id == upd.session_id);
let link = match link {
Some(link) => link,
None => {
self.goals.insert(goal_id.to_string(), prior);
return Err(SmGoalError::NotFound(upd.session_id));
}
};
if let Some(state) = upd.state {
link.state = state;
}
if let Some(evidence) = upd.evidence {
link.evidence = Some(evidence);
}
if let Some(note) = upd.note {
goal.notes.push(note);
}
goal.recompute_progress();
goal.updated = (self.clock)();
}
self.persist_or_restore(goal_id, prior).await?;
Ok(self.goals.get(goal_id).cloned().expect("present"))
}
pub async fn note(&mut self, goal_id: &str, note: impl Into<String>) -> SmGoalResult<Goal> {
let prior = self
.goals
.get(goal_id)
.cloned()
.ok_or_else(|| SmGoalError::NotFound(goal_id.to_string()))?;
{
let goal = self.goals.get_mut(goal_id).expect("present");
goal.notes.push(note.into());
goal.updated = (self.clock)();
}
self.persist_or_restore(goal_id, prior).await?;
Ok(self.goals.get(goal_id).cloned().expect("present"))
}
pub async fn set_status(&mut self, goal_id: &str, status: GoalStatus) -> SmGoalResult<Goal> {
let prior = self
.goals
.get(goal_id)
.cloned()
.ok_or_else(|| SmGoalError::NotFound(goal_id.to_string()))?;
{
let goal = self.goals.get_mut(goal_id).expect("present");
if status == GoalStatus::Done && !goal.all_verified() {
let total = goal.sessions.len();
let verified = goal
.sessions
.iter()
.filter(|s| s.state.is_verified())
.count();
return Err(SmGoalError::VerificationGate {
goal_id: goal_id.to_string(),
verified,
total,
});
}
goal.status = status;
goal.updated = (self.clock)();
}
self.persist_or_restore(goal_id, prior).await?;
Ok(self.goals.get(goal_id).cloned().expect("present"))
}
pub async fn close(&mut self, goal_id: &str) -> SmGoalResult<Goal> {
self.set_status(goal_id, GoalStatus::Done).await
}
pub fn get(&self, goal_id: &str) -> Option<&Goal> {
self.goals.get(goal_id)
}
pub fn all(&self) -> Vec<Goal> {
self.goals.values().cloned().collect()
}
async fn persist(&self, goal_id: &str) -> SmGoalResult<()> {
let goal = self
.goals
.get(goal_id)
.ok_or_else(|| SmGoalError::NotFound(goal_id.to_string()))?;
let json = serde_json::to_string(goal).map_err(|source| SmGoalError::Serde { source })?;
self.memory
.remember_goal(json, GOAL_TAG)
.await
.map_err(|message| SmGoalError::Palace { message })?;
self.persist_cache()
}
async fn persist_or_restore(&mut self, goal_id: &str, prior: Goal) -> SmGoalResult<()> {
if let Err(e) = self.persist(goal_id).await {
self.goals.insert(goal_id.to_string(), prior);
return Err(e);
}
Ok(())
}
fn persist_cache(&self) -> SmGoalResult<()> {
let goals = self.all();
self.cache.save(&goals)
}
}
const GOAL_ID_HEX_WIDTH: usize = 16;
fn new_goal_id() -> String {
let uuid = Uuid::new_v4().simple().to_string();
format!("g-{}", &uuid[..GOAL_ID_HEX_WIDTH])
}