//! MCP server -- JSON-RPC 2.0 over stdin/stdout.
//!
//! Reads one JSON object per line from stdin, dispatches to the
//! appropriate handler, and writes the response to stdout.
//! Tool handlers make real gRPC calls to the API Gateway via [`FoxhuntClient`].

use serde_json::{json, Value};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};

use crate::grpc::FoxhuntClient;

use super::protocol::{
    error_response, success_response, JsonRpcRequest, JsonRpcResponse, INVALID_PARAMS,
    INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
};
use super::tools;

/// MCP server that exposes Foxhunt operations as tools.
pub struct McpServer {
    /// API Gateway URL (for diagnostic logging).
    api_url: String,
    /// gRPC client connected to the API Gateway.
    client: FoxhuntClient,
}

impl McpServer {
    /// Create a new MCP server targeting the given API endpoint.
    pub fn new(api_url: String, client: FoxhuntClient) -> Self {
        Self { api_url, client }
    }

    /// Run the server loop: read JSON lines from stdin, write responses to stdout.
    ///
    /// Terminates when stdin reaches EOF.
    pub async fn run(&self) -> anyhow::Result<()> {
        let stdin = tokio::io::stdin();
        let mut stdout = tokio::io::stdout();
        let reader = BufReader::new(stdin);
        let mut lines = reader.lines();

        // Stderr for server diagnostics (never write diagnostics to stdout).
        eprintln!("[fxt-mcp] server started, api_url={}", self.api_url);

        loop {
            let Some(line) = lines.next_line().await? else {
                break; // EOF
            };

            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }

            // Parse the incoming JSON-RPC request.
            let request: JsonRpcRequest = match serde_json::from_str(trimmed) {
                Ok(r) => r,
                Err(e) => {
                    let resp = error_response(
                        Value::Null,
                        PARSE_ERROR,
                        &format!("Parse error: {e}"),
                    );
                    write_response(&mut stdout, &resp).await?;
                    continue;
                }
            };

            // Notifications (no id) do not get a response.
            let is_notification = request.id.is_none();

            let response = self.handle_request(&request).await;

            if !is_notification {
                if let Some(resp) = response {
                    write_response(&mut stdout, &resp).await?;
                }
            }
        }

