use crate::personality::{
    self, gradient_bottom_border, gradient_top_border, ANSI_BOLD, ANSI_CYAN, ANSI_DIM, ANSI_GOLD,
    ANSI_GREEN, ANSI_MAGENTA, ANSI_RED, ANSI_RESET, GLYPH_CONNECTION, GLYPH_DIMENSION, GLYPH_PATH,
    GLYPH_SIGNAL, GLYPH_STRUCTURE,
};
use crate::protocol::layers::{
    HelpGuidance, HelpInput, HelpMinimalCall, HelpMode, HelpRejectedAlternative, HelpRender,
    HelpSequenceStep, HelpStage,
};
use serde_json::{json, Map, Value};
use std::collections::{BTreeSet, HashMap};

#[derive(Clone, Debug)]
pub struct HelpParamSpec {
    pub name: String,
    pub description: String,
    pub required: bool,
    pub default: Option<Value>,
}

#[derive(Clone, Debug)]
pub struct HelpCatalogEntry {
    pub name: String,
    pub category: String,
    pub glyph: String,
    pub one_liner: String,
    pub returns: String,
    pub params: Vec<HelpParamSpec>,
    pub minimal_arguments: Value,
    pub next: Vec<String>,
    pub aliases: Vec<String>,
}

#[derive(Clone, Debug)]
pub struct HelpResolution {
    pub formatted: String,
    pub guidance: Option<HelpGuidance>,
    pub found: bool,
    pub suggestions: Vec<String>,
    pub tool: Option<String>,
    pub proof_state: String,
}

#[derive(Clone, Debug, PartialEq)]
pub struct RuntimeGuidanceProjection {
    pub proof_state: String,
    pub next_suggested_tool: Option<String>,
    pub next_suggested_target: Option<String>,
    pub next_step_hint: Option<String>,
    pub confidence: Option<f32>,
    pub why_this_next_step: Option<String>,
    pub what_is_missing: Option<String>,
}

#[derive(Clone, Debug)]
struct SchemaTool {
    name: String,
    description: String,
    params: Vec<HelpParamSpec>,
    input_schema: Value,
}

#[derive(Clone, Debug)]
struct OverviewCard {
    title: &'static str,
    why: &'static str,
    tool: &'static str,
    arguments: Value,
    reject_tool: &'static str,
    reject_reason: &'static str,
}

pub fn catalog_entries() -> Vec<HelpCatalogEntry> {
    let manual_docs: HashMap<String, personality::ToolDoc> = personality::tool_docs()
        .into_iter()
        .map(|doc| (doc.name.to_string(), doc))
        .collect();

    schema_tools()
        .into_iter()
        .map(|schema| {
            let manual = manual_docs.get(&schema.name);
            let (category, glyph) = manual
                .map(|doc| (doc.category.to_string(), doc.glyph.to_string()))
                .unwrap_or_else(|| infer_category_and_glyph(&schema.name));
            let one_liner = manual
                .map(|doc| doc.one_liner.to_string())
                .unwrap_or_else(|| schema.description.clone());
            let returns = manual
                .map(|doc| doc.returns.to_string())
                .unwrap_or_else(|| {
                    "Structured output documented by the live runtime schema.".into()
                });
            let next = manual
                .map(|doc| doc.next.iter().map(|next| (*next).to_string()).collect())
                .unwrap_or_else(|| inferred_next_tools(&schema.name));
            let aliases = vec![
                format!("m1nd.{}", schema.name),
                format!("m1nd_{}", schema.name),
            ];
            let minimal_arguments =
                choose_minimal_arguments(&schema.name, manual, &schema.input_schema);

            HelpCatalogEntry {
                name: schema.name,
                category,
                glyph,
                one_liner,
                returns,
                params: schema.params,
                minimal_arguments,
                next,
                aliases,
            }
        })
        .collect()
}

pub fn catalog_entry(tool_name: &str) -> Option<HelpCatalogEntry> {
    catalog_entries()
        .into_iter()
        .find(|entry| entry.name == tool_name)
}

pub fn live_tool_names() -> Vec<String> {
    catalog_entries()
        .into_iter()
        .map(|entry| entry.name)
        .collect()
}

pub fn find_similar_tool_names(query: &str, max_suggestions: usize) -> Vec<String> {
    let query = query.trim().to_lowercase();
    let mut scored: Vec<(String, usize)> = live_tool_names()
        .into_iter()
        .map(|name| {
            let distance = levenshtein_distance(&query, &name.to_lowercase());
            (name, distance)
        })
        .filter(|(_, distance)| *distance <= 6)
        .collect();
    scored.sort_by_key(|(_, distance)| *distance);
    scored
        .into_iter()
        .take(max_suggestions.max(1))
        .map(|(name, _)| name)
        .collect()
}

pub fn build_tool_resolution(entry: &HelpCatalogEntry, input: &HelpInput) -> HelpResolution {
    let render = input.render.unwrap_or(HelpRender::Full);
    let guidance = tool_guidance(entry, input);
    let formatted = match render {
        HelpRender::None => String::new(),
        HelpRender::Compact => render_tool(entry, &guidance, true),
        HelpRender::Full => render_tool(entry, &guidance, false),
    };

    HelpResolution {
        formatted,
        guidance: Some(guidance),
        found: true,
        suggestions: vec![],
        tool: Some(entry.name.clone()),
        proof_state: "triaging".into(),
    }
}

pub fn build_unknown_tool_resolution(query: &str, max_suggestions: usize) -> HelpResolution {
    let suggestions = find_similar_tool_names(query, max_suggestions);
    let guidance = HelpGuidance {
        decision_type: "recovery".into(),
        recommended_tools: suggestions.clone(),
        recommended_sequence: vec![HelpSequenceStep {
            tool: "help".into(),
            reason: "Retry with a canonical live tool name from the current runtime surface."
                .into(),
        }],
        required_inputs: vec!["tool_name".into()],
        missing_context: vec!["A valid canonical tool name".into()],
        minimal_calls: suggestions
            .first()
            .map(|tool| {
                vec![HelpMinimalCall {
                    tool: "help".into(),
                    arguments: json!({
                        "agent_id": "jimi",
                        "tool_name": tool,
                    }),
                }]
            })
            .unwrap_or_default(),
        rejected_alternatives: vec![HelpRejectedAlternative {
            tool: query.to_string(),
            reason: "That name does not resolve to a live canonical tool in this runtime.".into(),
        }],
        recovery_steps: vec![
            "Use one of the suggested canonical names.".into(),
            "If you do not know the tool name, call help with stage or intent instead of guessing."
                .into(),
        ],
        next_step: suggestions
            .first()
            .map(|tool| format!("Retry help with tool_name=\"{}\".", tool)),
        why: "The requested tool name did not match the live m1nd tool surface.".into(),
        confidence: 0.36,
        canonical_name: None,
        aliases: vec![],
    };

    HelpResolution {
        formatted: render_unknown_tool(query, &suggestions),
        guidance: Some(guidance),
        found: false,
        suggestions,
        tool: Some(query.to_string()),
        proof_state: "blocked".into(),
    }
}

pub fn build_overview_resolution(input: &HelpInput, show_temponizer: bool) -> HelpResolution {
    let total_tools = live_tool_names().len();
    let cards = overview_cards(input);
    let formatted = match input.render.unwrap_or(HelpRender::Full) {
        HelpRender::None => String::new(),
        HelpRender::Compact => render_overview(total_tools, &cards, true, show_temponizer),
        HelpRender::Full => render_overview(total_tools, &cards, false, show_temponizer),
    };
    let guidance = HelpGuidance {
        decision_type: "overview".into(),
        recommended_tools: cards.iter().map(|card| card.tool.to_string()).collect(),
        recommended_sequence: cards
            .iter()
            .take(3)
            .map(|card| HelpSequenceStep {
                tool: card.tool.to_string(),
                reason: card.why.into(),
            })
            .collect(),
        required_inputs: vec![],
        missing_context: vec!["Pick a stage, a tool, or an intent to get a tighter route.".into()],
        minimal_calls: cards
            .iter()
            .take(3)
            .map(|card| HelpMinimalCall {
                tool: card.tool.to_string(),
                arguments: card.arguments.clone(),
            })
            .collect(),
        rejected_alternatives: vec![HelpRejectedAlternative {
            tool: "blind file hunting".into(),
            reason: "The help overview is designed to route by agent state before shell search."
                .into(),
        }],
        recovery_steps: vec![
            "Describe the stage or paste the error instead of guessing a tool.".into(),
        ],
        next_step: Some(
            "Choose the card that matches your current state, then open tool help if needed."
                .into(),
        ),
        why: "Overview mode is a router for agent state, not a memorization-heavy catalog.".into(),
        confidence: 0.61,
        canonical_name: None,
        aliases: vec![],
    };

    HelpResolution {
        formatted,
        guidance: Some(guidance),
        found: true,
        suggestions: vec![],
        tool: None,
        proof_state: "triaging".into(),
    }
}

pub fn build_route_resolution(input: &HelpInput) -> HelpResolution {
    let guidance = route_guidance(input, false);
    let formatted = match input.render.unwrap_or(HelpRender::Full) {
        HelpRender::None => String::new(),
        HelpRender::Compact => render_guidance("ROUTE", &guidance, true),
        HelpRender::Full => render_guidance("ROUTE", &guidance, false),
    };

    HelpResolution {
        formatted,
        guidance: Some(guidance),
        found: true,
        suggestions: vec![],
        tool: None,
        proof_state: "triaging".into(),
    }
}

pub fn build_workflow_resolution(input: &HelpInput) -> HelpResolution {
    let guidance = route_guidance(input, true);
    let formatted = match input.render.unwrap_or(HelpRender::Full) {
        HelpRender::None => String::new(),
        HelpRender::Compact => render_guidance("WORKFLOW", &guidance, true),
        HelpRender::Full => render_guidance("WORKFLOW", &guidance, false),
    };

    HelpResolution {
        formatted,
        guidance: Some(guidance),
        found: true,
        suggestions: vec![],
        tool: None,
        proof_state: "triaging".into(),
    }
}

