//! set — Set shell options (like set -e, set -o latch).

use async_trait::async_trait;
use clap::{CommandFactory, Parser};

use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};

/// Set tool: configure shell options.
///
/// Supports:
/// - `-e` / `+e`: Enable/disable error-exit mode (exit on command failure)
/// - `-o latch` / `+o latch`: Enable/disable confirmation latch for dangerous ops
/// - `-o trash` / `+o trash`: Enable/disable trash-on-delete for rm
///
/// Unrecognized options are silently ignored for bash compatibility.
pub struct Set;

/// clap-derived argv layer for set.
///
/// `set` has bespoke argv handling — it reads `-e`/`+e`/`-o NAME` from
/// args.flags and args.positional directly. clap is only used here as a
/// schema sink and to honor the global `--json` flag.
#[derive(Parser, Debug)]
#[command(name = "set", about = "Set shell options")]
struct SetArgs {
    #[command(flatten)]
    global: GlobalFlags,

    /// Shell option arguments (`-e`, `+e`, `-o NAME`, `+o NAME`).
    options: Vec<String>,
}

#[async_trait]
impl Tool for Set {
    fn name(&self) -> &str {
        "set"
    }

    fn schema(&self) -> ToolSchema {
        schema_from_clap(
            &SetArgs::command(),
            "set",
            "Set shell options",
            [
                ("Exit on error", "set -e"),
                ("Disable exit on error", "set +e"),
                ("Enable confirmation latch", "set -o latch"),
                ("Enable trash-on-delete", "set -o trash"),
                ("Disable glob expansion", "set +o glob"),
            ],
        )
    }

    async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
        let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
            return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
        };
        // set has bespoke argv handling — strip the user-provided -e / -o etc.
        // tokens from the argv before handing to clap, otherwise clap would
        // reject unknown flags. Only `--json` (global) needs to clap-parse.
        let mut clap_argv: Vec<String> = Vec::new();
        if args.flags.contains("json") {
            clap_argv.push("--json".to_string());
        }
        let parsed = match SetArgs::try_parse_from(
            std::iter::once("set".to_string()).chain(clap_argv),
        ) {
            Ok(p) => p,
            Err(e) => return ExecResult::failure(2, format!("set: {e}")),
        };
        parsed.global.apply(ctx);

        // No arguments: show current settings
        if args.positional.is_empty() && args.flags.is_empty() {
            let mut output = String::new();
            if ctx.scope.error_exit_enabled() {
                output.push_str("set -e\n");
            }
            if ctx.scope.latch_enabled() {
                output.push_str("set -o latch\n");
            }
            if ctx.scope.trash_enabled() {
                output.push_str("set -o trash\n");
            }
            if !ctx.scope.glob_enabled() {
                output.push_str("set +o glob\n");
            }
            if let Some(bytes) = ctx.output_limit.max_bytes() {
                output.push_str(&format!("set -o output-limit={}\n", format_size_for_set(bytes)));
            }
            return ExecResult::with_output(OutputData::text(output.trim_end()));
        }

        // Process flags (from parser: ShortFlag("e") -> args.flags contains "e")
        for flag in &args.flags {
            match flag.as_str() {
                "e" => ctx.scope.set_error_exit(true),
                "o" => {} // handled below with positional args
                _ => {}   // silently ignore for bash compatibility
            }
        }

        // Process positional args.
        // From parser: PlusFlag("e") -> String("+e"), String("-o") followed by String("latch"), etc.
        let positionals: Vec<&str> = args
            .positional
            .iter()
            .filter_map(|v| match v {
                Value::String(s) => Some(s.as_str()),
                _ => None,
            })
            .collect();

        let mut i = 0;
        while i < positionals.len() {
            let opt = positionals[i];
            match opt {
                "-e" => ctx.scope.set_error_exit(true),
                "+e" => ctx.scope.set_error_exit(false),
                "-o" => {
                    // Consume next positional as option name
                    if let Some(&name) = positionals.get(i + 1) {
                        match name {
                            "latch" => ctx.scope.set_latch_enabled(true),
                            "trash" => ctx.scope.set_trash_enabled(true),
                            "glob" => ctx.scope.set_glob_enabled(true),
                            _ => {
                                if name == "output-limit" || name.starts_with("output-limit=") {
                                    if let Some(size_str) = name.strip_prefix("output-limit=") {
                                        if let Ok(bytes) = crate::output_limit::parse_size(size_str) {
                                            ctx.output_limit.set_limit(Some(bytes));
                                        }
                                    } else if ctx.output_limit.max_bytes().is_none() {
                                        ctx.output_limit.set_limit(Some(crate::output_limit::OutputLimitConfig::default_limit()));
                                    }
                                }
                            }
                        }
                        i += 1; // skip the option name
                    }
                }
                "+o" => {
                    if let Some(&name) = positionals.get(i + 1) {
                        match name {
                            "latch" => ctx.scope.set_latch_enabled(false),
                            "trash" => ctx.scope.set_trash_enabled(false),
                            "glob" => ctx.scope.set_glob_enabled(false),
                            "output-limit" => ctx.output_limit.set_limit(None),
                            _ => {}
                        }
                        i += 1;
                    }
                }
                _ => {} // silently ignore
            }
            i += 1;
        }

        // Handle case where parser split `-o` into flags and the option name
        // ended up as a bare positional (flags=["o"], positional=["latch"]).
        // Only fire if no "-o" or "+o" appeared in positionals (which would have
        // already consumed the option name above).
        if args.flags.contains("o")
            && !positionals.iter().any(|p| *p == "-o" || *p == "+o")
        {
            // The first positional that matches a known option name gets enabled
            for &name in &positionals {
                match name {
                    "latch" => { ctx.scope.set_latch_enabled(true); break; }
                    "trash" => { ctx.scope.set_trash_enabled(true); break; }
                    "glob" => { ctx.scope.set_glob_enabled(true); break; }
                    _ => {
                        if name == "output-limit" || name.starts_with("output-limit=") {
                            if let Some(size_str) = name.strip_prefix("output-limit=") {
                                if let Ok(bytes) = crate::output_limit::parse_size(size_str) {
                                    ctx.output_limit.set_limit(Some(bytes));
                                }
                            } else if ctx.output_limit.max_bytes().is_none() {
                                ctx.output_limit.set_limit(Some(crate::output_limit::OutputLimitConfig::default_limit()));
                            }
                            break;
                        }
                    }
                }
            }
        }

        ExecResult::success("")
    }
}

