//! WhatsApp Web gateway — embedded Node.js process management.
//!
//! Embeds the gateway JS at compile time, extracts it to `~/.openfang/whatsapp-gateway/`,
//! runs `npm install` if needed, and spawns `node index.js` as a managed child process
//! that auto-restarts on crash.

use crate::config::openfang_home;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{info, warn};

/// Gateway source files embedded at compile time.
const GATEWAY_INDEX_JS: &str =
    include_str!("../../../packages/whatsapp-gateway/index.js");
const GATEWAY_PACKAGE_JSON: &str =
    include_str!("../../../packages/whatsapp-gateway/package.json");

/// Default port for the WhatsApp Web gateway.
const DEFAULT_GATEWAY_PORT: u16 = 3009;

/// Maximum restart attempts before giving up.
const MAX_RESTARTS: u32 = 3;

/// Restart backoff delays in seconds: 5s, 10s, 20s.
const RESTART_DELAYS: [u64; 3] = [5, 10, 20];

/// Get the gateway installation directory.
fn gateway_dir() -> PathBuf {
    openfang_home().join("whatsapp-gateway")
}

/// Compute a simple hash of content for change detection.
fn content_hash(content: &str) -> String {
    // Use a simple FNV-style hash — no crypto needed, just change detection.
    let mut hash: u64 = 0xcbf29ce484222325;
    for byte in content.as_bytes() {
        hash ^= *byte as u64;
        hash = hash.wrapping_mul(0x100000001b3);
    }
    format!("{hash:016x}")
}

/// Write a file only if its content hash differs from the existing file.
/// Returns `true` if the file was written (content changed).
fn write_if_changed(path: &std::path::Path, content: &str) -> std::io::Result<bool> {
    let hash_path = path.with_extension("hash");
    let new_hash = content_hash(content);

    // Check existing hash
    if let Ok(existing_hash) = std::fs::read_to_string(&hash_path) {
        if existing_hash.trim() == new_hash {
            return Ok(false); // No change
        }
    }

    std::fs::write(path, content)?;
    std::fs::write(&hash_path, &new_hash)?;
    Ok(true)
}

/// Ensure the gateway files are extracted and npm dependencies installed.
///
/// Returns the gateway directory path on success, or an error message.
async fn ensure_gateway_installed() -> Result<PathBuf, String> {
    let dir = gateway_dir();
    std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create gateway dir: {e}"))?;

    let index_path = dir.join("index.js");
    let package_path = dir.join("package.json");

    // Write files only if content changed (avoids unnecessary npm install)
    let index_changed =
        write_if_changed(&index_path, GATEWAY_INDEX_JS).map_err(|e| format!("Write index.js: {e}"))?;
    let package_changed = write_if_changed(&package_path, GATEWAY_PACKAGE_JSON)
        .map_err(|e| format!("Write package.json: {e}"))?;

    let node_modules = dir.join("node_modules");
    let needs_install = !node_modules.exists() || package_changed;

    if needs_install {
        info!("Installing WhatsApp gateway npm dependencies...");

        // Determine npm command (npm.cmd on Windows, npm elsewhere)
        let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };

        let output = tokio::process::Command::new(npm_cmd)
            .arg("install")
            .arg("--production")
            .current_dir(&dir)
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::piped())
            .output()
            .await
            .map_err(|e| format!("npm install failed to start: {e}"))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(format!("npm install failed: {stderr}"));
        }

        info!("WhatsApp gateway npm dependencies installed");
    } else if index_changed {
        info!("WhatsApp gateway index.js updated (binary upgrade)");
    }

    Ok(dir)
}

/// Check if Node.js is available on the system.
async fn node_available() -> bool {
    let node_cmd = if cfg!(windows) { "node.exe" } else { "node" };
    tokio::process::Command::new(node_cmd)
        .arg("--version")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .await
        .map(|s| s.success())
        .unwrap_or(false)
}

