use super::id_gen::generate_unique_id;
use balls::error::Result;
use balls::plugin::SyncReport;
use balls::store::{task_lock, Store};
use balls::task::{NewTaskOpts, Status, Task, TaskType};
use chrono::Utc;
use serde_json::Value;
pub fn apply_sync_report(store: &Store, plugin_name: &str, report: &SyncReport) {
let id_length = store
.load_config()
.map(|c| c.id_length)
.unwrap_or(4);
for item in &report.created {
warn_on_err("create", apply_created(store, plugin_name, item, id_length));
}
for item in &report.updated {
warn_on_err(
&format!("update {}", item.task_id),
apply_updated(store, plugin_name, item),
);
}
for item in &report.deleted {
warn_on_err(
&format!("delete {}", item.task_id),
apply_deleted(store, plugin_name, item),
);
}
}
fn warn_on_err(what: &str, result: Result<()>) {
if let Err(e) = result {
eprintln!("warning: sync-report {what} failed: {e}");
}
}
fn apply_created(
store: &Store,
plugin_name: &str,
item: &balls::plugin::SyncCreate,
id_length: usize,
) -> Result<()> {
let task_type = TaskType::parse(&item.task_type).unwrap_or(TaskType::Task);
let priority = item.priority.clamp(1, 4);
let status = Status::parse(&item.status).unwrap_or(Status::Open);
let opts = NewTaskOpts {
title: item.title.clone(),
task_type,
priority,
parent: None,
depends_on: Vec::new(),
description: item.description.clone(),
tags: item.tags.clone(),
};
let id = generate_unique_id(&item.title, store, id_length)?;
let mut task = Task::new(opts, id.clone());
task.status = status;
task.external
.insert(plugin_name.to_string(), Value::Object(item.external.clone()));
task.synced_at.insert(plugin_name.to_string(), Utc::now());
let _g = task_lock(store, &id)?;
store.save_task(&task)?;
store.commit_task(&id, &format!("balls: sync-create {id} from {plugin_name}"))?;
Ok(())
}
fn apply_updated(
store: &Store,
plugin_name: &str,
item: &balls::plugin::SyncUpdate,
) -> Result<()> {
let _g = task_lock(store, &item.task_id)?;
let Ok(mut task) = store.load_task(&item.task_id) else {
eprintln!(
"warning: sync update references unknown task {}, skipping",
item.task_id
);
return Ok(());
};
for (field, value) in &item.fields {
apply_field_update(&mut task, field, value);
}
if !item.external.is_empty() {
task.external
.insert(plugin_name.to_string(), Value::Object(item.external.clone()));
}
task.synced_at.insert(plugin_name.to_string(), Utc::now());
task.touch();
store.save_task(&task)?;
if let Some(note) = &item.add_note {
let task_path = store.task_path(&item.task_id)?;
balls::task_io::append_note_to(&task_path, plugin_name, note)?;
}
store.commit_task(
&item.task_id,
&format!("balls: sync-update {} from {}", item.task_id, plugin_name),
)?;
Ok(())
}
fn apply_field_update(task: &mut Task, field: &str, value: &Value) {
match field {
"title" => {
if let Some(s) = value.as_str() {
task.title = s.to_string();
}
}
"priority" => {
if let Some(n) = value.as_u64() {
task.priority = u8::try_from(n.clamp(1, 4)).unwrap_or(4);
}
}
"status" => {
if let Some(s) = value.as_str() {
if let Ok(st) = Status::parse(s) {
task.status = st;
}
}
}
"description" => {
if let Some(s) = value.as_str() {
task.description = s.to_string();
}
}
_ => {}
}
}
fn apply_deleted(
store: &Store,
plugin_name: &str,
item: &balls::plugin::SyncDelete,
) -> Result<()> {
let _g = task_lock(store, &item.task_id)?;
let Ok(mut task) = store.load_task(&item.task_id) else {
return Ok(());
};
if task.status == Status::Closed {
return Ok(());
}
task.status = Status::Deferred;
let reason = if item.reason.is_empty() {
format!("Deleted in remote tracker ({plugin_name})")
} else {
item.reason.clone()
};
task.synced_at.insert(plugin_name.to_string(), Utc::now());
task.touch();
store.save_task(&task)?;
let task_path = store.task_path(&item.task_id)?;
balls::task_io::append_note_to(&task_path, plugin_name, &reason)?;
store.commit_task(
&item.task_id,
&format!("balls: sync-defer {} from {}", item.task_id, plugin_name),
)?;
Ok(())
}