use std::fmt;
use std::os::fd::{AsFd, OwnedFd};

use crate::protocol::WindowSize;

#[derive(Debug)]
pub struct PtyPair {
    pub master: OwnedFd,
    pub slave: OwnedFd,
}

#[cfg(all(unix, not(target_os = "aix")))]
#[derive(Debug)]
pub struct PtyChild {
    pub master: OwnedFd,
    pub child: nix::unistd::Pid,
}

#[cfg(all(unix, not(target_os = "aix")))]
pub fn open_pty(size: Option<WindowSize>) -> Result<PtyPair, PtyError> {
    let winsize = size.map(to_nix_winsize);
    let result = nix::pty::openpty(winsize.as_ref(), None).map_err(PtyError::Open)?;

    Ok(PtyPair {
        master: result.master,
        slave: result.slave,
    })
}

#[cfg(all(unix, not(target_os = "aix")))]
pub fn spawn_pty_command(
    program: &str,
    argv0: Option<&str>,
    args: &[&str],
    env_overrides: &[(&str, &str)],
    cwd: Option<&str>,
    size: Option<WindowSize>,
) -> Result<PtyChild, PtyError> {
    use std::ffi::CString;

    use nix::pty::ForkptyResult;
    use nix::unistd::execvp;

    let default_argv0 = program;
    let program = CString::new(program).map_err(|_| PtyError::NulByte("program"))?;
    let argv0 =
        CString::new(argv0.unwrap_or(default_argv0)).map_err(|_| PtyError::NulByte("argument"))?;
    let mut argv = Vec::with_capacity(args.len() + 1);
    argv.push(argv0);
    for arg in args {
        argv.push(CString::new(*arg).map_err(|_| PtyError::NulByte("argument"))?);
    }

    let mut env = Vec::with_capacity(env_overrides.len());
    for (name, value) in env_overrides {
        if name.as_bytes().contains(&b'=') {
            return Err(PtyError::InvalidEnvironmentName((*name).to_string()));
        }

        env.push((
            CString::new(*name).map_err(|_| PtyError::NulByte("environment"))?,
            CString::new(*value).map_err(|_| PtyError::NulByte("environment"))?,
        ));
    }
    let cwd = cwd
        .map(|cwd| CString::new(cwd).map_err(|_| PtyError::NulByte("cwd")))
        .transpose()?;

    let termios = interactive_termios()?;
    let winsize = size.map(to_nix_winsize);
    // SAFETY: forkpty is called before this function starts any threads. The child branch only
    // applies prebuilt environment overrides, invokes execvp with prebuilt C strings, then _exit
    // if setup or exec fails.
    match unsafe { nix::pty::forkpty(winsize.as_ref(), Some(&termios)) }.map_err(PtyError::Fork)? {
        ForkptyResult::Parent { child, master } => Ok(PtyChild { master, child }),
        ForkptyResult::Child => {
            if let Some(cwd) = &cwd
                && unsafe { nix::libc::chdir(cwd.as_ptr()) } != 0
            {
                // SAFETY: We are in the post-fork child and cannot recover from chdir failure
                // before exec.
                unsafe { nix::libc::_exit(127) };
            }
            for (name, value) in &env {
                if unsafe { nix::libc::setenv(name.as_ptr(), value.as_ptr(), 1) } != 0 {
                    // SAFETY: We are in the post-fork child and cannot recover from environment
                    // setup failure before exec.
                    unsafe { nix::libc::_exit(127) };
                }
            }
            let _ = execvp(&program, &argv);
            // SAFETY: We are in the post-fork child and exec failed; _exit avoids running parent
            // Rust destructors in the child process.
            unsafe { nix::libc::_exit(127) };
        }
    }
}

