use anyhow::{Context, Result, bail};
use std::io::{Read, Write};
use std::time::Duration;

pub struct SerialConnection {
    inner: tokio::sync::Mutex<Option<SerialInner>>,
    port_path: String,
    baud_rate: u32,
}

struct SerialInner {
    port: Box<dyn serialport::SerialPort>,
}

impl SerialConnection {
    pub fn new(port_path: &str, baud_rate: u32) -> Self {
        Self {
            inner: tokio::sync::Mutex::new(None),
            port_path: port_path.to_string(),
            baud_rate,
        }
    }

    fn open_port(path: &str, baud_rate: u32) -> Result<Box<dyn serialport::SerialPort>> {
        let port = serialport::new(path, baud_rate)
            .data_bits(serialport::DataBits::Eight)
            .parity(serialport::Parity::None)
            .stop_bits(serialport::StopBits::One)
            .flow_control(serialport::FlowControl::None)
            .timeout(Duration::from_millis(100))
            .open()
            .with_context(|| format!("Failed to open serial port {path}"))?;
        Ok(port)
    }

    async fn ensure_connected(&self) -> Result<()> {
        let mut guard = self.inner.lock().await;
        if guard.is_none() {
            let path = self.port_path.clone();
            let baud = self.baud_rate;
            let port = tokio::task::spawn_blocking(move || {
                let mut port = Self::open_port(&path, baud)?;
                // Wait for GRBL to boot after DTR reset, then drain banner
                std::thread::sleep(Duration::from_secs(2));
                let mut buf = [0u8; 512];
                while port.read(&mut buf).unwrap_or(0) > 0 {}
                Ok::<_, anyhow::Error>(port)
            })
            .await??;
            *guard = Some(SerialInner { port });
        }
        Ok(())
    }

    pub async fn send_command(&self, cmd: &str, timeout: Duration) -> Result<String> {
        self.ensure_connected().await?;
        let mut guard = self.inner.lock().await;
        let inner = guard.as_mut().unwrap();

        let cmd_bytes = format!("{cmd}\n");
        let port = &mut inner.port;

        // Clone the port for spawn_blocking
        let mut port_clone = port.try_clone().context("Failed to clone serial port")?;
        let cmd_owned = cmd_bytes.clone();
        let timeout_owned = timeout;

        tokio::task::spawn_blocking(move || {
            // Flush any pending input
            let mut flush_buf = [0u8; 256];
            while port_clone.read(&mut flush_buf).unwrap_or(0) > 0 {}

            port_clone
                .write_all(cmd_owned.as_bytes())
                .context("Failed to write to serial port")?;
            port_clone.flush().context("Failed to flush serial port")?;

            read_until_ok_or_error(&mut port_clone, timeout_owned)
        })
        .await?
    }

    pub async fn send_realtime(&self, byte: u8) -> Result<String> {
        self.ensure_connected().await?;
        let mut guard = self.inner.lock().await;
        let inner = guard.as_mut().unwrap();

        let mut port_clone = inner
            .port
            .try_clone()
            .context("Failed to clone serial port")?;

        tokio::task::spawn_blocking(move || {
            // Flush any pending input (e.g. boot banner)
            let mut flush_buf = [0u8; 256];
            while port_clone.read(&mut flush_buf).unwrap_or(0) > 0 {}

            port_clone
                .write_all(&[byte])
                .context("Failed to write realtime command")?;

            // Read response with short timeout
            let mut response = String::new();
            let mut buf = [0u8; 256];
            let deadline = std::time::Instant::now() + Duration::from_secs(2);

            while std::time::Instant::now() < deadline {
                match port_clone.read(&mut buf) {
                    Ok(n) if n > 0 => {
                        let chunk = String::from_utf8_lossy(&buf[..n]);
                        response.push_str(&chunk);
                        // Status reports end with >
                        if response.contains('>') {
                            break;
                        }
                    }
                    _ => {
                        std::thread::sleep(Duration::from_millis(50));
                    }
                }
            }

            Ok(response)
        })
        .await?
    }

    pub async fn probe(&self) -> Result<String> {
        self.ensure_connected().await?;
        let mut guard = self.inner.lock().await;
        let inner = guard.as_mut().unwrap();

        let mut port_clone = inner
            .port
            .try_clone()
            .context("Failed to clone serial port")?;

        tokio::task::spawn_blocking(move || {
            let mut output = String::new();
            let mut buf = [0u8; 512];

            // Flush and read any existing data/banner
            std::thread::sleep(Duration::from_millis(200));
            while let Ok(n) = port_clone.read(&mut buf) {
                if n == 0 {
                    break;
                }
                output.push_str(&String::from_utf8_lossy(&buf[..n]));
            }

            // Send newline to wake up
            port_clone.write_all(b"\n")?;
            port_clone.flush()?;
            std::thread::sleep(Duration::from_millis(500));
            while let Ok(n) = port_clone.read(&mut buf) {
                if n == 0 {
                    break;
                }
                output.push_str(&String::from_utf8_lossy(&buf[..n]));
            }

            // Request build info
            port_clone.write_all(b"$I\n")?;
            port_clone.flush()?;
            std::thread::sleep(Duration::from_millis(500));
            while let Ok(n) = port_clone.read(&mut buf) {
                if n == 0 {
                    break;
                }
                output.push_str(&String::from_utf8_lossy(&buf[..n]));
            }

            Ok(output)
        })
        .await?
    }
}

fn read_until_ok_or_error(
    port: &mut Box<dyn serialport::SerialPort>,
    timeout: Duration,
) -> Result<String> {
    let mut response = String::new();
    let mut buf = [0u8; 256];
    let deadline = std::time::Instant::now() + timeout;

    loop {
        if std::time::Instant::now() > deadline {
            bail!("Timeout waiting for GRBL response. Received so far: {response}");
        }

        match port.read(&mut buf) {
            Ok(n) if n > 0 => {
                response.push_str(&String::from_utf8_lossy(&buf[..n]));

                // Check each line for ok or error
                for line in response.lines() {
                    let line = line.trim();
                    if line == "ok" {
                        return Ok(response);
                    }
                    if line.starts_with("error:") {
                        return Ok(response);
                    }
                    if line.starts_with("ALARM:") {
                        return Ok(response);
                    }
                }
            }
            _ => {
                std::thread::sleep(Duration::from_millis(50));
            }
        }
    }
}