pub fn build_recovery_resolution(input: &HelpInput) -> HelpResolution {
    let guidance = recovery_guidance(input);
    let formatted = match input.render.unwrap_or(HelpRender::Full) {
        HelpRender::None => String::new(),
        HelpRender::Compact => render_guidance("RECOVERY", &guidance, true),
        HelpRender::Full => render_guidance("RECOVERY", &guidance, false),
    };

    HelpResolution {
        formatted,
        guidance: Some(guidance),
        found: true,
        suggestions: vec![],
        tool: input.tool_name.clone(),
        proof_state: "blocked".into(),
    }
}

pub fn runtime_projection_from_guidance(
    guidance: Option<&HelpGuidance>,
    proof_state: impl Into<String>,
) -> RuntimeGuidanceProjection {
    let proof_state = proof_state.into();
    let next_suggested_tool = guidance.and_then(|guidance| {
        guidance
            .recommended_sequence
            .first()
            .map(|step| step.tool.clone())
            .or_else(|| guidance.recommended_tools.first().cloned())
    });
    let next_suggested_target = guidance.and_then(|guidance| {
        if !guidance.missing_context.is_empty() {
            Some(guidance.missing_context.join(", "))
        } else {
            guidance
                .canonical_name
                .clone()
                .or_else(|| guidance.aliases.first().cloned())
        }
    });
    let next_step_hint = guidance.and_then(|guidance| guidance.next_step.clone());
    let confidence = guidance.map(|guidance| guidance.confidence);
    let why_this_next_step = guidance.map(|guidance| guidance.why.clone());
    let what_is_missing = guidance.and_then(|guidance| {
        if guidance.missing_context.is_empty() {
            None
        } else {
            Some(guidance.missing_context.join(", "))
        }
    });

    RuntimeGuidanceProjection {
        proof_state,
        next_suggested_tool,
        next_suggested_target,
        next_step_hint,
        confidence,
        why_this_next_step,
        what_is_missing,
    }
}

pub fn runtime_projection_from_resolution(
    resolution: &HelpResolution,
    default_proof_state: &str,
) -> RuntimeGuidanceProjection {
    let proof_state = if resolution.proof_state.is_empty() {
        default_proof_state.to_string()
    } else {
        resolution.proof_state.clone()
    };
    runtime_projection_from_guidance(resolution.guidance.as_ref(), proof_state)
}

pub fn runtime_projection_for_tool(
    tool_name: &str,
    input: &HelpInput,
    default_proof_state: &str,
) -> Option<RuntimeGuidanceProjection> {
    let canonical = normalize_tool_name(tool_name);
    let entry = catalog_entry(&canonical)?;
    let mut normalized_input = input.clone();
    normalized_input.tool_name = Some(canonical);
    normalized_input.render = Some(HelpRender::None);
    let resolution = build_tool_resolution(&entry, &normalized_input);
    Some(runtime_projection_from_resolution(
        &resolution,
        default_proof_state,
    ))
}

pub fn runtime_error_guidance_hint(tool_name: &str, detail: &str) -> String {
    let canonical = normalize_tool_name(tool_name);
    let input = HelpInput {
        agent_id: "runtime".into(),
        tool_name: Some(canonical.clone()),
        mode: Some(HelpMode::Recovery),
        intent: None,
        stage: None,
        path: None,
        error_text: Some(detail.to_string()),
        recent_tools: vec![],
        max_suggestions: Some(3),
        render: Some(HelpRender::None),
    };
    let resolution = build_recovery_resolution(&input);
    let projection = runtime_projection_from_resolution(&resolution, "blocked");

    let mut parts = vec![detail.trim().to_string()];
    if let Some(call) = resolution
        .guidance
        .as_ref()
        .and_then(|guidance| guidance.minimal_calls.first())
    {
        parts.push(format!(
            "minimal call: {}({})",
            call.tool,
            render_inline_json(&call.arguments)
        ));
    }
    if let Some(next_step) = projection.next_step_hint {
        parts.push(format!("next: {next_step}"));
    }
    if let Some(missing) = projection.what_is_missing {
        parts.push(format!("missing: {missing}"));
    }

    parts.join("; ")
}

fn schema_tools() -> Vec<SchemaTool> {
    crate::server::tool_schemas()
        .get("tools")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .filter_map(|tool| {
            let name = tool.get("name")?.as_str()?.to_string();
            let description = tool
                .get("description")
                .and_then(Value::as_str)
                .unwrap_or_default()
                .to_string();
            let input_schema = tool
                .get("inputSchema")
                .cloned()
                .unwrap_or_else(|| json!({}));
            let params = schema_params(&input_schema);
            Some(SchemaTool {
                name,
                description,
                params,
                input_schema,
            })
        })
        .collect()
}

fn normalize_tool_name(tool_name: &str) -> String {
    let trimmed = tool_name.trim();
    trimmed
        .strip_prefix("m1nd.")
        .or_else(|| trimmed.strip_prefix("m1nd_"))
        .unwrap_or(trimmed)
        .to_string()
}

fn schema_params(input_schema: &Value) -> Vec<HelpParamSpec> {
    let required = input_schema
        .get("required")
        .and_then(Value::as_array)
        .map(|entries| {
            entries
                .iter()
                .filter_map(Value::as_str)
                .map(str::to_string)
                .collect::<BTreeSet<_>>()
        })
        .unwrap_or_default();

    input_schema
        .get("properties")
        .and_then(Value::as_object)
        .map(|properties| {
            properties
                .iter()
                .map(|(name, schema)| HelpParamSpec {
                    name: name.to_string(),
                    description: schema
                        .get("description")
                        .and_then(Value::as_str)
                        .unwrap_or_default()
                        .to_string(),
                    required: required.contains(name),
                    default: schema.get("default").cloned(),
                })
                .collect()
        })
        .unwrap_or_default()
}

fn choose_minimal_arguments(
    tool_name: &str,
    manual: Option<&personality::ToolDoc>,
    input_schema: &Value,
) -> Value {
    if let Some(override_value) = minimal_call_override(tool_name) {
        return override_value;
    }

    if let Some(manual) = manual {
        if let Ok(parsed) = serde_json::from_str::<Value>(manual.example) {
            if validate_against_schema(&parsed, input_schema) {
                return parsed;
            }
        }
    }

    build_minimal_arguments_from_schema(tool_name, input_schema)
}

fn build_minimal_arguments_from_schema(tool_name: &str, input_schema: &Value) -> Value {
    let required = input_schema
        .get("required")
        .and_then(Value::as_array)
        .cloned()
        .unwrap_or_default();
    let properties = input_schema
        .get("properties")
        .and_then(Value::as_object)
        .cloned()
        .unwrap_or_default();

    let mut object = Map::new();
    for key in required.iter().filter_map(Value::as_str) {
        if let Some(schema) = properties.get(key) {
            object.insert(key.to_string(), example_value_for(tool_name, key, schema));
        }
    }
    Value::Object(object)
}

fn example_value_for(tool_name: &str, param_name: &str, schema: &Value) -> Value {
    if let Some(default) = schema.get("default") {
        return default.clone();
    }
    if let Some(variants) = schema.get("enum").and_then(Value::as_array) {
        if let Some(value) = variants.first() {
            return value.clone();
        }
    }

    match schema.get("type").and_then(Value::as_str) {
        Some("string") => Value::String(guess_string(tool_name, param_name)),
        Some("integer") => Value::Number(guess_integer(param_name).into()),
        Some("number") => json!(guess_number(param_name)),
        Some("boolean") => Value::Bool(guess_boolean(param_name)),
        Some("array") => {
            let item = schema
                .get("items")
                .map(|items| example_value_for(tool_name, param_name, items))
                .unwrap_or_else(|| Value::String("example".into()));
            Value::Array(vec![item])
        }
        Some("object") => {
            let required = schema
                .get("required")
                .and_then(Value::as_array)
                .cloned()
                .unwrap_or_default();
            let properties = schema
                .get("properties")
                .and_then(Value::as_object)
                .cloned()
                .unwrap_or_default();
            let mut object = Map::new();
            for key in required.iter().filter_map(Value::as_str) {
                if let Some(child) = properties.get(key) {
                    object.insert(key.to_string(), example_value_for(tool_name, key, child));
                }
            }
            Value::Object(object)
        }
        _ => Value::String("example".into()),
    }
}

fn guess_string(tool_name: &str, param_name: &str) -> String {
    match param_name {
        "agent_id" => "jimi".into(),
        "query" => match tool_name {
            "search" => "authentication middleware".into(),
            "seek" => "where retry backoff is decided".into(),
            "activate" => "payment pipeline".into(),
            "missing" => "session invalidation".into(),
            _ => "authentication flow".into(),
        },
        "pattern" => "**/*.rs".into(),
        "path" => match tool_name {
            "audit" => "/path/to/project".into(),
            "document_resolve" | "document_bindings" | "document_drift" => "docs/spec.md".into(),
            _ => "/path/to/input".into(),
        },
        "scope" => "src".into(),
        "file_path" => "src/example.rs".into(),
        "new_content" | "content" => "...".into(),
        "description" => "Apply the validated change".into(),
        "node_id" | "source" | "target" | "changed_node" | "anchor_node" => {
            "file::src/main.rs".into()
        }
        "trail_id" => "trail_auth_flow".into(),
        "preview_id" => "preview_jimi_1".into(),
        "perspective_id" => "persp_auth".into(),
        "route_id" => "route_auth_1".into(),
        "error_text" => "TypeError: undefined is not a function".into(),
        "tool_name" => "activate".into(),
        "branch_name" => "follow-up".into(),
        "service_name" => "auth-service".into(),
        "current_repo_name" => "current-repo".into(),
        "namespace" => "docs".into(),
        _ if param_name.contains("path") => "/path/to/input".into(),
        _ if param_name.contains("file") => "src/example.rs".into(),
        _ => "example".into(),
    }
}

