use crate::api::ApiError;
use crate::brain::Brain;
use crate::model::{Asked, DecisionState, Kind, Task};
use anyhow::{Context, Result, bail};
use std::time::Duration;
const WAIT_POLL: Duration = Duration::from_secs(1);
const WAIT_DEFAULT: Duration = Duration::from_secs(8 * 60);
pub(super) fn wait_timeout(args: &mut Vec<String>) -> Result<Option<Duration>> {
if args.is_empty() {
let Some(value) = std::env::var_os("KANBRAIN_WAIT_TIMEOUT") else {
return Ok(Some(WAIT_DEFAULT));
};
return minutes(&value.to_string_lossy()).context("KANBRAIN_WAIT_TIMEOUT must be a number of minutes");
}
if args.remove(0) != "--timeout" || args.is_empty() {
bail!("wait takes an id, then optionally --timeout MINUTES");
}
minutes(&args.remove(0))
}
pub(super) fn minutes(arg: &str) -> Result<Option<Duration>> {
let minutes: u64 = arg.parse().with_context(|| format!("`{arg}` isn't a number of minutes"))?;
let seconds = minutes.checked_mul(60).with_context(|| format!("`{arg}` minutes is longer than any wait"))?;
Ok((minutes > 0).then(|| Duration::from_secs(seconds)))
}
pub(super) fn wait(brain: &Brain, id: u32, timeout: Option<Duration>) -> Result<()> {
let started = std::time::Instant::now();
let mut watch = Watch::default();
loop {
if let Some(said) = watch.look(id, brain.store.load_entries().map(|(tasks, _)| tasks)) {
println!("{}", said?);
return Ok(());
}
if timeout.is_some_and(|limit| started.elapsed() >= limit) {
bail!("no answer on #{id} yet: stop here and don't wait again; once answered, it comes first in `context`");
}
std::thread::sleep(WAIT_POLL);
}
}
#[derive(Default)]
pub(super) struct Watch {
asked_at: Option<chrono::DateTime<chrono::Local>>,
looked: bool,
missed: bool,
}
impl Watch {
pub(super) fn look(&mut self, id: u32, read: Result<Vec<Task>>) -> Option<Result<String>> {
let first = !std::mem::replace(&mut self.looked, true);
match read {
Ok(tasks) => match tasks.into_iter().find(|t| t.id == id) {
None if first || self.missed => Some(Err(ApiError::NotFound(id).into())),
None => {
self.missed = true;
None
}
Some(task) => {
self.missed = false;
if let Some(Asked::Waiting(question)) = task.asked() {
self.asked_at.get_or_insert(question.created);
}
settled(&task, self.asked_at).map(Ok)
}
},
Err(e) if first => Some(Err(e)),
Err(_) => None,
}
}
}
pub(super) fn settled(task: &Task, asked_at: Option<chrono::DateTime<chrono::Local>>) -> Option<String> {
let head = format!("#{} {}", task.id, task.title);
if task.kind == Kind::Decision {
if task.decision == DecisionState::Proposed {
return None;
}
let verdict = task.decision.as_str();
return Some(match task.comments.last() {
Some(why) => format!("{head}: {verdict}\n\n{}", why.body),
None => format!("{head}: {verdict}"),
});
}
match task.asked() {
Some(Asked::Waiting(_)) => None,
Some(Asked::Answered(answer)) => Some(format!("{head}: answered\n\n{}", answer.body)),
None if task.is_task() => {
let answers = |c: &&crate::model::Comment| c.author == crate::model::Author::Human || c.answer;
let answer = asked_at
.and_then(|at| task.comments.iter().rev().find(|c| c.created >= at && !c.question && answers(c)));
Some(match answer {
Some(answer) => format!("{head}: answered, now {}\n\n{}", task.status.as_str(), answer.body),
None => format!("{head}: not waiting for an answer ({})", task.status.as_str()),
})
}
None => Some(format!("{head}: a note, not waiting for an answer")),
}
}