//! Controller schemas and handler dispatch for the MCP clients domain.
//!
//! Every `schemas(function)` match arm defines the RPC method's input/output
//! shape. Every `handle_*` function deserialises params and delegates to
//! `ops.rs`.

use serde::de::DeserializeOwned;
use serde_json::{Map, Value};

use crate::core::all::{ControllerFuture, RegisteredController};
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
use crate::openhuman::config::rpc as config_rpc;
use crate::rpc::RpcOutcome;

// ── Schema registry ──────────────────────────────────────────────────────────

pub fn all_controller_schemas() -> Vec<ControllerSchema> {
    vec![
        schemas("registry_search"),
        schemas("registry_get"),
        schemas("installed_list"),
        schemas("install"),
        schemas("uninstall"),
        schemas("connect"),
        schemas("disconnect"),
        schemas("status"),
        schemas("tool_call"),
        schemas("config_assist"),
    ]
}

pub fn all_registered_controllers() -> Vec<RegisteredController> {
    vec![
        RegisteredController {
            schema: schemas("registry_search"),
            handler: handle_registry_search,
        },
        RegisteredController {
            schema: schemas("registry_get"),
            handler: handle_registry_get,
        },
        RegisteredController {
            schema: schemas("installed_list"),
            handler: handle_installed_list,
        },
        RegisteredController {
            schema: schemas("install"),
            handler: handle_install,
        },
        RegisteredController {
            schema: schemas("uninstall"),
            handler: handle_uninstall,
        },
        RegisteredController {
            schema: schemas("connect"),
            handler: handle_connect,
        },
        RegisteredController {
            schema: schemas("disconnect"),
            handler: handle_disconnect,
        },
        RegisteredController {
            schema: schemas("status"),
            handler: handle_status,
        },
        RegisteredController {
            schema: schemas("tool_call"),
            handler: handle_tool_call,
        },
        RegisteredController {
            schema: schemas("config_assist"),
            handler: handle_config_assist,
        },
    ]
}

