//! Virtual orchestration tools — enable the orchestrator agent to create,
//! configure, and manage subagents at runtime via tool calls.
//!
//! These tools mirror the subagent CRUD API (`/api/subagents`) but are callable
//! from within the agent inference loop, allowing the primary agent to
//! autonomously compose its own team of specialists.

use super::AppState;

/// Returns `true` for tool names that are orchestration management tools.
pub(super) fn is_virtual_orchestration_tool(tool_name: &str) -> bool {
    matches!(
        tool_name.trim().to_ascii_lowercase().as_str(),
        "compose-subagent"
            | "compose_subagent"
            | "update-subagent-skills"
            | "update_subagent_skills"
            | "list-subagent-roster"
            | "list_subagent_roster"
            | "list-available-skills"
            | "list_available_skills"
            | "remove-subagent"
            | "remove_subagent"
            | "retire-unused-subagents"
            | "retire_unused_subagents"
            | "task-status"
            | "task_status"
            | "retry-task"
            | "retry_task"
            | "list-open-tasks"
            | "list_open_tasks"
            | "compose-skill"
            | "compose_skill"
            | "validate-subagent-roster"
            | "validate_subagent_roster"
    )
}

/// Execute a virtual orchestration tool call.
///
/// All tools apply the same policy + approval checks as delegation tools,
/// then dispatch to the appropriate subagent management operation.
///
/// Orchestration tools are sensitive roster-management operations.
/// They run under the caller's resolved authority and must never be
/// privilege-escalated implicitly.
pub(super) async fn execute_virtual_orchestration_tool(
    state: &AppState,
    tool_name: &str,
    params: &serde_json::Value,
    turn_id: &str,
    authority: roboticus_core::InputAuthority,
    tier: roboticus_core::SurvivalTier,
) -> Result<String, String> {
    // ── policy gate ──────────────────────────────────────────────
    let policy_result = super::check_tool_policy(
        &state.policy_engine,
        tool_name,
        params,
        authority,
        tier,
        roboticus_core::RiskLevel::Caution,
    );
    let (decision_str, rule_name, reason) = match &policy_result {
        Ok(()) => ("allow".to_string(), None, None),
        Err(super::super::JsonError(_status, msg)) => (
            "deny".to_string(),
            Some("policy_engine"),
            Some(msg.as_str()),
        ),
    };
    roboticus_db::policy::record_policy_decision(
        &state.db,
        Some(turn_id),
        tool_name,
        &decision_str,
        rule_name,
        reason,
    )
    .inspect_err(|e| tracing::warn!(error = %e, "failed to record policy decision"))
    .ok();
    if let Err(super::super::JsonError(_status, msg)) = policy_result {
        return Err(format!("Policy denied: {msg}"));
    }

    // ── approval gate ────────────────────────────────────────────
    match state.approvals.check_tool(tool_name) {
        Ok(roboticus_agent::approvals::ToolClassification::Gated) => {
            let request = state
                .approvals
                .request_approval(
                    tool_name,
                    &params.to_string(),
                    None,
                    Some(turn_id),
                    authority,
                )
                .map_err(|e| format!("Approval error: {e}"))?;
            roboticus_db::approvals::record_approval_request(
                &state.db,
                &request.id,
                &request.tool_name,
                &request.tool_input,
                request.session_id.as_deref(),
                request.turn_id.as_deref(),
                "pending",
                &request.timeout_at.to_rfc3339(),
            )
            .inspect_err(|e| tracing::warn!(error = %e, "failed to persist approval request"))
            .ok();
            return Err(format!(
                "Tool '{tool_name}' requires approval (request: {})",
                request.id
            ));
        }
        Err(e) => return Err(format!("Tool blocked: {e}")),
        Ok(_) => {}
    }

    // ── dispatch ─────────────────────────────────────────────────
    let action = tool_name.trim().to_ascii_lowercase();
    match action.as_str() {
        "compose-subagent" | "compose_subagent" => compose_subagent(state, params).await,
        "update-subagent-skills" | "update_subagent_skills" => {
            update_subagent_skills(state, params).await
        }
        "list-subagent-roster" | "list_subagent_roster" => list_subagent_roster(state).await,
        "list-available-skills" | "list_available_skills" => {
            list_available_skills(state, params).await
        }
        "remove-subagent" | "remove_subagent" => remove_subagent(state, params).await,
        "retire-unused-subagents" | "retire_unused_subagents" => {
            retire_unused_subagents_tool(state, params).await
        }
        "task-status" | "task_status" => task_status_tool(state, params).await,
        "retry-task" | "retry_task" => retry_task_tool(state, params, authority, tier).await,
        "list-open-tasks" | "list_open_tasks" => list_open_tasks_tool(state).await,
        "compose-skill" | "compose_skill" => compose_skill(state, params).await,
        "validate-subagent-roster" | "validate_subagent_roster" => {
            validate_subagent_roster(state).await
        }
        _ => Err(format!("unrecognized orchestration tool: {tool_name}")),
    }
}

