use std::fmt;

use super::session::PtyOptions;

/// Structured command execution request.
///
/// Recommended for single-command execution scenarios.
/// Compared to passing a raw string, this type explicitly expresses the working directory,
/// environment variables, timeout, and sudo execution intent.
#[derive(Clone, Default, Eq, PartialEq)]
pub struct ExecRequest {
    /// The command to execute.
    pub command: String,
    /// Remote working directory; the command will cd here before execution.
    pub cwd: Option<String>,
    /// Remote environment variables.
    pub env: Vec<(String, String)>,
    /// Timeout for a single command, in milliseconds.
    pub timeout_ms: Option<u64>,
    /// Whether to execute via `sh -lc`.
    pub use_shell: bool,
    /// Whether to execute via sudo.
    pub sudo: bool,
    /// Sudo password; only used when the remote host requires `sudo -S`.
    pub sudo_password: Option<String>,
}

impl fmt::Debug for ExecRequest {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ExecRequest")
            .field("command", &self.command)
            .field("cwd", &self.cwd)
            .field("env", &self.env)
            .field("timeout_ms", &self.timeout_ms)
            .field("use_shell", &self.use_shell)
            .field("sudo", &self.sudo)
            .field(
                "sudo_password",
                &self.sudo_password.as_ref().map(|_| "<redacted>"),
            )
            .finish()
    }
}

impl ExecRequest {
    /// Quickly create an execution request with minimal parameters.
    pub fn new(command: impl Into<String>) -> Self {
        Self {
            command: command.into(),
            ..Self::default()
        }
    }
}

/// Structured command execution result.
///
/// It is recommended to evaluate based on `exit_code`, `timed_out`, `stdout`, and `stderr`
/// rather than concatenating them into a single text block.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ExecResult {
    /// Remote command exit code.
    pub exit_code: u32,
    /// Raw stdout bytes.
    pub stdout: Vec<u8>,
    /// Raw stderr bytes.
    pub stderr: Vec<u8>,
    /// Whether the command was terminated due to timeout.
    pub timed_out: bool,
}

impl ExecResult {
    /// Decode stdout as UTF-8 lossy string.
    pub fn stdout_string(&self) -> String {
        String::from_utf8_lossy(&self.stdout).into_owned()
    }

    /// Decode stderr as UTF-8 lossy string.
    pub fn stderr_string(&self) -> String {
        String::from_utf8_lossy(&self.stderr).into_owned()
    }
}

/// Request parameters for opening a PTY session.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PtyOpenRequest {
    /// Terminal type, e.g. `xterm-256color`.
    pub term: String,
    /// Terminal columns.
    pub cols: u32,
    /// Terminal rows.
    pub rows: u32,
    /// Terminal pixel width.
    pub pix_width: u32,
    /// Terminal pixel height.
    pub pix_height: u32,
    /// Optional startup command; opens the default shell when empty.
    pub command: Option<String>,
}

impl Default for PtyOpenRequest {
    fn default() -> Self {
        Self {
            term: "xterm-256color".to_string(),
            cols: 80,
            rows: 24,
            pix_width: 0,
            pix_height: 0,
            command: None,
        }
    }
}

impl From<PtyOpenRequest> for PtyOptions {
    fn from(value: PtyOpenRequest) -> Self {
        Self {
            term: value.term,
            cols: value.cols,
            rows: value.rows,
            pix_width: value.pix_width,
            pix_height: value.pix_height,
        }
    }
}

impl From<PtyOptions> for PtyOpenRequest {
    fn from(value: PtyOptions) -> Self {
        Self {
            term: value.term,
            cols: value.cols,
            rows: value.rows,
            pix_width: value.pix_width,
            pix_height: value.pix_height,
            command: None,
        }
    }
}

/// Remote system profile detection result.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct SystemProfile {
    /// OS identifier from `/etc/os-release`, e.g. `ubuntu`, `rocky`.
    pub os_id: Option<String>,
    /// OS version string, e.g. `22.04`.
    pub os_version: Option<String>,
    /// CPU architecture, e.g. `x86_64`, `aarch64`.
    pub arch: Option<String>,
    /// Detected package manager, e.g. `apt`, `dnf`, `yum`.
    pub package_manager: Option<String>,
    /// Current shell path, e.g. `/bin/bash`.
    pub shell: Option<String>,
}

/// Directory entry metadata.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct FileEntry {
    /// Full path.
    pub path: String,
    /// File or directory name.
    pub name: String,
    /// Whether this is a directory.
    pub is_dir: bool,
    /// File size; for directories, depends on the remote filesystem.
    pub size: u64,
    /// Permission info; may be empty in the current implementation.
    pub permissions: Option<String>,
    /// Last modification Unix timestamp.
    pub modified_unix_ts: Option<u64>,
}

/// Remote file read request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ReadFileRequest {
    /// Remote file path.
    pub path: String,
    /// Starting offset in bytes.
    pub offset: Option<u64>,
    /// Maximum bytes to read.
    pub limit: Option<u64>,
}

