mod detect;
mod posix_linux;
mod posix_macos;
pub(crate) mod profile;
mod windows_ps;

pub(crate) use detect::detect_remote_shell;
pub(crate) use posix_linux::PosixLinuxShell;
pub(crate) use posix_macos::PosixMacOsShell;
pub(crate) use windows_ps::WindowsPowerShell;

use std::sync::Arc;

use super::models::types::{ExecRequest, SystemProfile};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SystemType {
    Linux,
    MacOs,
    Windows,
    Unknown,
}

impl SystemType {
    pub fn is_posix(&self) -> bool {
        matches!(self, SystemType::Linux | SystemType::MacOs)
    }
}

pub(crate) trait RemoteShell: Send + Sync {
    fn system_type(&self) -> SystemType;

    fn build_exec_command(&self, req: &ExecRequest) -> String;

    fn build_system_profile_commands(&self) -> SystemProfileCommands;

    fn parse_system_profile(&self, outputs: &SystemProfileOutputs) -> SystemProfile;
}

pub(crate) struct SystemProfileCommands {
    pub os_info: ExecRequest,
    pub arch: ExecRequest,
    pub shell: ExecRequest,
    pub package_manager: ExecRequest,
}

pub(crate) struct SystemProfileOutputs {
    pub os_info: String,
    pub arch: String,
    pub shell: String,
    pub package_manager: String,
}

pub(crate) fn shell_single_quote(s: &str) -> String {
    let mut out = String::with_capacity(s.len() + 2);
    out.push('\'');
    for ch in s.chars() {
        if ch == '\'' {
            out.push_str("'\\''");
        } else {
            out.push(ch);
        }
    }
    out.push('\'');
    out
}

pub(crate) fn is_shell_identifier(value: &str) -> bool {
    let mut chars = value.chars();
    match chars.next() {
        Some(ch) if ch.is_ascii_alphabetic() || ch == '_' => {}
        _ => return false,
    }

    chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}

pub(crate) fn shell_for_system_type(system_type: SystemType) -> Arc<dyn RemoteShell> {
    match system_type {
        SystemType::Linux => Arc::new(PosixLinuxShell),
        SystemType::MacOs => Arc::new(PosixMacOsShell),
        SystemType::Windows => Arc::new(WindowsPowerShell),
        SystemType::Unknown => Arc::new(PosixLinuxShell),
    }
}

/// POSIX shared command building logic.
pub(crate) fn posix_build_exec_command(req: &ExecRequest) -> String {
    let needs_shell = req.use_shell || req.cwd.is_some() || !req.env.is_empty() || req.sudo;
    if !needs_shell {
        return req.command.clone();
    }

    let mut script_parts: Vec<String> = req
        .env
        .iter()
        .filter(|(key, _)| is_shell_identifier(key))
        .map(|(key, value)| format!("export {}={}", key, shell_single_quote(value)))
        .collect();

    if let Some(cwd) = req.cwd.as_deref() {
        script_parts.push(format!("cd {}", shell_single_quote(cwd)));
    }

    script_parts.push(req.command.clone());

    let shell_command = format!("sh -lc {}", shell_single_quote(&script_parts.join(" && ")));
    if req.sudo {
        if let Some(password) = req.sudo_password.as_deref() {
            return format!(
                "printf '%s\\n' {} | sudo -S -p '' {}",
                shell_single_quote(password),
                shell_command
            );
        }

        return format!("sudo {}", shell_command);
    }

    shell_command
}

/// Convert empty string to None, non-empty to Some(trimmed).
pub(crate) fn normalize_optional(value: &str) -> Option<String> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        None
    } else {
        Some(trimmed.to_string())
    }
}

/// Simple interactive shell REPL (non-PTY).
///
/// Reads commands from stdin line by line and forwards them to the remote host. Supports `cd`, `exit`/`quit`.
pub(crate) async fn run_shell(
    session: &russh::client::Handle<super::backend::russh::connect::ClientHandler>,
) -> anyhow::Result<()> {
    use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Stderr, Stdout};

    let exec_collect = super::backend::russh::exec::exec_collect;
    let exec_on_session = super::backend::russh::exec::exec_on_session;

    let mut stdout: Stdout = tokio::io::stdout();
    let mut stderr: Stderr = tokio::io::stderr();
    let stdin = tokio::io::stdin();
    let mut lines = BufReader::new(stdin).lines();

    let mut cwd = {
        let (code, out, err) = exec_collect(session, "pwd").await?;
        if code != 0 {
            let _ = err;
            "/".to_string()
        } else {
            String::from_utf8_lossy(&out).trim().to_string()
        }
    };

    loop {
        stdout
            .write_all(format!("ssh:{}> ", cwd).as_bytes())
            .await?;
        stdout.flush().await?;

        let Some(line) = lines.next_line().await? else {
            break;
        };
        let cmd = line.trim();
        if cmd.is_empty() {
            continue;
        }
        if cmd == "exit" || cmd == "quit" {
            break;
        }

        if cmd == "cd" || cmd.starts_with("cd ") {
            let target = cmd.strip_prefix("cd").unwrap().trim();
            let target = if target.is_empty() { "~" } else { target };
            let script = format!("cd {} && pwd", shell_single_quote(target));
            let (code, out, err) = exec_collect(session, &wrap_sh_lc(&script)).await?;
            if code == 0 {
                let new_cwd = String::from_utf8_lossy(&out).trim().to_string();
                if !new_cwd.is_empty() {
                    cwd = new_cwd;
                }
            } else if !err.is_empty() {
                stderr.write_all(&err).await?;
                stderr.flush().await?;
            }
            stdout.write_all(b"\n").await?;
            stdout.flush().await?;
            continue;
        }

        let script = format!("cd {} && {}", shell_single_quote(&cwd), cmd);
        let wrapped = wrap_sh_lc(&script);
        let _ = exec_on_session(session, wrapped.as_str(), &mut stdout, &mut stderr).await?;
        stdout.write_all(b"\n").await?;
        stdout.flush().await?;
    }

    Ok(())
}