async fn retire_unused_subagents_tool(
    state: &AppState,
    params: &serde_json::Value,
) -> Result<String, String> {
    let min_age_days = params
        .get("min_age_days")
        .and_then(|v| v.as_i64())
        .map(|v| v.clamp(i64::from(i32::MIN), i64::from(i32::MAX)) as i32)
        .unwrap_or(30);
    let dry_run = params
        .get("dry_run")
        .and_then(|v| v.as_bool())
        .unwrap_or(false);
    let names = params.get("names").and_then(|v| v.as_array()).map(|arr| {
        arr.iter()
            .filter_map(|x| x.as_str().map(std::string::ToString::to_string))
            .collect::<Vec<_>>()
    });
    let v = crate::api::routes::subagents::execute_retire_unused_subagents_tool(
        state,
        min_age_days,
        dry_run,
        names,
    )
    .await?;
    serde_json::to_string_pretty(&v).map_err(|e| e.to_string())
}

// ── compose-subagent ─────────────────────────────────────────────

async fn compose_subagent(state: &AppState, params: &serde_json::Value) -> Result<String, String> {
    use crate::api::routes::subagents::{
        ROLE_SUBAGENT, normalize_fallback_models, normalize_model_input, normalize_role,
        normalize_skills, resolve_taskable_subagent_runtime_model, validate_subagent_contract,
        validate_subagent_name,
    };

    let name = params
        .get("name")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .ok_or_else(|| {
            "compose-subagent requires `name` (alphanumeric, hyphens, underscores)".to_string()
        })?;

    validate_subagent_name(&name).map_err(|e| format!("invalid subagent name: {}", e.1))?;

    // ── skill prerequisite gate ──────────────────────────────────
    let skills_raw: Vec<String> = match params.get("skills") {
        Some(v) if v.is_array() => v
            .as_array()
            .unwrap()
            .iter()
            .filter_map(|v| v.as_str().map(|s| s.to_string()))
            .collect(),
        _ => vec![],
    };
    if skills_raw.is_empty() {
        return Err(
            "Cannot create subagent with no skills. Use compose-skill to install \
             required skills first, then re-attempt compose-subagent with a `skills` array."
                .to_string(),
        );
    }

    // Check for existing agent with same name.
    let existing = roboticus_db::agents::list_sub_agents(&state.db)
        .map_err(|e| format!("failed to query sub-agents: {e}"))?;
    if existing.iter().any(|a| a.name.eq_ignore_ascii_case(&name)) {
        return Err(format!(
            "subagent '{name}' already exists; use update-subagent-skills to modify it"
        ));
    }

    let model_raw = params
        .get("model")
        .and_then(|v| v.as_str())
        .unwrap_or("auto");
    let model = normalize_model_input(model_raw);

    let description = params
        .get("description")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());

    let skills_normalized = normalize_skills(&skills_raw);
    if skills_normalized.is_empty() {
        return Err(
            "Cannot create subagent with no valid skills (all provided names were \
             empty or invalid after normalization). Use compose-skill to install \
             required skills first, then re-attempt compose-subagent."
                .to_string(),
        );
    }

    // Strict skill validation — no fuzzy matching, no silent defaults
    let skills = {
        match crate::api::routes::subagent_integrity::validate_skills_against_registry_strict(
            state,
            &skills_normalized,
        ) {
            Ok(valid) => valid,
            Err(err) => {
                let result = roboticus_core::composition::CompositionResult {
                    agent_name: name.clone(),
                    validated_skills: {
                        let registry =
                            crate::api::routes::subagent_integrity::skill_registry_names(state);
                        skills_normalized
                            .iter()
                            .filter(|s| registry.contains(&s.to_ascii_lowercase()))
                            .map(|s| s.to_ascii_lowercase())
                            .collect()
                    },
                    missing_skills: err.missing_skills,
                    auto_created_skills: vec![],
                    warnings: vec![],
                };
                return Err(format!(
                    "COMPOSITION_INCOMPLETE: {}\n\nMissing skills must be created first via \
                     compose-skill, then re-attempt compose-subagent.",
                    serde_json::to_string_pretty(&result)
                        .unwrap_or_else(|_| format!("{:?}", result))
                ));
            }
        }
    };

    let fallback_models_raw: Vec<String> = match params.get("fallback_models") {
        Some(v) if v.is_array() => v
            .as_array()
            .unwrap()
            .iter()
            .filter_map(|v| v.as_str().map(|s| s.to_string()))
            .collect(),
        _ => vec![],
    };
    let fallback_models = normalize_fallback_models(&fallback_models_raw, &model);

    let role = params
        .get("role")
        .and_then(|v| v.as_str())
        .unwrap_or(ROLE_SUBAGENT);
    let normalized_role = normalize_role(role)
        .ok_or_else(|| "role must be 'subagent' or 'model-proxy'".to_string())?;

    validate_subagent_contract(normalized_role, &model, &skills, None)
        .map_err(|e| format!("contract violation: {}", e.1))?;

    // Generate display name from hyphenated name.
    let display_name = params
        .get("display_name")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .unwrap_or_else(|| {
            name.split('-')
                .map(|w| {
                    let mut c = w.chars();
                    match c.next() {
                        None => String::new(),
                        Some(f) => f.to_uppercase().to_string() + c.as_str(),
                    }
                })
                .collect::<Vec<_>>()
                .join(" ")
        });

    let agent = roboticus_db::agents::SubAgentRow {
        id: uuid::Uuid::new_v4().to_string(),
        name: name.clone(),
        display_name: Some(display_name.clone()),
        model: model.clone(),
        fallback_models_json: Some(
            serde_json::to_string(&fallback_models).unwrap_or_else(|_| "[]".to_string()),
        ),
        role: normalized_role.to_string(),
        description: description.clone(),
        skills_json: Some(serde_json::to_string(&skills).unwrap_or_else(|_| "[]".to_string())),
        enabled: true,
        session_count: 0,
        last_used_at: None,
    };

    roboticus_db::agents::upsert_sub_agent(&state.db, &agent)
        .map_err(|e| format!("failed to persist subagent: {e}"))?;

    // Register and start in runtime.
    if normalized_role == ROLE_SUBAGENT {
        let config = roboticus_agent::subagents::AgentInstanceConfig {
            id: name.clone(),
            name: display_name.clone(),
            model: resolve_taskable_subagent_runtime_model(state, &model).await,
            skills: skills.clone(),
            allowed_subagents: vec![],
            max_concurrent: 4,
        };
        let mut warnings = Vec::new();
        if let Err(e) = state.registry.register(config).await {
            tracing::error!(agent = %name, error = %e, "orchestration: failed to register sub-agent");
            warnings.push(format!("WARNING: registration failed: {e}"));
        }
        if let Err(e) = state.registry.start_agent(&name).await {
            tracing::error!(agent = %name, error = %e, "orchestration: failed to start sub-agent");
            warnings.push(format!("WARNING: start failed: {e}"));
        }
        if !warnings.is_empty() {
            let warn_str = warnings.join("; ");
            let skills_label = if skills.is_empty() {
                "(none)".to_string()
            } else {
                skills.join(", ")
            };
            return Ok(format!(
                "created subagent '{name}' (display: {display_name}) model={model} fallback_models={} role={normalized_role} skills=[{skills_label}] enabled=true | {warn_str}",
                serde_json::to_string(&fallback_models).unwrap_or_else(|_| "[]".to_string())
            ));
        }
    }

    let skills_label = if skills.is_empty() {
        "(none)".to_string()
    } else {
        skills.join(", ")
    };
    Ok(format!(
        "created subagent '{name}' (display: {display_name}) model={model} fallback_models={} role={normalized_role} skills=[{skills_label}] enabled=true",
        serde_json::to_string(&fallback_models).unwrap_or_else(|_| "[]".to_string())
    ))
}

