//! Hardcoded security floor. Settings cannot bypass these rules.
//!
//! Initial rule set lifted from Zed's `HARDCODED_SECURITY_RULES`:
//! catastrophic recursive deletes (`rm -rf /`, `~`, `$HOME`, `.`, `..`).

pub mod shell;

use crate::tool::ToolContext;
use regex::Regex;
use std::sync::OnceLock;

#[derive(Debug)]
pub enum FloorDecision {
    Pass,
    Block { reason: &'static str },
}

pub struct SecurityFloor {
    terminal_deny: Vec<Regex>,
}

impl SecurityFloor {
    pub fn new() -> Self {
        Self { terminal_deny: terminal_deny_patterns().clone() }
    }

    /// Check whether the given tool invocation hits a hard rule.
    /// `tool` is the tool name; `input` is the JSON arguments object.
    pub fn check(&self, tool: &str, input: &serde_json::Value, _ctx: &ToolContext) -> FloorDecision {
        if tool != "terminal" && tool != "bash" {
            return FloorDecision::Pass;
        }
        let Some(cmd) = extract_command(input) else { return FloorDecision::Pass };

        // Check the raw command first
        if self.matches_any(cmd) {
            return FloorDecision::Block {
                reason: "Blocked by built-in security rule (recursive delete of \
                         root, home, current, or parent directory).",
            };
        }
        // Then each sub-command in a chain
        for sub in shell::split_chain(cmd) {
            if self.matches_any(sub) {
                return FloorDecision::Block {
                    reason: "Blocked by built-in security rule (chained recursive \
                             delete detected).",
                };
            }
        }
        if interpolation_pattern().is_match(cmd) {
            return FloorDecision::Block {
                reason: "Blocked: shell substitutions/interpolations are not allowed \
                         in terminal commands. Resolve $VAR, ${VAR}, $(...), backticks, \
                         $((...)), <(...), >(...) before calling.",
            };
        }
        FloorDecision::Pass
    }

    fn matches_any(&self, cmd: &str) -> bool {
        self.terminal_deny.iter().any(|re| re.is_match(cmd))
    }
}

impl Default for SecurityFloor { fn default() -> Self { Self::new() } }

fn extract_command(input: &serde_json::Value) -> Option<&str> {
    input.get("command").and_then(|v| v.as_str())
}

fn terminal_deny_patterns() -> &'static Vec<Regex> {
    static SET: OnceLock<Vec<Regex>> = OnceLock::new();
    SET.get_or_init(|| {
        const FLAGS: &str = r"(--[a-zA-Z0-9][-a-zA-Z0-9_]*(=[^\s]*)?\s+|-[a-zA-Z]+\s+)*";
        const TRAIL: &str = r"(\s+--[a-zA-Z0-9][-a-zA-Z0-9_]*(=[^\s]*)?|\s+-[a-zA-Z]+)*\s*";
        vec![
            Regex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?/\*?{TRAIL}$")).unwrap(),
            Regex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?~/?\*?{TRAIL}$")).unwrap(),
            Regex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?(\$HOME|\$\{{HOME\}})/?\*?{TRAIL}$")).unwrap(),
            Regex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?\./?\*?{TRAIL}$")).unwrap(),
            Regex::new(&format!(r"\brm\s+{FLAGS}(--\s+)?\.\./?\*?{TRAIL}$")).unwrap(),
        ]
    })
}

fn interpolation_pattern() -> &'static Regex {
    static RE: OnceLock<Regex> = OnceLock::new();
    RE.get_or_init(|| {
        // $VAR, ${...}, $(...), $((...)), `...`, <(...), >(...)
        Regex::new(r#"(\$[A-Za-z_][A-Za-z0-9_]*|\$\{[^}]*\}|\$\([^)]*\)|\$\(\([^)]*\)\)|`[^`]*`|<\([^)]*\)|>\([^)]*\))"#).unwrap()
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{PermissionConfig, SandboxConfig, WhitelistConfig};
    use crate::permission::check::PermissionContext;
    use crate::permission::whitelist::CompiledWhitelist;
    use crate::tool::{ClientKind, ProtocolKind};
    use std::path::PathBuf;

    fn ctx() -> ToolContext {
        let wl = CompiledWhitelist::compile(&WhitelistConfig::default()).unwrap();
        let pc = PermissionContext::new("c".to_string(), None, wl);
        ToolContext::for_test(
            "c", ClientKind::claude(), ProtocolKind::acp(),
            PathBuf::from("/tmp"),
            SandboxConfig::default(), PermissionConfig::default(), pc,
        )
    }

    fn input(cmd: &str) -> serde_json::Value {
        serde_json::json!({ "command": cmd })
    }

    #[test]
    fn floor_passes_non_terminal_tools() {
        let f = SecurityFloor::new();
        assert!(matches!(f.check("read", &input("rm -rf /"), &ctx()), FloorDecision::Pass));
    }

    #[test]
    fn floor_blocks_rm_rf_root() {
        let f = SecurityFloor::new();
        for cmd in ["rm -rf /", "rm -rfv /", "rm -rf /*"] {
            assert!(matches!(f.check("terminal", &input(cmd), &ctx()), FloorDecision::Block { .. }),
                    "expected block for {cmd}");
        }
    }

    #[test]
    fn floor_blocks_home_variants() {
        let f = SecurityFloor::new();
        for cmd in ["rm -rf ~", "rm -rf ~/", "rm -rf $HOME", "rm -rf ${HOME}", "rm -rf ${HOME}/*"] {
            assert!(matches!(f.check("terminal", &input(cmd), &ctx()), FloorDecision::Block { .. }),
                    "expected block for {cmd}");
        }
    }

    #[test]
    fn floor_blocks_dot_dotdot() {
        let f = SecurityFloor::new();
        for cmd in ["rm -rf .", "rm -rf ./", "rm -rf ./*", "rm -rf ..", "rm -rf ../*"] {
            assert!(matches!(f.check("terminal", &input(cmd), &ctx()), FloorDecision::Block { .. }),
                    "expected block for {cmd}");
        }
    }

    #[test]
    fn floor_passes_safe_commands() {
        let f = SecurityFloor::new();
        for cmd in ["ls", "rm foo.txt", "rm -rf ./build", "rm -rf /tmp/work"] {
            assert!(matches!(f.check("terminal", &input(cmd), &ctx()), FloorDecision::Pass),
                    "expected pass for {cmd}");
        }
    }

    #[test]
    fn floor_blocks_chained_rm_rf_root() {
        let f = SecurityFloor::new();
        assert!(matches!(
            f.check("terminal", &input("ls && rm -rf /"), &ctx()),
            FloorDecision::Block { .. }
        ));
    }

    #[test]
    fn floor_blocks_interpolation_in_terminal() {
        let f = SecurityFloor::new();
        for cmd in ["echo $VAR", "echo ${VAR}", "echo $(date)", "echo `date`",
                    "echo $((1+1))", "cat <(ls)", "tee >(ls)"] {
            assert!(matches!(
                f.check("terminal", &input(cmd), &ctx()),
                FloorDecision::Block { .. }
            ), "expected block for {cmd}");
        }
    }

    #[test]
    fn floor_passes_command_without_interpolation() {
        let f = SecurityFloor::new();
        assert!(matches!(
            f.check("terminal", &input("git status"), &ctx()),
            FloorDecision::Pass
        ));
    }
}
