pub mod registry;
mod commands;

use std::sync::{Arc, RwLock};

use registry::*;
use crate::server::database::PvDatabase;

/// Interactive IOC shell with extensible command registration.
pub struct IocShell {
    registry: Arc<RwLock<CommandRegistry>>,
    ctx: CommandContext,
}

impl IocShell {
    /// Create a new shell with built-in commands registered.
    pub fn new(db: Arc<PvDatabase>, handle: tokio::runtime::Handle) -> Self {
        let mut registry = CommandRegistry::new();
        commands::register_builtins(&mut registry);
        Self {
            registry: Arc::new(RwLock::new(registry)),
            ctx: CommandContext::new(db, handle),
        }
    }

    /// Register an additional command (thread-safe, takes &self).
    pub fn register(&self, def: CommandDef) {
        self.registry.write().unwrap().register(def);
    }

    /// Execute a single line of input.
    pub fn execute_line(&self, line: &str) -> CommandResult {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            return Ok(CommandOutcome::Continue);
        }

        // Handle `< filename` include syntax
        if let Some(rest) = line.strip_prefix('<') {
            let filename = registry::substitute_env_vars(rest.trim());
            return self.execute_script(&filename).map(|_| CommandOutcome::Continue);
        }

        let tokens = tokenize(line);
        if tokens.is_empty() {
            return Ok(CommandOutcome::Continue);
        }

        let cmd_name = &tokens[0];
        let arg_tokens = &tokens[1..];

        let registry = self.registry.read().unwrap();

        // Special handling for help — needs access to the registry
        if cmd_name == "help" {
            return self.execute_help(arg_tokens, &registry);
        }

        let def = registry
            .get(cmd_name)
            .ok_or_else(|| format!("unknown command: '{cmd_name}'"))?;

        let args = parse_args(arg_tokens, &def.args)?;
        def.handler.call(&args, &self.ctx)
    }

    /// Execute a script file line by line, echoing each line like C++ iocsh.
    pub fn execute_script(&self, path: &str) -> Result<(), String> {
        let content = std::fs::read_to_string(path)
            .map_err(|e| format!("cannot read '{}': {}", path, e))?;

        for (line_num, line) in content.lines().enumerate() {
            // Echo each line (C++ iocsh behavior)
            println!("{line}");
            match self.execute_line(line) {
                Ok(CommandOutcome::Continue) => {}
                Ok(CommandOutcome::Exit) => return Ok(()),
                Err(e) => {
                    eprintln!("{}:{}: Error: {}", path, line_num + 1, e);
                }
            }
        }
        Ok(())
    }

    /// Run the interactive REPL. Blocks until exit or EOF.
    pub fn run_repl(&self) -> Result<(), String> {
        let mut rl = rustyline::DefaultEditor::new()
            .map_err(|e| format!("failed to initialize readline: {e}"))?;

        loop {
            match rl.readline("epics> ") {
                Ok(line) => {
                    let line = line.trim().to_string();
                    if line.is_empty() {
                        continue;
                    }
                    let _ = rl.add_history_entry(&line);

                    match self.execute_line(&line) {
                        Ok(CommandOutcome::Continue) => {}
                        Ok(CommandOutcome::Exit) => break,
                        Err(e) => eprintln!("Error: {e}"),
                    }
                }
                Err(rustyline::error::ReadlineError::Eof) => break,
                Err(rustyline::error::ReadlineError::Interrupted) => continue,
                Err(e) => {
                    eprintln!("readline error: {e}");
                    break;
                }
            }
        }

        Ok(())
    }

    fn execute_help(&self, arg_tokens: &[String], registry: &CommandRegistry) -> CommandResult {
        if let Some(name) = arg_tokens.first() {
            if let Some(def) = registry.get(name) {
                println!("{}", def.usage);
            } else {
                println!("unknown command: '{name}'");
            }
        } else {
            println!("Available commands:");
            for name in registry.list() {
                println!("  {name}");
            }
        }
        Ok(CommandOutcome::Continue)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::server::records::ai::AiRecord;

    fn make_shell() -> IocShell {
        let rt = tokio::runtime::Runtime::new().unwrap();
        let db = Arc::new(PvDatabase::new());
        let handle = rt.handle().clone();
        rt.block_on(async {
            db.add_record("TEST_REC", Box::new(AiRecord::new(42.0))).await;
        });
        std::mem::forget(rt);
        IocShell::new(db, handle)
    }

    #[test]
    fn test_execute_line_dbl() {
        let shell = make_shell();
        let result = shell.execute_line("dbl");
        assert!(matches!(result, Ok(CommandOutcome::Continue)));
    }

    #[test]
    fn test_execute_line_unknown() {
        let shell = make_shell();
        let result = shell.execute_line("nonexistent_cmd");
        assert!(result.is_err());
    }

    #[test]
    fn test_execute_line_empty() {
        let shell = make_shell();
        let result = shell.execute_line("");
        assert!(matches!(result, Ok(CommandOutcome::Continue)));
    }

    #[test]
    fn test_execute_line_comment() {
        let shell = make_shell();
        let result = shell.execute_line("# this is a comment");
        assert!(matches!(result, Ok(CommandOutcome::Continue)));
    }

    #[test]
    fn test_execute_line_missing_required_arg() {
        let shell = make_shell();
        let result = shell.execute_line("dbgf");
        assert!(result.is_err());
    }

    #[test]
    fn test_execute_line_help() {
        let shell = make_shell();
        let result = shell.execute_line("help");
        assert!(matches!(result, Ok(CommandOutcome::Continue)));
    }

    #[test]
    fn test_execute_line_help_specific() {
        let shell = make_shell();
        let result = shell.execute_line("help dbl");
        assert!(matches!(result, Ok(CommandOutcome::Continue)));
    }

    #[test]
    fn test_execute_line_include_syntax() {
        let shell = make_shell();
        // A non-existent file should return an error
        let result = shell.execute_line("< nonexistent_file.cmd");
        assert!(result.is_err());
    }

    #[test]
    fn test_register_custom_command() {
        let shell = make_shell();
        shell.register(CommandDef::new(
            "myCmd",
            vec![],
            "myCmd - custom command",
            |_args: &[ArgValue], _ctx: &CommandContext| Ok(CommandOutcome::Continue),
        ));
        let result = shell.execute_line("myCmd");
        assert!(matches!(result, Ok(CommandOutcome::Continue)));
    }
}
