pub async fn wallet_balance(State(state): State<AppState>) -> impl IntoResponse {
    let balances = state.wallet.wallet.get_all_balances().await;
    let address = state.wallet.wallet.address();
    let chain_id = state.wallet.wallet.chain_id();
    let network = state.wallet.wallet.network_name();
    let config = state.config.read().await;
    let revenue_accounting =
        roboticus_db::revenue_accounting::revenue_accounting_summary(&state.db)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue accounting summary"))
            .unwrap_or_default();
    let revenue_swap_queue =
        roboticus_db::revenue_accounting::revenue_swap_queue_summary(&state.db)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue swap queue summary"))
            .unwrap_or_default();
    // Revenue introspection class — unified operational visibility surface
    let revenue_strategy_summary =
        roboticus_db::revenue_introspection::strategy_summary(&state.db)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue strategy summary"))
            .unwrap_or_default();
    let revenue_profitability =
        roboticus_db::revenue_introspection::strategy_profitability(&state.db)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue profitability"))
            .unwrap_or_default();
    let revenue_audit_log =
        roboticus_db::revenue_introspection::audit_log(&state.db, 50)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue audit log"))
            .unwrap_or_default();
    let pipeline_health =
        roboticus_db::revenue_introspection::pipeline_health(&state.db)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load pipeline health"))
            .unwrap_or_default();
    let revenue_feedback_summary =
        roboticus_db::revenue_feedback::revenue_feedback_summary_by_strategy(&state.db)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue feedback summary"))
            .unwrap_or_default();
    let revenue_swap_tasks =
        roboticus_db::revenue_swap_tasks::list_revenue_swap_tasks(&state.db, 200)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue swap tasks"))
            .unwrap_or_default();
    let revenue_tax_tasks =
        roboticus_db::revenue_tax_tasks::list_revenue_tax_tasks(&state.db, 200)
            .inspect_err(|e| tracing::warn!(error = %e, "failed to load revenue tax tasks"))
            .unwrap_or_default();
    let default_swap_chain = config.treasury.revenue_swap.default_chain.clone();
    let default_swap_chain_cfg = config
        .treasury
        .revenue_swap
        .chains
        .iter()
        .find(|c| c.chain.trim().eq_ignore_ascii_case(&default_swap_chain));
    let revenue_swap_chains: Vec<serde_json::Value> = config
        .treasury
        .revenue_swap
        .chains
        .iter()
        .map(|chain| {
            json!({
                "chain": chain.chain,
                "target_contract_address": chain.target_contract_address,
                "swap_contract_address": chain.swap_contract_address,
            })
        })
        .collect();

    // Backward compat: "balance" field is still the USDC balance
    let usdc_balance = balances
        .iter()
        .find(|b| b.symbol == "USDC")
        .map(|b| b.balance)
        .unwrap_or(0.0);

    let tokens: Vec<serde_json::Value> = balances
        .iter()
        .map(|b| {
            json!({
                "symbol": b.symbol,
                "name": b.name,
                "balance": b.balance,
                "formatted": format_balance(b.balance, &b.symbol),
                "contract": b.contract,
                "decimals": b.decimals,
                "is_native": b.is_native,
            })
        })
        .collect();
    let stable_balance = balances
        .iter()
        .filter(|b| matches!(b.symbol.as_str(), "USDC" | "USDT" | "DAI"))
        .map(|b| b.balance)
        .sum::<f64>();
    let seed_target_usdc = 50.0;
    let swap_submitted = revenue_swap_tasks.iter().any(|row| {
        row["source"]["swap_tx_hash"]
            .as_str()
            .map(|s| !s.trim().is_empty())
            .unwrap_or(false)
    });
    let swap_reconciled = revenue_swap_tasks.iter().any(|row| {
        row["status"]
            .as_str()
            .map(|s| matches!(s, "completed" | "failed"))
            .unwrap_or(false)
    });
    let tax_submitted = revenue_tax_tasks.iter().any(|row| {
        row["source"]["tax_tx_hash"]
            .as_str()
            .map(|s| !s.trim().is_empty())
            .unwrap_or(false)
    });
    let tax_reconciled = revenue_tax_tasks.iter().any(|row| {
        row["status"]
            .as_str()
            .map(|s| matches!(s, "completed" | "failed"))
            .unwrap_or(false)
    });
    let seed_readiness = json!({
        "seed_target_usdc": seed_target_usdc,
        "stable_balance_usdc": stable_balance,
        "meets_seed_target": stable_balance >= seed_target_usdc,
        "minimum_reserve_configured": config.treasury.minimum_reserve > 0.0,
        "swap_enabled": config.treasury.revenue_swap.enabled,
        "default_chain": default_swap_chain,
        "default_chain_has_target_contract": default_swap_chain_cfg
            .map(|c| !c.target_contract_address.trim().is_empty())
            .unwrap_or(false),
        "default_chain_has_swap_contract": default_swap_chain_cfg
            .and_then(|c| c.swap_contract_address.as_deref())
            .map(|s| !s.trim().is_empty())
            .unwrap_or(false),
    });
    let seed_progress = json!({
        "phase_1_seeded_and_visible": stable_balance > 0.0 && !address.trim().is_empty(),
        "phase_1_meets_target": stable_balance >= seed_target_usdc,
        "phase_2_revenue_cycle_complete": revenue_accounting.settled_jobs > 0,
        "phase_3_swap_submitted": swap_submitted,
        "phase_3_swap_reconciled": swap_reconciled,
        "phase_3_tax_submitted": tax_submitted,
        "phase_3_tax_reconciled": tax_reconciled,
        "phase_4_mechanic_clear": revenue_swap_queue.stale_in_progress == 0,
        "next_action": if stable_balance < seed_target_usdc {
            "seed treasury to the $50 exercise target"
        } else if revenue_accounting.settled_jobs <= 0 {
            "run one low-cost opportunity through settlement"
        } else if !swap_submitted {
            "submit the queued swap task"
        } else if !swap_reconciled {
            "reconcile the submitted swap receipt"
        } else if config.self_funding.tax.enabled && !tax_submitted {
            "submit the queued tax payout task"
        } else if config.self_funding.tax.enabled && !tax_reconciled {
            "reconcile the submitted tax payout receipt"
        } else {
            "run the failure-and-repair drill"
        },
    });
    let seed_exercise_plan = json!({
        "seed_target_usdc": seed_target_usdc,
        "phases": [
            {
                "id": "seed-and-visibility",
                "label": "Phase 1: seed and visibility",
                "goal": "Fund the treasury and verify that the balance and policy surfaces reflect the live seed.",
                "max_spend_usdc": 0.0,
                "success_signal": "Stable balance is visible and the treasury meets the configured seed target.",
            },
            {
                "id": "low-cost-revenue-cycle",
                "label": "Phase 2: low-cost revenue cycle",
                "goal": "Run one low-risk opportunity end-to-end through intake, qualification, planning, fulfillment, and settlement.",
                "max_spend_usdc": 10.0,
                "success_signal": "At least one revenue opportunity reaches settled with attribution recorded.",
            },
            {
                "id": "post-settlement-routing",
                "label": "Phase 3: post-settlement routing",
                "goal": "Submit and reconcile the queued swap and tax payout tasks generated by settlement.",
                "max_spend_usdc": 10.0,
                "success_signal": "Swap and tax payout tasks have tx hashes and reconciled terminal state.",
            },
            {
                "id": "failure-and-repair",
                "label": "Phase 4: failure and repair",
                "goal": "Exercise mechanic reconciliation to prove failed or stale revenue tasks can be repaired without state corruption.",
                "max_spend_usdc": 0.0,
                "success_signal": "Mechanic reports the revenue control plane as clear after repair.",
            }
        ],
        "abort_conditions": [
            "Stable treasury balance falls below the configured minimum reserve.",
            "Any swap or tax payout targets a chain or contract that is not explicitly configured.",
            "A submitted on-chain task remains pending beyond the operator's allowed retry window.",
            "Mechanic reports unrepaired stale revenue tasks after a repair attempt."
        ],
        "operator_guidance": [
            "Do not exceed the per-step spend cap until the previous phase is visibly complete.",
            "Pause after each submitted on-chain action and confirm the recorded tx hash before moving forward.",
            "If any abort condition trips, stop the exercise and inspect wallet, revenue queue, and mechanic output before retrying."
        ]
    });

    axum::Json(json!({
        "balance": format!("{usdc_balance:.2}"),
        "currency": "USDC",
        "address": address,
        "chain_id": chain_id,
        "network": network,
        "tokens": tokens,
        "treasury": {
            "per_payment_cap": config.treasury.per_payment_cap,
            "daily_inference_budget": config.treasury.daily_inference_budget,
            "minimum_reserve": config.treasury.minimum_reserve,
            "revenue_swap": {
                "enabled": config.treasury.revenue_swap.enabled,
                "target_symbol": config.treasury.revenue_swap.target_symbol,
                "default_chain": config.treasury.revenue_swap.default_chain,
                "chains": revenue_swap_chains,
            },
        },
        "self_funding": {
            "tax": {
                "enabled": config.self_funding.tax.enabled,
                "rate": config.self_funding.tax.rate,
                "destination_wallet": config.self_funding.tax.destination_wallet,
            },
        },
        "revenue_accounting": {
            "settled_jobs": revenue_accounting.settled_jobs,
            "gross_revenue_usdc": revenue_accounting.gross_revenue_usdc,
            "attributable_costs_usdc": revenue_accounting.attributable_costs_usdc,
            "net_profit_usdc": revenue_accounting.net_profit_usdc,
            "tax_paid_usdc": revenue_accounting.tax_paid_usdc,
            "retained_earnings_usdc": revenue_accounting.retained_earnings_usdc,
        },
        "revenue_swap_queue": {
            "total": revenue_swap_queue.total,
            "pending": revenue_swap_queue.pending,
            "in_progress": revenue_swap_queue.in_progress,
            "failed": revenue_swap_queue.failed,
            "completed": revenue_swap_queue.completed,
            "stale_in_progress": revenue_swap_queue.stale_in_progress,
        },
        "revenue_tax_queue": {
            "total": revenue_tax_tasks.len(),
            "pending": revenue_tax_tasks.iter().filter(|r| r["status"].as_str() == Some("pending")).count(),
            "in_progress": revenue_tax_tasks.iter().filter(|r| r["status"].as_str() == Some("in_progress")).count(),
            "failed": revenue_tax_tasks.iter().filter(|r| r["status"].as_str() == Some("failed")).count(),
            "completed": revenue_tax_tasks.iter().filter(|r| r["status"].as_str() == Some("completed")).count(),
        },
        "revenue_strategy_summary": revenue_strategy_summary,
        "revenue_profitability": revenue_profitability,
        "revenue_audit_log": revenue_audit_log,
        "revenue_pipeline_health": pipeline_health,
        "revenue_feedback_summary": revenue_feedback_summary,
        "seed_exercise_readiness": seed_readiness,
        "seed_exercise_progress": seed_progress,
        "seed_exercise_plan": seed_exercise_plan,
    }))
}