pub fn schemas(function: &str) -> ControllerSchema {
    match function {
        "registry_search" => ControllerSchema {
            namespace: "mcp_clients",
            function: "registry_search",
            description: "Search the Smithery.ai MCP server registry.",
            inputs: vec![
                FieldSchema {
                    name: "query",
                    ty: TypeSchema::Option(Box::new(TypeSchema::String)),
                    comment: "Free-text search query.",
                    required: false,
                },
                FieldSchema {
                    name: "page",
                    ty: TypeSchema::Option(Box::new(TypeSchema::U64)),
                    comment: "1-based page number (default: 1).",
                    required: false,
                },
                FieldSchema {
                    name: "page_size",
                    ty: TypeSchema::Option(Box::new(TypeSchema::U64)),
                    comment: "Results per page (default: 20).",
                    required: false,
                },
            ],
            outputs: vec![
                FieldSchema {
                    name: "servers",
                    ty: TypeSchema::Array(Box::new(TypeSchema::Ref("SmitheryServerSummary"))),
                    comment: "Matching server summaries from the registry.",
                    required: true,
                },
                FieldSchema {
                    name: "page",
                    ty: TypeSchema::U64,
                    comment: "Current page number.",
                    required: true,
                },
                FieldSchema {
                    name: "total_pages",
                    ty: TypeSchema::U64,
                    comment: "Total number of pages available.",
                    required: true,
                },
            ],
        },

        "registry_get" => ControllerSchema {
            namespace: "mcp_clients",
            function: "registry_get",
            description: "Fetch full details for one MCP server from the Smithery registry.",
            inputs: vec![FieldSchema {
                name: "qualified_name",
                ty: TypeSchema::String,
                comment: "Registry qualified name, e.g. `@modelcontextprotocol/server-filesystem`.",
                required: true,
            }],
            outputs: vec![FieldSchema {
                name: "server",
                ty: TypeSchema::Ref("SmitheryServerDetail"),
                comment: "Full server detail including connection specs.",
                required: true,
            }],
        },

        "installed_list" => ControllerSchema {
            namespace: "mcp_clients",
            function: "installed_list",
            description: "List all locally installed MCP servers.",
            inputs: vec![],
            outputs: vec![FieldSchema {
                name: "installed",
                ty: TypeSchema::Array(Box::new(TypeSchema::Ref("InstalledServer"))),
                comment: "Installed server records (env values omitted).",
                required: true,
            }],
        },

        "install" => ControllerSchema {
            namespace: "mcp_clients",
            function: "install",
            description: "Install an MCP server from the Smithery registry.",
            inputs: vec![
                FieldSchema {
                    name: "qualified_name",
                    ty: TypeSchema::String,
                    comment: "Registry qualified name.",
                    required: true,
                },
                FieldSchema {
                    name: "env",
                    ty: TypeSchema::Map(Box::new(TypeSchema::String)),
                    comment: "Environment variable values required by the server. Values are stored encrypted and never returned.",
                    required: true,
                },
                FieldSchema {
                    name: "config",
                    ty: TypeSchema::Option(Box::new(TypeSchema::Json)),
                    comment: "Optional JSON configuration blob.",
                    required: false,
                },
            ],
            outputs: vec![FieldSchema {
                name: "server",
                ty: TypeSchema::Ref("InstalledServer"),
                comment: "The newly installed server record.",
                required: true,
            }],
        },

        "uninstall" => ControllerSchema {
            namespace: "mcp_clients",
            function: "uninstall",
            description: "Uninstall a locally installed MCP server.",
            inputs: vec![FieldSchema {
                name: "server_id",
                ty: TypeSchema::String,
                comment: "UUID of the server to remove.",
                required: true,
            }],
            outputs: vec![
                FieldSchema {
                    name: "server_id",
                    ty: TypeSchema::String,
                    comment: "The server id that was targeted.",
                    required: true,
                },
                FieldSchema {
                    name: "removed",
                    ty: TypeSchema::Bool,
                    comment: "True when the server was actually removed.",
                    required: true,
                },
            ],
        },

        "connect" => ControllerSchema {
            namespace: "mcp_clients",
            function: "connect",
            description: "Spawn the MCP server subprocess and run the initialize handshake.",
            inputs: vec![FieldSchema {
                name: "server_id",
                ty: TypeSchema::String,
                comment: "UUID of the installed server to connect.",
                required: true,
            }],
            outputs: vec![
                FieldSchema {
                    name: "server_id",
                    ty: TypeSchema::String,
                    comment: "Connected server id.",
                    required: true,
                },
                FieldSchema {
                    name: "status",
                    ty: TypeSchema::Enum {
                        variants: vec!["connected"],
                    },
                    comment: "Always `connected` on success.",
                    required: true,
                },
                FieldSchema {
                    name: "tools",
                    ty: TypeSchema::Array(Box::new(TypeSchema::Ref("McpTool"))),
                    comment: "Tools exposed by the connected server.",
                    required: true,
                },
            ],
        },

        "disconnect" => ControllerSchema {
            namespace: "mcp_clients",
            function: "disconnect",
            description: "Disconnect a running MCP server and stop its process.",
            inputs: vec![FieldSchema {
                name: "server_id",
                ty: TypeSchema::String,
                comment: "UUID of the server to disconnect.",
                required: true,
            }],
            outputs: vec![
                FieldSchema {
                    name: "server_id",
                    ty: TypeSchema::String,
                    comment: "Disconnected server id.",
                    required: true,
                },
                FieldSchema {
                    name: "status",
                    ty: TypeSchema::Enum {
                        variants: vec!["disconnected"],
                    },
                    comment: "Always `disconnected` on success.",
                    required: true,
                },
            ],
        },

        "status" => ControllerSchema {
            namespace: "mcp_clients",
            function: "status",
            description: "Return connection status for all installed MCP servers.",
            inputs: vec![],
            outputs: vec![FieldSchema {
                name: "servers",
                ty: TypeSchema::Array(Box::new(TypeSchema::Ref("ConnStatus"))),
                comment: "Per-server connection status summaries.",
                required: true,
            }],
        },

        "tool_call" => ControllerSchema {
            namespace: "mcp_clients",
            function: "tool_call",
            description: "Invoke a tool on a connected MCP server.",
            inputs: vec![
                FieldSchema {
                    name: "server_id",
                    ty: TypeSchema::String,
                    comment: "UUID of the connected server.",
                    required: true,
                },
                FieldSchema {
                    name: "tool_name",
                    ty: TypeSchema::String,
                    comment: "Name of the tool to call.",
                    required: true,
                },
                FieldSchema {
                    name: "arguments",
                    ty: TypeSchema::Json,
                    comment: "Tool arguments as a JSON value.",
                    required: true,
                },
            ],
            outputs: vec![
                FieldSchema {
                    name: "result",
                    ty: TypeSchema::Json,
                    comment: "Tool result value.",
                    required: true,
                },
                FieldSchema {
                    name: "is_error",
                    ty: TypeSchema::Bool,
                    comment: "True when the tool returned an error.",
                    required: true,
                },
            ],
        },

        "config_assist" => ControllerSchema {
            namespace: "mcp_clients",
            function: "config_assist",
            description: "AI assistant that helps configure an MCP server's required env vars.",
            inputs: vec![
                FieldSchema {
                    name: "qualified_name",
                    ty: TypeSchema::String,
                    comment: "Registry qualified name of the server being configured.",
                    required: true,
                },
                FieldSchema {
                    name: "user_message",
                    ty: TypeSchema::String,
                    comment: "User's question or reply.",
                    required: true,
                },
                FieldSchema {
                    name: "history",
                    ty: TypeSchema::Option(Box::new(TypeSchema::Array(Box::new(TypeSchema::Ref(
                        "ChatTurn",
                    ))))),
                    comment: "Prior conversation turns `[{role, content}]`.",
                    required: false,
                },
            ],
            outputs: vec![
                FieldSchema {
                    name: "reply",
                    ty: TypeSchema::String,
                    comment: "Assistant reply (markdown).",
                    required: true,
                },
                FieldSchema {
                    name: "suggested_env",
                    ty: TypeSchema::Option(Box::new(TypeSchema::Map(Box::new(TypeSchema::String)))),
                    comment: "Env vars extracted from the user's message, if any.",
                    required: false,
                },
            ],
        },

        _other => ControllerSchema {
            namespace: "mcp_clients",
            function: "unknown",
            description: "Unknown mcp_clients controller function.",
            inputs: vec![FieldSchema {
                name: "function",
                ty: TypeSchema::String,
                comment: "Unknown function requested for schema lookup.",
                required: true,
            }],
            outputs: vec![FieldSchema {
                name: "error",
                ty: TypeSchema::String,
                comment: "Lookup error details.",
                required: true,
            }],
        },
    }
}

