govctl 0.9.3

Project governance CLI for RFC, ADR, and Work Item management
use super::{
    output::print_loop,
    state::{ensure_loop_not_terminal, ensure_work_values},
};
use crate::config::Config;
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult, Diagnostics};
use crate::loop_planner::replan_loop_state_from_config;
use crate::loop_state::{LoopState, load_loop_state, write_loop_state_with_op};
use crate::write::WriteOp;
use std::collections::BTreeSet;

const WORK_FIELD: &str = "work";
const WI_FIELD_ALIAS: &str = "wi";

pub fn replan(config: &Config, loop_id: &str, op: WriteOp) -> DiagnosticResult<Diagnostics> {
    mutate_scope(config, loop_id, ScopeMutation::Replan, &[], op)
}

pub fn add_work_item(
    config: &Config,
    loop_id: &str,
    field: &str,
    work_item: &str,
    op: WriteOp,
) -> DiagnosticResult<Diagnostics> {
    ensure_work_field(field)?;
    let work_ids = [work_item.to_string()];
    mutate_scope(config, loop_id, ScopeMutation::Add, &work_ids, op)
}

pub fn remove_work_item(
    config: &Config,
    loop_id: &str,
    field: &str,
    work_item: &str,
    op: WriteOp,
) -> DiagnosticResult<Diagnostics> {
    ensure_work_field(field)?;
    let work_ids = [work_item.to_string()];
    mutate_scope(config, loop_id, ScopeMutation::Remove, &work_ids, op)
}

#[derive(Debug, Clone, Copy)]
enum ScopeMutation {
    Replan,
    Add,
    Remove,
}

fn mutate_scope(
    config: &Config,
    loop_id: &str,
    mutation: ScopeMutation,
    work: &[String],
    op: WriteOp,
) -> DiagnosticResult<Diagnostics> {
    let state = load_loop_state(config, loop_id)?;
    ensure_loop_not_terminal(&state, "mutate")?;
    let updated_work = mutated_work_set(&state, mutation, work)?;
    let plan = replan_loop_state_from_config(config, &state, &updated_work)?;
    write_loop_state_with_op(config, &plan.state, op)?;
    let verb = if op.is_preview() {
        match mutation {
            ScopeMutation::Replan => "Would replan",
            ScopeMutation::Add | ScopeMutation::Remove => "Would update",
        }
    } else {
        match mutation {
            ScopeMutation::Replan => "Replanned",
            ScopeMutation::Add | ScopeMutation::Remove => "Updated",
        }
    };
    print_loop(verb, &plan.state)?;
    Ok(vec![])
}

fn mutated_work_set(
    state: &LoopState,
    mutation: ScopeMutation,
    work: &[String],
) -> DiagnosticResult<Vec<String>> {
    match mutation {
        ScopeMutation::Replan => {
            if !work.is_empty() {
                ensure_work_values(work)?;
            }
            Ok(state.loop_meta.work.clone())
        }
        ScopeMutation::Add => {
            ensure_work_values(work)?;
            let mut work_items = loop_work_set(state);
            work_items.extend(work.iter().cloned());
            Ok(work_items.into_iter().collect())
        }
        ScopeMutation::Remove => {
            ensure_work_values(work)?;
            let mut work_items = loop_work_set(state);
            for work_id in work {
                if !work_items.remove(work_id) {
                    return Err(Diagnostic::new(
                        DiagnosticCode::E1209LoopWorkMismatch,
                        format!("Loop work field does not contain item to remove: {work_id}"),
                        state.loop_meta.id.clone(),
                    ));
                }
            }
            if work_items.is_empty() {
                return Err(Diagnostic::new(
                    DiagnosticCode::E0801MissingRequiredArg,
                    "Loop work field must not be empty after remove",
                    state.loop_meta.id.clone(),
                ));
            }
            Ok(work_items.into_iter().collect())
        }
    }
}

fn loop_work_set(state: &LoopState) -> BTreeSet<String> {
    state.loop_meta.work.iter().cloned().collect()
}

fn ensure_work_field(field: &str) -> DiagnosticResult<()> {
    if matches!(field, WORK_FIELD | WI_FIELD_ALIAS) {
        return Ok(());
    }
    Err(Diagnostic::new(
        DiagnosticCode::E0803UnknownField,
        format!("Unknown loop field: {field}"),
        "loop",
    ))
}