/// Start the WhatsApp Web gateway as a managed child process.
///
/// This function:
/// 1. Checks if Node.js is available
/// 2. Extracts and installs the gateway files
/// 3. Spawns `node index.js` with appropriate env vars
/// 4. Sets `WHATSAPP_WEB_GATEWAY_URL` so the daemon finds it
/// 5. Monitors the process and restarts on crash (up to 3 times)
///
/// The PID is stored in the kernel's `whatsapp_gateway_pid` for shutdown cleanup.
pub async fn start_whatsapp_gateway(kernel: &Arc<super::kernel::OpenFangKernel>) {
    // Only start if WhatsApp is configured
    let wa_config = match &kernel.config.channels.whatsapp {
        Some(cfg) => cfg.clone(),
        None => return,
    };

    // Check for Node.js
    if !node_available().await {
        warn!(
            "WhatsApp Web gateway requires Node.js >= 18 but `node` was not found. \
             Install Node.js to enable WhatsApp Web integration."
        );
        return;
    }

    // Extract and install
    let gateway_path = match ensure_gateway_installed().await {
        Ok(p) => p,
        Err(e) => {
            warn!("WhatsApp Web gateway setup failed: {e}");
            return;
        }
    };

    let port = DEFAULT_GATEWAY_PORT;
    let api_listen = &kernel.config.api_listen;
    let openfang_url = format!("http://{api_listen}");
    let default_agent = wa_config
        .default_agent
        .as_deref()
        .unwrap_or("assistant")
        .to_string();

    // Auto-set the env var so the rest of the system finds the gateway
    std::env::set_var("WHATSAPP_WEB_GATEWAY_URL", format!("http://127.0.0.1:{port}"));
    info!("WHATSAPP_WEB_GATEWAY_URL set to http://127.0.0.1:{port}");

    // Spawn with crash monitoring
    let kernel_weak = Arc::downgrade(kernel);
    let gateway_pid = Arc::clone(&kernel.whatsapp_gateway_pid);

    tokio::spawn(async move {
        let mut restarts = 0u32;

        loop {
            let node_cmd = if cfg!(windows) { "node.exe" } else { "node" };

            info!("Starting WhatsApp Web gateway (attempt {})", restarts + 1);

            let child = tokio::process::Command::new(node_cmd)
                .arg("index.js")
                .current_dir(&gateway_path)
                .env("WHATSAPP_GATEWAY_PORT", port.to_string())
                .env("OPENFANG_URL", &openfang_url)
                .env("OPENFANG_DEFAULT_AGENT", &default_agent)
                .stdout(std::process::Stdio::inherit())
                .stderr(std::process::Stdio::inherit())
                .spawn();

            let mut child = match child {
                Ok(c) => c,
                Err(e) => {
                    warn!("Failed to spawn WhatsApp gateway: {e}");
                    return;
                }
            };

            // Store PID for shutdown cleanup
            if let Some(pid) = child.id() {
                if let Ok(mut guard) = gateway_pid.lock() {
                    *guard = Some(pid);
                }
                info!("WhatsApp Web gateway started (PID {pid})");
            }

            // Wait for process exit
            match child.wait().await {
                Ok(status) => {
                    // Clear stored PID
                    if let Ok(mut guard) = gateway_pid.lock() {
                        *guard = None;
                    }

                    // Check if kernel is still alive (not shutting down)
                    let kernel = match kernel_weak.upgrade() {
                        Some(k) => k,
                        None => {
                            info!("WhatsApp gateway exited (kernel dropped)");
                            return;
                        }
                    };

                    if kernel.supervisor.is_shutting_down() {
                        info!("WhatsApp gateway stopped (daemon shutting down)");
                        return;
                    }

                    if status.success() {
                        info!("WhatsApp gateway exited cleanly");
                        return;
                    }

                    warn!(
                        "WhatsApp gateway crashed (exit: {status}), restart {}/{MAX_RESTARTS}",
                        restarts + 1
                    );
                }
                Err(e) => {
                    if let Ok(mut guard) = gateway_pid.lock() {
                        *guard = None;
                    }
                    warn!("WhatsApp gateway wait error: {e}");
                }
            }

            restarts += 1;
            if restarts >= MAX_RESTARTS {
                warn!(
                    "WhatsApp gateway exceeded max restarts ({MAX_RESTARTS}), giving up"
                );
                return;
            }

            // Backoff before restart
            let delay = RESTART_DELAYS
                .get(restarts as usize - 1)
                .copied()
                .unwrap_or(20);
            info!("Restarting WhatsApp gateway in {delay}s...");
            tokio::time::sleep(std::time::Duration::from_secs(delay)).await;
        }
    });
}

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

    #[test]
    fn test_embedded_files_not_empty() {
        assert!(!GATEWAY_INDEX_JS.is_empty());
        assert!(!GATEWAY_PACKAGE_JSON.is_empty());
        assert!(GATEWAY_INDEX_JS.contains("WhatsApp"));
        assert!(GATEWAY_PACKAGE_JSON.contains("@openfang/whatsapp-gateway"));
    }

    #[test]
    fn test_content_hash_deterministic() {
        let h1 = content_hash("hello world");
        let h2 = content_hash("hello world");
        assert_eq!(h1, h2);
    }

    #[test]
    fn test_content_hash_changes_on_different_input() {
        let h1 = content_hash("version 1");
        let h2 = content_hash("version 2");
        assert_ne!(h1, h2);
    }

    #[test]
    fn test_gateway_dir_under_openfang_home() {
        let dir = gateway_dir();
        assert!(dir.ends_with("whatsapp-gateway"));
        assert!(dir
            .parent()
            .unwrap()
            .to_string_lossy()
            .contains(".openfang"));
    }

    #[test]
    fn test_write_if_changed_creates_new_file() {
        let tmp = std::env::temp_dir().join("openfang_test_gateway");
        let _ = std::fs::create_dir_all(&tmp);
        let path = tmp.join("test_write.js");
        let hash_path = path.with_extension("hash");

        // Clean up any previous runs
        let _ = std::fs::remove_file(&path);
        let _ = std::fs::remove_file(&hash_path);

        // First write should return true (new file)
        let changed = write_if_changed(&path, "console.log('v1')").unwrap();
        assert!(changed);
        assert!(path.exists());
        assert!(hash_path.exists());

        // Same content should return false
        let changed = write_if_changed(&path, "console.log('v1')").unwrap();
        assert!(!changed);

        // Different content should return true
        let changed = write_if_changed(&path, "console.log('v2')").unwrap();
        assert!(changed);

        // Clean up
        let _ = std::fs::remove_file(&path);
        let _ = std::fs::remove_file(&hash_path);
        let _ = std::fs::remove_dir(&tmp);
    }

    #[test]
    fn test_default_gateway_port() {
        assert_eq!(DEFAULT_GATEWAY_PORT, 3009);
    }

    #[test]
    fn test_restart_backoff_delays() {
        assert_eq!(RESTART_DELAYS, [5, 10, 20]);
        assert_eq!(MAX_RESTARTS, 3);
    }
}