// ── Handler implementations ──────────────────────────────────────────────────

fn handle_registry_search(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let config = config_rpc::load_config_with_timeout().await?;
        let query = read_optional_string(&params, "query")?;
        let page = read_optional_u32(&params, "page")?;
        let page_size = read_optional_u32(&params, "page_size")?;
        to_json(
            crate::openhuman::mcp_clients::ops::mcp_clients_registry_search(
                &config, query, page, page_size,
            )
            .await?,
        )
    })
}

fn handle_registry_get(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let config = config_rpc::load_config_with_timeout().await?;
        let qualified_name = read_required::<String>(&params, "qualified_name")?;
        to_json(
            crate::openhuman::mcp_clients::ops::mcp_clients_registry_get(&config, qualified_name)
                .await?,
        )
    })
}

fn handle_installed_list(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let _ = params;
        let config = config_rpc::load_config_with_timeout().await?;
        to_json(crate::openhuman::mcp_clients::ops::mcp_clients_installed_list(&config).await?)
    })
}

fn handle_install(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let config = config_rpc::load_config_with_timeout().await?;
        let qualified_name = read_required::<String>(&params, "qualified_name")?;
        let env = read_required::<std::collections::HashMap<String, String>>(&params, "env")?;
        let config_value = read_optional_json(&params, "config")?;
        to_json(
            crate::openhuman::mcp_clients::ops::mcp_clients_install(
                &config,
                qualified_name,
                env,
                config_value,
            )
            .await?,
        )
    })
}