fn wrap_sh_lc(script: &str) -> String {
    format!("sh -lc {}", shell_single_quote(script))
}

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

    // ===== shell_single_quote tests =====

    #[test]
    fn shell_single_quote_simple() {
        assert_eq!(shell_single_quote("hello"), "'hello'");
    }

    #[test]
    fn shell_single_quote_empty() {
        assert_eq!(shell_single_quote(""), "''");
    }

    #[test]
    fn shell_single_quote_with_single_quote() {
        // strings with single quotes need escaping: it'\''s
        assert_eq!(shell_single_quote("it's"), "'it'\\''s'");
    }

    #[test]
    fn shell_single_quote_with_special_chars() {
        let result = shell_single_quote("rm -rf / && echo pwned");
        assert_eq!(result, "'rm -rf / && echo pwned'");
    }

    #[test]
    fn shell_single_quote_with_dollar_sign() {
        // $ is not expanded inside single quotes
        assert_eq!(shell_single_quote("$HOME"), "'$HOME'");
    }

    // ===== is_shell_identifier tests =====

    #[test]
    fn is_shell_identifier_valid() {
        assert!(is_shell_identifier("PATH"));
        assert!(is_shell_identifier("my_var"));
        assert!(is_shell_identifier("_private"));
        assert!(is_shell_identifier("VAR123"));
    }

    #[test]
    fn is_shell_identifier_invalid() {
        assert!(!is_shell_identifier(""));
        assert!(!is_shell_identifier("123abc"));
        assert!(!is_shell_identifier("my-var"));
        assert!(!is_shell_identifier("my var"));
        assert!(!is_shell_identifier("$VAR"));
    }

    // ===== posix_build_exec_command tests =====

    #[test]
    fn posix_build_simple_command() {
        let req = ExecRequest::new("ls -la");
        assert_eq!(posix_build_exec_command(&req), "ls -la");
    }

    #[test]
    fn posix_build_command_with_shell() {
        let req = ExecRequest {
            command: "echo hello".to_string(),
            use_shell: true,
            ..Default::default()
        };
        let result = posix_build_exec_command(&req);
        assert!(
            result.starts_with("sh -lc "),
            "use_shell=true should wrap with sh -lc: {}",
            result
        );
        assert!(result.contains("echo hello"));
    }

    #[test]
    fn posix_build_command_with_cwd() {
        let req = ExecRequest {
            command: "pwd".to_string(),
            cwd: Some("/tmp".to_string()),
            ..Default::default()
        };
        let result = posix_build_exec_command(&req);
        // shell_single_quote("/tmp") = '\''/tmp'\''
        assert!(result.contains("cd "), "should contain cd: {}", result);
        assert!(result.contains("/tmp"), "should contain path: {}", result);
        assert!(result.contains("pwd"), "should contain command: {}", result);
    }

    #[test]
    fn posix_build_command_with_env() {
        let req = ExecRequest {
            command: "echo $MY_VAR".to_string(),
            env: vec![("MY_VAR".to_string(), "hello".to_string())],
            ..Default::default()
        };
        let result = posix_build_exec_command(&req);
        assert!(
            result.contains("export MY_VAR="),
            "should contain export: {}",
            result
        );
        assert!(result.contains("hello"), "should contain value: {}", result);
    }

    #[test]
    fn posix_build_command_with_sudo() {
        let req = ExecRequest {
            command: "id".to_string(),
            sudo: true,
            ..Default::default()
        };
        let result = posix_build_exec_command(&req);
        assert!(
            result.starts_with("sudo "),
            "should start with sudo: {}",
            result
        );
    }

    #[test]
    fn posix_build_command_with_sudo_password() {
        let req = ExecRequest {
            command: "id".to_string(),
            sudo: true,
            sudo_password: Some("secret".to_string()),
            ..Default::default()
        };
        let result = posix_build_exec_command(&req);
        assert!(result.contains("sudo -S"), "should use sudo -S: {}", result);
        assert!(
            result.contains("'secret'"),
            "should contain password: {}",
            result
        );
    }

    #[test]
    fn posix_build_command_with_invalid_env_key() {
        let req = ExecRequest {
            command: "echo".to_string(),
            env: vec![("123INVALID".to_string(), "val".to_string())],
            ..Default::default()
        };
        let result = posix_build_exec_command(&req);
        assert!(
            !result.contains("123INVALID"),
            "invalid environment variable name should not be exported: {}",
            result
        );
    }

    // ===== SystemType tests =====

    #[test]
    fn system_type_is_posix() {
        assert!(SystemType::Linux.is_posix());
        assert!(SystemType::MacOs.is_posix());
        assert!(!SystemType::Windows.is_posix());
        assert!(!SystemType::Unknown.is_posix());
    }

    // ===== shell_for_system_type tests =====

    #[test]
    fn shell_for_system_type_returns_correct_types() {
        assert_eq!(
            shell_for_system_type(SystemType::Linux).system_type(),
            SystemType::Linux
        );
        assert_eq!(
            shell_for_system_type(SystemType::MacOs).system_type(),
            SystemType::MacOs
        );
        assert_eq!(
            shell_for_system_type(SystemType::Windows).system_type(),
            SystemType::Windows
        );
        assert_eq!(
            shell_for_system_type(SystemType::Unknown).system_type(),
            SystemType::Linux
        );
    }
}