// ── update-subagent-skills ───────────────────────────────────────

async fn update_subagent_skills(
    state: &AppState,
    params: &serde_json::Value,
) -> Result<String, String> {
    use crate::api::routes::subagents::normalize_skills;

    let name = params
        .get("name")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .ok_or_else(|| "update-subagent-skills requires `name`".to_string())?;

    let agents = roboticus_db::agents::list_sub_agents(&state.db)
        .map_err(|e| format!("failed to query sub-agents: {e}"))?;
    let existing = agents
        .iter()
        .find(|a| a.name.eq_ignore_ascii_case(&name))
        .ok_or_else(|| format!("subagent '{name}' not found"))?;

    let new_skills_raw: Vec<String> = match params.get("skills") {
        Some(v) if v.is_array() => v
            .as_array()
            .unwrap()
            .iter()
            .filter_map(|v| v.as_str().map(|s| s.to_string()))
            .collect(),
        _ => return Err("update-subagent-skills requires `skills` array".to_string()),
    };

    let mode = params
        .get("mode")
        .and_then(|v| v.as_str())
        .unwrap_or("replace");

    let merged_skills = match mode {
        "append" => {
            let mut current: Vec<String> = existing
                .skills_json
                .as_deref()
                .and_then(|s| serde_json::from_str(s).ok())
                .unwrap_or_default();
            current.extend(new_skills_raw);
            normalize_skills(&current)
        }
        _ => normalize_skills(&new_skills_raw), // "replace" or default
    };

    let mut updated = existing.clone();
    updated.skills_json =
        Some(serde_json::to_string(&merged_skills).unwrap_or_else(|_| "[]".to_string()));

    roboticus_db::agents::upsert_sub_agent(&state.db, &updated)
        .map_err(|e| format!("failed to update subagent skills: {e}"))?;

    let skills_label = if merged_skills.is_empty() {
        "(none)".to_string()
    } else {
        merged_skills.join(", ")
    };
    Ok(format!(
        "updated subagent '{name}' skills=[{skills_label}] (mode={mode})"
    ))
}

