use crossterm::{
    cursor::{Hide, Show},
    event::{DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture},
    execute,
    style::ResetColor,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::io::{self, Write};

/// Ein Besitzer für Raw Mode und Terminalzustand, auch bei teilweiser Initialisierung.
pub struct TerminalGuard<W: Write> {
    writer: W,
    raw: fn(bool) -> io::Result<()>,
    #[cfg(windows)]
    modes: Option<ConsoleModes>,
}
impl<W: Write> TerminalGuard<W> {
    pub fn enter(writer: W) -> io::Result<Self> {
        // Farben tragen UI-/BASIC-Zustand und müssen auch ohne Farberkennung
        // oder mit NO_COLOR als explizite RGB-Werte ausgegeben werden.
        crossterm::style::force_color_output(true);
        #[cfg(windows)]
        let modes = ConsoleModes::capture()?;
        #[cfg(windows)]
        modes.enable_output()?;
        #[allow(unused_mut)]
        let mut guard = Self::with_raw(writer, |on| {
            if on {
                enable_raw_mode()
            } else {
                disable_raw_mode()
            }
        })?;
        #[cfg(windows)]
        {
            guard.modes = Some(modes);
        }
        Ok(guard)
    }
    fn with_raw(writer: W, raw: fn(bool) -> io::Result<()>) -> io::Result<Self> {
        let mut guard = Self {
            writer,
            raw,
            #[cfg(windows)]
            modes: None,
        };
        guard.resume()?;
        Ok(guard)
    }
    fn resume(&mut self) -> io::Result<()> {
        #[cfg(windows)]
        if let Some(modes) = &self.modes {
            modes.enable_output()?;
        }
        (self.raw)(true)?;
        execute!(
            self.writer,
            EnterAlternateScreen,
            EnableMouseCapture,
            EnableBracketedPaste,
            Hide
        )
    }
    fn suspend(&mut self) -> io::Result<()> {
        let raw = (self.raw)(false);
        let output = execute!(
            self.writer,
            ResetColor,
            Show,
            DisableMouseCapture,
            DisableBracketedPaste,
            LeaveAlternateScreen
        );
        #[cfg(windows)]
        let output = output.and(self.modes.as_ref().map_or(Ok(()), ConsoleModes::restore));
        raw.and(output)
    }
    /// Foreground child owns cooked terminal input. Always restore the IDE,
    /// including spawn/wait errors and a child interrupted by Ctrl+C.
    pub fn shell(
        &mut self,
        command: &str,
        signals: &crate::signale::Signalquelle,
    ) -> io::Result<i32> {
        let result = self.shell_with_interrupt(command, || signals.abholen().is_some());
        // A child may exit before try_wait observes the terminal signal.
        // It belongs to the handed-off terminal, not the resumed BASIC run.
        while signals.abholen().is_some() {}
        result
    }
    fn shell_with_interrupt(
        &mut self,
        command: &str,
        mut interrupted: impl FnMut() -> bool,
    ) -> io::Result<i32> {
        let result = self.suspend().and_then(|_| {
            let mut child = tb_runtime::host::shell_command(command).spawn()?;
            loop {
                match child.try_wait() {
                    Ok(Some(status)) => return Ok(status.code().unwrap_or(0)),
                    Ok(None) => {}
                    Err(error) => {
                        let _ = child.kill();
                        let _ = child.wait();
                        return Err(error);
                    }
                }
                if interrupted() {
                    let _ = child.kill();
                    return child.wait().map(|status| status.code().unwrap_or(0));
                }
                std::thread::sleep(std::time::Duration::from_millis(10));
            }
        });
        let restored = self.resume();
        restored.and(result)
    }
}
impl<W: Write> Drop for TerminalGuard<W> {
    fn drop(&mut self) {
        #[cfg(windows)]
        if let Some(modes) = &self.modes {
            let _ = modes.enable_output();
        }
        let _ = (self.raw)(false);
        let _ = execute!(
            self.writer,
            ResetColor,
            Show,
            DisableMouseCapture,
            DisableBracketedPaste,
            LeaveAlternateScreen
        );
    }
}

// Crossterm stellt unter Windows Raw-/Mausflags pauschal zurück. Der Besitzer
// muss zusätzlich die tatsächlichen Eingabe- und Ausgabemodi wiederherstellen.
#[cfg(windows)]
#[derive(Debug)]
struct ConsoleModes([(std::os::windows::io::RawHandle, u32); 2]);
#[cfg(windows)]
#[link(name = "kernel32")]
extern "system" {
    fn GetStdHandle(kind: u32) -> std::os::windows::io::RawHandle;
    fn GetConsoleMode(handle: std::os::windows::io::RawHandle, mode: *mut u32) -> i32;
    fn SetConsoleMode(handle: std::os::windows::io::RawHandle, mode: u32) -> i32;
}
#[cfg(windows)]
impl ConsoleModes {
    fn capture() -> io::Result<Self> {
        let mut modes = [(std::ptr::null_mut(), 0); 2];
        for (slot, kind) in modes.iter_mut().zip([(-10i32) as u32, (-11i32) as u32]) {
            // Borrowed standard handles: valid until the terminal owner exits.
            slot.0 = unsafe { GetStdHandle(kind) };
            if unsafe { GetConsoleMode(slot.0, &mut slot.1) } == 0 {
                return Err(io::Error::last_os_error());
            }
        }
        Ok(Self(modes))
    }
    fn enable_output(&self) -> io::Result<()> {
        // Crossterm caches ANSI support, but a Shell handoff restores the old
        // output flags. Re-enable VT for each ownership cycle, including re-entry.
        if crossterm::ansi_support::supports_ansi()
            && unsafe { SetConsoleMode(self.0[1].0, self.0[1].1 | 0x0004) } == 0
        {
            return Err(io::Error::last_os_error());
        }
        Ok(())
    }
    fn restore(&self) -> io::Result<()> {
        let mut result = Ok(());
        for (handle, mode) in self.0 {
            if unsafe { SetConsoleMode(handle, mode) } == 0 && result.is_ok() {
                result = Err(io::Error::last_os_error());
            }
        }
        result
    }
}
#[cfg(windows)]
impl Drop for ConsoleModes {
    fn drop(&mut self) {
        let _ = self.restore();
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::{
        cell::RefCell,
        rc::Rc,
        sync::atomic::{AtomicI32, Ordering},
    };
    static RAW: AtomicI32 = AtomicI32::new(0);
    #[derive(Clone)]
    struct Output(Rc<RefCell<Vec<u8>>>);
    impl Write for Output {
        fn write(&mut self, b: &[u8]) -> io::Result<usize> {
            self.0.borrow_mut().extend_from_slice(b);
            Ok(b.len())
        }
        fn flush(&mut self) -> io::Result<()> {
            Ok(())
        }
    }
    fn raw(on: bool) -> io::Result<()> {
        RAW.store(i32::from(on), Ordering::SeqCst);
        Ok(())
    }
    #[test]
    fn terminal_cleanup_on_normal_error_and_partial_initialization() {
        for fail in [false, true] {
            let out = Output(Rc::default());
            let copy = out.clone();
            let result = (|| -> io::Result<()> {
                let _guard = TerminalGuard::with_raw(out, raw)?;
                if fail {
                    return Err(io::Error::other("Renderfehler"));
                }
                Ok(())
            })();
            assert_eq!(result.is_err(), fail);
            assert_eq!(RAW.load(Ordering::SeqCst), 0);
            let s = String::from_utf8(copy.0.borrow().clone()).unwrap();
            assert!(s.contains("?1049h") && s.contains("?1049l") && s.contains("?25h"));
        }
        for (command, abort, expected) in [
            ("exit 0", false, 0),
            ("exit 7", false, 7),
            (
                if cfg!(windows) {
                    "for /L %i in (1,1,1000000000) do @rem"
                } else {
                    "exec sleep 30"
                },
                true,
                0,
            ),
        ] {
            let out = Output(Rc::default());
            let copy = out.clone();
            let mut guard = TerminalGuard::with_raw(out, raw).unwrap();
            let started = std::time::Instant::now();
            let code = guard.shell_with_interrupt(command, || abort).unwrap();
            if !abort {
                assert_eq!(code, expected);
            }
            // Windows TerminateProcess liefert einen Code; Unix ein Signal.
            assert!(started.elapsed() < std::time::Duration::from_secs(5));
            assert_eq!(RAW.load(Ordering::SeqCst), 1);
            let text = String::from_utf8(copy.0.borrow().clone()).unwrap();
            assert_eq!(text.matches("?1049h").count(), 2);
            assert_eq!(text.matches("?1049l").count(), 1);
            drop(guard);
            assert_eq!(RAW.load(Ordering::SeqCst), 0);
        }
        let out = Output(Rc::default());
        let copy = out.clone();
        assert!(TerminalGuard::with_raw(out, |on| {
            raw(on)?;
            if on {
                Err(io::Error::other("Raw-Fehler"))
            } else {
                Ok(())
            }
        })
        .is_err());
        assert_eq!(RAW.load(Ordering::SeqCst), 0);
        assert!(String::from_utf8(copy.0.borrow().clone())
            .unwrap()
            .contains("?25h"));
    }
    /// Run explicitly on Windows; never counts as a Windows Terminal UI test.
    #[cfg(windows)]
    #[test]
    #[ignore = "benötigt Windows mit echter Konsole; isolierter Kindprozess"]
    fn windows_console_restores_modes() {
        use std::os::windows::{io::AsRawHandle, process::CommandExt};
        use std::process::{Command, Stdio};
        use std::time::{Duration, Instant};
        const CHILD: &str = "TB_CONSOLE_TEST_CHILD";
        if std::env::var_os(CHILD).is_none() {
            let mut child = Command::new(std::env::current_exe().unwrap())
                .args([
                    "--exact",
                    "terminal::tests::windows_console_restores_modes",
                    "--ignored",
                    "--nocapture",
                ])
                .env(CHILD, "1")
                .creation_flags(0x00000010) // CREATE_NEW_CONSOLE: never touches the caller's console.
                .stdin(Stdio::null())
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .spawn()
                .unwrap();
            let deadline = Instant::now() + Duration::from_secs(30);
            loop {
                if let Some(status) = child.try_wait().unwrap() {
                    assert!(
                        status.success(),
                        "Isolierte Windows-Konsolenprüfung: {status}"
                    );
                    return;
                }
                if Instant::now() >= deadline {
                    let _ = child.kill();
                    let _ = child.wait();
                    panic!("Windows-Konsolenprüfung hat die Frist überschritten");
                }
                std::thread::sleep(Duration::from_millis(20));
            }
        }
        #[link(name = "kernel32")]
        extern "system" {
            fn SetStdHandle(kind: u32, handle: std::os::windows::io::RawHandle) -> i32;
        }
        let open = |name| {
            std::fs::OpenOptions::new()
                .read(true)
                .write(true)
                .open(name)
                .unwrap()
        };
        let input = open("CONIN$");
        let output = open("CONOUT$");
        // Standard streams started as NUL; bind to this child's own new console.
        assert_ne!(
            unsafe { SetStdHandle((-10i32) as u32, input.as_raw_handle()) },
            0
        );
        assert_ne!(
            unsafe { SetStdHandle((-11i32) as u32, output.as_raw_handle()) },
            0
        );
        let original = ConsoleModes::capture().unwrap();
        // Non-default echo flags catch a reset-to-default masquerading as restore.
        let mode = original.0[0].1 & !0x0004; // ENABLE_ECHO_INPUT
        assert_ne!(unsafe { SetConsoleMode(input.as_raw_handle(), mode) }, 0);
        let expected = ConsoleModes::capture().unwrap();
        let assert_restored = || assert_eq!(ConsoleModes::capture().unwrap().0, expected.0);
        for (command, abort, code) in [
            ("exit 0", false, 0),
            ("exit 7", false, 7),
            ("for /L %i in (1,1,1000000000) do @rem", true, 0),
        ] {
            let mut guard = TerminalGuard::enter(io::stdout()).unwrap();
            guard.suspend().unwrap();
            assert_restored();
            guard.resume().unwrap();
            if crossterm::ansi_support::supports_ansi() {
                assert_ne!(ConsoleModes::capture().unwrap().0[1].1 & 0x0004, 0);
            }
            let result = guard.shell_with_interrupt(command, || abort).unwrap();
            if !abort {
                assert_eq!(result, code);
            }
            assert!(crossterm::terminal::is_raw_mode_enabled().unwrap());
            drop(guard);
            assert_restored();
        }
        let error = (|| -> io::Result<()> {
            let _guard = TerminalGuard::enter(io::stdout())?;
            Err(io::Error::other("Erzwungener Renderfehler"))
        })();
        assert!(error.is_err());
        assert_restored();
        struct Broken;
        impl Write for Broken {
            fn write(&mut self, _: &[u8]) -> io::Result<usize> {
                Err(io::Error::other("Initialisierung"))
            }
            fn flush(&mut self) -> io::Result<()> {
                Ok(())
            }
        }
        assert!(TerminalGuard::enter(Broken).is_err());
        assert_restored();
    }
}
