use std::{io::ErrorKind, path::Path, time::Duration};

use serde::Serialize;
use serde_json::Value;
use tokio::{
    io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
    net::UnixStream,
    time::{sleep, Instant},
};

use plasmavmc_types::{Error, Result, VmState, VmStatus};

/// Minimal async QMP client for lifecycle operations.
pub struct QmpClient {
    reader: BufReader<tokio::net::unix::OwnedReadHalf>,
    writer: tokio::net::unix::OwnedWriteHalf,
}

impl QmpClient {
    /// Connect to a QMP Unix socket and negotiate capabilities.
    pub async fn connect(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref();
        let deadline = Instant::now() + Duration::from_secs(2);
        let mut last_error = None;

        loop {
            match UnixStream::connect(path).await {
                Ok(stream) => match Self::from_stream(stream).await {
                    Ok(client) => return Ok(client),
                    Err(e) => last_error = Some(e.to_string()),
                },
                Err(e) if is_transient_connect_error(&e) => {
                    last_error = Some(format!("Failed to connect QMP: {e}"));
                }
                Err(e) => {
                    return Err(Error::HypervisorError(format!(
                        "Failed to connect QMP: {e}"
                    )));
                }
            }

            if Instant::now() >= deadline {
                return Err(Error::HypervisorError(
                    last_error.unwrap_or_else(|| "Failed to connect QMP".to_string()),
                ));
            }

            sleep(Duration::from_millis(50)).await;
        }
    }

    async fn from_stream(stream: UnixStream) -> Result<Self> {
        let (read, write) = stream.into_split();
        let mut client = QmpClient {
            reader: BufReader::new(read),
            writer: write,
        };

        client.read_greeting().await?;
        // Negotiate capabilities per QMP handshake.
        client
            .command::<Value>("qmp_capabilities", None::<Value>)
            .await?;

        Ok(client)
    }

    /// Send an arbitrary QMP command with optional arguments.
    pub async fn command<A: Serialize>(&mut self, name: &str, args: Option<A>) -> Result<Value> {
        let mut payload = serde_json::json!({ "execute": name });
        if let Some(arguments) = args {
            payload["arguments"] = serde_json::to_value(arguments).map_err(|e| {
                Error::HypervisorError(format!("Failed to serialize QMP args: {e}"))
            })?;
        }

        let mut buf = serde_json::to_vec(&payload)
            .map_err(|e| Error::HypervisorError(format!("Failed to encode QMP command: {e}")))?;
        buf.push(b'\n');
        self.writer
            .write_all(&buf)
            .await
            .map_err(|e| Error::HypervisorError(format!("Failed to send QMP command: {e}")))?;
        self.writer
            .flush()
            .await
            .map_err(|e| Error::HypervisorError(format!("Failed to flush QMP command: {e}")))?;

        let response = self.read_message().await?;
        if let Some(error) = response.get("error") {
            let desc = error
                .get("desc")
                .and_then(Value::as_str)
                .unwrap_or("unknown error");
            return Err(Error::HypervisorError(format!(
                "QMP command {name} failed: {desc}"
            )));
        }

        response.get("return").cloned().ok_or_else(|| {
            Error::HypervisorError(format!("Unexpected QMP response for {name}: {}", response))
        })
    }

    /// Query VM status and map to VmStatus.
    pub async fn query_status(&mut self) -> Result<VmStatus> {
        let resp = self.command::<Value>("query-status", None::<Value>).await?;
        let status = resp
            .get("status")
            .and_then(Value::as_str)
            .unwrap_or("unknown");

        let mapped_state = match status {
            "running" => VmState::Running,
            // QEMU reports a freshly created `-S` guest as `prelaunch`.
            "prelaunch" | "paused" => VmState::Stopped,
            "shutdown" | "quit" => VmState::Stopped,
            "inmigrate" | "postmigrate" => VmState::Migrating,
            "watchdog" | "guest-panicked" | "internal-error" | "io-error" => VmState::Error,
            _ => VmState::Error,
        };

        Ok(VmStatus {
            actual_state: mapped_state,
            ..VmStatus::default()
        })
    }

    async fn read_greeting(&mut self) -> Result<()> {
        let greeting = self.read_message().await?;
        if greeting.get("QMP").is_some() {
            Ok(())
        } else {
            Err(Error::HypervisorError(format!(
                "Invalid QMP greeting: {greeting}"
            )))
        }
    }

    async fn read_message(&mut self) -> Result<Value> {
        loop {
            let mut line = String::new();
            let read = self
                .reader
                .read_line(&mut line)
                .await
                .map_err(|e| Error::HypervisorError(format!("Failed to read QMP: {e}")))?;
            if read == 0 {
                return Err(Error::HypervisorError("QMP connection closed".to_string()));
            }

            if line.trim().is_empty() {
                continue;
            }

            let value: Value = serde_json::from_str(&line).map_err(|e| {
                Error::HypervisorError(format!("Failed to parse QMP message: {e}: {line}"))
            })?;

            // Skip async events; return first reply.
            if value.get("event").is_some() {
                continue;
            }

            return Ok(value);
        }
    }
}