fn guess_integer(param_name: &str) -> i64 {
    match param_name {
        "route_set_version" | "route_index" | "page" => 1,
        "page_size" => 6,
        "top_k" | "max_results" | "max_repos" | "max_output_chars" => 5,
        "debounce_ms" => 200,
        _ => 1,
    }
}

fn guess_number(param_name: &str) -> f64 {
    match param_name {
        "similarity_threshold" => 0.85,
        "min_score" => 0.3,
        "boost_strength" => 0.15,
        _ => 1.0,
    }
}

fn guess_boolean(param_name: &str) -> bool {
    match param_name {
        "atomic" | "reingest" | "verify" => true,
        _ => true,
    }
}

fn minimal_call_override(tool_name: &str) -> Option<Value> {
    match tool_name {
        "help" => Some(json!({
            "agent_id": "jimi",
            "stage": "plan",
            "intent": "validate a risky change before editing"
        })),
        "apply_batch" => Some(json!({
            "agent_id": "jimi",
            "edits": [{
                "file_path": "src/example.rs",
                "new_content": "...",
                "description": "Apply the validated change"
            }],
            "atomic": true,
            "reingest": true,
            "verify": true
        })),
        "document_resolve" | "document_bindings" | "document_drift" => Some(json!({
            "agent_id": "jimi",
            "path": "docs/spec.md"
        })),
        "perspective_inspect"
        | "perspective_peek"
        | "perspective_follow"
        | "perspective_affinity" => Some(json!({
            "agent_id": "jimi",
            "perspective_id": "persp_auth",
            "route_set_version": 1,
            "route_index": 1
        })),
        "runtime_overlay" => Some(json!({
            "agent_id": "jimi",
            "spans": [{
                "name": "auth.request",
                "duration_us": 1200
            }]
        })),
        "auto_ingest_start" => Some(json!({
            "agent_id": "jimi",
            "roots": ["docs"]
        })),
        "daemon_start" => Some(json!({
            "agent_id": "jimi",
            "watch_paths": ["/path/to/project"]
        })),
        _ => None,
    }
}

fn validate_against_schema(value: &Value, schema: &Value) -> bool {
    if let Some(variants) = schema.get("enum").and_then(Value::as_array) {
        if !variants.iter().any(|candidate| candidate == value) {
            return false;
        }
    }

    match schema.get("type").and_then(Value::as_str) {
        Some("object") => {
            let Some(object) = value.as_object() else {
                return false;
            };
            let properties = schema
                .get("properties")
                .and_then(Value::as_object)
                .cloned()
                .unwrap_or_default();
            let required = schema
                .get("required")
                .and_then(Value::as_array)
                .cloned()
                .unwrap_or_default();

            for key in required.iter().filter_map(Value::as_str) {
                if !object.contains_key(key) {
                    return false;
                }
            }
            if properties.is_empty() {
                return true;
            }
            for (key, child_value) in object {
                if let Some(child_schema) = properties.get(key) {
                    if !validate_against_schema(child_value, child_schema) {
                        return false;
                    }
                } else {
                    return false;
                }
            }
            true
        }
        Some("array") => {
            let Some(values) = value.as_array() else {
                return false;
            };
            let Some(item_schema) = schema.get("items") else {
                return true;
            };
            values
                .iter()
                .all(|entry| validate_against_schema(entry, item_schema))
        }
        Some("string") => value.is_string(),
        Some("integer") => value.as_i64().is_some() || value.as_u64().is_some(),
        Some("number") => value.is_number(),
        Some("boolean") => value.is_boolean(),
        _ => true,
    }
}

fn infer_category_and_glyph(tool_name: &str) -> (String, String) {
    let (category, glyph) = match tool_name {
        name if name.starts_with("document_") || name.starts_with("auto_ingest_") => {
            ("Docs", GLYPH_DIMENSION)
        }
        name if name.starts_with("daemon_")
            || name.starts_with("alerts_")
            || matches!(name, "metrics" | "persist" | "diagram") =>
        {
            ("Operations", GLYPH_SIGNAL)
        }
        name if name.starts_with("trail_") || name == "boot_memory" => ("Trail", GLYPH_PATH),
        name if name.starts_with("lock_") => ("Lock", GLYPH_CONNECTION),
        name if name.starts_with("perspective_") => ("Perspective", GLYPH_PATH),
        "heuristics_surface" | "type_trace" => ("Superpowers", GLYPH_STRUCTURE),
        _ => ("Extended", GLYPH_SIGNAL),
    };
    (category.into(), glyph.into())
}

fn inferred_next_tools(tool_name: &str) -> Vec<String> {
    match tool_name {
        "document_provider_health" => vec!["document_resolve".into(), "document_bindings".into()],
        "document_resolve" => vec!["document_bindings".into(), "document_drift".into()],
        "document_bindings" => vec!["document_drift".into(), "search".into()],
        "document_drift" => vec!["document_bindings".into(), "audit".into()],
        "auto_ingest_start" => vec!["auto_ingest_status".into(), "auto_ingest_tick".into()],
        "auto_ingest_tick" | "auto_ingest_status" => {
            vec!["document_bindings".into(), "audit".into()]
        }
        "daemon_start" => vec!["daemon_status".into(), "alerts_list".into()],
        "daemon_status" => vec!["alerts_list".into(), "audit".into()],
        "alerts_list" => vec!["alerts_ack".into(), "daemon_status".into()],
        "boot_memory" => vec!["trail_save".into(), "trail_resume".into()],
        "heuristics_surface" => vec!["batch_view".into(), "validate_plan".into()],
        "type_trace" => vec!["taint_trace".into(), "validate_plan".into()],
        "metrics" | "persist" => vec!["report".into(), "audit".into()],
        "diagram" => vec!["audit".into(), "panoramic".into()],
        _ => vec!["help".into()],
    }
}

fn tool_guidance(entry: &HelpCatalogEntry, input: &HelpInput) -> HelpGuidance {
    let required_inputs: Vec<String> = entry
        .params
        .iter()
        .filter(|param| param.required)
        .map(|param| param.name.clone())
        .collect();
    let missing_context = missing_context_for_tool(entry, input);
    let rejected_alternatives = likely_wrong_tool(entry)
        .map(|(tool, reason)| {
            vec![HelpRejectedAlternative {
                tool: tool.into(),
                reason: reason.into(),
            }]
        })
        .unwrap_or_default();
    let recovery_steps = tool_recovery_steps(entry, input);
    let why = format!(
        "{} {}",
        entry.one_liner.trim_end_matches('.'),
        if entry.returns.is_empty() {
            String::new()
        } else {
            format!("Returns: {}.", entry.returns.trim_end_matches('.'))
        }
    )
    .trim()
    .to_string();

    HelpGuidance {
        decision_type: "tool".into(),
        recommended_tools: vec![entry.name.clone()],
        recommended_sequence: entry
            .next
            .iter()
            .take(3)
            .map(|tool| HelpSequenceStep {
                tool: tool.clone(),
                reason: format!("Typical downstream follow-up after `{}`.", entry.name),
            })
            .collect(),
        required_inputs,
        missing_context,
        minimal_calls: vec![HelpMinimalCall {
            tool: entry.name.clone(),
            arguments: entry.minimal_arguments.clone(),
        }],
        rejected_alternatives,
        recovery_steps,
        next_step: entry
            .next
            .first()
            .map(|tool| format!("Run `{}` if this tool finishes cleanly.", tool)),
        why,
        confidence: 0.88,
        canonical_name: Some(entry.name.clone()),
        aliases: entry.aliases.clone(),
    }
}

fn missing_context_for_tool(entry: &HelpCatalogEntry, input: &HelpInput) -> Vec<String> {
    let mut missing = Vec::new();
    if entry
        .params
        .iter()
        .any(|param| param.required && param.name == "path")
        && input.path.is_none()
        && !entry
            .minimal_arguments
            .get("path")
            .map(Value::is_string)
            .unwrap_or(false)
    {
        missing.push("path".into());
    }
    if entry.name == "trace" && input.error_text.is_none() {
        missing.push("error_text".into());
    }
    missing
}

fn likely_wrong_tool(entry: &HelpCatalogEntry) -> Option<(&'static str, &'static str)> {
    match entry.name.as_str() {
        "search" => Some((
            "seek",
            "Use `search` only when the text or regex is already known.",
        )),
        "seek" => Some((
            "search",
            "Use `seek` when the purpose is known but the exact location is not.",
        )),
        "glob" => Some((
            "search",
            "Use `glob` when the problem is mainly about filenames or path patterns.",
        )),
        "trace" => Some((
            "search",
            "Use `trace` when you already have failure text and need the best next file, not a raw text search.",
        )),
        "validate_plan" => Some((
            "apply_batch",
            "Do not execute a connected edit before the plan has passed structural validation.",
        )),
        "apply_batch" => Some((
            "surgical_context_v2",
            "Do not use `apply_batch` while you are still discovering the edit surface.",
        )),
        _ => None,
    }
}

fn tool_recovery_steps(entry: &HelpCatalogEntry, input: &HelpInput) -> Vec<String> {
    let mut steps: Vec<String> = personality::error_recovery_notes(&entry.name)
        .iter()
        .map(|line| (*line).to_string())
        .collect();
    if entry.name == "trace" && input.error_text.is_none() {
        steps
            .push("Paste the error text or stacktrace so trace can route you structurally.".into());
    }
    if steps.is_empty() {
        steps.push("Read the missing inputs and retry with the canonical tool contract.".into());
    }
    steps
}