// ── list-subagent-roster ─────────────────────────────────────────

async fn list_subagent_roster(state: &AppState) -> Result<String, String> {
    use crate::api::routes::subagents::{ROLE_MODEL_PROXY, parse_fallback_models_json};

    let agents = roboticus_db::agents::list_sub_agents(&state.db)
        .map_err(|e| format!("failed to query sub-agents: {e}"))?;

    if agents.is_empty() {
        return Ok("no subagents configured".to_string());
    }

    let runtime = state.registry.list_agents().await;
    let runtime_by_name: std::collections::HashMap<
        String,
        roboticus_agent::subagents::AgentInstance,
    > = runtime
        .into_iter()
        .map(|a| (a.id.to_ascii_lowercase(), a))
        .collect();

    let mut lines = Vec::new();
    let mut taskable_count = 0usize;
    let mut proxy_count = 0usize;

    for a in &agents {
        let is_proxy = a.role.eq_ignore_ascii_case(ROLE_MODEL_PROXY);
        if is_proxy {
            proxy_count += 1;
        }
        let skills: Vec<String> = a
            .skills_json
            .as_deref()
            .and_then(|s| serde_json::from_str(s).ok())
            .unwrap_or_default();

        let runtime_state = if is_proxy {
            "n/a".to_string()
        } else if let Some(inst) = runtime_by_name.get(&a.name.to_ascii_lowercase()) {
            format!("{:?}", inst.state).to_ascii_lowercase()
        } else if a.enabled {
            "booting".to_string()
        } else {
            "stopped".to_string()
        };

        let taskable = a.enabled && runtime_state == "running" && !is_proxy;
        if taskable {
            taskable_count += 1;
        }

        let skills_label = if skills.is_empty() {
            "(none)".to_string()
        } else {
            skills.join(", ")
        };
        let fallback_models = parse_fallback_models_json(a.fallback_models_json.as_deref());
        let fallback_label = if fallback_models.is_empty() {
            "[]".to_string()
        } else {
            format!("[{}]", fallback_models.join(", "))
        };

        lines.push(format!(
            "- {} [{}] model={} fallbacks={} skills=[{}] enabled={} runtime={}{}",
            a.name,
            a.role,
            a.model,
            fallback_label,
            skills_label,
            a.enabled,
            runtime_state,
            if taskable { " ★taskable" } else { "" },
        ));
    }

    Ok(format!(
        "subagent roster ({} total, {} taskable, {} proxies):\n{}",
        agents.len(),
        taskable_count,
        proxy_count,
        lines.join("\n"),
    ))
}

// ── list-available-skills ────────────────────────────────────────

async fn list_available_skills(
    state: &AppState,
    params: &serde_json::Value,
) -> Result<String, String> {
    let keyword = params
        .get("keyword")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());

    let skills = if let Some(ref kw) = keyword {
        roboticus_db::skills::find_by_trigger(&state.db, kw)
            .map_err(|e| format!("failed to search skills: {e}"))?
    } else {
        roboticus_db::skills::list_skills(&state.db)
            .map_err(|e| format!("failed to list skills: {e}"))?
    };

    if skills.is_empty() {
        return Ok(if let Some(kw) = keyword {
            format!("no skills match keyword '{kw}'")
        } else {
            "no skills registered in workspace catalog".to_string()
        });
    }

    let mut lines: Vec<String> = skills
        .iter()
        .map(|s| {
            let desc = s.description.as_deref().unwrap_or("(no description)");
            let status = if s.enabled { "enabled" } else { "disabled" };
            format!(
                "- {} [{}] risk={} {}: {}",
                s.name, s.kind, s.risk_level, status, desc
            )
        })
        .collect();

    lines.insert(
        0,
        format!("workspace skill catalog ({} skills):", skills.len()),
    );

    Ok(lines.join("\n"))
}

// ── remove-subagent ──────────────────────────────────────────────

async fn remove_subagent(state: &AppState, params: &serde_json::Value) -> Result<String, String> {
    let name = params
        .get("name")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .ok_or_else(|| "remove-subagent requires `name`".to_string())?;

    let deleted = roboticus_db::agents::delete_sub_agent(&state.db, &name)
        .map_err(|e| format!("failed to delete subagent: {e}"))?;

    if !deleted {
        return Err(format!("subagent '{name}' not found"));
    }

    // Tear down from runtime.
    if let Err(e) = state.registry.stop_agent(&name).await {
        tracing::warn!(agent = %name, error = %e, "orchestration: failed to stop agent during removal");
    }
    state.registry.unregister(&name).await;

    Ok(format!("removed subagent '{name}'"))
}

// ── task-status ──────────────────────────────────────────────

