govctl 0.9.1

Project governance CLI for RFC, ADR, and Work Item management
use super::super::support::{ensure_path_mut_with_leaf, type_mismatch};
use crate::cmd::edit::path::{self, PathSegment};
use crate::cmd::edit::rules::{NestedNodeKind, NestedNodeRule, Verb};
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult};
use serde_json::Value;

pub(super) fn descend_get<'a>(
    node: &'static NestedNodeRule,
    value: Option<&'a Value>,
    root_segment: &PathSegment,
    rest: &[PathSegment],
    verb: Verb,
    id: &str,
) -> DiagnosticResult<(&'static NestedNodeRule, Option<&'a Value>)> {
    let root_path = format_segment(root_segment);
    let (node, value) = apply_optional_index(
        node,
        value,
        root_segment.index,
        id,
        &root_path,
        &root_segment.name,
    )?;
    descend_get_rest(node, value, rest, verb, id, &root_path)
}

fn descend_get_rest<'a>(
    node: &'static NestedNodeRule,
    value: Option<&'a Value>,
    rest: &[PathSegment],
    verb: Verb,
    id: &str,
    current_path: &str,
) -> DiagnosticResult<(&'static NestedNodeRule, Option<&'a Value>)> {
    if rest.is_empty() {
        ensure_verb_supported(node, verb, id)?;
        return Ok((node, value));
    }
    let seg = &rest[0];
    let child_node = resolve_child_node(node, seg, current_path, id)?;
    let child_value = value.and_then(|value| value.get(seg.name.as_str()));
    let next_path = append_segment(current_path, seg);
    let (child_node, child_value) = apply_optional_index(
        child_node,
        child_value,
        seg.index,
        id,
        &next_path,
        &seg.name,
    )?;
    descend_get_rest(child_node, child_value, &rest[1..], verb, id, &next_path)
}

fn apply_optional_index<'a>(
    node: &'static NestedNodeRule,
    value: Option<&'a Value>,
    index: Option<i32>,
    id: &str,
    path: &str,
    field_name: &str,
) -> DiagnosticResult<(&'static NestedNodeRule, Option<&'a Value>)> {
    let Some(index) = index else {
        return Ok((node, value));
    };
    ensure_indexable_list(node, id, path, field_name)?;
    let item = node
        .item
        .ok_or_else(|| type_mismatch("List node missing item rule", id))?;
    let selected = match value.and_then(Value::as_array) {
        Some(items) => Some(&items[path::resolve_index(index, items.len())?]),
        None => None,
    };
    Ok((item, selected))
}

pub(super) fn descend_mut<'a>(
    node: &'static NestedNodeRule,
    value: &'a mut Value,
    root_segment: &PathSegment,
    rest: &[PathSegment],
    verb: Verb,
    id: &str,
) -> DiagnosticResult<(&'static NestedNodeRule, &'a mut Value)> {
    let root_path = format_segment(root_segment);
    let (node, value) = apply_optional_index_mut(
        node,
        value,
        root_segment.index,
        id,
        &root_path,
        &root_segment.name,
    )?;
    descend_mut_rest(node, value, rest, verb, id, &root_path)
}

fn descend_mut_rest<'a>(
    node: &'static NestedNodeRule,
    value: &'a mut Value,
    rest: &[PathSegment],
    verb: Verb,
    id: &str,
    current_path: &str,
) -> DiagnosticResult<(&'static NestedNodeRule, &'a mut Value)> {
    if rest.is_empty() {
        ensure_verb_supported(node, verb, id)?;
        return Ok((node, value));
    }
    let seg = &rest[0];
    let child_node = resolve_child_node(node, seg, current_path, id)?;
    let obj = value
        .as_object_mut()
        .ok_or_else(|| type_mismatch("Expected object value", id))?;
    let child_value = obj
        .entry(seg.name.clone())
        .or_insert_with(|| default_value_for_node(child_node));
    let next_path = append_segment(current_path, seg);
    let (child_node, child_value) = apply_optional_index_mut(
        child_node,
        child_value,
        seg.index,
        id,
        &next_path,
        &seg.name,
    )?;
    descend_mut_rest(child_node, child_value, &rest[1..], verb, id, &next_path)
}

fn apply_optional_index_mut<'a>(
    node: &'static NestedNodeRule,
    value: &'a mut Value,
    index: Option<i32>,
    id: &str,
    path: &str,
    field_name: &str,
) -> DiagnosticResult<(&'static NestedNodeRule, &'a mut Value)> {
    let Some(index) = index else {
        return Ok((node, value));
    };
    ensure_indexable_list(node, id, path, field_name)?;
    let arr = value
        .as_array_mut()
        .ok_or_else(|| type_mismatch("Expected array value", id))?;
    let resolved = path::resolve_index(index, arr.len())?;
    let item = node
        .item
        .ok_or_else(|| type_mismatch("List node missing item rule", id))?;
    Ok((item, &mut arr[resolved]))
}

fn ensure_verb_supported(node: &NestedNodeRule, verb: Verb, id: &str) -> DiagnosticResult<()> {
    if node.verbs.contains(&verb.as_str()) {
        return Ok(());
    }
    Err(Diagnostic::new(
        DiagnosticCode::E0817PathTypeMismatch,
        format!("Path does not support verb '{}'", verb.as_str()),
        id,
    ))
}

fn resolve_child_node(
    node: &'static NestedNodeRule,
    seg: &PathSegment,
    current_path: &str,
    id: &str,
) -> DiagnosticResult<&'static NestedNodeRule> {
    if node.kind != NestedNodeKind::Object {
        return Err(type_mismatch(
            &format!(
                "Cannot descend into non-object path '{}'",
                append_segment(current_path, seg)
            ),
            id,
        ));
    }
    node.fields
        .iter()
        .find(|field| field.name == seg.name)
        .map(|field| field.node)
        .ok_or_else(|| {
            Diagnostic::new(
                DiagnosticCode::E0815PathFieldNotFound,
                format!("Unknown nested field '{}'", seg.name),
                id,
            )
        })
}

fn ensure_indexable_list(
    node: &NestedNodeRule,
    id: &str,
    path: &str,
    field_name: &str,
) -> DiagnosticResult<()> {
    if node.kind == NestedNodeKind::List {
        return Ok(());
    }
    Err(type_mismatch(
        &format!("Cannot index into non-list field '{field_name}' at '{path}'"),
        id,
    ))
}

pub(super) fn ensure_node_path_mut<'a>(
    doc: &'a mut Value,
    path: &[&str],
    node: &'static NestedNodeRule,
    id: &str,
) -> DiagnosticResult<&'a mut Value> {
    ensure_path_mut_with_leaf(doc, path, id, || default_value_for_node(node))
}

pub(super) fn default_value_for_node(node: &NestedNodeRule) -> Value {
    match node.kind {
        NestedNodeKind::Scalar => Value::Null,
        NestedNodeKind::Object => Value::Object(serde_json::Map::new()),
        NestedNodeKind::List => Value::Array(Vec::new()),
    }
}

fn format_segment(seg: &PathSegment) -> String {
    match seg.index {
        Some(idx) => format!("{}[{idx}]", seg.name),
        None => seg.name.clone(),
    }
}

fn append_segment(prefix: &str, seg: &PathSegment) -> String {
    format!("{prefix}.{}", format_segment(seg))
}