use anyhow::Result;
use serde_json::{Map, Value};
use toml::Value as TomlValue;

/// Identity transform: keep the source servers as-is.
/// Used by clients whose JSON schema matches the source (claude, gemini, cursor).
pub fn identity_transform(source: &Map<String, Value>) -> Result<Map<String, Value>> {
    Ok(source.clone())
}

/// Convert a single MCP server JSON Value into a TOML Table.
pub fn json_server_to_toml(json: &Value) -> Result<toml::map::Map<String, TomlValue>> {
    let mut table = toml::map::Map::new();
    let obj = json
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("MCP server config must be a JSON object"))?;

    for (k, v) in obj {
        if k == "env" {
            let env_obj = v
                .as_object()
                .ok_or_else(|| anyhow::anyhow!("env must be a JSON object"))?;
            let mut env_table = toml::map::Map::new();
            for (ek, ev) in env_obj {
                let s = ev
                    .as_str()
                    .ok_or_else(|| anyhow::anyhow!("env value must be string"))?;
                env_table.insert(ek.clone(), TomlValue::String(s.to_string()));
            }
            table.insert("env".to_string(), TomlValue::Table(env_table));
            continue;
        }
        if k == "args" {
            let arr = v
                .as_array()
                .ok_or_else(|| anyhow::anyhow!("args must be a JSON array"))?;
            let toml_arr: Result<Vec<TomlValue>> = arr
                .iter()
                .map(|a| {
                    a.as_str()
                        .map(|s| TomlValue::String(s.to_string()))
                        .ok_or_else(|| anyhow::anyhow!("args elements must be strings"))
                })
                .collect();
            table.insert("args".to_string(), TomlValue::Array(toml_arr?));
            continue;
        }
        if let Some(s) = v.as_str() {
            // command, type, url and other string fields.
            table.insert(k.clone(), TomlValue::String(s.to_string()));
            continue;
        }
        if let Some(n) = v.as_i64() {
            // timeout and other integer fields.
            table.insert(k.clone(), TomlValue::Integer(n));
            continue;
        }
        if let Some(f) = v.as_f64() {
            table.insert(k.clone(), TomlValue::Float(f));
            continue;
        }
        if let Some(b) = v.as_bool() {
            table.insert(k.clone(), TomlValue::Boolean(b));
            continue;
        }
        anyhow::bail!("field '{}' has unsupported type", k);
    }
    Ok(table)
}

/// Roughly convert a TOML Table into a serde_json Value (only used for diff comparison).
pub fn toml_table_to_json(table: &toml::map::Map<String, TomlValue>) -> Map<String, Value> {
    let mut m = Map::new();
    for (k, v) in table {
        let jv = match v {
            TomlValue::String(s) => Value::String(s.clone()),
            TomlValue::Integer(n) => Value::Number((*n).into()),
            TomlValue::Float(f) => serde_json::Number::from_f64(*f)
                .map(Value::Number)
                .unwrap_or(Value::Null),
            TomlValue::Boolean(b) => Value::Bool(*b),
            TomlValue::Array(arr) => Value::Array(
                arr.iter()
                    .map(|a| match a {
                        TomlValue::String(s) => Value::String(s.clone()),
                        _ => Value::Null,
                    })
                    .collect(),
            ),
            TomlValue::Table(t) => Value::Object(toml_table_to_json(t)),
            _ => Value::Null,
        };
        m.insert(k.clone(), jv);
    }
    m
}

/// Extract the existing mcp_servers section from a codex config.toml, as a JSON Map for diffing.
pub fn extract_codex_old_mcp(root: &TomlValue) -> Map<String, Value> {
    let mut result = Map::new();
    let mcp = match root.get("mcp_servers").and_then(|v| v.as_table()) {
        Some(t) => t,
        None => return result,
    };
    for (name, val) in mcp {
        if let Some(server_table) = val.as_table() {
            let json_str =
                serde_json::to_string(&toml_table_to_json(server_table)).unwrap_or_default();
            result.insert(name.clone(), Value::String(json_str));
        }
    }
    result
}