async fn task_status_tool(state: &AppState, params: &serde_json::Value) -> Result<String, String> {
    // Accept task_id or id for task lookup, or subagent name for agent lookup.
    let task_id = params
        .get("task_id")
        .or_else(|| params.get("id"))
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());
    let subagent_name = params
        .get("subagent")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());
    let parent_task_id = params
        .get("parent_task_id")
        .or_else(|| params.get("turn_id"))
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty());

    if task_id.is_none() && subagent_name.is_none() && parent_task_id.is_none() {
        return Err(
            "task-status requires `task_id` (or `id`), `subagent`, or `parent_task_id`/`turn_id` parameter"
                .to_string(),
        );
    }

    if let Some(ref tid) = task_id {
        let events = roboticus_db::task_events::task_events_for_task(&state.db, tid)
            .map_err(|e| format!("failed to query task events: {e}"))?;
        if events.is_empty() {
            return Ok(format!("no events found for task '{tid}'"));
        }
        let latest = events.last().unwrap();
        let mut lines = vec![format!("task '{}' status: {}", tid, latest.event_type)];
        if let Some(ref summary) = latest.summary {
            lines.push(format!("  summary: {summary}"));
        }
        if let Some(pct) = latest.percentage {
            lines.push(format!("  progress: {pct:.0}%"));
        }
        if latest.retry_count > 0 {
            lines.push(format!("  retries: {}", latest.retry_count));
        }
        if let Some(ref detail) = latest.detail_json {
            lines.push(format!("  detail: {detail}"));
        }
        lines.push(format!("  last_updated: {}", latest.created_at));
        lines.push(format!("  event_history: {} events total", events.len()));
        return Ok(lines.join("\n"));
    }

    if let Some(ref parent_id) = parent_task_id {
        let subtasks = roboticus_db::task_events::subtask_events_for_parent(&state.db, parent_id)
            .map_err(|e| format!("failed to query subtask events: {e}"))?;
        if subtasks.is_empty() {
            return Ok(format!("no subtask events found for parent '{parent_id}'"));
        }

        let active_count = subtasks
            .iter()
            .filter(|e| !e.event_type.is_terminal())
            .count();
        let mut lines = vec![format!(
            "subtasks for parent '{}' ({} total, {} active):",
            parent_id,
            subtasks.len(),
            active_count,
        )];
        for evt in &subtasks {
            let assignee = evt.assigned_to.as_deref().unwrap_or("(unassigned)");
            let summary = evt.summary.as_deref().unwrap_or("(no summary)");
            let pct = evt
                .percentage
                .map(|p| format!(" {p:.0}%"))
                .unwrap_or_default();
            lines.push(format!(
                "- [{}] {} via {} — {}{} ({})",
                evt.task_id, evt.event_type, assignee, summary, pct, evt.created_at,
            ));
        }
        return Ok(lines.join("\n"));
    }

    // Lookup by subagent name — find tasks assigned to this subagent via `assigned_to`.
    if let Some(ref name) = subagent_name {
        let events = roboticus_db::task_events::task_events_for_agent(&state.db, name)
            .map_err(|e| format!("failed to query task events for agent: {e}"))?;
        if events.is_empty() {
            return Ok(format!("no task events found for subagent '{name}'"));
        }

        // Group by task_id and show the latest event per task.
        // task_events_for_agent returns rows ordered by created_at DESC, so the
        // first occurrence of each task_id is the most recent event for that task.
        let mut seen_tasks = std::collections::HashSet::new();
        let mut latest_per_task = Vec::new();
        for evt in &events {
            if seen_tasks.insert(&evt.task_id) {
                latest_per_task.push(evt);
            }
        }

        let active_count = latest_per_task
            .iter()
            .filter(|e| !e.event_type.is_terminal())
            .count();
        let mut lines = vec![format!(
            "tasks for subagent '{name}' ({} total, {} active):",
            latest_per_task.len(),
            active_count,
        )];
        for evt in &latest_per_task {
            let summary = evt.summary.as_deref().unwrap_or("(no summary)");
            let pct = evt
                .percentage
                .map(|p| format!(" {p:.0}%"))
                .unwrap_or_default();
            lines.push(format!(
                "- [{}] {} — {}{} ({})",
                evt.task_id, evt.event_type, summary, pct, evt.created_at,
            ));
        }
        return Ok(lines.join("\n"));
    }

    Err("task-status: unexpected state".into())
}

// ── retry-task ───────────────────────────────────────────────