#[cfg(all(unix, not(target_os = "aix")))]
fn interactive_termios() -> Result<nix::sys::termios::Termios, PtyError> {
    use nix::sys::termios::{
        BaudRate, ControlFlags, InputFlags, LocalFlags, OutputFlags, SpecialCharacterIndices,
        cfsetspeed, tcgetattr,
    };

    let pair = nix::pty::openpty(None, None).map_err(PtyError::Open)?;
    let mut termios = tcgetattr(pair.slave.as_fd()).map_err(PtyError::Termios)?;

    termios.input_flags.remove(
        InputFlags::IGNBRK
            | InputFlags::IGNCR
            | InputFlags::INLCR
            | InputFlags::ISTRIP
            | InputFlags::IXOFF
            | InputFlags::PARMRK,
    );
    termios
        .input_flags
        .insert(InputFlags::BRKINT | InputFlags::ICRNL | InputFlags::IXON);
    #[cfg(not(any(target_os = "redox", target_os = "haiku")))]
    termios.input_flags.insert(InputFlags::IMAXBEL);
    #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
    termios.input_flags.insert(InputFlags::IUTF8);

    termios
        .output_flags
        .insert(OutputFlags::OPOST | OutputFlags::ONLCR);

    termios
        .control_flags
        .remove(ControlFlags::CSIZE | ControlFlags::PARENB);
    termios
        .control_flags
        .insert(ControlFlags::CREAD | ControlFlags::CS8);

    termios
        .local_flags
        .remove(LocalFlags::ECHONL | LocalFlags::NOFLSH | LocalFlags::TOSTOP);
    termios
        .local_flags
        .insert(LocalFlags::ECHO | LocalFlags::ECHOE | LocalFlags::ECHOK);
    #[cfg(not(target_os = "redox"))]
    termios.local_flags.insert(LocalFlags::ECHOKE);
    #[cfg(not(any(target_os = "redox", target_os = "cygwin")))]
    termios.local_flags.insert(LocalFlags::ECHOCTL);
    termios
        .local_flags
        .insert(LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG);

    set_control_char(&mut termios, SpecialCharacterIndices::VEOF, 4);
    set_control_char(&mut termios, SpecialCharacterIndices::VEOL, 0);
    set_control_char(&mut termios, SpecialCharacterIndices::VERASE, 127);
    set_control_char(&mut termios, SpecialCharacterIndices::VINTR, 3);
    set_control_char(&mut termios, SpecialCharacterIndices::VKILL, 21);
    set_control_char(&mut termios, SpecialCharacterIndices::VMIN, 1);
    set_control_char(&mut termios, SpecialCharacterIndices::VQUIT, 28);
    set_control_char(&mut termios, SpecialCharacterIndices::VSTART, 17);
    set_control_char(&mut termios, SpecialCharacterIndices::VSTOP, 19);
    set_control_char(&mut termios, SpecialCharacterIndices::VSUSP, 26);
    set_control_char(&mut termios, SpecialCharacterIndices::VTIME, 0);
    #[cfg(not(target_os = "haiku"))]
    set_control_char(&mut termios, SpecialCharacterIndices::VLNEXT, 22);
    #[cfg(not(target_os = "haiku"))]
    set_control_char(&mut termios, SpecialCharacterIndices::VREPRINT, 18);
    #[cfg(not(any(target_os = "aix", target_os = "haiku")))]
    set_control_char(&mut termios, SpecialCharacterIndices::VDISCARD, 15);
    #[cfg(not(any(target_os = "aix", target_os = "haiku")))]
    set_control_char(&mut termios, SpecialCharacterIndices::VWERASE, 23);

    cfsetspeed(&mut termios, BaudRate::B38400).map_err(PtyError::Termios)?;

    Ok(termios)
}

#[cfg(all(unix, not(target_os = "aix")))]
fn set_control_char(
    termios: &mut nix::sys::termios::Termios,
    index: nix::sys::termios::SpecialCharacterIndices,
    value: nix::libc::cc_t,
) {
    termios.control_chars[index as usize] = value;
}