fn route_guidance(input: &HelpInput, workflow: bool) -> HelpGuidance {
    let stage = input
        .stage
        .or_else(|| infer_stage_from_intent(input.intent.as_deref()))
        .unwrap_or(HelpStage::Find);
    let recent = normalize_recent_tools(&input.recent_tools);
    let intent = input.intent.as_deref().unwrap_or_default();
    let path = input.path.clone();

    match stage {
        HelpStage::Orient => HelpGuidance {
            decision_type: if workflow { "sequence" } else { "tool" }.into(),
            recommended_tools: vec!["audit".into()],
            recommended_sequence: vec![
                HelpSequenceStep {
                    tool: "audit".into(),
                    reason: "Get a one-call structural orientation pass before narrower retrieval."
                        .into(),
                },
                HelpSequenceStep {
                    tool: "batch_view".into(),
                    reason: "Open the strongest files or findings after the audit narrows the surface."
                        .into(),
                },
            ],
            required_inputs: vec!["path".into()],
            missing_context: path
                .as_ref()
                .map(|_| Vec::new())
                .unwrap_or_else(|| vec!["path".into()]),
            minimal_calls: vec![HelpMinimalCall {
                tool: "audit".into(),
                arguments: json!({
                    "agent_id": "jimi",
                    "path": path.unwrap_or_else(|| "/path/to/project".into()),
                    "profile": "auto"
                }),
            }],
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "search".into(),
                reason: "Do not start with text search when you do not yet know the repo shape or likely surface.".into(),
            }],
            recovery_steps: vec!["If the repo is still too broad after audit, use batch_view or activate on the strongest finding.".into()],
            next_step: Some("Run audit first, then inspect the strongest finding.".into()),
            why: "Orientation work should start with a top-level structural pass, not a blind string hunt.".into(),
            confidence: 0.92,
            canonical_name: None,
            aliases: vec![],
        },
        HelpStage::Ground => {
            let adapter = if intent.to_lowercase().contains("light") {
                "light"
            } else {
                "universal"
            };
            HelpGuidance {
                decision_type: "sequence".into(),
                recommended_tools: vec!["ingest".into(), "document_bindings".into()],
                recommended_sequence: vec![
                    HelpSequenceStep {
                        tool: "ingest".into(),
                        reason: format!("Ground the document or spec into the graph with adapter=\"{}\".", adapter),
                    },
                    HelpSequenceStep {
                        tool: "document_bindings".into(),
                        reason: "Resolve the strongest implementation bindings after ingest.".into(),
                    },
                ],
                required_inputs: vec!["path".into()],
                missing_context: input
                    .path
                    .as_ref()
                    .map(|_| Vec::new())
                    .unwrap_or_else(|| vec!["path".into()]),
                minimal_calls: vec![HelpMinimalCall {
                    tool: "ingest".into(),
                    arguments: json!({
                        "agent_id": "jimi",
                        "path": input.path.clone().unwrap_or_else(|| "docs/spec.md".into()),
                        "adapter": adapter,
                        "mode": "merge"
                    }),
                }],
                rejected_alternatives: vec![HelpRejectedAlternative {
                    tool: "search".into(),
                    reason: "Do not search a spec or PDF blind when you can ingest and bind it to code first.".into(),
                }],
                recovery_steps: vec!["If the document is already ingested, move straight to document_bindings or document_drift.".into()],
                next_step: Some("Ingest the document, then bind it back to code.".into()),
                why: "Docs/spec work is strongest after the source is grounded into the graph.".into(),
                confidence: 0.9,
                canonical_name: None,
                aliases: vec![],
            }
        }
        HelpStage::Diagnose => HelpGuidance {
            decision_type: "sequence".into(),
            recommended_tools: vec!["trace".into(), "view".into(), "impact".into()],
            recommended_sequence: vec![
                HelpSequenceStep {
                    tool: "trace".into(),
                    reason: "Turn failure text into the strongest next inspection target.".into(),
                },
                HelpSequenceStep {
                    tool: "view".into(),
                    reason: "Open the suggested file or seam that trace identifies.".into(),
                },
                HelpSequenceStep {
                    tool: "impact".into(),
                    reason: "Check the downstream blast radius if the seam looks risky.".into(),
                },
            ],
            required_inputs: vec!["error_text".into()],
            missing_context: input
                .error_text
                .as_ref()
                .map(|_| Vec::new())
                .unwrap_or_else(|| vec!["error_text".into()]),
            minimal_calls: vec![HelpMinimalCall {
                tool: "trace".into(),
                arguments: json!({
                    "agent_id": "jimi",
                    "error_text": input
                        .error_text
                        .clone()
                        .unwrap_or_else(|| "TypeError: undefined is not a function".into())
                }),
            }],
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "search".into(),
                reason: "Failure text already contains structure; let trace turn it into a route first.".into(),
            }],
            recovery_steps: vec!["If trace points at a broad seam, open it with view and then narrow further with search or impact.".into()],
            next_step: Some("Run trace with the failure text, then inspect the suggested seam.".into()),
            why: "Diagnosis is best routed from the observed failure, not from manual keyword guessing.".into(),
            confidence: 0.94,
            canonical_name: None,
            aliases: vec![],
        },
        HelpStage::Plan => {
            let first_tool = if recent.contains("impact") || recent.contains("predict") {
                "validate_plan"
            } else {
                "impact"
            };
            let sequence = if first_tool == "validate_plan" {
                vec![
                    HelpSequenceStep {
                        tool: "validate_plan".into(),
                        reason: "The flow already has blast-radius evidence; validate the connected plan next.".into(),
                    },
                    HelpSequenceStep {
                        tool: "surgical_context_v2".into(),
                        reason: "Ground the compact proof packet if the plan still feels implicit.".into(),
                    },
                ]
            } else {
                vec![
                    HelpSequenceStep {
                        tool: "impact".into(),
                        reason: "Measure blast radius before deciding that the change is locally safe.".into(),
                    },
                    HelpSequenceStep {
                        tool: "predict".into(),
                        reason: "Check historical co-change pressure before freezing the plan.".into(),
                    },
                    HelpSequenceStep {
                        tool: "validate_plan".into(),
                        reason: "Validate the proposed file/action set against the graph.".into(),
                    },
                ]
            };
            HelpGuidance {
                decision_type: "sequence".into(),
                recommended_tools: sequence.iter().map(|step| step.tool.clone()).collect(),
                recommended_sequence: sequence,
                required_inputs: if first_tool == "validate_plan" {
                    vec!["plan".into()]
                } else {
                    vec!["node_id".into()]
                },
                missing_context: if first_tool == "validate_plan" {
                    vec!["plan".into()]
                } else {
                    vec!["node_id or affected seam".into()]
                },
                minimal_calls: vec![if first_tool == "validate_plan" {
                    HelpMinimalCall {
                        tool: "validate_plan".into(),
                        arguments: json!({
                            "agent_id": "jimi",
                            "plan": [{
                                "file": "src/example.rs",
                                "action": "modify"
                            }]
                        }),
                    }
                } else {
                    HelpMinimalCall {
                        tool: "impact".into(),
                        arguments: json!({
                            "agent_id": "jimi",
                            "node_id": "file::src/example.rs"
                        }),
                    }
                }],
                rejected_alternatives: vec![HelpRejectedAlternative {
                    tool: "apply_batch".into(),
                    reason: "Do not execute before impact and plan validity are strong enough.".into(),
                }],
                recovery_steps: vec!["If the plan is still vague, use surgical_context_v2 to ground the coupled surface before validating again.".into()],
                next_step: Some(format!("Run `{}` first for this planning stage.", first_tool)),
                why: "Planning mode should prove the change surface before any write surface is allowed to execute.".into(),
                confidence: 0.86,
                canonical_name: None,
                aliases: vec![],
            }
        }
        HelpStage::Edit => {
            let ready_to_execute = recent.contains("validate_plan") || recent.contains("surgical_context_v2");
            if ready_to_execute {
                HelpGuidance {
                    decision_type: "sequence".into(),
                    recommended_tools: vec!["apply_batch".into(), "predict".into()],
                    recommended_sequence: vec![
                        HelpSequenceStep {
                            tool: "apply_batch".into(),
                            reason: "The flow already passed through proof; execute atomically and verify.".into(),
                        },
                        HelpSequenceStep {
                            tool: "predict".into(),
                            reason: "Check ripple effects after the write surface completes.".into(),
                        },
                    ],
                    required_inputs: vec!["edits".into()],
                    missing_context: vec!["edits".into()],
                    minimal_calls: vec![HelpMinimalCall {
                        tool: "apply_batch".into(),
                        arguments: json!({
                            "agent_id": "jimi",
                            "edits": [{
                                "file_path": "src/example.rs",
                                "new_content": "...",
                                "description": "Apply the validated change"
                            }],
                            "atomic": true,
                            "reingest": true,
                            "verify": true
                        }),
                    }],
                    rejected_alternatives: vec![HelpRejectedAlternative {
                        tool: "search".into(),
                        reason: "The search phase should already be over if you are at execution.".into(),
                    }],
                    recovery_steps: vec!["If execution still feels premature, step back to surgical_context_v2 or validate_plan instead of forcing the write.".into()],
                    next_step: Some("Run apply_batch with the validated edit set.".into()),
                    why: "Edit mode can execute once the proof surface is already grounded and validated.".into(),
                    confidence: 0.87,
                    canonical_name: None,
                    aliases: vec![],
                }
            } else {
                HelpGuidance {
                    decision_type: "sequence".into(),
                    recommended_tools: vec![
                        "surgical_context_v2".into(),
                        "validate_plan".into(),
                        "apply_batch".into(),
                    ],
                    recommended_sequence: vec![
                        HelpSequenceStep {
                            tool: "surgical_context_v2".into(),
                            reason: "Ground the compact connected edit packet before writing.".into(),
                        },
                        HelpSequenceStep {
                            tool: "validate_plan".into(),
                            reason: "Validate the proposed change set once the surface is explicit.".into(),
                        },
                        HelpSequenceStep {
                            tool: "apply_batch".into(),
                            reason: "Execute only after the plan is no longer proving.".into(),
                        },
                    ],
                    required_inputs: vec!["query".into()],
                    missing_context: vec!["query".into()],
                    minimal_calls: vec![HelpMinimalCall {
                        tool: "surgical_context_v2".into(),
                        arguments: json!({
                            "agent_id": "jimi",
                            "query": input
                                .intent
                                .clone()
                                .unwrap_or_else(|| "chat handler message routing".into())
                        }),
                    }],
                    rejected_alternatives: vec![HelpRejectedAlternative {
                        tool: "apply_batch".into(),
                        reason: "Do not write while the edit surface is still implicit.".into(),
                    }],
                    recovery_steps: vec!["If you already know the exact file set, move to validate_plan before executing.".into()],
                    next_step: Some("Ground the edit surface with surgical_context_v2.".into()),
                    why: "Edit mode starts with proof-focused grounding unless the flow already passed validation.".into(),
                    confidence: 0.85,
                    canonical_name: None,
                    aliases: vec![],
                }
            }
        }
        HelpStage::Review => HelpGuidance {
            decision_type: "sequence".into(),
            recommended_tools: vec!["timeline".into(), "impact".into(), "batch_view".into()],
            recommended_sequence: vec![
                HelpSequenceStep {
                    tool: "timeline".into(),
                    reason: "Start with recent change history when the task is to review existing change.".into(),
                },
                HelpSequenceStep {
                    tool: "impact".into(),
                    reason: "Measure blast radius on the seam that changed.".into(),
                },
                HelpSequenceStep {
                    tool: "batch_view".into(),
                    reason: "Read the tight set of files once history and impact narrow the surface.".into(),
                },
            ],
            required_inputs: vec!["path".into()],
            missing_context: input
                .path
                .as_ref()
                .map(|_| Vec::new())
                .unwrap_or_else(|| vec!["path".into()]),
            minimal_calls: vec![HelpMinimalCall {
                tool: "timeline".into(),
                arguments: json!({
                    "agent_id": "jimi",
                    "path": input.path.clone().unwrap_or_else(|| "src/example.rs".into())
                }),
            }],
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "search".into(),
                reason: "Review is about changed seams and history, not a fresh blind query.".into(),
            }],
            recovery_steps: vec!["If you do not yet know the changed seam, start from audit(path) instead.".into()],
            next_step: Some("Run timeline on the changed seam, then inspect impact.".into()),
            why: "Review mode should pull in history and blast radius before opening a broad file set.".into(),
            confidence: 0.8,
            canonical_name: None,
            aliases: vec![],
        },
        HelpStage::Operate => HelpGuidance {
            decision_type: "sequence".into(),
            recommended_tools: vec!["audit".into(), "daemon_status".into(), "alerts_list".into()],
            recommended_sequence: vec![
                HelpSequenceStep {
                    tool: "audit".into(),
                    reason: "Get a current operational health pass over topology, verification, and git state.".into(),
                },
                HelpSequenceStep {
                    tool: "daemon_status".into(),
                    reason: "Check whether structural monitoring is already active.".into(),
                },
                HelpSequenceStep {
                    tool: "alerts_list".into(),
                    reason: "Surface pending alerts if the daemon is already watching.".into(),
                },
            ],
            required_inputs: vec!["path".into()],
            missing_context: input
                .path
                .as_ref()
                .map(|_| Vec::new())
                .unwrap_or_else(|| vec!["path".into()]),
            minimal_calls: vec![HelpMinimalCall {
                tool: "audit".into(),
                arguments: json!({
                    "agent_id": "jimi",
                    "path": input.path.clone().unwrap_or_else(|| "/path/to/project".into()),
                    "profile": "production"
                }),
            }],
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "search".into(),
                reason: "Operations work needs system state, not just text retrieval.".into(),
            }],
            recovery_steps: vec!["If you need continuous monitoring, move from audit to daemon_start rather than polling search.".into()],
            next_step: Some("Run audit first, then inspect daemon status and alerts.".into()),
            why: "Operations mode should start from repo or runtime health signals rather than content search.".into(),
            confidence: 0.84,
            canonical_name: None,
            aliases: vec![],
        },
        HelpStage::Handoff => {
            let resume = input.intent.as_deref().unwrap_or_default().to_lowercase().contains("resume");
            if resume {
                HelpGuidance {
                    decision_type: "sequence".into(),
                    recommended_tools: vec!["trail_resume".into(), "coverage_session".into()],
                    recommended_sequence: vec![
                        HelpSequenceStep {
                            tool: "trail_resume".into(),
                            reason: "Rehydrate the saved investigation and recover the next likely step.".into(),
                        },
                        HelpSequenceStep {
                            tool: "coverage_session".into(),
                            reason: "Check what has and has not been inspected in this session.".into(),
                        },
                    ],
                    required_inputs: vec!["trail_id".into()],
                    missing_context: vec!["trail_id".into()],
                    minimal_calls: vec![HelpMinimalCall {
                        tool: "trail_resume".into(),
                        arguments: json!({
                            "agent_id": "jimi",
                            "trail_id": "trail_auth_flow"
                        }),
                    }],
                    rejected_alternatives: vec![HelpRejectedAlternative {
                        tool: "search".into(),
                        reason: "Do not restart a saved investigation as a blind fresh search.".into(),
                    }],
                    recovery_steps: vec!["If there is no saved trail yet, use trail_save before ending the current session.".into()],
                    next_step: Some("Resume the saved trail, then inspect the suggested next move.".into()),
                    why: "Handoff or resume mode should preserve and reactivate investigation state instead of rebuilding it.".into(),
                    confidence: 0.82,
                    canonical_name: None,
                    aliases: vec![],
                }
            } else {
                HelpGuidance {
                    decision_type: "sequence".into(),
                    recommended_tools: vec!["trail_save".into(), "coverage_session".into()],
                    recommended_sequence: vec![
                        HelpSequenceStep {
                            tool: "trail_save".into(),
                            reason: "Persist the investigation state before handing off or closing the session.".into(),
                        },
                        HelpSequenceStep {
                            tool: "coverage_session".into(),
                            reason: "Capture what was covered and what remains unread.".into(),
                        },
                    ],
                    required_inputs: vec!["title".into()],
                    missing_context: vec![],
                    minimal_calls: vec![HelpMinimalCall {
                        tool: "trail_save".into(),
                        arguments: json!({
                            "agent_id": "jimi",
                            "title": "Auth flow investigation"
                        }),
                    }],
                    rejected_alternatives: vec![HelpRejectedAlternative {
                        tool: "search".into(),
                        reason: "Search does not preserve continuity or handoff state.".into(),
                    }],
                    recovery_steps: vec!["If the work must stay hot across restarts, complement the trail with boot_memory for short canonical doctrine.".into()],
                    next_step: Some("Save the trail before handing the work to another agent or session.".into()),
                    why: "Handoff mode should preserve continuity explicitly instead of trusting chat history alone.".into(),
                    confidence: 0.83,
                    canonical_name: None,
                    aliases: vec![],
                }
            }
        }
        HelpStage::Find => {
            let lowered = intent.to_lowercase();
            let (tool, reason, reject_tool, reject_reason) = if lowered.contains("regex")
                || lowered.contains("exact text")
                || lowered.contains("symbol")
                || lowered.contains("identifier")
            {
                (
                    "search",
                    "The query already sounds textual or regex-shaped.",
                    "seek",
                    "Seek is for purpose-first discovery when exact text is not known.",
                )
            } else if lowered.contains("file")
                || lowered.contains("filename")
                || lowered.contains("path")
                || lowered.contains("glob")
            {
                (
                    "glob",
                    "The task is mainly about narrowing a file set by path shape.",
                    "search",
                    "Search is weaker when the problem is path-pattern selection, not content.",
                )
            } else if lowered.contains("flow")
                || lowered.contains("subsystem")
                || lowered.contains("neighborhood")
                || lowered.contains("connected")
            {
                (
                    "activate",
                    "The request sounds relational or subsystem-oriented.",
                    "search",
                    "Text search will miss connected context when the problem is neighborhood-driven.",
                )
            } else {
                (
                    "seek",
                    "The goal is known but the location is not.",
                    "search",
                    "Search assumes exact text, while this is purpose-first retrieval.",
                )
            };
            let arguments = match tool {
                "search" => json!({
                    "agent_id": "jimi",
                    "query": if intent.is_empty() { "retry backoff" } else { intent },
                    "mode": "literal"
                }),
                "glob" => json!({
                    "agent_id": "jimi",
                    "pattern": "**/*.rs",
                    "scope": input.path.clone().unwrap_or_else(|| "src".into())
                }),
                "activate" => json!({
                    "agent_id": "jimi",
                    "query": if intent.is_empty() { "authentication flow" } else { intent }
                }),
                _ => json!({
                    "agent_id": "jimi",
                    "query": if intent.is_empty() { "where retry backoff is decided" } else { intent }
                }),
            };

            HelpGuidance {
                decision_type: if workflow { "sequence" } else { "tool" }.into(),
                recommended_tools: vec![tool.into()],
                recommended_sequence: vec![
                    HelpSequenceStep {
                        tool: tool.into(),
                        reason: reason.into(),
                    },
                    HelpSequenceStep {
                        tool: if tool == "glob" { "batch_view" } else { "view" }.into(),
                        reason: "Open the strongest result after retrieval narrows the surface."
                            .into(),
                    },
                ],
                required_inputs: vec![if tool == "glob" { "pattern" } else { "query" }.into()],
                missing_context: if intent.is_empty() {
                    vec!["intent or query".into()]
                } else {
                    Vec::new()
                },
                minimal_calls: vec![HelpMinimalCall {
                    tool: tool.into(),
                    arguments,
                }],
                rejected_alternatives: vec![HelpRejectedAlternative {
                    tool: reject_tool.into(),
                    reason: reject_reason.into(),
                }],
                recovery_steps: vec!["If the first retrieval is too broad, tighten scope/path or switch to the rejected alternative only when the retrieval shape changes.".into()],
                next_step: Some(format!("Run `{}` first, then open the strongest result.", tool)),
                why: "Find mode should choose retrieval shape from the kind of uncertainty: text, path, purpose, or connected neighborhood.".into(),
                confidence: 0.89,
                canonical_name: None,
                aliases: vec![],
            }
        }
    }
}

