//! IPC client for communicating with the record-daemon.
//!
//! Connects to the daemon via named pipes on Windows or Unix sockets on Linux,
//! using the same JSON-over-newline-delimited protocol.

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Duration;

// ---------------------------------------------------------------------------
// Protocol types (mirror of record-daemon IPC protocol)
// ---------------------------------------------------------------------------

/// IPC message wrapper.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpcMessage {
    #[serde(rename = "type")]
    pub message_type: MessageType,
    pub id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub command: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub payload: Option<serde_json::Value>,
}

/// Message type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageType {
    Request,
    Response,
    Notification,
}

/// IPC response from the daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpcResponse {
    pub request_id: String,
    pub success: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

// ---------------------------------------------------------------------------
// IPC commands (camelCase to match daemon's serde rename)
// ---------------------------------------------------------------------------

/// IPC command names matching the daemon's `IpcCommand` enum.
pub mod commands {
    pub const GET_SETTINGS: &str = "getSettings";
    pub const UPDATE_SETTINGS: &str = "updateSettings";
    pub const RESET_SETTINGS: &str = "resetSettings";
    pub const GET_STATUS: &str = "getStatus";
    pub const GET_ENCODERS: &str = "getEncoders";
    pub const START_RECORDING: &str = "startRecording";
    pub const STOP_RECORDING: &str = "stopRecording";
    pub const SHUTDOWN: &str = "shutdown";
}

// ---------------------------------------------------------------------------
// Default socket path (same logic as daemon)
// ---------------------------------------------------------------------------

/// Default socket path for the IPC server.
#[cfg(target_os = "windows")]
pub fn default_socket_path() -> PathBuf {
    PathBuf::from(r"\\.\pipe\record-daemon")
}

