//! JSON envelope path resolution, extracted from `executor::mod` to keep that
//! module under the production size guard. The grammar is shared with the
//! security red-team oracle so both navigate trace envelopes identically.

use serde_json::Value;

/// Walk a dot-and-bracket path inside a JSON envelope. Empty path
/// returns the whole envelope.
///
/// Supports `a.b.c`, `a[0]`, `a[0].b`, and quoted keys `a["weird key"]`.
/// Missing path returns [`Value::Null`] so matchers see a typed
/// "absent" value rather than panicking.
///
/// Public so the observable-evidence oracle (the security red-team layer in
/// [`crate::security::redteam`]) resolves trace-envelope targets with the same
/// grammar the executor uses, instead of reimplementing it.
pub fn resolve_target(value: &Value, path: &str) -> Value {
    if path.is_empty() {
        return value.clone();
    }
    let mut cursor = value;
    for segment in split_path(path) {
        cursor = match (cursor, segment) {
            (Value::Object(map), PathSegment::Field(name)) => {
                if let Some(v) = map.get(&name) {
                    v
                } else {
                    return Value::Null;
                }
            }
            (Value::Array(arr), PathSegment::Index(idx)) => {
                if let Some(v) = arr.get(idx) {
                    v
                } else {
                    return Value::Null;
                }
            }
            // `.length` on an array is a documented count accessor
            // (`tool_calls.length`). It yields a number, so it terminates
            // the walk rather than returning a borrow to step into.
            (Value::Array(arr), PathSegment::Field(name)) if name == "length" => {
                return Value::from(arr.len());
            }
            _ => return Value::Null,
        };
    }
    cursor.clone()
}

/// Diagnose a path that does not resolve. Returns a hint naming the first
/// absent segment and the keys (or length) available at its parent, or `None`
/// when every segment resolves (even to a genuine JSON null). This lets a
/// matcher failure read "target did not resolve" instead of the cryptic
/// "got null" that a typo'd or wrong-case field name produces.
pub fn absent_target_hint(value: &Value, path: &str) -> Option<String> {
    if path.is_empty() {
        return None;
    }
    let mut cursor = value;
    let mut walked: Vec<String> = Vec::new();
    for segment in split_path(path) {
        match (cursor, segment) {
            (Value::Array(arr), PathSegment::Field(name)) if name == "length" => {
                // `.length` on an array resolves to a count, never absent.
                let _ = arr;
                return None;
            }
            (Value::Object(map), PathSegment::Field(name)) => match map.get(&name) {
                Some(v) => {
                    cursor = v;
                    walked.push(name);
                }
                None => {
                    let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
                    keys.sort_unstable();
                    return Some(format!(
                        "target `{path}` did not resolve: no field `{name}` at `{}` (available: {})\nsee: {}",
                        parent_label(&walked),
                        keys.join(", "),
                        crate::error_anchors::TARGET_DID_NOT_RESOLVE
                    ));
                }
            },
            (Value::Array(arr), PathSegment::Index(idx)) => match arr.get(idx) {
                Some(v) => {
                    cursor = v;
                    walked.push(format!("[{idx}]"));
                }
                None => {
                    return Some(format!(
                        "target `{path}` did not resolve: index {idx} out of bounds at `{}` (length {})\nsee: {}",
                        parent_label(&walked),
                        arr.len(),
                        crate::error_anchors::TARGET_DID_NOT_RESOLVE
                    ))
                }
            },
            _ => {
                return Some(format!(
                    "target `{path}` did not resolve past `{}`\nsee: {}",
                    parent_label(&walked),
                    crate::error_anchors::TARGET_DID_NOT_RESOLVE
                ))
            }
        }
    }
    None
}