async fn retry_task_tool(
    state: &AppState,
    params: &serde_json::Value,
    authority: roboticus_core::InputAuthority,
    tier: roboticus_core::SurvivalTier,
) -> Result<String, String> {
    let task_id = params
        .get("task_id")
        .or_else(|| params.get("id"))
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .ok_or_else(|| "retry-task requires `task_id` (or `id`)".to_string())?;

    // Verify the task exists and is in a failed state.
    let current_state = roboticus_db::task_events::current_task_state(&state.db, &task_id)
        .map_err(|e| format!("failed to query task state: {e}"))?;

    match current_state {
        None => return Err(format!("no events found for task '{task_id}'")),
        Some(roboticus_db::task_events::TaskLifecycleState::Failed) => {
            // Eligible for retry — continue below.
        }
        Some(state) => {
            return Err(format!(
                "task '{task_id}' is in state '{state}' — only failed tasks can be retried"
            ));
        }
    }

    // Check retry count — max 2 retries.
    let retry_count = roboticus_db::task_events::retry_count_for_task(&state.db, &task_id)
        .map_err(|e| format!("failed to query retry count: {e}"))?;

    if retry_count >= 2 {
        return Err(format!(
            "task '{task_id}' has already been retried {retry_count} times (max 2) — \
             escalate to user instead"
        ));
    }

    // Retrieve the assigned_to from the most recent event for this task.
    let latest = roboticus_db::task_events::latest_task_event(&state.db, &task_id)
        .map_err(|e| format!("failed to query latest event: {e}"))?;
    let latest = latest.ok_or_else(|| format!("no latest event found for task '{task_id}'"))?;
    let assigned_to = latest.assigned_to.clone();
    let parent_task_id = latest
        .parent_task_id
        .clone()
        .unwrap_or_else(|| task_id.clone());
    let detail_value = latest
        .detail_json
        .as_deref()
        .and_then(|raw| serde_json::from_str::<serde_json::Value>(raw).ok());
    let retry_subtask = detail_value
        .as_ref()
        .and_then(|v| v.get("subtask"))
        .and_then(|v| v.as_str())
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .map(ToOwned::to_owned);

    // Record the retry event.
    let retry_event = roboticus_db::task_events::TaskEventRow {
        id: uuid::Uuid::new_v4().to_string(),
        task_id: task_id.clone(),
        parent_task_id: Some(parent_task_id.clone()),
        assigned_to: assigned_to.clone(),
        event_type: roboticus_db::task_events::TaskLifecycleState::Retry,
        summary: Some(format!("Retry attempt {} initiated", retry_count + 1)),
        detail_json: latest.detail_json.clone(),
        percentage: None,
        retry_count: retry_count + 1,
        created_at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S").to_string(),
    };
    roboticus_db::task_events::insert_task_event(&state.db, &retry_event)
        .map_err(|e| format!("failed to record retry event: {e}"))?;

    if let Some(subtask) = retry_subtask {
        let mut retry_params = serde_json::json!({
            "task": subtask,
            "task_id": task_id,
            "parent_task_id": parent_task_id,
        });
        if let Some(name) = assigned_to.as_deref() {
            retry_params["specialist"] = serde_json::Value::String(name.to_string());
        }
        let output = super::execute_virtual_subagent_tool_call(
            state,
            "assign-tasks",
            &retry_params,
            &parent_task_id,
            authority,
            tier,
        )
        .await
        .map_err(|e| format!("retry execution failed for task '{task_id}': {e}"))?;
        return Ok(format!(
            "retry executed for task '{task_id}' (attempt {}/{})\n{}",
            retry_count + 1,
            2,
            output
        ));
    }

    // Retrieve the original task title from the tasks table.
    let open_tasks = roboticus_db::tasks::list_open_tasks(&state.db)
        .map_err(|e| format!("failed to list tasks: {e}"))?;
    let task_entry = open_tasks
        .iter()
        .find(|t| t["id"].as_str() == Some(task_id.as_str()));

    // If the task isn't in the open tasks list (e.g. it was marked failed/closed),
    // we still proceed — the retry event has been recorded and the orchestrator
    // should pick it up on the next delegation pass.
    let task_title = task_entry
        .and_then(|t| t["title"].as_str())
        .unwrap_or("(original task)");

    Ok(format!(
        "retry initiated for task '{task_id}' (attempt {}/{}) — \
         task '{task_title}' re-queued for delegation",
        retry_count + 1,
        2
    ))
}

// ── list-open-tasks ──────────────────────────────────────────────

async fn list_open_tasks_tool(state: &AppState) -> Result<String, String> {
    let tasks = roboticus_db::tasks::list_open_tasks_with_events(&state.db)
        .map_err(|e| format!("failed to list open tasks: {e}"))?;

    if tasks.is_empty() {
        return Ok(
            "no open tasks (all pending/in_progress/needs_review tasks have been resolved)"
                .to_string(),
        );
    }

    let mut lines: Vec<String> = Vec::with_capacity(tasks.len() + 1);
    lines.push(format!("open tasks ({} total):", tasks.len()));
    for t in &tasks {
        let lifecycle = t
            .get("lifecycle_state")
            .and_then(|v| v.as_str())
            .unwrap_or("");
        let pct = t
            .get("lifecycle_percentage")
            .and_then(|v| v.as_f64())
            .map(|p| format!(" {p:.0}%"))
            .unwrap_or_default();
        let lifecycle_info = if lifecycle.is_empty() {
            String::new()
        } else {
            format!(", lifecycle={lifecycle}{pct}")
        };
        lines.push(format!(
            "- [{}] {} (priority={}, status={}{}; created={})",
            t["id"].as_str().unwrap_or("?"),
            t["title"].as_str().unwrap_or("(untitled)"),
            t["priority"].as_i64().unwrap_or(0),
            t["status"].as_str().unwrap_or("?"),
            lifecycle_info,
            t["created_at"].as_str().unwrap_or("?"),
        ));
    }
    Ok(lines.join("\n"))
}