#[cfg(all(unix, not(target_os = "aix")))]
pub fn set_window_size<Fd>(fd: Fd, size: WindowSize) -> Result<(), PtyError>
where
    Fd: std::os::fd::AsFd,
{
    use std::os::fd::AsRawFd;

    let winsize = to_libc_winsize(size);
    let result = unsafe {
        nix::libc::ioctl(
            fd.as_fd().as_raw_fd(),
            nix::libc::TIOCSWINSZ,
            &winsize as *const nix::libc::winsize,
        )
    };

    nix::errno::Errno::result(result)
        .map(drop)
        .map_err(PtyError::SetWindowSize)
}

#[cfg(not(all(unix, not(target_os = "aix"))))]
pub fn open_pty(_size: Option<WindowSize>) -> Result<PtyPair, PtyError> {
    Err(PtyError::UnsupportedPlatform(
        "PTY allocation requires a Unix platform supported by nix::pty::openpty",
    ))
}

#[cfg(all(unix, not(target_os = "aix")))]
fn to_nix_winsize(size: WindowSize) -> nix::pty::Winsize {
    nix::pty::Winsize {
        ws_row: size.rows,
        ws_col: size.cols,
        ws_xpixel: size.pixel_width,
        ws_ypixel: size.pixel_height,
    }
}

#[cfg(all(unix, not(target_os = "aix")))]
fn to_libc_winsize(size: WindowSize) -> nix::libc::winsize {
    nix::libc::winsize {
        ws_row: size.rows,
        ws_col: size.cols,
        ws_xpixel: size.pixel_width,
        ws_ypixel: size.pixel_height,
    }
}

#[derive(Debug)]
pub enum PtyError {
    Open(nix::errno::Errno),
    Fork(nix::errno::Errno),
    SetWindowSize(nix::errno::Errno),
    Termios(nix::errno::Errno),
    NulByte(&'static str),
    InvalidEnvironmentName(String),
    UnsupportedPlatform(&'static str),
}

impl fmt::Display for PtyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Open(err) => write!(f, "failed to open PTY: {err}"),
            Self::Fork(err) => write!(f, "failed to fork PTY child: {err}"),
            Self::SetWindowSize(err) => write!(f, "failed to set PTY window size: {err}"),
            Self::Termios(err) => write!(f, "failed to configure PTY terminal settings: {err}"),
            Self::NulByte(field) => write!(f, "PTY {field} contains a NUL byte"),
            Self::InvalidEnvironmentName(name) => {
                write!(f, "environment variable name contains '=': {name}")
            }
            Self::UnsupportedPlatform(reason) => write!(f, "{reason}"),
        }
    }
}