fn format_size_for_set(bytes: usize) -> String {
    if bytes % (1024 * 1024) == 0 {
        format!("{}M", bytes / (1024 * 1024))
    } else if bytes % 1024 == 0 {
        format!("{}K", bytes / 1024)
    } else {
        bytes.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::vfs::{MemoryFs, VfsRouter};
    use std::sync::Arc;

    fn make_ctx() -> ExecContext {
        let mut vfs = VfsRouter::new();
        vfs.mount("/", MemoryFs::new());
        ExecContext::new(Arc::new(vfs))
    }

    #[tokio::test]
    async fn test_set_e_enables_error_exit() {
        let mut ctx = make_ctx();
        assert!(!ctx.scope.error_exit_enabled());

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-e".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(ctx.scope.error_exit_enabled());
    }

    #[tokio::test]
    async fn test_set_plus_e_disables_error_exit() {
        let mut ctx = make_ctx();
        ctx.scope.set_error_exit(true);
        assert!(ctx.scope.error_exit_enabled());

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("+e".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(!ctx.scope.error_exit_enabled());
    }

    #[tokio::test]
    async fn test_set_ignores_unknown_options() {
        let mut ctx = make_ctx();

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-u".into()));
        args.positional.push(Value::String("-o".into()));
        args.positional.push(Value::String("pipefail".into()));
        args.positional.push(Value::String("-x".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
    }

    #[tokio::test]
    async fn test_set_no_args_shows_settings() {
        let mut ctx = make_ctx();
        ctx.scope.set_error_exit(true);

        let args = ToolArgs::new();
        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(result.text_out().contains("set -e"));
    }

    #[tokio::test]
    async fn test_set_euo_pipefail() {
        // Common bash idiom: set -euo pipefail
        let mut ctx = make_ctx();

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-e".into()));
        args.positional.push(Value::String("-u".into()));
        args.positional.push(Value::String("-o".into()));
        args.positional.push(Value::String("pipefail".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(ctx.scope.error_exit_enabled());
    }

    #[tokio::test]
    async fn test_set_o_latch_enables() {
        let mut ctx = make_ctx();
        assert!(!ctx.scope.latch_enabled());

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-o".into()));
        args.positional.push(Value::String("latch".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(ctx.scope.latch_enabled());
    }

    #[tokio::test]
    async fn test_set_plus_o_latch_disables() {
        let mut ctx = make_ctx();
        ctx.scope.set_latch_enabled(true);

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("+o".into()));
        args.positional.push(Value::String("latch".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(!ctx.scope.latch_enabled());
    }

    #[tokio::test]
    async fn test_set_o_trash_enables() {
        let mut ctx = make_ctx();
        assert!(!ctx.scope.trash_enabled());

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-o".into()));
        args.positional.push(Value::String("trash".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(ctx.scope.trash_enabled());
    }

    #[tokio::test]
    async fn test_set_plus_o_trash_disables() {
        let mut ctx = make_ctx();
        ctx.scope.set_trash_enabled(true);

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("+o".into()));
        args.positional.push(Value::String("trash".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(!ctx.scope.trash_enabled());
    }

    #[tokio::test]
    async fn test_set_no_args_shows_all_options() {
        let mut ctx = make_ctx();
        ctx.scope.set_latch_enabled(true);
        ctx.scope.set_trash_enabled(true);

        let args = ToolArgs::new();
        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(result.text_out().contains("set -o latch"));
        assert!(result.text_out().contains("set -o trash"));
    }

    #[tokio::test]
    async fn test_set_o_unknown_ignored() {
        let mut ctx = make_ctx();

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-o".into()));
        args.positional.push(Value::String("pipefail".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
    }

    #[tokio::test]
    async fn test_set_o_output_limit_enables_default() {
        let mut ctx = make_ctx();
        assert!(!ctx.output_limit.is_enabled());

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-o".into()));
        args.positional.push(Value::String("output-limit".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(ctx.output_limit.is_enabled());
        assert_eq!(ctx.output_limit.max_bytes(), Some(crate::output_limit::OutputLimitConfig::default_limit()));
    }

    #[tokio::test]
    async fn test_set_o_output_limit_with_size() {
        let mut ctx = make_ctx();

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("-o".into()));
        args.positional.push(Value::String("output-limit=16K".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert_eq!(ctx.output_limit.max_bytes(), Some(16 * 1024));
    }

    #[tokio::test]
    async fn test_set_plus_o_output_limit_disables() {
        let mut ctx = make_ctx();
        ctx.output_limit.set_limit(Some(8 * 1024));
        assert!(ctx.output_limit.is_enabled());

        let mut args = ToolArgs::new();
        args.positional.push(Value::String("+o".into()));
        args.positional.push(Value::String("output-limit".into()));

        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(!ctx.output_limit.is_enabled());
    }

    #[tokio::test]
    async fn test_set_no_args_shows_output_limit() {
        let mut ctx = make_ctx();
        ctx.output_limit.set_limit(Some(4 * 1024));

        let args = ToolArgs::new();
        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(result.text_out().contains("set -o output-limit=4K"));
    }

    #[tokio::test]
    async fn test_set_no_args_hides_output_limit_when_disabled() {
        let mut ctx = make_ctx();
        // output_limit disabled by default in test ctx

        let args = ToolArgs::new();
        let result = Set.execute(args, &mut ctx).await;
        assert!(result.ok());
        assert!(!result.text_out().contains("output-limit"));
    }

    #[test]
    fn test_format_size_for_set() {
        assert_eq!(format_size_for_set(1024), "1K");
        assert_eq!(format_size_for_set(8 * 1024), "8K");
        assert_eq!(format_size_for_set(1024 * 1024), "1M");
        assert_eq!(format_size_for_set(512), "512");
    }
}