/// Label the parent location walked so far for an absent-target hint.
fn parent_label(walked: &[String]) -> String {
    if walked.is_empty() {
        "(root)".to_string()
    } else {
        walked.join(".")
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum PathSegment {
    Field(String),
    Index(usize),
}

fn split_path(path: &str) -> Vec<PathSegment> {
    let mut out = Vec::new();
    let mut buf = String::new();
    let mut chars = path.chars().peekable();
    while let Some(c) = chars.next() {
        match c {
            '.' => {
                if !buf.is_empty() {
                    out.push(PathSegment::Field(std::mem::take(&mut buf)));
                }
            }
            '[' => {
                if !buf.is_empty() {
                    out.push(PathSegment::Field(std::mem::take(&mut buf)));
                }
                let mut idx = String::new();
                let mut quoted = None;
                if let Some(&'"') = chars.peek() {
                    chars.next();
                    quoted = Some(String::new());
                    for c2 in chars.by_ref() {
                        if c2 == '"' {
                            break;
                        }
                        if let Some(s) = quoted.as_mut() {
                            s.push(c2);
                        }
                    }
                    // skip trailing ]
                    if let Some(&']') = chars.peek() {
                        chars.next();
                    }
                } else {
                    for c2 in chars.by_ref() {
                        if c2 == ']' {
                            break;
                        }
                        idx.push(c2);
                    }
                }
                if let Some(s) = quoted {
                    out.push(PathSegment::Field(s));
                } else if let Ok(n) = idx.parse::<usize>() {
                    out.push(PathSegment::Index(n));
                }
            }
            _ => buf.push(c),
        }
    }
    if !buf.is_empty() {
        out.push(PathSegment::Field(buf));
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn resolves_indexed_and_nested_fields() {
        let env = json!({"tool_calls": [{"name": "get_weather", "args": {"city": "Sacramento"}}]});
        assert_eq!(
            resolve_target(&env, "tool_calls[0].name"),
            json!("get_weather")
        );
        assert_eq!(
            resolve_target(&env, "tool_calls[0].args.city"),
            json!("Sacramento")
        );
    }

    #[test]
    fn length_on_array_returns_the_count() {
        let env = json!({"tool_calls": [{"name": "a"}, {"name": "b"}, {"name": "c"}]});
        assert_eq!(resolve_target(&env, "tool_calls.length"), json!(3));
    }

    #[test]
    fn length_on_empty_array_is_zero() {
        let env = json!({"tool_calls": []});
        assert_eq!(resolve_target(&env, "tool_calls.length"), json!(0));
    }

    #[test]
    fn missing_path_is_null() {
        let env = json!({"tool_calls": []});
        assert_eq!(resolve_target(&env, "tool_calls[0].name"), Value::Null);
    }

    #[test]
    fn absent_field_hint_names_the_field_and_available_keys() {
        let env = json!({"tool_results": [{"is_error": false, "result": {}}]});
        // `isError` does not exist (the envelope uses `is_error`).
        let hint = absent_target_hint(&env, "tool_results[0].isError").unwrap();
        assert!(hint.contains("no field `isError`"), "{hint}");
        assert!(hint.contains("is_error"), "lists available keys: {hint}");
    }

    #[test]
    fn absent_target_hint_links_the_troubleshooting_entry() {
        // One path per hint shape: missing field, index out of bounds, and
        // a walk that dead-ends on a non-container.
        let env = json!({"tool_calls": [{"name": "a"}]});
        for path in ["nope", "tool_calls[3].name", "tool_calls.foo"] {
            let hint = absent_target_hint(&env, path)
                .unwrap_or_else(|| panic!("`{path}` should produce a hint"));
            assert!(
                hint.contains(crate::error_anchors::TARGET_DID_NOT_RESOLVE),
                "hint for `{path}` should carry the see URL: {hint}"
            );
        }
    }

    #[test]
    fn present_null_leaf_has_no_hint() {
        let env = json!({"nextCursor": null});
        // The field exists and is genuinely null: not an absent path.
        assert_eq!(absent_target_hint(&env, "nextCursor"), None);
    }

    #[test]
    fn length_accessor_is_not_absent() {
        let env = json!({"tool_calls": [{"name": "a"}]});
        assert_eq!(absent_target_hint(&env, "tool_calls.length"), None);
    }

    #[test]
    fn index_out_of_bounds_hint_reports_length() {
        let env = json!({"tool_calls": [{"name": "a"}]});
        let hint = absent_target_hint(&env, "tool_calls[3].name").unwrap();
        assert!(hint.contains("out of bounds"), "{hint}");
        assert!(hint.contains("length 1"), "{hint}");
    }
}