fn handle_uninstall(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let config = config_rpc::load_config_with_timeout().await?;
        let server_id = read_required::<String>(&params, "server_id")?;
        to_json(
            crate::openhuman::mcp_clients::ops::mcp_clients_uninstall(&config, server_id).await?,
        )
    })
}

fn handle_connect(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let config = config_rpc::load_config_with_timeout().await?;
        let server_id = read_required::<String>(&params, "server_id")?;
        to_json(crate::openhuman::mcp_clients::ops::mcp_clients_connect(&config, server_id).await?)
    })
}

fn handle_disconnect(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let server_id = read_required::<String>(&params, "server_id")?;
        to_json(crate::openhuman::mcp_clients::ops::mcp_clients_disconnect(server_id).await?)
    })
}

fn handle_status(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let _ = params;
        let config = config_rpc::load_config_with_timeout().await?;
        to_json(crate::openhuman::mcp_clients::ops::mcp_clients_status(&config).await?)
    })
}

fn handle_tool_call(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let server_id = read_required::<String>(&params, "server_id")?;
        let tool_name = read_required::<String>(&params, "tool_name")?;
        let arguments = params
            .get("arguments")
            .cloned()
            .unwrap_or(Value::Object(Map::new()));
        to_json(
            crate::openhuman::mcp_clients::ops::mcp_clients_tool_call(
                server_id, tool_name, arguments,
            )
            .await?,
        )
    })
}

fn handle_config_assist(params: Map<String, Value>) -> ControllerFuture {
    Box::pin(async move {
        let config = config_rpc::load_config_with_timeout().await?;
        let qualified_name = read_required::<String>(&params, "qualified_name")?;
        let user_message = read_required::<String>(&params, "user_message")?;
        let history = read_optional::<Vec<crate::openhuman::mcp_clients::types::ChatTurn>>(
            &params, "history",
        )?;
        to_json(
            crate::openhuman::mcp_clients::ops::mcp_clients_config_assist(
                &config,
                qualified_name,
                user_message,
                history,
            )
            .await?,
        )
    })
}

// ── Param helpers ─────────────────────────────────────────────────────────────

fn read_required<T: DeserializeOwned>(params: &Map<String, Value>, key: &str) -> Result<T, String> {
    let value = params
        .get(key)
        .cloned()
        .ok_or_else(|| format!("missing required param '{key}'"))?;
    serde_json::from_value(value).map_err(|e| format!("invalid '{key}': {e}"))
}

fn read_optional<T: DeserializeOwned>(
    params: &Map<String, Value>,
    key: &str,
) -> Result<Option<T>, String> {
    match params.get(key) {
        None | Some(Value::Null) => Ok(None),
        Some(v) => serde_json::from_value(v.clone())
            .map(Some)
            .map_err(|e| format!("invalid '{key}': {e}")),
    }
}

fn read_optional_string(params: &Map<String, Value>, key: &str) -> Result<Option<String>, String> {
    read_optional::<String>(params, key)
}

fn read_optional_u32(params: &Map<String, Value>, key: &str) -> Result<Option<u32>, String> {
    match params.get(key) {
        None | Some(Value::Null) => Ok(None),
        Some(Value::Number(n)) => n
            .as_u64()
            .and_then(|v| u32::try_from(v).ok())
            .map(Some)
            .ok_or_else(|| format!("invalid '{key}': expected u32")),
        Some(other) => Err(format!(
            "invalid '{key}': expected number, got {}",
            type_name(other)
        )),
    }
}

fn read_optional_json(params: &Map<String, Value>, key: &str) -> Result<Option<Value>, String> {
    match params.get(key) {
        None | Some(Value::Null) => Ok(None),
        Some(v) => Ok(Some(v.clone())),
    }
}

fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
    outcome.into_cli_compatible_json()
}