/// Convert a single source server entry into Copilot CLI format.
/// - stdio servers (has "command"): type → "local", add "tools": ["*"]
/// - http/sse servers: keep type/url, add "tools": ["*"]
fn source_to_copilot_server(val: &Value) -> Result<Value> {
    let obj = val
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("server config must be a JSON object"))?;

    let mut new_obj = Map::new();

    if obj.contains_key("command") {
        new_obj.insert("type".to_string(), Value::String("local".to_string()));
        for (k, v) in obj {
            if k != "type" {
                new_obj.insert(k.clone(), v.clone());
            }
        }
    } else {
        for (k, v) in obj {
            new_obj.insert(k.clone(), v.clone());
        }
    }

    new_obj.insert(
        "tools".to_string(),
        Value::Array(vec![Value::String("*".to_string())]),
    );

    Ok(Value::Object(new_obj))
}

/// Convert all source servers into Copilot CLI format.
pub fn build_copilot_servers(source: &Map<String, Value>) -> Result<Map<String, Value>> {
    let mut result = Map::new();
    for (name, val) in source {
        result.insert(name.clone(), source_to_copilot_server(val)?);
    }
    Ok(result)
}

/// Convert a single source server entry into OpenCode format.
/// OpenCode expects local command arrays and uses `environment` instead of `env`.
fn source_to_opencode_server(val: &Value) -> Result<Value> {
    let obj = val
        .as_object()
        .ok_or_else(|| anyhow::anyhow!("server config must be a JSON object"))?;

    if obj.contains_key("command") {
        return build_opencode_local_server(obj);
    }
    if obj.contains_key("url") {
        return build_opencode_remote_server(obj);
    }
    anyhow::bail!("OpenCode MCP server config must contain command or url");
}

fn build_opencode_local_server(obj: &Map<String, Value>) -> Result<Value> {
    let mut new_obj = Map::new();
    new_obj.insert("type".to_string(), Value::String("local".to_string()));
    new_obj.insert("command".to_string(), build_opencode_command(obj)?);

    copy_opencode_common_fields(obj, &mut new_obj);
    add_default_enabled(&mut new_obj);
    Ok(Value::Object(new_obj))
}

fn build_opencode_command(obj: &Map<String, Value>) -> Result<Value> {
    let command = obj
        .get("command")
        .and_then(Value::as_str)
        .ok_or_else(|| anyhow::anyhow!("command must be a string"))?;
    let mut command_parts = vec![Value::String(command.to_string())];

    if let Some(args) = obj.get("args") {
        let args = args
            .as_array()
            .ok_or_else(|| anyhow::anyhow!("args must be a JSON array"))?;
        for arg in args {
            let arg = arg
                .as_str()
                .ok_or_else(|| anyhow::anyhow!("args elements must be strings"))?;
            command_parts.push(Value::String(arg.to_string()));
        }
    }

    Ok(Value::Array(command_parts))
}

fn build_opencode_remote_server(obj: &Map<String, Value>) -> Result<Value> {
    let mut new_obj = Map::new();
    new_obj.insert("type".to_string(), Value::String("remote".to_string()));
    copy_opencode_common_fields(obj, &mut new_obj);
    add_default_enabled(&mut new_obj);
    Ok(Value::Object(new_obj))
}

fn copy_opencode_common_fields(source: &Map<String, Value>, target: &mut Map<String, Value>) {
    for (key, value) in source {
        if matches!(key.as_str(), "args" | "command" | "env" | "type") {
            continue;
        }
        target.insert(key.clone(), value.clone());
    }

    if let Some(env) = source.get("env") {
        target.insert("environment".to_string(), env.clone());
    }
}

fn add_default_enabled(target: &mut Map<String, Value>) {
    if target.contains_key("enabled") {
        return;
    }
    target.insert("enabled".to_string(), Value::Bool(true));
}