fn format_balance(balance: f64, symbol: &str) -> String {
    match symbol {
        "USDC" | "USDT" | "DAI" => format!("{balance:.2}"),
        "ETH" | "WETH" | "MATIC" => format!("{balance:.6}"),
        "WBTC" | "cbBTC" => format!("{balance:.8}"),
        _ => format!("{balance:.4}"),
    }
}

fn plugin_tool_required_permissions(tool_name: &str, input: &Value) -> Vec<&'static str> {
    let mut required = Vec::new();
    let _ = tool_name;
    let scan = input_capability_scan::scan_input_capabilities(input);
    if scan.requires_filesystem {
        required.push("filesystem");
    }
    if scan.requires_network && !required.contains(&"network") {
        required.push("network");
    }
    required
}

pub async fn wallet_address(State(state): State<AppState>) -> impl IntoResponse {
    let address = state.wallet.wallet.address().to_string();
    let chain_id = state.wallet.wallet.chain_id();
    let network = state.wallet.wallet.network_name();

    axum::Json(json!({
        "address": address,
        "chain_id": chain_id,
        "network": network,
    }))
}

pub async fn get_plugins(State(state): State<AppState>) -> impl IntoResponse {
    let plugins = state.plugins.list_plugins().await;
    let count = plugins.len();
    let tools = state.plugins.list_all_tools().await;
    Json(json!({
        "plugins": plugins,
        "count": count,
        "total_tools": tools.len(),
    }))
}