fn recovery_guidance(input: &HelpInput) -> HelpGuidance {
    let error_text = input.error_text.clone().unwrap_or_default();
    let lowered = error_text.to_lowercase();

    if lowered.contains("node not found")
        || lowered.contains("no valid node ids found")
        || (lowered.contains("not found")
            && matches!(
                input.tool_name.as_deref(),
                Some(
                    "impact" | "predict" | "counterfactual" | "why" | "fingerprint" | "type_trace"
                )
            ))
    {
        return HelpGuidance {
            decision_type: "recovery".into(),
            recommended_tools: vec!["search".into(), "seek".into()],
            recommended_sequence: vec![
                HelpSequenceStep {
                    tool: "search".into(),
                    reason: "Recover the exact node_id or file identity when you already know the text or path shape.".into(),
                },
                HelpSequenceStep {
                    tool: "seek".into(),
                    reason: "Recover the seam by purpose when you do not know the exact node identifier.".into(),
                },
            ],
            required_inputs: vec!["node_id".into()],
            missing_context: vec!["A canonical node_id present in the current graph".into()],
            minimal_calls: vec![HelpMinimalCall {
                tool: "search".into(),
                arguments: json!({
                    "agent_id": "jimi",
                    "query": "file::src/example.rs",
                    "mode": "literal"
                }),
            }],
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "blind retry".into(),
                reason: "Retrying the same analysis tool will fail again until the target resolves to a live node.".into(),
            }],
            recovery_steps: vec![
                "Recover the canonical node identity first.".into(),
                "Retry the original analysis tool only after the graph resolves the node_id.".into(),
            ],
            next_step: Some("Recover the canonical node_id with search or seek, then retry the analysis tool.".into()),
            why: "Node-scoped tools need a live graph identity before they can reason over change or causality.".into(),
            confidence: 0.88,
            canonical_name: input.tool_name.clone(),
            aliases: vec![],
        };
    }

    if lowered.contains("missing field")
        || lowered.contains("missing required")
        || lowered.contains("invalid type")
    {
        let tool_name = input.tool_name.as_deref().unwrap_or("help");
        let entry = catalog_entry(tool_name);
        let required_inputs = entry
            .as_ref()
            .map(|entry| {
                entry
                    .params
                    .iter()
                    .filter(|param| param.required)
                    .map(|param| param.name.clone())
                    .collect::<Vec<_>>()
            })
            .unwrap_or_else(|| vec!["tool_name".into()]);
        let minimal_calls = entry
            .as_ref()
            .map(|entry| {
                vec![HelpMinimalCall {
                    tool: entry.name.clone(),
                    arguments: entry.minimal_arguments.clone(),
                }]
            })
            .unwrap_or_default();

        return HelpGuidance {
            decision_type: "recovery".into(),
            recommended_tools: entry
                .as_ref()
                .map(|entry| vec![entry.name.clone()])
                .unwrap_or_else(|| vec!["help".into()]),
            recommended_sequence: vec![HelpSequenceStep {
                tool: entry
                    .as_ref()
                    .map(|entry| entry.name.clone())
                    .unwrap_or_else(|| "help".into()),
                reason: "Retry with the minimal valid contract instead of reformulating blindly.".into(),
            }],
            required_inputs,
            missing_context: vec!["One or more required inputs are missing or malformed.".into()],
            minimal_calls,
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "blind retry".into(),
                reason: "Do not repeat the same malformed call when the contract already tells you what is missing.".into(),
            }],
            recovery_steps: vec![
                "Read the required inputs and default knobs.".into(),
                "Retry with the canonical minimal call first, then widen only if needed.".into(),
            ],
            next_step: Some("Retry with the minimal valid call.".into()),
            why: "Schema or input errors should route you back to the live contract immediately.".into(),
            confidence: 0.91,
            canonical_name: entry.as_ref().map(|entry| entry.name.clone()),
            aliases: entry
                .as_ref()
                .map(|entry| entry.aliases.clone())
                .unwrap_or_default(),
        };
    }

    if lowered.contains("path") || lowered.contains("no such file") || lowered.contains("scope") {
        return HelpGuidance {
            decision_type: "recovery".into(),
            recommended_tools: vec!["audit".into(), "glob".into()],
            recommended_sequence: vec![
                HelpSequenceStep {
                    tool: "audit".into(),
                    reason: "Normalize the workspace root or repo path before narrower operations.".into(),
                },
                HelpSequenceStep {
                    tool: "glob".into(),
                    reason: "Confirm the path pattern once the workspace root is right.".into(),
                },
            ],
            required_inputs: vec!["path".into()],
            missing_context: vec!["A canonical path or scope".into()],
            minimal_calls: vec![HelpMinimalCall {
                tool: "audit".into(),
                arguments: json!({
                    "agent_id": "jimi",
                    "path": input.path.clone().unwrap_or_else(|| "/path/to/project".into())
                }),
            }],
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "search".into(),
                reason: "Do not keep searching until the path or scope is canonicalized.".into(),
            }],
            recovery_steps: vec!["Fix the path/scope first, then re-run the narrower tool.".into()],
            next_step: Some("Canonicalize the path with audit, then narrow again.".into()),
            why: "Path and scope failures usually mean the tool choice is fine but the target is not canonical yet.".into(),
            confidence: 0.83,
            canonical_name: None,
            aliases: vec![],
        };
    }

    if !error_text.is_empty() {
        return HelpGuidance {
            decision_type: "recovery".into(),
            recommended_tools: vec!["trace".into(), "help".into()],
            recommended_sequence: vec![
                HelpSequenceStep {
                    tool: "trace".into(),
                    reason: "Turn the observed failure into the strongest next file or seam.".into(),
                },
                HelpSequenceStep {
                    tool: "help".into(),
                    reason: "Use tool-specific help only after the failure is mapped to the right seam.".into(),
                },
            ],
            required_inputs: vec!["error_text".into()],
            missing_context: vec![],
            minimal_calls: vec![HelpMinimalCall {
                tool: "trace".into(),
                arguments: json!({
                    "agent_id": "jimi",
                    "error_text": error_text
                }),
            }],
            rejected_alternatives: vec![HelpRejectedAlternative {
                tool: "search".into(),
                reason: "Do not translate runtime failure back into blind keyword search until trace has routed it.".into(),
            }],
            recovery_steps: vec!["If trace returns a broad seam, open it and then narrow with search or impact.".into()],
            next_step: Some("Trace the failure text first.".into()),
            why: "Observed failures should route through trace before any generic retry strategy.".into(),
            confidence: 0.87,
            canonical_name: None,
            aliases: vec![],
        };
    }

    HelpGuidance {
        decision_type: "recovery".into(),
        recommended_tools: vec!["help".into()],
        recommended_sequence: vec![HelpSequenceStep {
            tool: "help".into(),
            reason: "Use stage or intent to route the next action when the failure is still underspecified.".into(),
        }],
        required_inputs: vec!["stage or intent".into()],
        missing_context: vec!["Observed error or clearer intent".into()],
        minimal_calls: vec![HelpMinimalCall {
            tool: "help".into(),
            arguments: json!({
                "agent_id": "jimi",
                "stage": "find",
                "intent": "where retry backoff is decided"
            }),
        }],
        rejected_alternatives: vec![],
        recovery_steps: vec!["Give help either the tool name, the stage, the intent, or the error text.".into()],
        next_step: Some("Provide stage, intent, or error text for a tighter route.".into()),
        why: "Recovery mode needs at least one strong signal: tool, stage, intent, or error.".into(),
        confidence: 0.52,
        canonical_name: None,
        aliases: vec![],
    }
}