/// Convert all source servers into OpenCode config format.
pub fn build_opencode_servers(source: &Map<String, Value>) -> Result<Map<String, Value>> {
    let mut result = Map::new();
    for (name, val) in source {
        result.insert(name.clone(), source_to_opencode_server(val)?);
    }
    Ok(result)
}

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

    #[test]
    fn json_server_to_toml_handles_all_field_types() {
        let server = json!({
            "command": "npx",
            "args": ["-y", "some-mcp"],
            "env": { "KEY": "value" },
            "timeout": 60000
        });
        let table = json_server_to_toml(&server).unwrap();
        assert_eq!(table.get("command").unwrap().as_str().unwrap(), "npx");
        assert_eq!(table.get("args").unwrap().as_array().unwrap().len(), 2);
        assert!(table.get("env").unwrap().is_table());
        assert_eq!(table.get("timeout").unwrap().as_integer().unwrap(), 60000);
    }

    #[test]
    fn json_server_to_toml_rejects_non_object() {
        assert!(json_server_to_toml(&json!("not an object")).is_err());
    }

    #[test]
    fn copilot_transform_marks_stdio_as_local_and_adds_tools() {
        let mut source = Map::new();
        source.insert(
            "srv".to_string(),
            json!({ "command": "npx", "args": ["x"] }),
        );
        let result = build_copilot_servers(&source).unwrap();
        let srv = result.get("srv").unwrap();
        assert_eq!(srv.get("type").unwrap().as_str().unwrap(), "local");
        assert_eq!(srv.get("tools").unwrap(), &json!(["*"]));
        assert_eq!(srv.get("command").unwrap().as_str().unwrap(), "npx");
    }

    #[test]
    fn copilot_transform_keeps_http_url_and_adds_tools() {
        let mut source = Map::new();
        source.insert(
            "web".to_string(),
            json!({ "type": "http", "url": "http://x" }),
        );
        let result = build_copilot_servers(&source).unwrap();
        let web = result.get("web").unwrap();
        assert_eq!(web.get("type").unwrap().as_str().unwrap(), "http");
        assert_eq!(web.get("url").unwrap().as_str().unwrap(), "http://x");
        assert_eq!(web.get("tools").unwrap(), &json!(["*"]));
    }

    #[test]
    fn identity_transform_returns_clone() {
        let mut source = Map::new();
        source.insert("a".to_string(), json!({ "command": "x" }));
        assert_eq!(identity_transform(&source).unwrap(), source);
    }

    #[test]
    fn opencode_transform_converts_stdio_server_to_local_command_array() {
        let mut source = Map::new();
        source.insert(
            "context7".to_string(),
            json!({
                "command": "npx",
                "args": ["-y", "@upstash/context7-mcp"],
                "env": { "CONTEXT7_API_KEY": "secret" },
                "cwd": "/tmp/work",
                "timeout": 10000
            }),
        );

        let result = build_opencode_servers(&source).unwrap();
        let context7 = result.get("context7").unwrap();

        assert_eq!(context7.get("type").unwrap(), "local");
        assert_eq!(
            context7.get("command").unwrap(),
            &json!(["npx", "-y", "@upstash/context7-mcp"])
        );
        assert_eq!(
            context7.get("environment").unwrap(),
            &json!({ "CONTEXT7_API_KEY": "secret" })
        );
        assert_eq!(context7.get("cwd").unwrap(), "/tmp/work");
        assert_eq!(context7.get("timeout").unwrap(), 10000);
        assert_eq!(context7.get("enabled").unwrap(), true);
        assert!(context7.get("args").is_none());
        assert!(context7.get("env").is_none());
    }

    #[test]
    fn opencode_transform_converts_url_server_to_remote() {
        let mut source = Map::new();
        source.insert(
            "grep".to_string(),
            json!({
                "type": "sse",
                "url": "https://mcp.example.com",
                "headers": { "Authorization": "Bearer token" },
                "enabled": false
            }),
        );

        let result = build_opencode_servers(&source).unwrap();
        let grep = result.get("grep").unwrap();

        assert_eq!(grep.get("type").unwrap(), "remote");
        assert_eq!(grep.get("url").unwrap(), "https://mcp.example.com");
        assert_eq!(
            grep.get("headers").unwrap(),
            &json!({ "Authorization": "Bearer token" })
        );
        assert_eq!(grep.get("enabled").unwrap(), false);
    }
}