// ── compose-skill ─────────────────────────────────────────────────

async fn compose_skill(state: &AppState, params: &serde_json::Value) -> Result<String, String> {
    let name = params
        .get("name")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .ok_or_else(|| "compose-skill requires `name`".to_string())?;

    let description = params
        .get("description")
        .and_then(|v| v.as_str())
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
        .ok_or_else(|| "compose-skill requires `description`".to_string())?;

    let kind = params
        .get("kind")
        .and_then(|v| v.as_str())
        .unwrap_or("instruction")
        .to_string();

    let triggers: Vec<String> = params
        .get("triggers")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_else(|| {
            name.split(|c: char| !c.is_alphanumeric())
                .filter(|w| w.len() >= 4)
                .map(|w| w.to_lowercase())
                .collect()
        });

    let tool_chain: Vec<String> = params
        .get("tool_chain")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect()
        })
        .unwrap_or_default();

    let config = state.config.read().await;
    let policy = config.agent.composition_policy;
    drop(config);

    if matches!(policy, roboticus_core::config::CompositionPolicy::Manual) {
        return Ok(format!(
            "PROPOSAL: Would create skill '{}' (kind={}, triggers=[{}]). \
             composition_policy=manual — no action taken.",
            name,
            kind,
            triggers.join(", ")
        ));
    }

    // Guard: skill must not already exist.
    let existing = roboticus_db::skills::list_skills(&state.db)
        .map_err(|e| format!("failed to query skills: {e}"))?;
    if existing.iter().any(|s| s.name.eq_ignore_ascii_case(&name)) {
        return Err(format!("skill '{}' already exists in the registry", name));
    }

    let skill_kind = match kind.as_str() {
        "structured" | "action" => "structured",
        _ => "instruction",
    };

    // Produce a content fingerprint from name + description.
    let fingerprint_input = format!("{name}\x00{description}\x00{kind}");
    let content_hash = format!("{:x}", content_fingerprint(fingerprint_input.as_bytes()));

    let triggers_json = serde_json::to_string(&serde_json::json!({
        "keywords": triggers,
    }))
    .unwrap_or_else(|_| "{}".to_string());

    let tool_chain_json = if tool_chain.is_empty() {
        None
    } else {
        let steps: Vec<serde_json::Value> = tool_chain
            .iter()
            .map(|t| serde_json::json!({"tool_name": t, "params": {}}))
            .collect();
        Some(serde_json::to_string(&steps).unwrap_or_else(|_| "[]".to_string()))
    };

    if matches!(policy, roboticus_core::config::CompositionPolicy::Propose) {
        return Ok(format!(
            "PROPOSAL: Validated skill '{}' (kind={}, triggers=[{}]). \
             Awaiting user approval to persist.",
            name,
            kind,
            triggers.join(", "),
        ));
    }

    // Persist the skill.
    let skill_id = roboticus_db::skills::register_skill_with_provenance(
        &state.db,
        &name,
        skill_kind,
        Some(&description),
        &format!("composed://{}", name),
        &content_hash,
        Some(&triggers_json),
        tool_chain_json.as_deref(),
        None,
        None,
        "Caution",
        "0.1.0",
        "orchestrator",
        "composed",
    )
    .map_err(|e| format!("failed to persist skill: {e}"))?;

    Ok(format!(
        "created skill '{}' (id={}, kind={}, triggers=[{}])",
        name,
        skill_id,
        skill_kind,
        triggers.join(", "),
    ))
}

fn content_fingerprint(data: &[u8]) -> u64 {
    use std::hash::{Hash, Hasher};
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    data.hash(&mut hasher);
    hasher.finish()
}

// ── validate-subagent-roster ──────────────────────────────────────