/// Default socket path for the IPC server.
#[cfg(target_os = "linux")]
pub fn default_socket_path() -> PathBuf {
    std::env::var("XDG_RUNTIME_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from("/tmp"))
        .join("record-daemon.sock")
}

// ---------------------------------------------------------------------------
// IPC Client
// ---------------------------------------------------------------------------

/// IPC client for communicating with the record-daemon.
pub struct IpcClient {
    socket_path: PathBuf,
    timeout: Duration,
}

impl IpcClient {
    /// Create a new IPC client with the default socket path.
    pub fn new() -> Self {
        Self {
            socket_path: default_socket_path(),
            timeout: Duration::from_secs(5),
        }
    }

    /// Create a new IPC client with a custom socket path.
    #[allow(dead_code)]
    pub fn with_socket_path(socket_path: PathBuf) -> Self {
        Self {
            socket_path,
            timeout: Duration::from_secs(5),
        }
    }

    /// Send a command to the daemon and return the response.
    pub async fn send_command(
        &self,
        command: &str,
        payload: Option<serde_json::Value>,
    ) -> Result<IpcResponse, String> {
        let id = uuid::Uuid::new_v4().to_string();
        let message = IpcMessage {
            message_type: MessageType::Request,
            id: id.clone(),
            command: Some(command.to_string()),
            payload,
        };

        let message_json = serde_json::to_string(&message)
            .map_err(|e| format!("Failed to serialize message: {}", e))?;

        let response_str = self.send_raw(&message_json).await?;

        let response: IpcResponse = serde_json::from_str(&response_str)
            .map_err(|e| format!("Failed to parse response: {}", e))?;

        Ok(response)
    }

    /// Send a raw JSON string and read the response.
    #[cfg(target_os = "windows")]
    async fn send_raw(&self, message: &str) -> Result<String, String> {
        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
        use tokio::net::windows::named_pipe::ClientOptions;

        // Connect to the named pipe
        let pipe_name = self.socket_path.to_string_lossy().to_string();

        let client = tokio::time::timeout(self.timeout, async {
            // Try to connect - if the pipe is busy, retry a few times
            let mut attempts = 0;
            loop {
                match ClientOptions::new().open(&pipe_name) {
                    Ok(client) => return Ok(client),
                    Err(e) if e.raw_os_error() == Some(231) => {
                        // ERROR_PIPE_BUSY = 231
                        attempts += 1;
                        if attempts > 5 {
                            return Err(format!("Pipe busy after 5 attempts: {}", e));
                        }
                        tokio::time::sleep(Duration::from_millis(100)).await;
                    }
                    Err(e) => return Err(format!("Failed to connect to daemon: {}", e)),
                }
            }
        })
        .await
        .map_err(|_| "Connection to daemon timed out".to_string())??;

        let (reader, mut writer) = tokio::io::split(client);

        // Send message
        writer
            .write_all(message.as_bytes())
            .await
            .map_err(|e| format!("Failed to write to pipe: {}", e))?;
        writer
            .write_all(b"\n")
            .await
            .map_err(|e| format!("Failed to write newline: {}", e))?;
        writer
            .flush()
            .await
            .map_err(|e| format!("Failed to flush pipe: {}", e))?;

        // Read response
        let mut lines = BufReader::new(reader).lines();
        let line = tokio::time::timeout(self.timeout, lines.next_line())
            .await
            .map_err(|_| "Reading from daemon timed out".to_string())?
            .map_err(|e| format!("Failed to read from pipe: {}", e))?;

        match line {
            Some(line) => Ok(line),
            None => Err("Daemon closed connection without response".to_string()),
        }
    }

    /// Send a raw JSON string and read the response (Linux).
    #[cfg(target_os = "linux")]
    async fn send_raw(&self, message: &str) -> Result<String, String> {
        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
        use tokio::net::UnixStream;

        let stream = tokio::time::timeout(self.timeout, UnixStream::connect(&self.socket_path))
            .await
            .map_err(|_| "Connection to daemon timed out".to_string())?
            .map_err(|e| format!("Failed to connect to daemon: {}", e))?;

        let (reader, mut writer) = stream.into_split();

        // Send message
        writer
            .write_all(message.as_bytes())
            .await
            .map_err(|e| format!("Failed to write to socket: {}", e))?;
        writer
            .write_all(b"\n")
            .await
            .map_err(|e| format!("Failed to write newline: {}", e))?;
        writer
            .flush()
            .await
            .map_err(|e| format!("Failed to flush socket: {}", e))?;

        // Read response
        let mut lines = BufReader::new(reader).lines();
        let line = tokio::time::timeout(self.timeout, lines.next_line())
            .await
            .map_err(|_| "Reading from daemon timed out".to_string())?
            .map_err(|e| format!("Failed to read from socket: {}", e))?;

        match line {
            Some(line) => Ok(line),
            None => Err("Daemon closed connection without response".to_string()),
        }
    }

    /// Check if the daemon is reachable.
    pub async fn is_daemon_running(&self) -> bool {
        self.send_command(commands::GET_STATUS, None).await.is_ok()
    }
}

// ---------------------------------------------------------------------------
// Daemon binary discovery & process spawning
// ---------------------------------------------------------------------------

/// Find the record-daemon binary.
///
/// Search order:
/// 1. Next to the current executable (bundled / installed mode)
/// 2. In `<exe_dir>/../resources/` (macOS .app bundle)
/// 3. Relative to the executable – walk up to the workspace root and look for
///    `record-daemon/target/{release,debug}/` (development mode)
/// 4. Relative to the current working directory (fallback for dev mode)
fn find_daemon_binary() -> Option<PathBuf> {
    let exe_name = if cfg!(windows) {
        "record-daemon.exe"
    } else {
        "record-daemon"
    };

    if let Ok(exe_path) = std::env::current_exe() {
        // 1. Next to the current executable
        if let Some(exe_dir) = exe_path.parent() {
            let candidate = exe_dir.join(exe_name);
            if candidate.exists() {
                return Some(candidate);
            }
        }

        // 2. In <exe_dir>/../resources/
        if let Some(exe_dir) = exe_path.parent().and_then(|p| p.parent()) {
            let candidate = exe_dir.join("resources").join(exe_name);
            if candidate.exists() {
                return Some(candidate);
            }
        }

        // 3. Development mode – relative to the exe location.
        //    The exe is typically at <workspace>/tauri-app/src-tauri/target/debug/tauri-app.exe
        //    so we walk up to find <workspace>/record-daemon/target/{release,debug}/
        if let Some(exe_dir) = exe_path.parent() {
            // Try walking up from the exe directory to find the daemon
            let dev_suffixes = [
                Path::new("record-daemon/target/release"),
                Path::new("record-daemon/target/debug"),
            ];
            // Walk up at most 6 levels from the exe directory
            let mut dir = exe_dir.to_path_buf();
            for _ in 0..6 {
                for suffix in &dev_suffixes {
                    let candidate = dir.join(suffix).join(exe_name);
                    if candidate.exists() {
                        return Some(candidate);
                    }
                }
                if !dir.pop() {
                    break;
                }
            }
        }
    }

    // 4. Fallback – look relative to the current working directory
    let cwd_dev_paths = [
        Path::new("../../record-daemon/target/release"),
        Path::new("../../record-daemon/target/debug"),
        Path::new("../record-daemon/target/release"),
        Path::new("../record-daemon/target/debug"),
    ];
    for dir in &cwd_dev_paths {
        let candidate = dir.join(exe_name);
        if candidate.exists() {
            return Some(candidate);
        }
    }

    None
}

/// Get the log file path for the daemon.
///
/// Returns a path in the app's data directory.
fn get_log_file_path() -> Result<PathBuf, String> {
    let data_dir = directories::ProjectDirs::from("com", "leaguerecorder", "record-daemon")
        .ok_or_else(|| "Failed to get app data directory".to_string())?;
    
    let log_dir = data_dir.data_dir().join("logs");
    std::fs::create_dir_all(&log_dir)
        .map_err(|e| format!("Failed to create log directory: {}", e))?;
    
    Ok(log_dir.join("daemon.log"))
}

/// Spawn the record-daemon as a detached background process.
///
/// Returns `Ok(())` if the process was successfully spawned.
fn spawn_daemon() -> Result<(), String> {
    let binary = find_daemon_binary()
        .ok_or_else(|| "record-daemon binary not found".to_string())?;

    let log_file = get_log_file_path()?;
    eprintln!("[daemon] Spawning record-daemon: {}", binary.display());
    eprintln!("[daemon] Log file: {}", log_file.display());

    let mut cmd = std::process::Command::new(&binary);
    cmd.arg("--foreground");
    cmd.arg("--log-file").arg(&log_file);

    // Set the working directory to the daemon binary's directory so it can
    // find its bundled resources (OBS plugins, etc.) relative to itself.
    if let Some(bin_dir) = binary.parent() {
        cmd.current_dir(bin_dir);
    }

    // Detach from the parent process group so the daemon survives if the
    // Tauri app is restarted.
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        // CREATE_NEW_PROCESS_GROUP (0x200) | DETACHED_PROCESS (0x8)
        cmd.creation_flags(0x00000200 | 0x00000008);
    }

    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        // Start in a new session so the daemon gets its own process group
        cmd.process_group(0);
    }

    // Redirect stdio so the daemon doesn't inherit our handles
    cmd.stdin(std::process::Stdio::null())
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null());

    cmd.spawn()
        .map_err(|e| format!("Failed to spawn record-daemon: {}", e))?;

    Ok(())
}