fn type_name(value: &Value) -> &'static str {
    match value {
        Value::Null => "null",
        Value::Bool(_) => "bool",
        Value::Number(_) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    }
}

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

    // ── schemas() coverage ────────────────────────────────────────────────────

    #[test]
    fn schemas_registry_search_has_no_required_inputs() {
        let s = schemas("registry_search");
        assert_eq!(s.namespace, "mcp_clients");
        assert!(s.inputs.iter().all(|f| !f.required));
    }

    #[test]
    fn schemas_registry_get_requires_qualified_name() {
        let s = schemas("registry_get");
        let qn = s
            .inputs
            .iter()
            .find(|f| f.name == "qualified_name")
            .unwrap();
        assert!(qn.required);
    }

    #[test]
    fn schemas_install_requires_qualified_name_and_env() {
        let s = schemas("install");
        let names: Vec<_> = s
            .inputs
            .iter()
            .filter(|f| f.required)
            .map(|f| f.name)
            .collect();
        assert!(names.contains(&"qualified_name"));
        assert!(names.contains(&"env"));
    }

    #[test]
    fn schemas_connect_requires_server_id() {
        let s = schemas("connect");
        let si = s.inputs.iter().find(|f| f.name == "server_id").unwrap();
        assert!(si.required);
    }

    #[test]
    fn schemas_tool_call_requires_three_fields() {
        let s = schemas("tool_call");
        let required: Vec<_> = s.inputs.iter().filter(|f| f.required).collect();
        assert_eq!(required.len(), 3);
    }

    #[test]
    fn schemas_config_assist_history_is_optional() {
        let s = schemas("config_assist");
        let history = s.inputs.iter().find(|f| f.name == "history").unwrap();
        assert!(!history.required);
    }

    #[test]
    fn schemas_unknown_function_returns_placeholder() {
        let s = schemas("not-a-real-function");
        assert_eq!(s.function, "unknown");
        assert_eq!(s.outputs[0].name, "error");
    }

    // ── all_controller_schemas / all_registered_controllers ────────────────────

    #[test]
    fn all_controller_schemas_covers_ten_methods() {
        let schemas = all_controller_schemas();
        assert_eq!(schemas.len(), 10);
    }

    #[test]
    fn all_registered_controllers_has_handler_per_schema() {
        let controllers = all_registered_controllers();
        assert_eq!(controllers.len(), 10);
    }

    #[test]
    fn all_registered_controllers_all_use_mcp_clients_namespace() {
        for c in all_registered_controllers() {
            assert_eq!(c.schema.namespace, "mcp_clients");
        }
    }

    // ── read_required ─────────────────────────────────────────────────────────

    #[test]
    fn read_required_returns_value_for_present_key() {
        let mut params = Map::new();
        params.insert("server_id".into(), json!("srv-1"));
        let got: String = read_required(&params, "server_id").unwrap();
        assert_eq!(got, "srv-1");
    }

    #[test]
    fn read_required_errors_on_missing_key() {
        let err = read_required::<String>(&Map::new(), "server_id").unwrap_err();
        assert!(err.contains("missing required param 'server_id'"));
    }

    // ── read_optional_u32 ─────────────────────────────────────────────────────

    #[test]
    fn read_optional_u32_absent_is_none() {
        assert_eq!(read_optional_u32(&Map::new(), "page").unwrap(), None);
    }

    #[test]
    fn read_optional_u32_valid_number() {
        let mut p = Map::new();
        p.insert("page".into(), json!(2));
        assert_eq!(read_optional_u32(&p, "page").unwrap(), Some(2));
    }

    #[test]
    fn read_optional_u32_rejects_negative() {
        let mut p = Map::new();
        p.insert("page".into(), json!(-1));
        assert!(read_optional_u32(&p, "page").is_err());
    }

    // ── type_name ─────────────────────────────────────────────────────────────

    #[test]
    fn type_name_covers_all_variants() {
        assert_eq!(type_name(&Value::Null), "null");
        assert_eq!(type_name(&json!(true)), "bool");
        assert_eq!(type_name(&json!(1)), "number");
        assert_eq!(type_name(&json!("s")), "string");
        assert_eq!(type_name(&json!([])), "array");
        assert_eq!(type_name(&json!({})), "object");
    }
}