async fn validate_subagent_roster(state: &AppState) -> Result<String, String> {
    let agents = roboticus_db::agents::list_sub_agents(&state.db)
        .map_err(|e| format!("failed to query sub-agents: {e}"))?;

    if agents.is_empty() {
        return Ok("no subagents to validate".to_string());
    }

    let mut lines = Vec::new();
    let mut total_issues = 0usize;

    for agent in &agents {
        if !agent.enabled {
            continue;
        }

        let skills: Vec<String> = agent
            .skills_json
            .as_deref()
            .and_then(|s| serde_json::from_str(s).ok())
            .unwrap_or_default();

        if skills.is_empty() {
            lines.push(format!("- {} [WARNING] no skills assigned", agent.name));
            total_issues += 1;
            continue;
        }

        let validation =
            crate::api::routes::subagent_integrity::validate_skills_against_registry_strict(
                state, &skills,
            );

        match validation {
            Ok(valid) => {
                lines.push(format!(
                    "- {} [OK] {} skill(s) validated: [{}]",
                    agent.name,
                    valid.len(),
                    valid.join(", ")
                ));
            }
            Err(err) => {
                lines.push(format!(
                    "- {} [STALE] missing skill(s): [{}]",
                    agent.name,
                    err.missing_skills.join(", ")
                ));
                total_issues += 1;
            }
        }
    }

    // Check delegation stats for retirement candidates
    let config = state.config.read().await;
    let threshold = config.agent.retirement_success_threshold;
    let min_delegations = config.agent.retirement_min_delegations;
    drop(config);

    if let Ok(stats) = roboticus_db::delegation::delegation_stats_by_agent(&state.db, 168) {
        for stat in &stats {
            if stat.total_delegations >= min_delegations && stat.success_rate < threshold {
                lines.push(format!(
                    "- {} [RETIREMENT CANDIDATE] success_rate={:.0}% ({}/{}) over last 7 days — below threshold {:.0}%",
                    stat.agent_id,
                    stat.success_rate * 100.0,
                    stat.successes,
                    stat.total_delegations,
                    threshold * 100.0,
                ));
                total_issues += 1;
            }
        }
    }

    let enabled_count = agents.iter().filter(|a| a.enabled).count();
    Ok(format!(
        "subagent roster validation ({} active, {} issues):\n{}",
        enabled_count,
        total_issues,
        lines.join("\n"),
    ))
}

// ── tests ─────────────────────────────────────────────────────────

#[cfg(test)]
mod tests {
    use super::is_virtual_orchestration_tool;

    #[test]
    fn recognizes_task_status_tool() {
        assert!(is_virtual_orchestration_tool("task-status"));
        assert!(is_virtual_orchestration_tool("task_status"));
        assert!(is_virtual_orchestration_tool("TASK-STATUS"));
        assert!(is_virtual_orchestration_tool("  task-status  "));
    }

    #[test]
    fn recognizes_retry_task_tool() {
        assert!(is_virtual_orchestration_tool("retry-task"));
        assert!(is_virtual_orchestration_tool("retry_task"));
        assert!(is_virtual_orchestration_tool("RETRY-TASK"));
        assert!(is_virtual_orchestration_tool("  retry-task  "));
    }

    #[test]
    fn recognizes_list_open_tasks_tool() {
        assert!(is_virtual_orchestration_tool("list-open-tasks"));
        assert!(is_virtual_orchestration_tool("list_open_tasks"));
        assert!(is_virtual_orchestration_tool("LIST-OPEN-TASKS"));
        assert!(is_virtual_orchestration_tool("  list-open-tasks  "));
    }

    #[test]
    fn total_orchestration_tools_count() {
        let hyphenated = [
            "compose-subagent",
            "update-subagent-skills",
            "list-subagent-roster",
            "list-available-skills",
            "remove-subagent",
            "retire-unused-subagents",
            "task-status",
            "retry-task",
            "list-open-tasks",
            "compose-skill",
            "validate-subagent-roster",
        ];
        for tool in &hyphenated {
            assert!(
                is_virtual_orchestration_tool(tool),
                "expected '{tool}' to be recognized"
            );
        }
        let underscored = [
            "compose_subagent",
            "update_subagent_skills",
            "list_subagent_roster",
            "list_available_skills",
            "remove_subagent",
            "retire_unused_subagents",
            "task_status",
            "retry_task",
            "list_open_tasks",
            "compose_skill",
            "validate_subagent_roster",
        ];
        for tool in &underscored {
            assert!(
                is_virtual_orchestration_tool(tool),
                "expected '{tool}' to be recognized"
            );
        }
        assert_eq!(hyphenated.len(), 11);
        assert_eq!(underscored.len(), 11);
    }

    #[test]
    fn recognizes_compose_skill_tool() {
        assert!(is_virtual_orchestration_tool("compose-skill"));
        assert!(is_virtual_orchestration_tool("compose_skill"));
        assert!(is_virtual_orchestration_tool("COMPOSE-SKILL"));
        assert!(is_virtual_orchestration_tool("  compose-skill  "));
    }

    #[test]
    fn recognizes_validate_subagent_roster_tool() {
        assert!(is_virtual_orchestration_tool("validate-subagent-roster"));
        assert!(is_virtual_orchestration_tool("validate_subagent_roster"));
        assert!(is_virtual_orchestration_tool("VALIDATE-SUBAGENT-ROSTER"));
    }
}