/// Ensure the record-daemon is running.
///
/// If the daemon is already reachable via IPC, returns immediately.
/// Otherwise, spawns the daemon binary and waits up to `timeout_secs` seconds
/// for it to become ready.
pub async fn ensure_daemon_running(timeout_secs: u64) -> Result<bool, String> {
    let client = IpcClient::new();

    // Fast path – already running
    if client.is_daemon_running().await {
        return Ok(true);
    }

    // Spawn the daemon
    spawn_daemon()?;

    // Poll until the daemon responds or we time out
    let deadline = tokio::time::Instant::now() + Duration::from_secs(timeout_secs);
    loop {
        if client.is_daemon_running().await {
            return Ok(true);
        }
        if tokio::time::Instant::now() >= deadline {
            return Ok(false);
        }
        tokio::time::sleep(Duration::from_millis(500)).await;
    }
}

// ---------------------------------------------------------------------------
// Tauri commands
// ---------------------------------------------------------------------------

/// Check if the record-daemon is running.
#[tauri::command]
pub async fn daemon_is_running() -> bool {
    let client = IpcClient::new();
    client.is_daemon_running().await
}

/// Ensure the record-daemon is running, starting it if necessary.
///
/// Returns `true` if the daemon is running (either already was or just started),
/// `false` if it could not be started within the timeout.
#[tauri::command]
pub async fn daemon_ensure_running() -> Result<bool, String> {
    ensure_daemon_running(30).await
}

