use codex_config::McpServerConfig;
use codex_config::McpServerEnvVar;
use codex_config::McpServerTransportConfig;
use serde::Deserialize;
use serde_json::Map as JsonMap;
use serde_json::Value as JsonValue;
use std::collections::BTreeMap;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use tracing::warn;

/// Placement applied while normalizing MCP servers declared by a plugin.
#[derive(Clone, Copy, Debug)]
pub enum PluginMcpServerPlacement<'a> {
    /// Preserve declared placement, resolving a relative working directory below the plugin root.
    Declared,
    /// Bind stdio servers to one environment and default their working directory to the plugin root.
    Environment { environment_id: &'a str },
}

/// One plugin MCP server that could not be normalized into runtime configuration.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PluginMcpServerParseError {
    pub name: String,
    pub message: String,
}

/// Valid servers and per-server errors parsed from one plugin MCP file.
#[derive(Debug, Default, PartialEq)]
pub struct PluginMcpConfigParseOutcome {
    pub servers: BTreeMap<String, McpServerConfig>,
    pub errors: Vec<PluginMcpServerParseError>,
}

#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PluginMcpServersFile {
    mcp_servers: BTreeMap<String, JsonValue>,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum PluginMcpFile {
    McpServersObject(PluginMcpServersFile),
    ServerMap(BTreeMap<String, JsonValue>),
}

impl PluginMcpFile {
    fn into_mcp_servers(self) -> BTreeMap<String, JsonValue> {
        match self {
            Self::McpServersObject(file) => file.mcp_servers,
            Self::ServerMap(mcp_servers) => mcp_servers,
        }
    }
}

/// Parses the two supported plugin MCP file shapes and normalizes each server.
///
/// Invalid individual servers are returned as errors without discarding valid
/// siblings. A malformed top-level document fails the whole parse.
pub fn parse_plugin_mcp_config(
    plugin_root: &Path,
    contents: &str,
    placement: PluginMcpServerPlacement<'_>,
) -> Result<PluginMcpConfigParseOutcome, serde_json::Error> {
    let parsed = serde_json::from_str::<PluginMcpFile>(contents)?;
    let mut outcome = PluginMcpConfigParseOutcome::default();

    for (name, config_value) in parsed.into_mcp_servers() {
        match normalize_plugin_mcp_server(plugin_root, config_value, placement) {
            Ok(config) => {
                outcome.servers.insert(name, config);
            }
            Err(message) => outcome
                .errors
                .push(PluginMcpServerParseError { name, message }),
        }
    }

    Ok(outcome)
}

fn normalize_plugin_mcp_server(
    plugin_root: &Path,
    value: JsonValue,
    placement: PluginMcpServerPlacement<'_>,
) -> Result<McpServerConfig, String> {
    let mut object = normalize_plugin_mcp_server_value(plugin_root, value, placement);
    if let PluginMcpServerPlacement::Environment { environment_id } = placement {
        object.insert(
            "environment_id".to_string(),
            JsonValue::String(environment_id.to_string()),
        );
        if object.contains_key("command") {
            match object.remove("cwd") {
                Some(JsonValue::String(cwd)) => object.insert(
                    "cwd".to_string(),
                    JsonValue::String(
                        executor_plugin_cwd(plugin_root, &cwd)?
                            .to_string_lossy()
                            .into_owned(),
                    ),
                ),
                Some(JsonValue::Null) | None => object.insert(
                    "cwd".to_string(),
                    JsonValue::String(plugin_root.to_string_lossy().into_owned()),
                ),
                Some(value) => object.insert("cwd".to_string(), value),
            };
        }
    }

    let mut config = serde_json::from_value::<McpServerConfig>(JsonValue::Object(object))
        .map_err(|err| err.to_string())?;
    if matches!(placement, PluginMcpServerPlacement::Environment { .. }) {
        bind_environment_env_vars(&mut config)?;
    }
    Ok(config)
}

fn executor_plugin_cwd(plugin_root: &Path, configured_cwd: &str) -> Result<PathBuf, String> {
    let cwd = Path::new(configured_cwd);
    if cwd.is_absolute() {
        return Ok(cwd.to_path_buf());
    }
    if cwd.components().any(|component| {
        matches!(
            component,
            Component::ParentDir | Component::RootDir | Component::Prefix(_)
        )
    }) {
        return Err(format!(
            "relative cwd `{configured_cwd}` must remain within plugin root `{}`",
            plugin_root.display()
        ));
    }
    Ok(plugin_root.join(cwd))
}

fn bind_environment_env_vars(config: &mut McpServerConfig) -> Result<(), String> {
    let is_local_environment = config.is_local_environment();
    let McpServerTransportConfig::Stdio { env_vars, .. } = &mut config.transport else {
        return Ok(());
    };
    for env_var in env_vars {
        match env_var {
            McpServerEnvVar::Name(name) if !is_local_environment => {
                *env_var = McpServerEnvVar::Config {
                    name: std::mem::take(name),
                    source: Some("remote".to_string()),
                };
            }
            McpServerEnvVar::Name(_) => {}
            McpServerEnvVar::Config { name, source } => {
                match (is_local_environment, source.as_deref()) {
                    (true, None | Some("local")) | (false, Some("remote")) => {}
                    (true, Some("remote")) => {
                        return Err(format!(
                            "env_vars entry `{name}` cannot use source `remote` in a local environment"
                        ));
                    }
                    (false, None) => *source = Some("remote".to_string()),
                    (false, Some("local")) => {
                        return Err(format!(
                            "env_vars entry `{name}` cannot use source `local` in an executor-owned plugin"
                        ));
                    }
                    (_, Some(source)) => unreachable!("validated env_vars source `{source}`"),
                }
            }
        }
    }
    Ok(())
}

fn normalize_plugin_mcp_server_value(
    plugin_root: &Path,
    value: JsonValue,
    placement: PluginMcpServerPlacement<'_>,
) -> JsonMap<String, JsonValue> {
    let mut object = match value {
        JsonValue::Object(object) => object,
        _ => return JsonMap::new(),
    };

    if let Some(JsonValue::String(transport_type)) = object.remove("type") {
        match transport_type.as_str() {
            "http" | "streamable_http" | "streamable-http" | "stdio" => {}
            other => {
                warn!(
                    plugin = %plugin_root.display(),
                    transport = other,
                    "plugin MCP server uses an unknown transport type"
                );
            }
        }
    }

    if let Some(JsonValue::Object(mut oauth)) = object.remove("oauth") {
        if oauth.remove("callbackPort").is_some() {
            warn!(
                plugin = %plugin_root.display(),
                "plugin MCP server OAuth callbackPort is ignored; Codex uses global MCP OAuth callback settings"
            );
        }

        if let Some(client_id) = oauth.remove("clientId") {
            oauth.entry("client_id".to_string()).or_insert(client_id);
        }

        if !oauth.is_empty() {
            object.insert("oauth".to_string(), JsonValue::Object(oauth));
        }
    }

    if matches!(placement, PluginMcpServerPlacement::Declared)
        && let Some(JsonValue::String(cwd)) = object.get("cwd")
        && !Path::new(cwd).is_absolute()
    {
        object.insert(
            "cwd".to_string(),
            JsonValue::String(plugin_root.join(cwd).display().to_string()),
        );
    }

    object
}

#[cfg(test)]
#[path = "plugin_config_tests.rs"]
mod tests;
