use crate::db::jira_inbox::JiraInbox;
use crate::db::tasks::Tasks;
use crate::libs::inbox_filter;
use crate::libs::inbox_rules;
use crate::libs::jira_bridge::{self, Goal, MoveOptions, Moved};
use crate::libs::task::{Task, TaskFilter};
use crate::libs::toast_action::{ToastAction, ToastRequest};
use anyhow::Result;
use chrono::{Local, NaiveDateTime};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Applied {
Taken {
started: Option<Result<Moved, String>>,
tags: Vec<String>,
},
AlreadyTaken(String),
Snoozed(NaiveDateTime),
Dismissed,
Completed(String),
AlreadyDone(String),
NoTask,
NotFound,
}
impl Applied {
pub fn summary(&self, key: &str) -> String {
match self {
Applied::Taken { started, tags } => {
let tagged = if tags.is_empty() {
String::new()
} else {
format!(" tagged {}", tags.join(", "))
};
match started {
None => format!("{key} is now a task{tagged}"),
Some(Ok(moved)) => format!("{key} is now a task{tagged}; {}", moved.sentence(key, Goal::Start)),
Some(Err(why)) => format!("{key} is now a task{tagged}; {why}"),
}
}
Applied::AlreadyTaken(name) => format!("{key} was already taken as '{name}'"),
Applied::Snoozed(until) => format!("{key} sleeps until {}", until.format("%b %-d %H:%M")),
Applied::Dismissed => format!("{key} dismissed"),
Applied::Completed(name) => format!("{} is done", task_named(key, name)),
Applied::AlreadyDone(name) => format!("{} was already done", task_named(key, name)),
Applied::NoTask => format!("{key} was never taken as a task"),
Applied::NotFound => format!("{key} is no longer in the inbox"),
}
}
pub fn settled(&self) -> bool {
!matches!(self, Applied::NotFound)
}
}
fn task_named(key: &str, name: &str) -> String {
if name.starts_with(key) {
format!("'{name}'")
} else {
format!("{key} '{name}'")
}
}
pub async fn apply(request: &ToastRequest) -> Result<Applied> {
match request.action {
ToastAction::Take => take(&request.issue_key, &MoveOptions::unattended()).await,
ToastAction::Snooze => {
let window = snooze_window();
snooze(&request.issue_key, window)
}
ToastAction::Dismiss => dismiss(&request.issue_key),
ToastAction::Done => complete(&request.issue_key),
}
}
pub fn snooze_window() -> chrono::Duration {
let configured = crate::libs::config::Config::read().ok().and_then(|c| c.jira_inbox).map(|i| i.toast_snooze_for);
match configured.as_deref().map(inbox_filter::parse_window) {
Some(Ok(window)) => window,
_ => chrono::Duration::days(1),
}
}
fn start_on_take() -> bool {
crate::libs::config::Config::read()
.ok()
.and_then(|c| c.jira_inbox)
.is_some_and(|inbox| inbox.start_on_take)
}
pub async fn take(key: &str, options: &MoveOptions) -> Result<Applied> {
let applied = take_locally(key)?;
let Applied::Taken { tags, .. } = applied else {
return Ok(applied);
};
if !start_on_take() {
return Ok(Applied::Taken { started: None, tags });
}
let started = match jira_bridge::client() {
Ok(mut jira) => jira_bridge::move_issue(&mut jira, key, Goal::Start, options)
.await
.map_err(|e| format!("Jira did not start it: {e}; `kasl inbox start {key}` tries again")),
Err(e) => Err(format!("Jira did not start it: {e}")),
};
Ok(Applied::Taken { started: Some(started), tags })
}
fn take_locally(key: &str) -> Result<Applied> {
let db = JiraInbox::new()?;
let Some(item) = db.get_by_key(key)? else {
return Ok(Applied::NotFound);
};
let mut tasks = Tasks::new()?;
if let Some(task) = tasks.fetch(TaskFilter::ByJiraKey(key.to_string()))?.first() {
if item.taken_at.is_none() {
let _ = db.set_taken(key, true)?;
}
return Ok(Applied::AlreadyTaken(task.name.clone()));
}
let name = format!("{} {}", item.issue_key, item.summary);
let task = Task::new(&name, "", Some(0)).from_jira(key);
tasks.insert(&task)?;
let _ = db.set_taken(key, true)?;
let wanted = inbox_rules::tags_for(&item);
let tags = if wanted.is_empty() {
Vec::new()
} else {
inbox_rules::tag_task(key, &wanted)?
};
Ok(Applied::Taken { started: None, tags })
}
pub fn complete(key: &str) -> Result<Applied> {
let mut tasks = Tasks::new()?;
let rows = tasks.fetch(TaskFilter::ByJiraKey(key.to_string()))?;
let Some(latest) = latest_row(&rows) else {
return Ok(Applied::NoTask);
};
if latest.completeness == Some(100) {
return Ok(Applied::AlreadyDone(latest.name.clone()));
}
let today = Local::now().format("%Y-%m-%d").to_string();
if latest.timestamp.as_deref().is_some_and(|stamp| stamp.starts_with(&today)) {
let mut row = latest.clone();
row.completeness = Some(100);
tasks.update(&row)?;
} else {
let mut row = Task::new(&latest.name, &latest.comment, Some(100)).from_jira(key);
row.task_id = latest.task_id.filter(|&chain| chain != 0).or(latest.id);
tasks.insert(&row)?;
}
Ok(Applied::Completed(latest.name.clone()))
}
pub fn task_open(key: &str) -> Result<bool> {
let rows = Tasks::new()?.fetch(TaskFilter::ByJiraKey(key.to_string()))?;
Ok(latest_row(&rows).is_some_and(|row| row.completeness != Some(100)))
}
fn latest_row(rows: &[Task]) -> Option<&Task> {
rows.iter().max_by(|a, b| a.timestamp.cmp(&b.timestamp).then(a.id.cmp(&b.id)))
}
pub fn snooze(key: &str, window: chrono::Duration) -> Result<Applied> {
let until = Local::now().naive_local() + window;
let db = JiraInbox::new()?;
if !db.set_snoozed(key, Some(until))? {
return Ok(Applied::NotFound);
}
Ok(Applied::Snoozed(until))
}
pub fn dismiss(key: &str) -> Result<Applied> {
let db = JiraInbox::new()?;
if !db.set_dismissed(key, true)? {
return Ok(Applied::NotFound);
}
Ok(Applied::Dismissed)
}