fn is_transient_connect_error(error: &std::io::Error) -> bool {
    matches!(
        error.kind(),
        ErrorKind::WouldBlock
            | ErrorKind::ConnectionRefused
            | ErrorKind::NotFound
            | ErrorKind::TimedOut
            | ErrorKind::Interrupted
            | ErrorKind::AddrNotAvailable
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use tokio::{
        io::BufReader,
        net::{UnixListener, UnixStream},
    };

    async fn spawn_qmp_server(socket_path: &Path) -> tokio::task::JoinHandle<()> {
        let listener = UnixListener::bind(socket_path).expect("bind qmp socket");
        let socket_path = socket_path.to_owned();
        tokio::spawn(async move {
            let (stream, _) = listener.accept().await.expect("accept connection");
            handle_qmp_session(stream).await;
            // Clean up socket file to avoid leaking between tests.
            let _ = std::fs::remove_file(&socket_path);
        })
    }

    async fn handle_qmp_session(stream: UnixStream) {
        let (read, mut write) = stream.into_split();
        // Send greeting first.
        let greeting =
            r#"{"QMP":{"version":{"qemu":{"major":8,"minor":0,"micro":0}},"capabilities":[]}}"#;
        write.write_all(greeting.as_bytes()).await.unwrap();
        write.write_all(b"\n").await.unwrap();
        write.flush().await.unwrap();

        let mut reader = BufReader::new(read);
        let mut line = String::new();

        // Expect qmp_capabilities.
        line.clear();
        reader.read_line(&mut line).await.unwrap();
        assert!(
            line.contains("qmp_capabilities"),
            "expected qmp_capabilities, got {line}"
        );
        write.write_all(br#"{"return":{}}"#).await.unwrap();
        write.write_all(b"\n").await.unwrap();
        write.flush().await.unwrap();

        // Next command (query-status) will be handled in tests.
        line.clear();
        reader.read_line(&mut line).await.unwrap();
        if line.contains("query-status") {
            write
                .write_all(br#"{"return":{"status":"running","running":true}}"#)
                .await
                .unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();
        } else {
            write
                .write_all(br#"{"error":{"class":"CommandNotFound","desc":"unexpected"}}"#)
                .await
                .unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();
        }
    }

    #[tokio::test]
    async fn qmp_client_performs_handshake_and_status() {
        let dir = tempfile::tempdir().unwrap();
        let socket_path = dir.path().join("qmp.sock");
        let server_handle = spawn_qmp_server(&socket_path).await;

        let mut client = QmpClient::connect(&socket_path).await.unwrap();
        let status = client.query_status().await.unwrap();
        assert_eq!(status.actual_state, VmState::Running);

        server_handle.await.unwrap();
    }

    #[tokio::test]
    async fn qmp_client_reports_command_error() {
        let dir = tempfile::tempdir().unwrap();
        let socket_path = dir.path().join("qmp.sock");

        // Server will only understand qmp_capabilities and then respond error for others.
        let listener = UnixListener::bind(&socket_path).unwrap();
        let server = tokio::spawn(async move {
            let (stream, _) = listener.accept().await.unwrap();
            let (read, mut write) = stream.into_split();
            let greeting =
                r#"{"QMP":{"version":{"qemu":{"major":8,"minor":0,"micro":0}},"capabilities":[]}}"#;
            write.write_all(greeting.as_bytes()).await.unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();

            let mut reader = BufReader::new(read);
            let mut line = String::new();
            reader.read_line(&mut line).await.unwrap(); // qmp_capabilities
            write.write_all(br#"{"return":{}}"#).await.unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();

            line.clear();
            reader.read_line(&mut line).await.unwrap(); // query-status
            write
                .write_all(br#"{"error":{"class":"GenericError","desc":"bad command"}}"#)
                .await
                .unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();
        });

        let mut client = QmpClient::connect(&socket_path).await.unwrap();
        let err = client.query_status().await.unwrap_err();
        let msg = format!("{err}");
        assert!(msg.contains("failed"), "unexpected error: {msg}");
        server.await.unwrap();
    }

    #[tokio::test]
    async fn qmp_client_maps_prelaunch_to_stopped() {
        let dir = tempfile::tempdir().unwrap();
        let socket_path = dir.path().join("qmp.sock");

        let listener = UnixListener::bind(&socket_path).unwrap();
        let server = tokio::spawn(async move {
            let (stream, _) = listener.accept().await.unwrap();
            let (read, mut write) = stream.into_split();
            let greeting =
                r#"{"QMP":{"version":{"qemu":{"major":8,"minor":0,"micro":0}},"capabilities":[]}}"#;
            write.write_all(greeting.as_bytes()).await.unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();

            let mut reader = BufReader::new(read);
            let mut line = String::new();
            reader.read_line(&mut line).await.unwrap(); // qmp_capabilities
            write.write_all(br#"{"return":{}}"#).await.unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();

            line.clear();
            reader.read_line(&mut line).await.unwrap(); // query-status
            write
                .write_all(br#"{"return":{"status":"prelaunch","running":false}}"#)
                .await
                .unwrap();
            write.write_all(b"\n").await.unwrap();
            write.flush().await.unwrap();
        });

        let mut client = QmpClient::connect(&socket_path).await.unwrap();
        let status = client.query_status().await.unwrap();
        assert_eq!(status.actual_state, VmState::Stopped);
        server.await.unwrap();
    }

    #[tokio::test]
    async fn qmp_client_retries_until_socket_is_ready() {
        let dir = tempfile::tempdir().unwrap();
        let socket_path = dir.path().join("qmp.sock");
        let delayed_socket = socket_path.clone();

        let server = tokio::spawn(async move {
            tokio::time::sleep(Duration::from_millis(150)).await;
            let listener = UnixListener::bind(&delayed_socket).unwrap();
            let (stream, _) = listener.accept().await.unwrap();
            handle_qmp_session(stream).await;
            let _ = std::fs::remove_file(&delayed_socket);
        });

        let mut client = QmpClient::connect(&socket_path).await.unwrap();
        let status = client.query_status().await.unwrap();
        assert_eq!(status.actual_state, VmState::Running);
        server.await.unwrap();
    }
}