fn infer_stage_from_intent(intent: Option<&str>) -> Option<HelpStage> {
    let lowered = intent?.to_lowercase();
    if lowered.contains("error") || lowered.contains("trace") || lowered.contains("stack") {
        Some(HelpStage::Diagnose)
    } else if lowered.contains("spec")
        || lowered.contains("doc")
        || lowered.contains("pdf")
        || lowered.contains("light")
    {
        Some(HelpStage::Ground)
    } else if lowered.contains("plan") || lowered.contains("blast") || lowered.contains("impact") {
        Some(HelpStage::Plan)
    } else if lowered.contains("edit")
        || lowered.contains("write")
        || lowered.contains("patch")
        || lowered.contains("batch")
    {
        Some(HelpStage::Edit)
    } else if lowered.contains("review") || lowered.contains("diff") || lowered.contains("history")
    {
        Some(HelpStage::Review)
    } else if lowered.contains("operate")
        || lowered.contains("monitor")
        || lowered.contains("daemon")
        || lowered.contains("alert")
    {
        Some(HelpStage::Operate)
    } else if lowered.contains("handoff")
        || lowered.contains("resume")
        || lowered.contains("continue")
        || lowered.contains("trail")
    {
        Some(HelpStage::Handoff)
    } else if lowered.contains("orient") || lowered.contains("understand the repo") {
        Some(HelpStage::Orient)
    } else {
        Some(HelpStage::Find)
    }
}

fn normalize_recent_tools(recent_tools: &[String]) -> BTreeSet<String> {
    recent_tools
        .iter()
        .map(|tool| {
            tool.trim()
                .trim_start_matches("m1nd.")
                .trim_start_matches("m1nd_")
        })
        .filter(|tool| !tool.is_empty())
        .map(str::to_string)
        .collect()
}