        eprintln!("[fxt-mcp] stdin closed, shutting down");
        Ok(())
    }

    /// Dispatch a request to the appropriate handler.
    ///
    /// Returns `None` for notifications that need no response.
    async fn handle_request(&self, req: &JsonRpcRequest) -> Option<JsonRpcResponse> {
        let id = req.id.clone().unwrap_or(Value::Null);

        if req.jsonrpc != "2.0" {
            return Some(error_response(
                id,
                INVALID_REQUEST,
                "Invalid JSON-RPC version",
            ));
        }

        match req.method.as_str() {
            "initialize" => Some(self.handle_initialize(id)),
            "notifications/initialized" | "notifications/cancelled" => None,
            "tools/list" => Some(self.handle_tools_list(id)),
            "tools/call" => Some(self.handle_tools_call(id, &req.params).await),
            _ => Some(error_response(
                id,
                METHOD_NOT_FOUND,
                &format!("unknown method: {}", req.method),
            )),
        }
    }

    /// Handle `initialize` -- return server capabilities.
    fn handle_initialize(&self, id: Value) -> JsonRpcResponse {
        success_response(
            id,
            json!({
                "protocolVersion": "2024-11-05",
                "serverInfo": {
                    "name": "fxt",
                    "version": env!("CARGO_PKG_VERSION")
                },
                "capabilities": {
                    "tools": {}
                }
            }),
        )
    }

    /// Handle `tools/list` -- enumerate all available tools.
    fn handle_tools_list(&self, id: Value) -> JsonRpcResponse {
        let tool_defs = tools::all_tools();

        // Convert to the MCP wire format.
        let tools_json: Vec<Value> = tool_defs
            .iter()
            .map(|t| {
                json!({
                    "name": t.name,
                    "description": t.description,
                    "inputSchema": t.input_schema
                })
            })
            .collect();

        success_response(id, json!({ "tools": tools_json }))
    }

    /// Handle `tools/call` -- execute a tool and return the result.
    async fn handle_tools_call(&self, id: Value, params: &Value) -> JsonRpcResponse {
        let Some(tool_name) = params.get("name").and_then(Value::as_str) else {
            return error_response(id, INVALID_PARAMS, "missing 'name' in tools/call params");
        };

        let arguments = params
            .get("arguments")
            .cloned()
            .unwrap_or(Value::Object(serde_json::Map::new()));

        // Verify the tool exists.
        let known = tools::all_tools();
        let tool_exists = known.iter().any(|t| t.name == tool_name);
        if !tool_exists {
            return error_response(
                id,
                INVALID_PARAMS,
                &format!("unknown tool: {tool_name}"),
            );
        }

        let result = self.execute_tool(tool_name, &arguments).await;

        match result {
            Ok(content) => success_response(
                id,
                json!({
                    "content": [{
                        "type": "text",
                        "text": content
                    }]
                }),
            ),
            Err(e) => {
                // Tool execution failed -- return as tool error, not protocol error.
                success_response(
                    id,
                    json!({
                        "content": [{
                            "type": "text",
                            "text": format!("error: {e}")
                        }],
                        "isError": true
                    }),
                )
            }
        }
    }

    /// Execute a tool by name with the given arguments.
    ///
    /// Each tool maps to a real gRPC call against the API Gateway.
    async fn execute_tool(
        &self,
        tool_name: &str,
        arguments: &Value,
    ) -> Result<String, anyhow::Error> {
        match tool_name {
            // ── Service ─────────────────────────────────────────────
            "fxt_service_list" => self.tool_service_list().await,
            "fxt_service_status" => self.tool_service_status(arguments).await,
            "fxt_service_health" => self.tool_service_health().await,

            // ── Training ────────────────────────────────────────────
            "fxt_train_start" => self.tool_train_start(arguments).await,
            "fxt_train_stop" => self.tool_train_stop(arguments).await,
            "fxt_train_status" => self.tool_train_status(arguments).await,
            "fxt_train_list" => self.tool_train_list().await,

            // ── Trading ─────────────────────────────────────────────
            "fxt_trade_positions" => self.tool_trade_positions().await,
            "fxt_trade_orders" => self.tool_trade_orders(arguments).await,
            "fxt_trade_account" => self.tool_trade_account().await,
            "fxt_trade_submit" => self.tool_trade_submit(arguments).await,

            // ── Models ──────────────────────────────────────────────
            "fxt_model_list" => self.tool_model_list().await,
            "fxt_model_status" => self.tool_model_status(arguments).await,
            "fxt_model_predict" => self.tool_model_predict(arguments).await,
            "fxt_model_ensemble" => self.tool_model_ensemble(arguments).await,

            // ── Broker ──────────────────────────────────────────────
            "fxt_broker_status" => self.tool_broker_status().await,
            "fxt_broker_connect" => self.tool_broker_connect().await,

            // ── Risk ────────────────────────────────────────────────
            "fxt_risk_status" => self.tool_risk_status().await,
            "fxt_risk_limits" => self.tool_risk_limits().await,
            "fxt_risk_drawdown" => self.tool_risk_drawdown().await,
            "fxt_risk_emergency" => self.tool_risk_emergency(arguments).await,

            // ── Data ────────────────────────────────────────────────
            "fxt_data_status" => self.tool_data_status().await,
            "fxt_data_feeds" => self.tool_data_feeds().await,

            // ── Cluster ─────────────────────────────────────────────
            "fxt_cluster_status" => self.tool_cluster_status().await,
            "fxt_cluster_resources" => self.tool_cluster_resources().await,

            // ── Agent ───────────────────────────────────────────────
            "fxt_agent_start" => self.tool_agent_start().await,
            "fxt_agent_stop" => self.tool_agent_stop().await,
            "fxt_agent_status" => self.tool_agent_status().await,

            // ── Config ──────────────────────────────────────────────
            "fxt_config_get" => self.tool_config_get(arguments).await,
            "fxt_config_set" => self.tool_config_set(arguments).await,

            // ── Tune ────────────────────────────────────────────────
            "fxt_tune_start" => self.tool_tune_start(arguments).await,
            "fxt_tune_status" => self.tool_tune_status(arguments).await,

            _ => Err(anyhow::anyhow!("unknown tool: {tool_name}")),
        }
    }

    // ══════════════════════════════════════════════════════════════════
    //  Argument helpers
    // ══════════════════════════════════════════════════════════════════

    /// Extract a required string argument.
    fn arg_str<'args>(args: &'args Value, key: &str) -> Result<&'args str, anyhow::Error> {
        args.get(key)
            .and_then(Value::as_str)
            .ok_or_else(|| anyhow::anyhow!("missing required argument: {key}"))
    }

    /// Extract an optional string argument.
    fn arg_str_opt<'args>(args: &'args Value, key: &str) -> Option<&'args str> {
        args.get(key).and_then(Value::as_str)
    }

    /// Extract an optional f64 argument.
    fn arg_f64_opt(args: &Value, key: &str) -> Option<f64> {
        args.get(key).and_then(Value::as_f64)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Service tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_service_list(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .monitoring()
            .get_system_status(crate::proto::monitoring::GetSystemStatusRequest {
                service_names: vec![],
            })
            .await?
            .into_inner();

        let overall = resp.overall_status.unwrap_or_default();
        let services: Vec<Value> = resp
            .service_statuses
            .iter()
            .map(|s| {
                json!({
                    "name": s.service_name,
                    "health": service_health_str(s.health),
                    "state": service_state_str(s.state),
                    "version": s.version.clone().unwrap_or_default(),
                    "uptime_seconds": s.uptime_seconds,
                    "error": s.error_message.clone().unwrap_or_default(),
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "overall_health": system_health_str(overall.overall_health),
            "healthy": overall.healthy_services,
            "total": overall.total_services,
            "uptime_seconds": overall.system_uptime_seconds,
            "services": services,
        }))?)
    }

    async fn tool_service_status(&self, args: &Value) -> Result<String, anyhow::Error> {
        let service = Self::arg_str(args, "service")?;
        let resp = self
            .client
            .monitoring()
            .get_system_status(crate::proto::monitoring::GetSystemStatusRequest {
                service_names: vec![service.to_owned()],
            })
            .await?
            .into_inner();

        let svc = resp
            .service_statuses
            .into_iter()
            .find(|s| s.service_name == service)
            .ok_or_else(|| anyhow::anyhow!("service '{service}' not found"))?;

        let deps: Vec<Value> = svc
            .dependencies
            .iter()
            .map(|d| {
                json!({
                    "name": d.name,
                    "status": health_status_str(d.status),
                    "endpoint": d.endpoint.clone().unwrap_or_default(),
                    "response_time_ms": d.response_time_ms.unwrap_or_default(),
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "name": svc.service_name,
            "health": service_health_str(svc.health),
            "state": service_state_str(svc.state),
            "version": svc.version.unwrap_or_default(),
            "uptime_seconds": svc.uptime_seconds,
            "error": svc.error_message.unwrap_or_default(),
            "metadata": svc.metadata,
            "dependencies": deps,
        }))?)
    }

    async fn tool_service_health(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .monitoring()
            .get_health_check(crate::proto::monitoring::GetHealthCheckRequest {
                service_name: None,
            })
            .await?
            .into_inner();

        let checks: Vec<Value> = resp
            .health_checks
            .iter()
            .map(|c| {
                json!({
                    "name": c.check_name,
                    "status": health_status_str(c.status),
                    "message": c.message.clone().unwrap_or_default(),
                    "response_time_ms": c.response_time_ms.unwrap_or_default(),
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "overall": health_status_str(resp.health_status),
            "checks": checks,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Training tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_train_start(&self, args: &Value) -> Result<String, anyhow::Error> {
        let model = Self::arg_str(args, "model")?;
        let symbol = Self::arg_str_opt(args, "symbol");

        let data_source = symbol.map(|s| crate::proto::ml_training::DataSource {
            source: Some(crate::proto::ml_training::data_source::Source::FilePath(
                s.to_owned(),
            )),
            start_time: 0,
            end_time: 0,
        });

        let resp = self
            .client
            .ml_training()
            .start_training(crate::proto::ml_training::StartTrainingRequest {
                model_type: model.to_uppercase(),
                data_source,
                hyperparameters: None,
                use_gpu: true,
                description: String::new(),
                tags: Default::default(),
                mode: crate::proto::ml_training::TrainingMode::Full as i32,
                resume_checkpoint_path: String::new(),
                max_epochs: 0,
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "job_id": resp.job_id,
            "status": training_status_str(resp.status),
            "message": resp.message,
        }))?)
    }

    async fn tool_train_stop(&self, args: &Value) -> Result<String, anyhow::Error> {
        let job_id = Self::arg_str(args, "job_id")?;
        let resp = self
            .client
            .ml_training()
            .stop_training(crate::proto::ml_training::StopTrainingRequest {
                job_id: job_id.to_owned(),
                reason: String::new(),
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "success": resp.success,
            "message": resp.message,
        }))?)
    }

    async fn tool_train_status(&self, args: &Value) -> Result<String, anyhow::Error> {
        let job_id = Self::arg_str(args, "job_id")?;
        let resp = self
            .client
            .ml_training()
            .get_training_job_details(
                crate::proto::ml_training::GetTrainingJobDetailsRequest {
                    job_id: job_id.to_owned(),
                },
            )
            .await?
            .into_inner();

        let details = resp.job_details.unwrap_or_default();
        let fm = details.final_financial_metrics.as_ref();

        Ok(serde_json::to_string_pretty(&json!({
            "job_id": details.job_id,
            "model_type": details.model_type,
            "status": training_status_str(details.status),
            "description": details.description,
            "created_at": details.created_at,
            "started_at": details.started_at,
            "completed_at": details.completed_at,
            "error": details.error_message,
            "artifact_path": details.model_artifact_path,
            "financial_metrics": fm.map(|m| json!({
                "sharpe_ratio": m.sharpe_ratio,
                "max_drawdown": m.max_drawdown,
                "hit_rate": m.hit_rate,
                "simulated_return": m.simulated_return,
            })),
        }))?)
    }

    async fn tool_train_list(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .ml_training()
            .list_training_jobs(crate::proto::ml_training::ListTrainingJobsRequest {
                page: 0,
                page_size: 50,
                status_filter: 0,
                model_type_filter: String::new(),
                start_time: 0,
                end_time: 0,
            })
            .await?
            .into_inner();

        let jobs: Vec<Value> = resp
            .jobs
            .iter()
            .map(|j| {
                json!({
                    "job_id": j.job_id,
                    "model_type": j.model_type,
                    "status": training_status_str(j.status),
                    "description": j.description,
                    "final_loss": j.final_loss,
                    "best_validation_score": j.best_validation_score,
                    "created_at": j.created_at,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "total": resp.total_count,
            "jobs": jobs,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Trading tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_trade_positions(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .trading()
            .get_positions(crate::proto::trading::GetPositionsRequest {
                account_id: None,
                symbol: None,
            })
            .await?
            .into_inner();

        let positions: Vec<Value> = resp
            .positions
            .iter()
            .map(|p| {
                json!({
                    "symbol": p.symbol,
                    "quantity": p.quantity,
                    "avg_price": p.average_price,
                    "market_value": p.market_value,
                    "unrealized_pnl": p.unrealized_pnl,
                    "realized_pnl": p.realized_pnl,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "positions": positions,
        }))?)
    }

    async fn tool_trade_orders(&self, args: &Value) -> Result<String, anyhow::Error> {
        let order_id = Self::arg_str_opt(args, "order_id");

        if let Some(oid) = order_id {
            let resp = self
                .client
                .trading()
                .get_order_status(crate::proto::trading::GetOrderStatusRequest {
                    order_id: oid.to_owned(),
                })
                .await?
                .into_inner();

            let order = resp
                .order
                .ok_or_else(|| anyhow::anyhow!("order {oid} not found"))?;

            Ok(serde_json::to_string_pretty(&json!({
                "order_id": order.order_id,
                "symbol": order.symbol,
                "side": side_name(order.side),
                "quantity": order.quantity,
                "filled_quantity": order.filled_quantity,
                "order_type": order_type_name(order.order_type),
                "price": order.price,
                "status": order_status_name(order.status),
            }))?)
        } else {
            // No order_id: return a hint
            Ok(serde_json::to_string_pretty(&json!({
                "message": "Provide an order_id argument to look up a specific order. Use fxt_trade_positions for current positions.",
            }))?)
        }
    }

    async fn tool_trade_account(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .broker_gateway()
            .get_account_state(crate::proto::broker_gateway::GetAccountStateRequest {
                account_id: String::new(),
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "account_id": resp.account_id,
            "cash_balance": resp.cash_balance,
            "equity": resp.equity,
            "margin_used": resp.margin_used,
            "margin_available": resp.margin_available,
            "buying_power": resp.buying_power,
            "unrealized_pnl": resp.unrealized_pnl,
            "realized_pnl": resp.realized_pnl,
        }))?)
    }

    async fn tool_trade_submit(&self, args: &Value) -> Result<String, anyhow::Error> {
        let symbol = Self::arg_str(args, "symbol")?;
        let side = Self::arg_str(args, "side")?;
        let quantity = Self::arg_f64_opt(args, "quantity")
            .ok_or_else(|| anyhow::anyhow!("missing required argument: quantity"))?;
        let order_type_str = Self::arg_str(args, "order_type")?;
        let price = Self::arg_f64_opt(args, "price");

        let side_val = match side.to_lowercase().as_str() {
            "buy" | "long" => crate::proto::trading::OrderSide::Buy as i32,
            "sell" | "short" => crate::proto::trading::OrderSide::Sell as i32,
            _ => return Err(anyhow::anyhow!("invalid side '{side}': expected 'buy' or 'sell'")),
        };

        let order_type = match order_type_str.to_lowercase().as_str() {
            "market" => crate::proto::trading::OrderType::Market as i32,
            "limit" => crate::proto::trading::OrderType::Limit as i32,
            "stop" => crate::proto::trading::OrderType::Stop as i32,
            _ => crate::proto::trading::OrderType::Market as i32,
        };

        let resp = self
            .client
            .trading()
            .submit_order(crate::proto::trading::SubmitOrderRequest {
                symbol: symbol.to_owned(),
                side: side_val,
                quantity,
                order_type,
                price,
                stop_price: None,
                account_id: String::new(),
                metadata: std::collections::HashMap::new(),
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "order_id": resp.order_id,
            "status": order_status_name(resp.status),
            "message": resp.message,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Model tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_model_list(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .ml()
            .get_available_models(crate::proto::ml::GetAvailableModelsRequest {})
            .await?
            .into_inner();

        let models: Vec<Value> = resp
            .available_models
            .iter()
            .map(|m| {
                json!({
                    "name": m.model_name,
                    "model_type": m.model_type,
                    "description": m.description,
                    "symbols": m.supported_symbols,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "count": models.len(),
            "models": models,
        }))?)
    }

    async fn tool_model_status(&self, args: &Value) -> Result<String, anyhow::Error> {
        let model = Self::arg_str(args, "model")?;
        let resp = self
            .client
            .ml()
            .get_model_status(crate::proto::ml::GetModelStatusRequest {
                model_name: Some(model.to_owned()),
            })
            .await?
            .into_inner();

        let s = resp
            .model_statuses
            .into_iter()
            .find(|s| s.model_name == model)
            .ok_or_else(|| anyhow::anyhow!("model '{model}' not found"))?;

        Ok(serde_json::to_string_pretty(&json!({
            "name": s.model_name,
            "state": model_state_str(s.state),
            "health": model_health_str(s.health),
            "error": s.error_message.unwrap_or_default(),
            "last_updated": s.last_updated,
            "last_prediction": s.last_prediction,
            "metadata": s.metadata,
        }))?)
    }

    async fn tool_model_predict(&self, args: &Value) -> Result<String, anyhow::Error> {
        let model = Self::arg_str(args, "model")?;
        let symbol = Self::arg_str(args, "symbol")?;
        let resp = self
            .client
            .ml()
            .get_prediction(crate::proto::ml::GetPredictionRequest {
                model_name: model.to_owned(),
                symbol: symbol.to_owned(),
                horizon_minutes: None,
                features: Default::default(),
            })
            .await?
            .into_inner();

        let pred = resp.prediction.unwrap_or_default();

        Ok(serde_json::to_string_pretty(&json!({
            "model": pred.model_name,
            "symbol": pred.symbol,
            "prediction_type": prediction_type_str(pred.prediction_type),
            "value": pred.value,
            "confidence": resp.confidence,
            "horizon_minutes": pred.horizon_minutes,
        }))?)
    }

    async fn tool_model_ensemble(&self, args: &Value) -> Result<String, anyhow::Error> {
        let symbol = Self::arg_str(args, "symbol")?;
        let resp = self
            .client
            .ml()
            .get_ensemble_vote(crate::proto::ml::GetEnsembleVoteRequest {
                symbol: symbol.to_owned(),
                horizon_minutes: None,
                model_names: vec![],
            })
            .await?
            .into_inner();

        let vote = resp.ensemble_vote.unwrap_or_default();
        let individual: Vec<Value> = resp
            .individual_votes
            .iter()
            .map(|v| {
                json!({
                    "model": v.model_name,
                    "prediction": prediction_type_str(v.prediction),
                    "confidence": v.confidence,
                    "weight": v.weight,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "symbol": vote.symbol,
            "consensus": prediction_type_str(vote.consensus_prediction),
            "confidence": resp.overall_confidence,
            "signal_strength": signal_strength_str(vote.signal_strength),
            "votes_buy": vote.votes_buy,
            "votes_sell": vote.votes_sell,
            "votes_hold": vote.votes_hold,
            "total_models": vote.total_models,
            "individual_votes": individual,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Broker tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_broker_status(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .broker_gateway()
            .get_session_status(crate::proto::broker_gateway::GetSessionStatusRequest {
                session_id: None,
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "session_id": resp.session_id,
            "state": session_state_name(resp.state),
            "sender_seq_num": resp.sender_seq_num,
            "target_seq_num": resp.target_seq_num,
            "heartbeat_rtt_ms": resp.heartbeat_rtt_ms,
            "details": resp.details,
        }))?)
    }

    async fn tool_broker_connect(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .broker_gateway()
            .health_check(crate::proto::broker_gateway::HealthCheckRequest {})
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "healthy": resp.healthy,
            "message": resp.message,
            "details": resp.details,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Risk tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_risk_status(&self) -> Result<String, anyhow::Error> {
        let metrics_resp = self
            .client
            .risk()
            .get_risk_metrics(crate::proto::risk::GetRiskMetricsRequest {
                portfolio_id: None,
            })
            .await?
            .into_inner();

        let cb_resp = self
            .client
            .risk()
            .get_circuit_breaker_status(
                crate::proto::risk::GetCircuitBreakerStatusRequest { symbol: None },
            )
            .await?
            .into_inner();

        let m = metrics_resp.metrics.unwrap_or_default();

        let breakers: Vec<Value> = cb_resp
            .circuit_breakers
            .iter()
            .map(|cb| {
                json!({
                    "name": cb.name,
                    "is_triggered": cb.is_triggered,
                    "trigger_reason": cb.trigger_reason.clone().unwrap_or_default(),
                    "breaker_type": breaker_type_name(cb.breaker_type),
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "var_1d": m.portfolio_var_1d,
            "var_5d": m.portfolio_var_5d,
            "current_drawdown": m.current_drawdown,
            "max_drawdown": m.max_drawdown,
            "sharpe_ratio": m.sharpe_ratio,
            "sortino_ratio": m.sortino_ratio,
            "volatility": m.volatility,
            "circuit_breakers": breakers,
        }))?)
    }

    async fn tool_risk_limits(&self) -> Result<String, anyhow::Error> {
        let metrics_resp = self
            .client
            .risk()
            .get_risk_metrics(crate::proto::risk::GetRiskMetricsRequest {
                portfolio_id: None,
            })
            .await?
            .into_inner();

        let m = metrics_resp.metrics.unwrap_or_default();

        let positions: Vec<Value> = m
            .position_risks
            .iter()
            .map(|p| {
                json!({
                    "symbol": p.symbol,
                    "position_size": p.position_size,
                    "market_value": p.market_value,
                    "var_contribution": p.var_contribution,
                    "concentration_risk": p.concentration_risk,
                    "liquidity_risk": p.liquidity_risk,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "var_1d": m.portfolio_var_1d,
            "var_5d": m.portfolio_var_5d,
            "var_30d": m.portfolio_var_30d,
            "positions": positions,
        }))?)
    }

    async fn tool_risk_drawdown(&self) -> Result<String, anyhow::Error> {
        let metrics_resp = self
            .client
            .risk()
            .get_risk_metrics(crate::proto::risk::GetRiskMetricsRequest {
                portfolio_id: None,
            })
            .await?
            .into_inner();

        let m = metrics_resp.metrics.unwrap_or_default();

        Ok(serde_json::to_string_pretty(&json!({
            "current_drawdown": m.current_drawdown,
            "max_drawdown": m.max_drawdown,
            "sharpe_ratio": m.sharpe_ratio,
            "sortino_ratio": m.sortino_ratio,
            "beta": m.beta,
            "alpha": m.alpha,
        }))?)
    }

    async fn tool_risk_emergency(&self, args: &Value) -> Result<String, anyhow::Error> {
        let action = Self::arg_str(args, "action")?;

        if action == "resume" {
            // Emergency resume is not a standard gRPC call -- return guidance.
            return Ok(serde_json::to_string_pretty(&json!({
                "message": "Resume from emergency halt requires manual operator action via the trading agent service.",
            }))?);
        }

        let resp = self
            .client
            .risk()
            .emergency_stop(crate::proto::risk::EmergencyStopRequest {
                stop_type: crate::proto::risk::EmergencyStopType::AllTrading as i32,
                reason: "Emergency halt via MCP tool".to_owned(),
                symbol: None,
                account_id: None,
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "success": resp.success,
            "message": resp.message,
            "affected_orders": resp.affected_orders,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Data tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_data_status(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .data_acquisition()
            .list_download_jobs(
                crate::proto::data_acquisition::ListDownloadJobsRequest {
                    page: 0,
                    page_size: 50,
                    status_filter: 0,
                    start_time: 0,
                    end_time: 0,
                },
            )
            .await?
            .into_inner();

        let jobs: Vec<Value> = resp
            .jobs
            .iter()
            .map(|j| {
                json!({
                    "job_id": j.job_id,
                    "status": download_status_name(j.status),
                    "dataset": j.dataset,
                    "symbols": j.symbols,
                    "start_date": j.start_date,
                    "end_date": j.end_date,
                    "progress_pct": j.progress_percentage,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "total": resp.total_count,
            "jobs": jobs,
        }))?)
    }

    async fn tool_data_feeds(&self) -> Result<String, anyhow::Error> {
        // The data acquisition service does not have a dedicated "list feeds" RPC.
        // Return status from the service discovery / monitoring instead.
        let resp = self
            .client
            .monitoring()
            .get_system_status(crate::proto::monitoring::GetSystemStatusRequest {
                service_names: vec!["data-acquisition".to_owned()],
            })
            .await?
            .into_inner();

        let svc = resp
            .service_statuses
            .first()
            .map(|s| {
                json!({
                    "service": s.service_name,
                    "health": service_health_str(s.health),
                    "state": service_state_str(s.state),
                })
            })
            .unwrap_or(json!({"message": "data-acquisition service not found"}));

        Ok(serde_json::to_string_pretty(&svc)?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Cluster tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_cluster_status(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .monitoring()
            .get_system_status(crate::proto::monitoring::GetSystemStatusRequest {
                service_names: vec![],
            })
            .await?
            .into_inner();

        let overall = resp.overall_status.unwrap_or_default();
        let sys_metrics = overall.system_metrics.unwrap_or_default();

        let services: Vec<Value> = resp
            .service_statuses
            .iter()
            .map(|s| {
                json!({
                    "name": s.service_name,
                    "health": service_health_str(s.health),
                    "state": service_state_str(s.state),
                    "version": s.version.clone().unwrap_or_default(),
                    "uptime_seconds": s.uptime_seconds,
                    "error": s.error_message.clone().unwrap_or_default(),
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "overall_health": system_health_str(overall.overall_health),
            "healthy_services": overall.healthy_services,
            "total_services": overall.total_services,
            "critical_issues": overall.critical_issues,
            "system_uptime_seconds": overall.system_uptime_seconds,
            "cpu_usage_pct": sys_metrics.cpu_usage_percent,
            "memory_usage_pct": sys_metrics.memory_usage_percent,
            "disk_usage_pct": sys_metrics.disk_usage_percent,
            "error_rate_pct": sys_metrics.error_rate_percent,
            "services": services,
        }))?)
    }

    async fn tool_cluster_resources(&self) -> Result<String, anyhow::Error> {
        let resource_names = vec![
            "cpu.usage".to_owned(),
            "memory.usage".to_owned(),
            "disk.usage".to_owned(),
            "gpu.utilization".to_owned(),
            "gpu.memory.used".to_owned(),
            "network.io".to_owned(),
        ];

        let resp = self
            .client
            .monitoring()
            .get_metrics(crate::proto::monitoring::GetMetricsRequest {
                metric_names: resource_names,
                start_time: None,
                end_time: None,
                aggregation: None,
            })
            .await?
            .into_inner();

        let metrics: Vec<Value> = resp
            .metrics
            .iter()
            .map(|m| {
                json!({
                    "name": m.name,
                    "value": m.value,
                    "unit": m.unit,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "metrics": metrics,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Agent tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_agent_start(&self) -> Result<String, anyhow::Error> {
        // List all strategies and enable each one.
        let list_resp = self
            .client
            .trading_agent()
            .list_strategies(crate::proto::trading_agent::ListStrategiesRequest {
                status_filter: None,
            })
            .await?
            .into_inner();

        let mut results = Vec::new();
        for s in &list_resp.strategies {
            let resp = self
                .client
                .trading_agent()
                .update_strategy_status(
                    crate::proto::trading_agent::UpdateStrategyStatusRequest {
                        strategy_id: s.strategy_id.clone(),
                        new_status: crate::proto::trading_agent::StrategyStatus::Enabled as i32,
                        reason: None,
                    },
                )
                .await?
                .into_inner();

            results.push(json!({
                "strategy_id": s.strategy_id,
                "success": resp.success,
                "message": resp.message,
            }));
        }

        Ok(serde_json::to_string_pretty(&json!({
            "action": "start",
            "strategies_updated": results.len(),
            "results": results,
        }))?)
    }

    async fn tool_agent_stop(&self) -> Result<String, anyhow::Error> {
        // List all strategies and disable each one.
        let list_resp = self
            .client
            .trading_agent()
            .list_strategies(crate::proto::trading_agent::ListStrategiesRequest {
                status_filter: None,
            })
            .await?
            .into_inner();

        let mut results = Vec::new();
        for s in &list_resp.strategies {
            let resp = self
                .client
                .trading_agent()
                .update_strategy_status(
                    crate::proto::trading_agent::UpdateStrategyStatusRequest {
                        strategy_id: s.strategy_id.clone(),
                        new_status: crate::proto::trading_agent::StrategyStatus::Disabled as i32,
                        reason: None,
                    },
                )
                .await?
                .into_inner();

            results.push(json!({
                "strategy_id": s.strategy_id,
                "success": resp.success,
                "message": resp.message,
            }));
        }

        Ok(serde_json::to_string_pretty(&json!({
            "action": "stop",
            "strategies_updated": results.len(),
            "results": results,
        }))?)
    }

    async fn tool_agent_status(&self) -> Result<String, anyhow::Error> {
        let resp = self
            .client
            .trading_agent()
            .get_agent_status(crate::proto::trading_agent::GetAgentStatusRequest {
                include_performance: true,
                include_positions: false,
            })
            .await?
            .into_inner();

        let status = resp.status.unwrap_or_default();
        let perf = resp.performance.as_ref();

        Ok(serde_json::to_string_pretty(&json!({
            "state": agent_state_name(status.state),
            "active_strategies": status.active_strategies,
            "selected_assets": status.selected_assets,
            "portfolio_utilization": status.portfolio_utilization,
            "current_universe_id": status.current_universe_id,
            "performance": perf.map(|p| json!({
                "total_pnl": p.total_pnl,
                "sharpe_ratio": p.sharpe_ratio,
                "max_drawdown": p.max_drawdown,
                "win_rate": p.win_rate,
                "total_trades": p.total_trades,
                "avg_trade_pnl": p.avg_trade_pnl,
            })),
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Config tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_config_get(&self, args: &Value) -> Result<String, anyhow::Error> {
        let key = Self::arg_str(args, "key")?;
        let resp = self
            .client
            .config()
            .get_configuration(crate::proto::config::GetConfigurationRequest {
                category: None,
                key: Some(key.to_owned()),
                environment: None,
            })
            .await?
            .into_inner();

        let settings: Vec<Value> = resp
            .settings
            .iter()
            .map(|s| {
                json!({
                    "category": s.category,
                    "key": s.key,
                    "value": if s.sensitive { "********" } else { &s.value },
                    "data_type": config_data_type_name(s.data_type),
                    "hot_reload": s.hot_reload,
                    "description": s.description,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "settings": settings,
        }))?)
    }

    async fn tool_config_set(&self, args: &Value) -> Result<String, anyhow::Error> {
        let key = Self::arg_str(args, "key")?;
        let value = Self::arg_str(args, "value")?;
        let resp = self
            .client
            .config()
            .update_configuration(crate::proto::config::UpdateConfigurationRequest {
                category: "default".to_owned(),
                key: key.to_owned(),
                value: value.to_owned(),
                changed_by: "fxt-mcp".to_owned(),
                change_reason: None,
                environment: None,
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "success": resp.success,
            "message": resp.message,
        }))?)
    }

    // ══════════════════════════════════════════════════════════════════
    //  Tune tools
    // ══════════════════════════════════════════════════════════════════

    async fn tool_tune_start(&self, args: &Value) -> Result<String, anyhow::Error> {
        let model = Self::arg_str(args, "model")?;
        let resp = self
            .client
            .ml_training()
            .start_tuning_job(crate::proto::ml_training::StartTuningJobRequest {
                model_type: model.to_uppercase(),
                num_trials: 50,
                config_path: String::new(),
                data_source: None,
                use_gpu: true,
                description: String::new(),
                tags: Default::default(),
            })
            .await?
            .into_inner();

        Ok(serde_json::to_string_pretty(&json!({
            "job_id": resp.job_id,
            "status": tuning_status_str(resp.status),
            "message": resp.message,
        }))?)
    }

    async fn tool_tune_status(&self, args: &Value) -> Result<String, anyhow::Error> {
        let job_id = Self::arg_str_opt(args, "job_id")
            .ok_or_else(|| anyhow::anyhow!("missing required argument: job_id (provide the tuning job ID)"))?;

        let resp = self
            .client
            .ml_training()
            .get_tuning_job_status(crate::proto::ml_training::GetTuningJobStatusRequest {
                job_id: job_id.to_owned(),
            })
            .await?
            .into_inner();

        let best_params: Vec<Value> = resp
            .best_params
            .iter()
            .map(|(k, v)| json!({"name": k, "value": v}))
            .collect();

        let best_metrics: Vec<Value> = resp
            .best_metrics
            .iter()
            .map(|(k, v)| json!({"name": k, "value": v}))
            .collect();

        let trials: Vec<Value> = resp
            .trial_history
            .iter()
            .map(|t| {
                json!({
                    "trial": t.trial_number,
                    "objective": t.objective_value,
                    "state": trial_state_str(t.state),
                    "started_at": t.started_at,
                    "completed_at": t.completed_at,
                })
            })
            .collect();

        Ok(serde_json::to_string_pretty(&json!({
            "job_id": resp.job_id,
            "status": tuning_status_str(resp.status),
            "current_trial": resp.current_trial,
            "total_trials": resp.total_trials,
            "best_params": best_params,
            "best_metrics": best_metrics,
            "message": resp.message,
            "started_at": resp.started_at,
            "updated_at": resp.updated_at,
            "trials": trials,
        }))?)
    }
}

// ══════════════════════════════════════════════════════════════════════
//  Enum display helpers (match the CLI command patterns)
// ══════════════════════════════════════════════════════════════════════

fn service_health_str(h: i32) -> &'static str {
    use crate::proto::monitoring::ServiceHealth;
    match ServiceHealth::try_from(h) {
        Ok(ServiceHealth::Healthy) => "healthy",
        Ok(ServiceHealth::Degraded) => "degraded",
        Ok(ServiceHealth::Unhealthy) => "unhealthy",
        Ok(ServiceHealth::Critical) => "critical",
        _ => "unknown",
    }
}

fn service_state_str(s: i32) -> &'static str {
    use crate::proto::monitoring::ServiceState;
    match ServiceState::try_from(s) {
        Ok(ServiceState::Starting) => "starting",
        Ok(ServiceState::Running) => "running",
        Ok(ServiceState::Stopping) => "stopping",
        Ok(ServiceState::Stopped) => "stopped",
        Ok(ServiceState::Error) => "error",
        _ => "unknown",
    }
}

fn system_health_str(h: i32) -> &'static str {
    use crate::proto::monitoring::SystemHealth;
    match SystemHealth::try_from(h) {
        Ok(SystemHealth::Healthy) => "healthy",
        Ok(SystemHealth::Degraded) => "degraded",
        Ok(SystemHealth::Unhealthy) => "unhealthy",
        Ok(SystemHealth::Critical) => "critical",
        _ => "unknown",
    }
}

fn health_status_str(h: i32) -> &'static str {
    use crate::proto::monitoring::HealthStatus;
    match HealthStatus::try_from(h) {
        Ok(HealthStatus::Healthy) => "healthy",
        Ok(HealthStatus::Degraded) => "degraded",
        Ok(HealthStatus::Unhealthy) => "unhealthy",
        Ok(HealthStatus::Critical) => "critical",
        _ => "unknown",
    }
}

fn training_status_str(s: i32) -> &'static str {
    use crate::proto::ml_training::TrainingStatus;
    match TrainingStatus::try_from(s) {
        Ok(TrainingStatus::Pending) => "pending",
        Ok(TrainingStatus::Running) => "running",
        Ok(TrainingStatus::Completed) => "completed",
        Ok(TrainingStatus::Failed) => "failed",
        Ok(TrainingStatus::Stopped) => "stopped",
        Ok(TrainingStatus::Paused) => "paused",
        _ => "unknown",
    }
}

fn tuning_status_str(s: i32) -> &'static str {
    use crate::proto::ml_training::TuningJobStatus;
    match TuningJobStatus::try_from(s) {
        Ok(TuningJobStatus::TuningPending) => "pending",
        Ok(TuningJobStatus::TuningRunning) => "running",
        Ok(TuningJobStatus::TuningCompleted) => "completed",
        Ok(TuningJobStatus::TuningFailed) => "failed",
        Ok(TuningJobStatus::TuningStopped) => "stopped",
        _ => "unknown",
    }
}

fn trial_state_str(s: i32) -> &'static str {
    use crate::proto::ml_training::TrialState;
    match TrialState::try_from(s) {
        Ok(TrialState::TrialRunning) => "running",
        Ok(TrialState::TrialComplete) => "complete",
        Ok(TrialState::TrialPruned) => "pruned",
        Ok(TrialState::TrialFailed) => "failed",
        _ => "unknown",
    }
}

fn side_name(v: i32) -> &'static str {
    use crate::proto::trading::OrderSide;
    match OrderSide::try_from(v) {
        Ok(OrderSide::Buy) => "BUY",
        Ok(OrderSide::Sell) => "SELL",
        _ => "UNKNOWN",
    }
}

fn order_type_name(v: i32) -> &'static str {
    use crate::proto::trading::OrderType;
    match OrderType::try_from(v) {
        Ok(OrderType::Market) => "MARKET",
        Ok(OrderType::Limit) => "LIMIT",
        Ok(OrderType::Stop) => "STOP",
        Ok(OrderType::StopLimit) => "STOP_LIMIT",
        _ => "UNKNOWN",
    }
}

fn order_status_name(v: i32) -> &'static str {
    use crate::proto::trading::OrderStatus;
    match OrderStatus::try_from(v) {
        Ok(OrderStatus::Pending) => "PENDING",
        Ok(OrderStatus::Submitted) => "SUBMITTED",
        Ok(OrderStatus::PartiallyFilled) => "PARTIALLY_FILLED",
        Ok(OrderStatus::Filled) => "FILLED",
        Ok(OrderStatus::Cancelled) => "CANCELLED",
        Ok(OrderStatus::Rejected) => "REJECTED",
        _ => "UNKNOWN",
    }
}

fn model_state_str(s: i32) -> &'static str {
    use crate::proto::ml::ModelState;
    match ModelState::try_from(s) {
        Ok(ModelState::Loading) => "loading",
        Ok(ModelState::Ready) => "ready",
        Ok(ModelState::Predicting) => "predicting",
        Ok(ModelState::Training) => "training",
        Ok(ModelState::Error) => "error",
        Ok(ModelState::Offline) => "offline",
        _ => "unknown",
    }
}

fn model_health_str(h: i32) -> &'static str {
    use crate::proto::ml::ModelHealth;
    match ModelHealth::try_from(h) {
        Ok(ModelHealth::Healthy) => "healthy",
        Ok(ModelHealth::Degraded) => "degraded",
        Ok(ModelHealth::Unhealthy) => "unhealthy",
        Ok(ModelHealth::Critical) => "critical",
        _ => "unknown",
    }
}

fn prediction_type_str(p: i32) -> &'static str {
    use crate::proto::ml::PredictionType;
    match PredictionType::try_from(p) {
        Ok(PredictionType::Buy) => "BUY",
        Ok(PredictionType::Sell) => "SELL",
        Ok(PredictionType::Hold) => "HOLD",
        Ok(PredictionType::PriceUp) => "PRICE_UP",
        Ok(PredictionType::PriceDown) => "PRICE_DOWN",
        Ok(PredictionType::VolatilityHigh) => "VOL_HIGH",
        Ok(PredictionType::VolatilityLow) => "VOL_LOW",
        _ => "UNKNOWN",
    }
}

fn signal_strength_str(s: i32) -> &'static str {
    use crate::proto::ml::SignalStrength;
    match SignalStrength::try_from(s) {
        Ok(SignalStrength::VeryWeak) => "very_weak",
        Ok(SignalStrength::Weak) => "weak",
        Ok(SignalStrength::Moderate) => "moderate",
        Ok(SignalStrength::Strong) => "strong",
        Ok(SignalStrength::VeryStrong) => "very_strong",
        _ => "unknown",
    }
}

fn session_state_name(v: i32) -> &'static str {
    use crate::proto::broker_gateway::SessionState;
    match SessionState::try_from(v) {
        Ok(SessionState::Disconnected) => "DISCONNECTED",
        Ok(SessionState::Connected) => "CONNECTED",
        Ok(SessionState::LoggingIn) => "LOGGING_IN",
        Ok(SessionState::Active) => "ACTIVE",
        Ok(SessionState::LoggingOut) => "LOGGING_OUT",
        _ => "UNKNOWN",
    }
}

fn breaker_type_name(value: i32) -> &'static str {
    use crate::proto::risk::CircuitBreakerType;
    match CircuitBreakerType::try_from(value) {
        Ok(CircuitBreakerType::PortfolioLoss) => "portfolio-loss",
        Ok(CircuitBreakerType::SymbolVolatility) => "symbol-volatility",
        Ok(CircuitBreakerType::PositionSize) => "position-size",
        Ok(CircuitBreakerType::Drawdown) => "drawdown",
        _ => "unknown",
    }
}

fn agent_state_name(v: i32) -> &'static str {
    use crate::proto::trading_agent::AgentState;
    match AgentState::try_from(v) {
        Ok(AgentState::Initializing) => "INITIALIZING",
        Ok(AgentState::Active) => "ACTIVE",
        Ok(AgentState::Paused) => "PAUSED",
        Ok(AgentState::Error) => "ERROR",
        Ok(AgentState::Shutdown) => "SHUTDOWN",
        _ => "UNKNOWN",
    }
}

fn config_data_type_name(value: i32) -> &'static str {
    use crate::proto::config::ConfigDataType;
    match ConfigDataType::try_from(value) {
        Ok(ConfigDataType::String) => "string",
        Ok(ConfigDataType::Number) => "number",
        Ok(ConfigDataType::Boolean) => "boolean",
        Ok(ConfigDataType::Json) => "json",
        Ok(ConfigDataType::Encrypted) => "encrypted",
        _ => "unspecified",
    }
}

fn download_status_name(value: i32) -> &'static str {
    use crate::proto::data_acquisition::DownloadStatus;
    match DownloadStatus::try_from(value) {
        Ok(DownloadStatus::Pending) => "PENDING",
        Ok(DownloadStatus::Downloading) => "DOWNLOADING",
        Ok(DownloadStatus::Validating) => "VALIDATING",
        Ok(DownloadStatus::Uploading) => "UPLOADING",
        Ok(DownloadStatus::Completed) => "COMPLETED",
        Ok(DownloadStatus::Failed) => "FAILED",
        Ok(DownloadStatus::Cancelled) => "CANCELLED",
        _ => "UNKNOWN",
    }
}

/// Write a JSON-RPC response as a single line to stdout.
async fn write_response(
    writer: &mut tokio::io::Stdout,
    response: &JsonRpcResponse,
) -> anyhow::Result<()> {
    let serialized = serde_json::to_string(response)?;
    writer.write_all(serialized.as_bytes()).await?;
    writer.write_all(b"\n").await?;
    writer.flush().await?;
    Ok(())
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::let_underscore_must_use, clippy::assertions_on_result_states, clippy::str_to_string, clippy::indexing_slicing)]
mod tests {
    use super::*;

    /// Create an MCP server with a lazily-connected gRPC client (no real server).
    /// Tests only exercise protocol handling; actual gRPC calls will return
    /// transport errors which are surfaced as `isError` tool results.
    async fn make_server() -> McpServer {
        // Point XDG_CONFIG_HOME to a writable temp dir so FileTokenStorage::new()
        // works even on CI containers where $HOME may be unwritable (e.g. /root/ as uid 1000).
        let tmp = std::env::temp_dir().join("fxt-test-config");
        std::fs::create_dir_all(&tmp).unwrap();
        std::env::set_var("XDG_CONFIG_HOME", &tmp);

        let client = FoxhuntClient::connect("http://localhost:9090")
            .await
            .unwrap();
        McpServer::new("http://localhost:9090".into(), client)
    }

    fn make_request(method: &str, id: Option<Value>, params: Value) -> JsonRpcRequest {
        JsonRpcRequest {
            jsonrpc: "2.0".into(),
            id,
            method: method.into(),
            params,
        }
    }

    #[tokio::test]
    async fn initialize_returns_capabilities() {
        let server = make_server().await;
        let req = make_request("initialize", Some(json!(1)), json!({}));
        let resp = server.handle_request(&req).await.unwrap();
        let result = resp.result.unwrap();
        assert_eq!(result["protocolVersion"], "2024-11-05");
        assert_eq!(result["serverInfo"]["name"], "fxt");
        assert!(result["capabilities"]["tools"].is_object());
    }

    #[tokio::test]
    async fn notification_returns_none() {
        let server = make_server().await;
        let req = make_request("notifications/initialized", None, json!({}));
        let resp = server.handle_request(&req).await;
        assert!(resp.is_none());
    }

    #[tokio::test]
    async fn tools_list_returns_all_tools() {
        let server = make_server().await;
        let req = make_request("tools/list", Some(json!(2)), json!({}));
        let resp = server.handle_request(&req).await.unwrap();
        let result = resp.result.unwrap();
        let tools_array = result["tools"].as_array().unwrap();
        assert_eq!(tools_array.len(), 32);

        // Each tool must have name, description, inputSchema.
        for tool in tools_array {
            assert!(tool["name"].is_string());
            assert!(tool["description"].is_string());
            assert!(tool["inputSchema"].is_object());
        }
    }

    #[tokio::test]
    async fn tools_call_unknown_tool() {
        let server = make_server().await;
        let req = make_request(
            "tools/call",
            Some(json!(4)),
            json!({"name": "fxt_nonexistent", "arguments": {}}),
        );
        let resp = server.handle_request(&req).await.unwrap();
        assert!(resp.error.is_some());
        assert_eq!(resp.error.unwrap().code, INVALID_PARAMS);
    }

    #[tokio::test]
    async fn tools_call_missing_name() {
        let server = make_server().await;
        let req = make_request("tools/call", Some(json!(5)), json!({"arguments": {}}));
        let resp = server.handle_request(&req).await.unwrap();
        assert!(resp.error.is_some());
        assert_eq!(resp.error.unwrap().code, INVALID_PARAMS);
    }

    #[tokio::test]
    async fn unknown_method_returns_error() {
        let server = make_server().await;
        let req = make_request("bogus/method", Some(json!(6)), json!({}));
        let resp = server.handle_request(&req).await.unwrap();
        assert!(resp.error.is_some());
        assert_eq!(resp.error.unwrap().code, METHOD_NOT_FOUND);
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn tools_call_known_tool_returns_grpc_error() {
        // With no real server, the gRPC call should fail with a transport error,
        // which is returned as an isError tool result (not a protocol error).
        // Requires multi_thread because the auth interceptor uses spawn_blocking.
        let server = make_server().await;
        let req = make_request(
            "tools/call",
            Some(json!(3)),
            json!({"name": "fxt_service_list", "arguments": {}}),
        );
        let resp = server.handle_request(&req).await.unwrap();
        // Protocol-level success (no error field), but tool returned isError.
        assert!(resp.error.is_none());
        let result = resp.result.unwrap();
        assert_eq!(result["isError"], true);
        let text = result["content"][0]["text"].as_str().unwrap();
        assert!(text.contains("error:"));
    }

    #[tokio::test]
    async fn initialize_response_has_version() {
        let server = make_server().await;
        let req = make_request("initialize", Some(json!(9)), json!({}));
        let resp = server.handle_request(&req).await.unwrap();
        let result = resp.result.unwrap();
        let version = result["serverInfo"]["version"].as_str().unwrap();
        assert!(!version.is_empty());
    }

    #[tokio::test]
    async fn malformed_json_returns_parse_error() {
        // We cannot drive the full `run()` loop easily, so test the parse
        // path directly: serde_json should fail, and server.rs uses PARSE_ERROR.
        let bad_json = "{ not valid json }";
        let result: Result<JsonRpcRequest, _> = serde_json::from_str(bad_json);
        assert!(result.is_err());
        // Verify the constant value matches the JSON-RPC 2.0 spec.
        assert_eq!(PARSE_ERROR, -32700);
    }

    #[tokio::test]
    async fn wrong_jsonrpc_version_returns_invalid_request() {
        let server = make_server().await;
        let req = JsonRpcRequest {
            jsonrpc: "1.0".into(),
            id: Some(json!(99)),
            method: "initialize".into(),
            params: json!({}),
        };
        let resp = server.handle_request(&req).await.unwrap();
        assert!(resp.error.is_some());
        let err = resp.error.unwrap();
        assert_eq!(err.code, INVALID_REQUEST);
        assert_eq!(err.message, "Invalid JSON-RPC version");
    }
}