pub async fn toggle_plugin(
    State(state): State<AppState>,
    axum::extract::Path(name): axum::extract::Path<String>,
) -> std::result::Result<impl IntoResponse, JsonError> {
    let plugins = state.plugins.list_plugins().await;
    let current = plugins.iter().find(|p| p.name == name);

    match current {
        Some(info) => {
            let result = if info.status == roboticus_plugin_sdk::PluginStatus::Active {
                state.plugins.disable_plugin(&name).await
            } else {
                state.plugins.enable_plugin(&name).await
            };

            match result {
                Ok(()) => {
                    let new_plugins = state.plugins.list_plugins().await;
                    let new_status = new_plugins
                        .iter()
                        .find(|p| p.name == name)
                        .map(|p| p.status);
                    Ok(Json(json!({
                        "name": name,
                        "toggled": true,
                        "status": new_status,
                    })))
                }
                Err(e) => Err(internal_err(&e)),
            }
        }
        None => Err(not_found(format!("plugin '{name}' not found"))),
    }
}

pub async fn execute_plugin_tool(
    State(state): State<AppState>,
    axum::extract::Path((name, tool)): axum::extract::Path<(String, String)>,
    Json(body): Json<Value>,
) -> std::result::Result<impl IntoResponse, JsonError> {
    let found = state.plugins.find_tool(&tool).await;
    match found {
        Some((plugin_name, tool_def)) if plugin_name == name => {
            let declared_permissions: Vec<String> = tool_def
                .permissions
                .iter()
                .map(|p| p.to_lowercase())
                .collect();
            let required_permissions = plugin_tool_required_permissions(&tool, &body);
            let missing: Vec<&str> = required_permissions
                .iter()
                .copied()
                .filter(|need| !declared_permissions.iter().any(|p| p == need))
                .collect();
            if !missing.is_empty() {
                return Err(JsonError(
                    StatusCode::FORBIDDEN,
                    format!(
                        "plugin '{}' tool '{}' missing required permissions: {}",
                        name,
                        tool,
                        missing.join(", ")
                    ),
                ));
            }
            let call = ToolCallRequest {
                tool_name: tool.clone(),
                params: body.clone(),
                risk_level: tool_def.risk_level,
            };
            let ctx = PolicyContext {
                authority: InputAuthority::External,
                survival_tier: SurvivalTier::Normal,
                claim: None,
            };
            let decision = state.policy_engine.evaluate_all(&call, &ctx);
            if !decision.is_allowed() {
                let reason = match &decision {
                    PolicyDecision::Deny { reason, .. } => reason.clone(),
                    _ => "policy denied".into(),
                };
                return Err(JsonError(StatusCode::FORBIDDEN, reason));
            }
            match state.plugins.execute_tool(&tool, &body).await {
                Ok(result) => Ok(Json(json!({
                    "plugin": name,
                    "tool": tool,
                    "result": result,
                }))),
                Err(e) => Err(internal_err(&e)),
            }
        }
        Some((other_plugin, _)) => Err(bad_request(format!(
            "tool '{tool}' belongs to plugin '{other_plugin}', not '{name}'"
        ))),
        None => Err(not_found(format!(
            "tool '{tool}' not found in plugin '{name}'"
        ))),
    }
}