fn overview_cards(input: &HelpInput) -> Vec<OverviewCard> {
    let repo_path = input
        .path
        .clone()
        .unwrap_or_else(|| "/path/to/project".into());
    vec![
        OverviewCard {
            title: "Need repo orientation",
            why: "Start from a one-call structural pass before narrower retrieval.",
            tool: "audit",
            arguments: json!({
                "agent_id": "jimi",
                "path": repo_path,
                "profile": "auto"
            }),
            reject_tool: "search",
            reject_reason: "Text search is too early when the repo surface is still unknown.",
        },
        OverviewCard {
            title: "Know the purpose, not the location",
            why: "Route by intent before opening files.",
            tool: "seek",
            arguments: json!({
                "agent_id": "jimi",
                "query": "where retry backoff is decided"
            }),
            reject_tool: "search",
            reject_reason: "Search assumes exact text and misses intent-first discovery.",
        },
        OverviewCard {
            title: "Have docs, spec, wiki, or PDF",
            why: "Ground the document first, then bind it back to code.",
            tool: "ingest",
            arguments: json!({
                "agent_id": "jimi",
                "path": "docs/spec.md",
                "adapter": "universal",
                "mode": "merge"
            }),
            reject_tool: "search",
            reject_reason: "Searching an ungrounded document loses bindings and drift detection.",
        },
        OverviewCard {
            title: "Have runtime failure text",
            why: "Turn the error into the best next seam before manual inspection.",
            tool: "trace",
            arguments: json!({
                "agent_id": "jimi",
                "error_text": "TypeError: undefined is not a function"
            }),
            reject_tool: "search",
            reject_reason: "Trace is a better first move when the failure text is already available.",
        },
        OverviewCard {
            title: "Need to plan a risky change",
            why: "Prove the seam before any write surface executes.",
            tool: "validate_plan",
            arguments: json!({
                "agent_id": "jimi",
                "plan": [{
                    "file": "src/example.rs",
                    "action": "modify"
                }]
            }),
            reject_tool: "apply_batch",
            reject_reason: "Batch execution is too early until the change stops proving and becomes safe to execute.",
        },
        OverviewCard {
            title: "Need deep architecture or risk lens",
            why: "Use RETROBUILDER tools for hidden co-change, taint, duplication, refactor seams, or runtime heat before direct proof.",
            tool: "ghost_edges",
            arguments: json!({
                "agent_id": "jimi",
                "depth": "30d",
                "scope": "src",
                "top_k": 10
            }),
            reject_tool: "search",
            reject_reason: "Raw text search cannot surface hidden git co-change, taint paths, structural twins, refactor communities, or runtime overlays.",
        },
        OverviewCard {
            title: "Need continuity or handoff",
            why: "Persist or resume investigation state explicitly.",
            tool: "trail_save",
            arguments: json!({
                "agent_id": "jimi",
                "title": "Auth flow investigation"
            }),
            reject_tool: "search",
            reject_reason: "Search does not preserve continuity across sessions or agents.",
        },
    ]
}

fn render_tool(entry: &HelpCatalogEntry, guidance: &HelpGuidance, compact: bool) -> String {
    let width = 72;
    let mut out = String::new();

    out.push_str(&gradient_top_border(width));
    out.push('\n');
    out.push_str(&format!(
        "{}{} {}  {}{}{}\n",
        ANSI_CYAN,
        entry.glyph,
        entry.category.to_uppercase(),
        ANSI_BOLD,
        entry.name,
        ANSI_RESET
    ));
    out.push_str(&format!(
        "{}canonical: {}{}",
        ANSI_DIM, entry.name, ANSI_RESET
    ));
    if !entry.aliases.is_empty() {
        out.push_str(&format!(
            "{}  aliases: {}{}{}\n",
            ANSI_DIM,
            ANSI_BOLD,
            entry.aliases.join(", "),
            ANSI_RESET
        ));
    } else {
        out.push('\n');
    }
    out.push('\n');

    render_section_title(&mut out, "USE THIS NOW");
    out.push_str(&format!(
        "  {}{}{}{}\n\n",
        ANSI_CYAN, ANSI_BOLD, entry.name, ANSI_RESET
    ));

    render_section_title(&mut out, "WHY");
    out.push_str(&format!(
        "  {}- {}{}\n",
        ANSI_DIM, entry.one_liner, ANSI_RESET
    ));
    if !entry.returns.is_empty() {
        out.push_str(&format!(
            "  {}- Returns: {}{}\n",
            ANSI_DIM, entry.returns, ANSI_RESET
        ));
    }
    for line in personality::when_to_use(&entry.name)
        .iter()
        .take(if compact { 1 } else { 2 })
    {
        out.push_str(&format!("  {}- {}{}\n", ANSI_DIM, line, ANSI_RESET));
    }
    out.push('\n');

    render_section_title(&mut out, "REQUIRED INPUT");
    let required: Vec<&HelpParamSpec> =
        entry.params.iter().filter(|param| param.required).collect();
    if required.is_empty() {
        out.push_str(&format!(
            "  {}- no required inputs beyond the call itself{}\n",
            ANSI_DIM, ANSI_RESET
        ));
    } else {
        for param in required {
            out.push_str(&format!(
                "  {}- {}{}{} — {}{}\n",
                ANSI_DIM, ANSI_BOLD, param.name, ANSI_RESET, param.description, ANSI_RESET
            ));
        }
    }
    let optional: Vec<String> = entry
        .params
        .iter()
        .filter(|param| !param.required)
        .map(|param| param.name.clone())
        .collect();
    if !optional.is_empty() {
        let max = if compact { 4 } else { 8 };
        out.push_str(&format!(
            "  {}- optional knobs: {}{}\n",
            ANSI_DIM,
            optional
                .into_iter()
                .take(max)
                .collect::<Vec<_>>()
                .join(", "),
            ANSI_RESET
        ));
    }
    out.push('\n');

    render_section_title(&mut out, "MINIMAL CALL");
    if let Some(call) = guidance.minimal_calls.first() {
        out.push_str(&format!(
            "  {}{}{}({})\n\n",
            ANSI_MAGENTA,
            call.tool,
            ANSI_RESET,
            render_inline_json(&call.arguments)
        ));
    }

    render_section_title(&mut out, "DO NOT USE IF");
    let avoid = personality::avoid_when(&entry.name);
    if avoid.is_empty() && guidance.rejected_alternatives.is_empty() {
        out.push_str(&format!(
            "  {}- the retrieval or reasoning shape you need is fundamentally different{}\n\n",
            ANSI_DIM, ANSI_RESET
        ));
    } else {
        for line in avoid.iter().take(if compact { 1 } else { 2 }) {
            out.push_str(&format!("  {}- {}{}\n", ANSI_DIM, line, ANSI_RESET));
        }
        for alternative in guidance
            .rejected_alternatives
            .iter()
            .take(if compact { 1 } else { 2 })
        {
            out.push_str(&format!(
                "  {}- do not substitute with `{}`: {}{}\n",
                ANSI_DIM, alternative.tool, alternative.reason, ANSI_RESET
            ));
        }
        out.push('\n');
    }

    render_section_title(&mut out, "IF THIS FAILS");
    for step in guidance
        .recovery_steps
        .iter()
        .take(if compact { 2 } else { 4 })
    {
        out.push_str(&format!("  {}- {}{}\n", ANSI_DIM, step, ANSI_RESET));
    }
    out.push('\n');

    render_section_title(&mut out, "NEXT");
    let next = if entry.next.is_empty() {
        vec!["help".into()]
    } else {
        entry.next.clone()
    };
    for tool in next.iter().take(if compact { 2 } else { 4 }) {
        out.push_str(&format!(
            "  {}- {}{}{}{}\n",
            ANSI_CYAN, ANSI_BOLD, tool, ANSI_RESET, ANSI_RESET
        ));
    }
    out.push('\n');
    out.push_str(&gradient_bottom_border(width));
    out.push('\n');
    out
}

fn render_overview(
    total_tools: usize,
    cards: &[OverviewCard],
    compact: bool,
    show_temponizer: bool,
) -> String {
    let width = 72;
    let mut out = String::new();
    out.push_str(&gradient_top_border(width));
    out.push('\n');
    out.push_str(&format!(
        "{}{}  m1nd help{}\n",
        ANSI_BOLD, ANSI_CYAN, ANSI_RESET
    ));
    out.push_str(&format!(
        "{}  {} live tools. route by stage, intent, or failure.{}\n",
        ANSI_DIM, total_tools, ANSI_RESET
    ));
    out.push_str(&format!(
        "{}  structure · change · docs · operations · continuity{}\n\n",
        ANSI_DIM, ANSI_RESET
    ));

    render_section_title(&mut out, "30-SECOND MAP");
    for card in cards.iter().take(if compact { 4 } else { cards.len() }) {
        out.push_str(&format!(
            "  {}- {}{}{} -> {}{}{} — {}{}\n",
            ANSI_DIM,
            ANSI_BOLD,
            card.title,
            ANSI_RESET,
            ANSI_CYAN,
            card.tool,
            ANSI_RESET,
            card.why,
            ANSI_RESET
        ));
    }
    out.push('\n');

    render_section_title(&mut out, "HOW TO ASK");
    out.push_str(&format!(
        "  {}- help(tool_name=\"activate\") for exact tool doctrine{}\n",
        ANSI_DIM, ANSI_RESET
    ));
    out.push_str(&format!(
        "  {}- help(stage=\"plan\", intent=\"validate a risky change\") for a route{}\n",
        ANSI_DIM, ANSI_RESET
    ));
    out.push_str(&format!(
        "  {}- help(error_text=\"...\") for failure recovery{}\n",
        ANSI_DIM, ANSI_RESET
    ));
    out.push_str(&format!(
        "  {}- help(mode=\"workflow\", stage=\"edit\") for a full sequence{}\n",
        ANSI_DIM, ANSI_RESET
    ));

    if show_temponizer {
        out.push('\n');
        render_section_title(&mut out, "TEMPONIZER");
        out.push_str(&format!(
            "  {}- you estimate in human-time but execute much faster; fill the recovered time with more deliverables, not padding.{}\n",
            ANSI_DIM, ANSI_RESET
        ));
    }

    out.push('\n');
    out.push_str(&gradient_bottom_border(width));
    out.push('\n');
    out
}