/// Get the daemon status.
#[tauri::command]
pub async fn daemon_get_status() -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::GET_STATUS, None)
        .await
        .map_err(|e| e)?;

    if response.success {
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Get the current daemon settings.
#[tauri::command]
pub async fn daemon_get_settings() -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::GET_SETTINGS, None)
        .await
        .map_err(|e| e)?;

    if response.success {
        // The daemon wraps settings in { settings: {...} }
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Update daemon settings.
#[tauri::command]
pub async fn daemon_update_settings(settings: serde_json::Value) -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::UPDATE_SETTINGS, Some(settings))
        .await
        .map_err(|e| e)?;

    if response.success {
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Reset daemon settings to defaults.
#[tauri::command]
pub async fn daemon_reset_settings() -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::RESET_SETTINGS, None)
        .await
        .map_err(|e| e)?;

    if response.success {
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Get available encoders from the daemon.
#[tauri::command]
pub async fn daemon_get_encoders() -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::GET_ENCODERS, None)
        .await
        .map_err(|e| e)?;

    if response.success {
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Start recording manually via the daemon.
#[tauri::command]
pub async fn daemon_start_recording() -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::START_RECORDING, None)
        .await
        .map_err(|e| e)?;

    if response.success {
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Stop recording via the daemon.
#[tauri::command]
pub async fn daemon_stop_recording() -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::STOP_RECORDING, None)
        .await
        .map_err(|e| e)?;

    if response.success {
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Shutdown the daemon.
#[tauri::command]
pub async fn daemon_shutdown() -> Result<serde_json::Value, String> {
    let client = IpcClient::new();
    let response = client
        .send_command(commands::SHUTDOWN, None)
        .await
        .map_err(|e| e)?;

    if response.success {
        Ok(response.data.unwrap_or(serde_json::Value::Null))
    } else {
        Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
    }
}

/// Get the daemon log file path.
#[tauri::command]
pub fn daemon_get_log_path() -> Result<String, String> {
    get_log_file_path()
        .map(|p| p.to_string_lossy().to_string())
}

/// Read the daemon log file.
///
/// Returns the last `lines` lines of the log file (default 100, max 1000).
#[tauri::command]
pub fn daemon_read_logs(lines: Option<usize>) -> Result<String, String> {
    let log_path = get_log_file_path()?;
    let max_lines = lines.unwrap_or(100).min(1000);
    
    let content = std::fs::read_to_string(&log_path)
        .map_err(|e| format!("Failed to read log file: {}", e))?;
    
    let lines: Vec<&str> = content.lines().rev().take(max_lines).collect();
    Ok(lines.into_iter().rev().collect::<Vec<_>>().join("\n"))
}