impl std::error::Error for PtyError {}

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

    #[cfg(all(unix, not(target_os = "aix")))]
    #[test]
    fn window_size_maps_to_nix_winsize() {
        let winsize = to_nix_winsize(WindowSize {
            rows: 24,
            cols: 80,
            pixel_width: 640,
            pixel_height: 480,
        });

        assert_eq!(winsize.ws_row, 24);
        assert_eq!(winsize.ws_col, 80);
        assert_eq!(winsize.ws_xpixel, 640);
        assert_eq!(winsize.ws_ypixel, 480);
    }

    #[cfg(all(unix, not(target_os = "aix")))]
    #[test]
    fn open_pty_returns_two_file_descriptors() {
        let pair = open_pty(Some(WindowSize {
            rows: 24,
            cols: 80,
            pixel_width: 0,
            pixel_height: 0,
        }))
        .unwrap();

        drop(pair);
    }

    #[cfg(all(unix, not(target_os = "aix")))]
    #[test]
    fn interactive_termios_is_sane_for_line_editing() {
        use nix::sys::termios::{
            ControlFlags, InputFlags, LocalFlags, OutputFlags, SpecialCharacterIndices,
        };

        let termios = interactive_termios().unwrap();

        assert!(termios.input_flags.contains(InputFlags::ICRNL));
        assert!(termios.output_flags.contains(OutputFlags::OPOST));
        assert!(termios.output_flags.contains(OutputFlags::ONLCR));
        assert!(termios.control_flags.contains(ControlFlags::CREAD));
        assert!(termios.control_flags.contains(ControlFlags::CS8));
        assert!(termios.local_flags.contains(LocalFlags::ECHO));
        assert!(termios.local_flags.contains(LocalFlags::ICANON));
        assert!(termios.local_flags.contains(LocalFlags::ISIG));
        assert_eq!(
            termios.control_chars[SpecialCharacterIndices::VERASE as usize],
            127
        );
    }

    #[cfg(all(unix, not(target_os = "aix")))]
    #[test]
    fn set_window_size_applies_to_pty() {
        use std::os::fd::{AsFd, AsRawFd};

        let pair = open_pty(None).unwrap();
        set_window_size(
            pair.master.as_fd(),
            WindowSize {
                rows: 33,
                cols: 101,
                pixel_width: 640,
                pixel_height: 480,
            },
        )
        .unwrap();

        let mut winsize = std::mem::MaybeUninit::<nix::libc::winsize>::uninit();
        let result = unsafe {
            nix::libc::ioctl(
                pair.master.as_fd().as_raw_fd(),
                nix::libc::TIOCGWINSZ,
                winsize.as_mut_ptr(),
            )
        };
        nix::errno::Errno::result(result).unwrap();
        let winsize = unsafe { winsize.assume_init() };

        assert_eq!(winsize.ws_row, 33);
        assert_eq!(winsize.ws_col, 101);
        assert_eq!(winsize.ws_xpixel, 640);
        assert_eq!(winsize.ws_ypixel, 480);
    }

    #[cfg(all(unix, not(target_os = "aix")))]
    #[test]
    fn spawn_pty_command_execs_child_with_environment() {
        use std::io::Read;
        use std::time::{Duration, Instant};

        use nix::sys::wait::{WaitStatus, waitpid};

        let child = spawn_pty_command(
            "/bin/sh",
            None,
            &["-c", "printf '%s' \"$SSH_OBI_TEST_VALUE\""],
            &[("SSH_OBI_TEST_VALUE", "pty-ok")],
            None,
            Some(WindowSize {
                rows: 24,
                cols: 80,
                pixel_width: 0,
                pixel_height: 0,
            }),
        )
        .unwrap();
        let mut master = std::fs::File::from(child.master);
        let start = Instant::now();
        let mut output = Vec::new();
        let mut buf = [0u8; 64];

        while start.elapsed() < Duration::from_secs(2) && !output.windows(6).any(|w| w == b"pty-ok")
        {
            match master.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => output.extend_from_slice(&buf[..n]),
                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
                Err(_) => break,
            }
        }

        assert!(
            output.windows(6).any(|w| w == b"pty-ok"),
            "pty output was {:?}",
            String::from_utf8_lossy(&output)
        );

        let status = waitpid(child.child, None).unwrap();
        assert!(matches!(status, WaitStatus::Exited(_, 0)));
    }

    #[cfg(all(unix, not(target_os = "aix")))]
    #[test]
    fn spawn_pty_command_sets_child_working_directory() {
        use std::io::Read;
        use std::time::{Duration, Instant};

        use nix::sys::wait::{WaitStatus, waitpid};

        let child = spawn_pty_command(
            "/bin/sh",
            None,
            &["-c", "pwd"],
            &[],
            Some("/tmp"),
            Some(WindowSize {
                rows: 24,
                cols: 80,
                pixel_width: 0,
                pixel_height: 0,
            }),
        )
        .unwrap();
        let mut master = std::fs::File::from(child.master);
        let start = Instant::now();
        let mut output = Vec::new();
        let mut buf = [0u8; 64];

        while start.elapsed() < Duration::from_secs(2) && !output.windows(4).any(|w| w == b"/tmp") {
            match master.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => output.extend_from_slice(&buf[..n]),
                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
                Err(_) => break,
            }
        }

        assert!(
            output.windows(4).any(|w| w == b"/tmp"),
            "pty output was {:?}",
            String::from_utf8_lossy(&output)
        );

        let status = waitpid(child.child, None).unwrap();
        assert!(matches!(status, WaitStatus::Exited(_, 0)));
    }
}