fn render_guidance(title: &str, guidance: &HelpGuidance, compact: bool) -> String {
    let width = 72;
    let mut out = String::new();
    out.push_str(&gradient_top_border(width));
    out.push('\n');
    out.push_str(&format!(
        "{}{}  help {}{}\n\n",
        ANSI_BOLD, ANSI_CYAN, title, ANSI_RESET
    ));

    render_section_title(&mut out, "USE THIS NOW");
    for tool in guidance
        .recommended_tools
        .iter()
        .take(if compact { 2 } else { 4 })
    {
        out.push_str(&format!(
            "  {}- {}{}{}{}\n",
            ANSI_CYAN, ANSI_BOLD, tool, ANSI_RESET, ANSI_RESET
        ));
    }
    out.push('\n');

    render_section_title(&mut out, "WHY");
    out.push_str(&format!(
        "  {}- {}{}\n\n",
        ANSI_DIM, guidance.why, ANSI_RESET
    ));

    render_section_title(&mut out, "REQUIRED INPUT");
    if guidance.required_inputs.is_empty() {
        out.push_str(&format!(
            "  {}- no extra input beyond the current state{}\n",
            ANSI_DIM, ANSI_RESET
        ));
    } else {
        out.push_str(&format!(
            "  {}- {}{}\n",
            ANSI_DIM,
            guidance.required_inputs.join(", "),
            ANSI_RESET
        ));
    }
    if !guidance.missing_context.is_empty() {
        out.push_str(&format!(
            "  {}- still missing: {}{}\n",
            ANSI_DIM,
            guidance.missing_context.join(", "),
            ANSI_RESET
        ));
    }
    out.push('\n');

    render_section_title(&mut out, "MINIMAL CALL");
    for call in guidance
        .minimal_calls
        .iter()
        .take(if compact { 1 } else { 3 })
    {
        out.push_str(&format!(
            "  {}{}{}({})\n",
            ANSI_MAGENTA,
            call.tool,
            ANSI_RESET,
            render_inline_json(&call.arguments)
        ));
    }
    out.push('\n');

    render_section_title(&mut out, "DO NOT USE IF");
    if guidance.rejected_alternatives.is_empty() {
        out.push_str(&format!(
            "  {}- the uncertainty shape has changed and now needs a different first tool{}\n",
            ANSI_DIM, ANSI_RESET
        ));
    } else {
        for alternative in guidance
            .rejected_alternatives
            .iter()
            .take(if compact { 1 } else { 3 })
        {
            out.push_str(&format!(
                "  {}- `{}`: {}{}\n",
                ANSI_DIM, alternative.tool, alternative.reason, ANSI_RESET
            ));
        }
    }
    out.push('\n');

    render_section_title(&mut out, "IF THIS FAILS");
    for step in guidance
        .recovery_steps
        .iter()
        .take(if compact { 2 } else { 4 })
    {
        out.push_str(&format!("  {}- {}{}\n", ANSI_DIM, step, ANSI_RESET));
    }
    out.push('\n');

    render_section_title(&mut out, "NEXT");
    for step in guidance
        .recommended_sequence
        .iter()
        .take(if compact { 2 } else { 4 })
    {
        out.push_str(&format!(
            "  {}- {}{}{} — {}{}\n",
            ANSI_CYAN, ANSI_BOLD, step.tool, ANSI_RESET, step.reason, ANSI_RESET
        ));
    }
    if let Some(next_step) = &guidance.next_step {
        out.push_str(&format!("  {}- {}{}\n", ANSI_DIM, next_step, ANSI_RESET));
    }
    out.push('\n');
    out.push_str(&gradient_bottom_border(width));
    out.push('\n');
    out
}

fn render_unknown_tool(query: &str, suggestions: &[String]) -> String {
    let width = 72;
    let mut out = String::new();
    out.push_str(&gradient_top_border(width));
    out.push('\n');
    out.push_str(&format!(
        "{}{}  help recovery{}\n\n",
        ANSI_BOLD, ANSI_RED, ANSI_RESET
    ));
    out.push_str(&format!(
        "{}unknown tool:{} {}{}{}\n",
        ANSI_RED, ANSI_RESET, ANSI_BOLD, query, ANSI_RESET
    ));
    if suggestions.is_empty() {
        out.push_str(&format!(
            "{}no similar live tool names found. Use stage or intent instead of guessing.{}\n",
            ANSI_DIM, ANSI_RESET
        ));
    } else {
        out.push_str(&format!(
            "{}did you mean:{} {}{}{}\n",
            ANSI_DIM,
            ANSI_RESET,
            ANSI_BOLD,
            suggestions.join(", "),
            ANSI_RESET
        ));
    }
    out.push('\n');
    out.push_str(&gradient_bottom_border(width));
    out.push('\n');
    out
}

fn render_section_title(out: &mut String, title: &str) {
    out.push_str(&format!("{}{}{}\n", ANSI_GOLD, title, ANSI_RESET));
}

fn render_inline_json(value: &Value) -> String {
    serde_json::to_string(value).unwrap_or_else(|_| "{}".into())
}

fn levenshtein_distance(a: &str, b: &str) -> usize {
    let a: Vec<char> = a.chars().collect();
    let b: Vec<char> = b.chars().collect();
    let mut prev: Vec<usize> = (0..=b.len()).collect();
    let mut curr = vec![0; b.len() + 1];

    for (i, ch_a) in a.iter().enumerate() {
        curr[0] = i + 1;
        for (j, ch_b) in b.iter().enumerate() {
            let cost = usize::from(ch_a != ch_b);
            curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
        }
        prev.clone_from(&curr);
    }

    prev[b.len()]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol::layers::{HelpMode, HelpRender};
    use std::collections::{BTreeSet, HashMap};

    fn base_input() -> HelpInput {
        HelpInput {
            agent_id: "jimi".into(),
            tool_name: None,
            mode: None,
            intent: None,
            stage: None,
            path: None,
            error_text: None,
            recent_tools: vec![],
            max_suggestions: None,
            render: Some(HelpRender::Full),
        }
    }

    #[test]
    fn catalog_covers_live_runtime_surface() {
        let schema_names: BTreeSet<String> = schema_tools()
            .into_iter()
            .map(|schema| schema.name)
            .collect();
        let catalog_names: BTreeSet<String> = catalog_entries()
            .into_iter()
            .map(|entry| entry.name)
            .collect();
        assert_eq!(
            catalog_names, schema_names,
            "help catalog must cover every tool exposed by tool_schemas()"
        );
    }

    #[test]
    fn catalog_minimal_arguments_validate_against_live_schema() {
        let catalog_by_name: HashMap<String, HelpCatalogEntry> = catalog_entries()
            .into_iter()
            .map(|entry| (entry.name.clone(), entry))
            .collect();

        for schema in schema_tools() {
            let entry = catalog_by_name
                .get(&schema.name)
                .unwrap_or_else(|| panic!("missing help catalog entry for {}", schema.name));
            assert!(
                validate_against_schema(&entry.minimal_arguments, &schema.input_schema),
                "minimal call for {} must validate against the live input schema",
                schema.name
            );
        }
    }

    #[test]
    fn route_mode_prefers_seek_for_purpose_first_queries() {
        let mut input = base_input();
        input.mode = Some(HelpMode::Route);
        input.stage = Some(HelpStage::Find);
        input.intent = Some("where retry backoff is decided".into());

        let guidance = route_guidance(&input, false);
        assert_eq!(
            guidance.recommended_tools.first().map(String::as_str),
            Some("seek")
        );
        assert_eq!(
            guidance
                .minimal_calls
                .first()
                .map(|call| call.tool.as_str()),
            Some("seek")
        );
    }

    #[test]
    fn recovery_mode_prefers_trace_when_error_text_is_present() {
        let mut input = base_input();
        input.mode = Some(HelpMode::Recovery);
        input.error_text = Some("TypeError: undefined is not a function".into());

        let guidance = recovery_guidance(&input);
        assert_eq!(
            guidance.recommended_tools.first().map(String::as_str),
            Some("trace")
        );
        assert_eq!(
            guidance
                .minimal_calls
                .first()
                .map(|call| call.tool.as_str()),
            Some("trace")
        );
    }

    #[test]
    fn recovery_mode_prefers_search_when_node_identity_is_missing() {
        let mut input = base_input();
        input.mode = Some(HelpMode::Recovery);
        input.tool_name = Some("impact".into());
        input.error_text = Some("Node not found: file::src/missing.rs".into());

        let guidance = recovery_guidance(&input);
        assert_eq!(
            guidance.recommended_tools.first().map(String::as_str),
            Some("search")
        );
        assert!(guidance
            .next_step
            .as_deref()
            .unwrap_or_default()
            .contains("canonical node_id"));
    }

    #[test]
    fn runtime_projection_prefers_sequence_head_and_surfaces_missing_context() {
        let projection = runtime_projection_from_guidance(
            Some(&HelpGuidance {
                decision_type: "sequence".into(),
                recommended_tools: vec!["audit".into()],
                recommended_sequence: vec![HelpSequenceStep {
                    tool: "glob".into(),
                    reason: "Open the next likely file.".into(),
                }],
                required_inputs: vec!["path".into()],
                missing_context: vec!["path".into()],
                minimal_calls: vec![],
                rejected_alternatives: vec![],
                recovery_steps: vec![],
                next_step: Some("Run glob next.".into()),
                why: "The path is still missing.".into(),
                confidence: 0.9,
                canonical_name: Some("audit".into()),
                aliases: vec!["m1nd.audit".into()],
            }),
            "blocked",
        );

        assert_eq!(projection.proof_state, "blocked");
        assert_eq!(projection.next_suggested_tool.as_deref(), Some("glob"));
        assert_eq!(projection.next_suggested_target.as_deref(), Some("path"));
        assert_eq!(projection.what_is_missing.as_deref(), Some("path"));
    }

    #[test]
    fn runtime_error_hint_includes_minimal_call_and_next_step() {
        let hint = runtime_error_guidance_hint("impact", "Node not found: file::src/missing.rs");
        assert!(hint.contains("minimal call: search("));
        assert!(hint.contains("next: Recover the canonical node_id"));
    }
}