/// Remote file write request.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct WriteFileRequest {
    /// Remote file path.
    pub path: String,
    /// Raw content to write.
    pub content: Vec<u8>,
    /// Whether to automatically create parent directories.
    pub create_parent: bool,
    /// Whether to allow overwriting existing files.
    pub overwrite: bool,
}

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

    // ===== ExecRequest tests =====

    #[test]
    fn exec_request_new() {
        let req = ExecRequest::new("uname -a");
        assert_eq!(req.command, "uname -a");
        assert_eq!(req.cwd, None);
        assert!(req.env.is_empty());
        assert_eq!(req.timeout_ms, None);
        assert!(!req.use_shell);
        assert!(!req.sudo);
        assert_eq!(req.sudo_password, None);
    }

    #[test]
    fn exec_request_default() {
        let req = ExecRequest::default();
        assert!(req.command.is_empty());
        assert_eq!(req.cwd, None);
        assert!(!req.use_shell);
        assert!(!req.sudo);
    }

    #[test]
    fn exec_request_debug_redacts_sudo_password() {
        let req = ExecRequest {
            command: "id".to_string(),
            sudo: true,
            sudo_password: Some("super_secret".to_string()),
            ..Default::default()
        };
        let debug_str = format!("{:?}", req);
        assert!(
            !debug_str.contains("super_secret"),
            "Debug output should not contain plaintext password: {}",
            debug_str
        );
        assert!(debug_str.contains("<redacted>"));
    }

    #[test]
    fn exec_request_clone() {
        let req = ExecRequest {
            command: "ls".to_string(),
            cwd: Some("/tmp".to_string()),
            ..Default::default()
        };
        let cloned = req.clone();
        assert_eq!(req, cloned);
    }

    // ===== ExecResult tests =====

    #[test]
    fn exec_result_stdout_string() {
        let result = ExecResult {
            exit_code: 0,
            stdout: b"hello world\n".to_vec(),
            stderr: Vec::new(),
            timed_out: false,
        };
        assert_eq!(result.stdout_string(), "hello world\n");
    }

    #[test]
    fn exec_result_stderr_string() {
        let result = ExecResult {
            exit_code: 1,
            stdout: Vec::new(),
            stderr: b"error: not found\n".to_vec(),
            timed_out: false,
        };
        assert_eq!(result.stderr_string(), "error: not found\n");
    }

    #[test]
    fn exec_result_default() {
        let result = ExecResult::default();
        assert_eq!(result.exit_code, 0);
        assert!(result.stdout.is_empty());
        assert!(result.stderr.is_empty());
        assert!(!result.timed_out);
    }

    #[test]
    fn exec_result_invalid_utf8() {
        let result = ExecResult {
            exit_code: 0,
            stdout: vec![0xFF, 0xFE],
            stderr: Vec::new(),
            timed_out: false,
        };
        // should not panic, should use lossy decoding
        let s = result.stdout_string();
        assert!(!s.is_empty());
    }

    // ===== PtyOpenRequest tests =====

    #[test]
    fn pty_open_request_default() {
        let req = PtyOpenRequest::default();
        assert_eq!(req.term, "xterm-256color");
        assert_eq!(req.cols, 80);
        assert_eq!(req.rows, 24);
        assert_eq!(req.pix_width, 0);
        assert_eq!(req.pix_height, 0);
        assert_eq!(req.command, None);
    }

    #[test]
    fn pty_open_request_from_pty_options_roundtrip() {
        let req = PtyOpenRequest {
            term: "xterm".to_string(),
            cols: 120,
            rows: 40,
            pix_width: 1024,
            pix_height: 768,
            command: Some("bash".to_string()),
        };
        let options: PtyOptions = req.clone().into();
        assert_eq!(options.term, "xterm");
        assert_eq!(options.cols, 120);
        assert_eq!(options.rows, 40);

        // Reverse conversion loses command
        let req2: PtyOpenRequest = options.into();
        assert_eq!(req2.term, "xterm");
        assert_eq!(req2.cols, 120);
        assert_eq!(
            req2.command, None,
            "From<PtyOptions> should set command to None"
        );
    }

    // ===== SystemProfile tests =====

    #[test]
    fn system_profile_default() {
        let profile = SystemProfile::default();
        assert_eq!(profile.os_id, None);
        assert_eq!(profile.os_version, None);
        assert_eq!(profile.arch, None);
        assert_eq!(profile.package_manager, None);
        assert_eq!(profile.shell, None);
    }

    // ===== FileEntry tests =====

    #[test]
    fn file_entry_default() {
        let entry = FileEntry::default();
        assert!(entry.path.is_empty());
        assert!(entry.name.is_empty());
        assert!(!entry.is_dir);
        assert_eq!(entry.size, 0);
    }

    // ===== ReadFileRequest / WriteFileRequest tests =====

    #[test]
    fn read_file_request_default() {
        let req = ReadFileRequest::default();
        assert!(req.path.is_empty());
        assert_eq!(req.offset, None);
        assert_eq!(req.limit, None);
    }

    #[test]
    fn write_file_request_default() {
        let req = WriteFileRequest::default();
        assert!(req.path.is_empty());
        assert!(req.content.is_empty());
        assert!(!req.create_parent);
        assert!(!req.overwrite);
    }
}