pub async fn browser_status(State(state): State<AppState>) -> impl IntoResponse {
    let running = state.browser.is_running().await;
    let config = state.config.read().await;
    Json(json!({
        "running": running,
        "enabled": config.browser.enabled,
        "headless": config.browser.headless,
        "cdp_port": config.browser.cdp_port,
    }))
}

pub async fn browser_start(
    State(state): State<AppState>,
) -> std::result::Result<impl IntoResponse, JsonError> {
    if state.browser.is_running().await {
        return Ok(Json(json!({"status": "already_running"})));
    }
    match state.browser.start().await {
        Ok(()) => Ok(Json(json!({
            "status": "started",
            "cdp_port": state.browser.cdp_port(),
        }))),
        Err(e) => Err(internal_err(&e)),
    }
}

pub async fn browser_stop(
    State(state): State<AppState>,
) -> std::result::Result<impl IntoResponse, JsonError> {
    match state.browser.stop().await {
        Ok(()) => Ok(Json(json!({"status": "stopped"}))),
        Err(e) => Err(internal_err(&e)),
    }
}

pub async fn browser_action(
    State(state): State<AppState>,
    Json(action): Json<roboticus_browser::actions::BrowserAction>,
) -> impl IntoResponse {
    let result = state.browser.execute_action(&action).await;
    let status = if result.success {
        StatusCode::OK
    } else {
        StatusCode::BAD_REQUEST
    };
    (status, Json(json!(result)))
}

pub async fn get_agents(State(state): State<AppState>) -> impl IntoResponse {
    let agents = state.registry.list_agents().await;
    let count = agents.len();
    Json(json!({"agents": agents, "count": count}))
}

pub async fn start_agent(
    State(state): State<AppState>,
    axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<impl IntoResponse, JsonError> {
    state.registry.start_agent(&id).await.map_err(not_found)?;
    let event = json!({"type": "agent_started", "agent_id": id});
    state.event_bus.publish(event.to_string());
    Ok(Json(json!({"id": id, "action": "started"})))
}

pub async fn stop_agent(
    State(state): State<AppState>,
    axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<impl IntoResponse, JsonError> {
    state.registry.stop_agent(&id).await.map_err(not_found)?;
    let event = json!({"type": "agent_stopped", "agent_id": id});
    state.event_bus.publish(event.to_string());
    Ok(Json(json!({"id": id, "action": "stopped"})))
}

const WORKSPACE_PALETTE: &[&str] = &[
    "#6366f1", "#22c55e", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4", "#ec4899", "#14b8a6",
    "#f97316", "#84cc16", "#a855f7", "#0ea5e9",
];
const ROLE_SUBAGENT: &str = "subagent";
const ROLE_MODEL_PROXY: &str = "model-proxy";

const WORKSPACE_ACTIVITY_WINDOW_SECS: i64 = 120;
