//! StandaloneLS — process lifecycle (spawn, wait, kill).

use super::discovery::{
    cleanup_orphaned_ls, find_free_port, find_ls_pid_for_user, read_oauth_from_state_db,
};
use super::stub::stub_handle_connection;
use super::{build_dns_redirect_so, paths, MainLSConfig, StandaloneMitmConfig};
use crate::constants;
use crate::platform;
use crate::proto;
use std::io::Write;
use std::net::TcpListener;
use std::process::{Child, Command, Stdio};
use tokio::time::{sleep, Duration};
use tracing::{debug, info};

/// A running standalone LS process.
pub struct StandaloneLS {
    child: Child,
    /// The actual LS process PID (may differ from child PID when spawned via sudo).
    ls_pid: Option<u32>,
    /// Whether the LS was spawned via sudo (needs sudo kill).
    use_sudo: bool,
    /// Whether kill() has already been called.
    killed: bool,
    pub port: u16,
    pub csrf: String,
}

impl StandaloneLS {
    /// Spawn a standalone LS process.
    ///
    /// Discovers the main LS's extension server port and CSRF token,
    /// picks a free port, builds init metadata, and launches the binary.
    ///
    /// If `mitm_config` is provided, sets HTTPS_PROXY and SSL_CERT_FILE
    /// so the LS routes LLM API calls through the MITM proxy.
    pub fn spawn(
        main_config: &MainLSConfig,
        mitm_config: Option<&StandaloneMitmConfig>,
        headless: bool,
    ) -> Result<Self, String> {
        // Kill any orphaned LS processes from previous runs
        cleanup_orphaned_ls();
        let port = find_free_port()?;
        let lsp_port = find_free_port()?;
        let ts = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        // Build init metadata protobuf
        let api_key = format!("standalone-api-key-{ts}");
        let session_id = format!("standalone-session-{ts}");
        let metadata = proto::build_init_metadata(
            &api_key,
            constants::antigravity_version(),
            constants::client_version(),
            &session_id,
            1, // DETECT_AND_USE_PROXY_ENABLED
        );

        let p = paths();
        let data_dir = &p.data_dir;

        // Setup data dir (mode 1777 so both current user and zerogravity-ls can write)
        let gemini_dir = format!("{data_dir}/.gemini");
        let app_data_dir = format!("{data_dir}/.gemini/zerogravity-standalone");
        let annotations_dir = format!("{app_data_dir}/annotations");
        let brain_dir = format!("{app_data_dir}/brain");
        for dir in [
            data_dir.as_str(),
            &gemini_dir,
            &app_data_dir,
            &annotations_dir,
            &brain_dir,
        ] {
            let _ = std::fs::create_dir_all(dir);
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let _ = std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o1777));
            }
        }
        // Check if data dir is writable by writing a test file.
        // Old runs as `zerogravity-ls` user leave dirs owned by that user.
        let test_path = format!("{app_data_dir}/.write_test");
        if std::fs::write(&test_path, b"ok").is_err() {
            eprintln!(
                "\n  ⚠ Data dir {} is not writable (owned by another user from previous sudo run)\n  \
                 Fix with: sudo chmod -R a+rwX {}\n",
                app_data_dir, data_dir
            );
        } else {
            let _ = std::fs::remove_file(&test_path);
        }

        // Pre-seed user_settings.pb with detect_and_use_proxy = ENABLED.
        // The LS reads this at startup when creating its HTTP transport.
        // Without it, the LS ignores HTTPS_PROXY and API traffic bypasses MITM.
        // UserSettings proto: field 34 (varint) = 1 (DETECT_AND_USE_PROXY_ENABLED)
        //   Tag: (34 << 3) | 0 = 272 → varint [0x90, 0x02]
        //   Value: 1 → varint [0x01]
        let settings_path = format!("{app_data_dir}/user_settings.pb");
        let settings_bytes: &[u8] = &[0x90, 0x02, 0x01];
        if let Err(e) = std::fs::write(&settings_path, settings_bytes) {
            tracing::warn!("Failed to pre-seed user_settings.pb: {e}");
        } else {
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let _ = std::fs::set_permissions(
                    &settings_path,
                    std::fs::Permissions::from_mode(0o0666),
                );
            }
            tracing::info!("Pre-seeded user_settings.pb (detect_and_use_proxy=ENABLED)");
        }

        // In headless mode, spawn a stub TCP listener to serve as the extension server.
        // The LS connects to this port and calls LanguageServerStarted — without it,
        // the LS never fully initializes and won't accept connections on its server_port.
        let _stub_listener = if headless {
            let stub_port: u16 = main_config.extension_server_port.parse().unwrap_or(0);
            if stub_port == 0 {
                // Create a real listener so the LS can connect
                let listener = TcpListener::bind("127.0.0.1:0")
                    .map_err(|e| format!("Failed to bind stub extension server: {e}"))?;
                let actual_port = listener
                    .local_addr()
                    .map_err(|e| format!("Failed to get stub port: {e}"))?
                    .port();
                info!(
                    port = actual_port,
                    "Stub extension server listening (headless)"
                );
                // Read OAuth state from Antigravity's state.vscdb if available.
                // The DB stores the exact Topic proto (access_token + refresh_token + expiry)
                // which lets the LS auto-refresh tokens via its built-in Google OAuth2 client.
                let (oauth_token, oauth_topic_bytes) = read_oauth_from_state_db()
                    .map(|(token, topic)| {
                        info!("Loaded OAuth token from Antigravity state.vscdb");
                        (token, Some(topic))
                    })
                    .unwrap_or_else(|| {
                        // Fall back to env var / token file
                        let token = std::env::var("ZEROGRAVITY_TOKEN")
                            .ok()
                            .filter(|s| !s.is_empty())
                            .or_else(|| {
                                std::fs::read_to_string(&p.token_path)
                                    .ok()
                                    .map(|s| s.trim().to_string())
                                    .filter(|s| !s.is_empty())
                            })
                            .unwrap_or_default();
                        if !token.is_empty() {
                            info!("Loaded OAuth token from env/file (no refresh token — manual refresh needed)");
                        } else {
                            eprintln!("[headless] ⚠ No OAuth token found. Login to Antigravity first, or set ZEROGRAVITY_TOKEN");
                        }
                        (token, None)
                    });
                let oauth_arc = std::sync::Arc::new(oauth_token);
                let topic_arc = std::sync::Arc::new(oauth_topic_bytes);
                // Spawn a thread to accept connections (just hold them open)
                let listener_clone = listener
                    .try_clone()
                    .map_err(|e| format!("Failed to clone stub listener: {e}"))?;
                std::thread::spawn(move || {
                    for stream in listener_clone.incoming() {
                        match stream {
                            Ok(conn) => {
                                let token = std::sync::Arc::clone(&oauth_arc);
                                let topic = std::sync::Arc::clone(&topic_arc);
                                // Handle each connection in its own thread
                                std::thread::spawn(move || {
                                    stub_handle_connection(conn, &token, &topic);
                                });
                            }
                            Err(_) => break,
                        }
                    }
                });
                // Update the extension_server_port to the stub's port
                // (we need to use this in args below)
                Some((listener, actual_port))
            } else {
                None
            }
        } else {
            None
        };

        // Determine the actual extension_server_port to use
        let ext_port = if let Some((_, stub_port)) = &_stub_listener {
            stub_port.to_string()
        } else {
            main_config.extension_server_port.clone()
        };

        // LS args — NO -standalone flag (it disables TCP listeners entirely)
        // NOTE: do NOT use -random_port — it overrides -server_port and the LS
        // would listen on a random port we can't discover.
        let args = vec![
            "-enable_lsp".to_string(),
            format!("-lsp_port={}", lsp_port),
            "-extension_server_port".to_string(),
            ext_port,
            "-csrf_token".to_string(),
            main_config.csrf.clone(),
            "-server_port".to_string(),
            port.to_string(),
            "-workspace_id".to_string(),
            format!("standalone_{ts}"),
            "-cloud_code_endpoint".to_string(),
            // When MITM is active, append the MITM port to the endpoint URL.
            // The LS's CodeAssistClient ignores HTTPS_PROXY (hardcoded Proxy:nil),
            // so we redirect at the DNS+port level instead:
            //   1. LD_PRELOAD hooks getaddrinfo() → 127.0.0.1 for API domains
            //   2. Custom port in URL → LS connects to 127.0.0.1:MITM_PORT
            //   3. MITM proxy intercepts the transparent TLS connection via SNI
            if let Some(mitm) = mitm_config {
                // Extract port from proxy_addr (e.g. "http://127.0.0.1:8742" → "8742")
                let mitm_port = mitm.proxy_addr.rsplit(':').next().unwrap_or("8742");
                format!("https://daily-cloudcode-pa.googleapis.com:{mitm_port}")
            } else {
                "https://daily-cloudcode-pa.googleapis.com".to_string()
            },
            "-app_data_dir".to_string(),
            "zerogravity-standalone".to_string(),
            "-gemini_dir".to_string(),
            gemini_dir,
        ];

        info!(port, "Spawning standalone LS");
        debug!(?args, "LS args");

        // Build env vars for the LS process
        let mut env_vars: Vec<(String, String)> =
            vec![("ANTIGRAVITY_EDITOR_APP_ROOT".into(), p.app_root.clone())];

        // If MITM is enabled, add SSL + proxy env vars
        if let Some(mitm) = mitm_config {
            // Go's SSL_CERT_FILE replaces the entire system cert pool, so we
            // need a combined bundle: system CAs + our MITM CA
            // Write to /tmp — accessible by zerogravity-ls user
            // (user's ~/.config/ is not traversable by other UIDs)
            let combined_ca_path = format!("{}/mitm-ca.pem", data_dir);
            let system_ca = std::fs::read_to_string(&p.ca_cert_path).unwrap_or_default();
            let mitm_ca = std::fs::read_to_string(&mitm.ca_cert_path)
                .map_err(|e| format!("Failed to read MITM CA cert: {e}"))?;
            std::fs::write(&combined_ca_path, format!("{system_ca}\n{mitm_ca}"))
                .map_err(|e| format!("Failed to write combined CA bundle: {e}"))?;
            // Make readable by zerogravity-ls user
            #[cfg(unix)]
            {
                use std::os::unix::fs::PermissionsExt;
                let _ = std::fs::set_permissions(
                    &combined_ca_path,
                    std::fs::Permissions::from_mode(0o644),
                );
            }

            info!(
                proxy = %mitm.proxy_addr,
                ca = %combined_ca_path,
                "Setting MITM env vars on standalone LS (combined CA bundle)"
            );
            env_vars.push(("SSL_CERT_FILE".into(), combined_ca_path));
            env_vars.push(("SSL_CERT_DIR".into(), "/dev/null".into()));
            env_vars.push(("NODE_EXTRA_CA_CERTS".into(), mitm.ca_cert_path.clone()));
            // Only set HTTPS_PROXY when iptables UID isolation is NOT available
            // OR when running in headless mode (no sudo at all).
            // With iptables, all outbound traffic is transparently redirected at the
            // kernel level — setting HTTPS_PROXY on top causes double-proxying.
            if headless || !platform::supports_uid_isolation() {
                // proxy_addr already includes the scheme (e.g. "http://127.0.0.1:8742")
                env_vars.push(("HTTPS_PROXY".into(), mitm.proxy_addr.clone()));
                env_vars.push(("HTTP_PROXY".into(), mitm.proxy_addr.clone()));

                // LD_PRELOAD DNS redirect: hooks getaddrinfo() so Google API domains
                // resolve to 127.0.0.1. Combined with the port-modified endpoint URL,
                // this makes the LS connect to our MITM proxy for ALL API calls —
                // even the CodeAssistClient which has Proxy:nil hardcoded.
                let so_path = build_dns_redirect_so();
                if let Some(so) = so_path {
                    info!(path = %so, "Enabling LD_PRELOAD DNS redirect for headless MITM");
                    env_vars.push(("LD_PRELOAD".into(), so));
                    env_vars.push((
                        "DNS_REDIRECT_LOG".into(),
                        format!("{data_dir}/dns-redirect.log"),
                    ));
                }
            }
        }

        // In headless mode, never use sudo — run as current user
        // In normal mode, use sudo if 'zerogravity-ls' user exists
        let use_sudo = !headless && platform::supports_uid_isolation();
        let ls_binary = &p.ls_binary_path;
        let ls_user = &p.ls_user;

        let mut cmd = if use_sudo {
            info!("Using UID isolation: spawning LS as '{}' user", ls_user);
            let mut c = Command::new("sudo");
            c.args(["-n", "-u", ls_user.as_str(), "--", "/usr/bin/env"]);
            for (k, v) in &env_vars {
                c.arg(format!("{k}={v}"));
            }
            c.arg(ls_binary.as_str());
            c.args(&args);
            c
        } else {
            debug!("Spawning LS as current user");
            let mut c = Command::new(ls_binary.as_str());
            c.args(&args);
            for (k, v) in &env_vars {
                c.env(k, v);
            }
            c
        };

        // Capture stderr for debugging — logs to /tmp so we can diagnose LS failures
        let stderr_file = std::fs::File::create(format!("{data_dir}/ls-debug.log"))
            .map_err(|e| format!("Failed to create LS debug log: {e}"))?;
        cmd.stdin(Stdio::piped())
            .stdout(Stdio::null())
            .stderr(Stdio::from(stderr_file));

        let mut child = cmd
            .spawn()
            .map_err(|e| format!("Failed to spawn LS binary: {e}"))?;

        // Feed init metadata via stdin, then close it
        if let Some(mut stdin) = child.stdin.take() {
            stdin
                .write_all(&metadata)
                .map_err(|e| format!("Failed to write init metadata to stdin: {e}"))?;
            // stdin drops here → EOF (LS handles this fine in non-standalone mode)
        }

        info!(pid = child.id(), port, "Standalone LS spawned");

        // When spawned via sudo, the child is the sudo process which exits after
        // launching the LS as the target user. We need the actual LS PID for cleanup.
        let ls_pid = if use_sudo {
            // Give sudo a moment to spawn the real process
            std::thread::sleep(std::time::Duration::from_millis(500));
            // Find the LS process owned by zerogravity-ls user
            find_ls_pid_for_user(ls_user).ok()
        } else {
            Some(child.id())
        };

        if let Some(pid) = ls_pid {
            info!(
                ls_pid = pid,
                sudo = use_sudo,
                "Discovered actual LS process"
            );
        }

        Ok(StandaloneLS {
            child,
            ls_pid,
            use_sudo,
            killed: false,
            port,
            csrf: main_config.csrf.clone(),
        })
    }

    /// Wait for the standalone LS to be ready (accepting TCP connections).
    ///
    /// Retries up to `max_attempts` times with a 1-second delay between each.
    pub async fn wait_ready(&mut self, max_attempts: u32) -> Result<(), String> {
        info!(port = self.port, "Waiting for standalone LS to be ready...");

        for attempt in 1..=max_attempts {
            sleep(Duration::from_secs(1)).await;

            // Check if the process is still alive
            match self.child.try_wait() {
                Ok(Some(status)) => {
                    return Err(format!(
                        "Standalone LS exited prematurely with status: {status}"
                    ));
                }
                Ok(None) => {} // still running
                Err(e) => {
                    return Err(format!("Failed to check LS process status: {e}"));
                }
            }

            // Simple TCP connect check — if the LS is listening, it's ready
            match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", self.port)).await {
                Ok(_) => {
                    info!(attempt, "Standalone LS is ready (accepting connections)");
                    return Ok(());
                }
                Err(e) => {
                    debug!(attempt, error = %e, "LS not ready yet");
                }
            }
        }

        Err(format!(
            "Standalone LS failed to become ready after {max_attempts} attempts on port {}",
            self.port
        ))
    }

    /// Check if the child process is still running.
    #[allow(dead_code)]
    pub fn is_alive(&mut self) -> bool {
        matches!(self.child.try_wait(), Ok(None))
    }

    /// Kill the standalone LS process.
    pub fn kill(&mut self) {
        if self.killed {
            return;
        }
        self.killed = true;
        info!("Killing standalone LS");

        if self.use_sudo {
            // The child is sudo which already exited. Kill the actual LS.
            if let Some(pid) = self.ls_pid {
                let ls_user = &paths().ls_user;
                info!(pid, "Killing LS process via sudo -u {}", ls_user);
                // Run kill AS the zerogravity-ls user (same UID can signal)
                let ok = std::process::Command::new("sudo")
                    .args([
                        "-n",
                        "-u",
                        ls_user.as_str(),
                        "kill",
                        "-TERM",
                        &pid.to_string(),
                    ])
                    .stdout(Stdio::null())
                    .stderr(Stdio::null())
                    .status()
                    .map(|s| s.success())
                    .unwrap_or(false);

                if ok {
                    std::thread::sleep(std::time::Duration::from_millis(500));
                    // Force kill if still alive
                    let _ = std::process::Command::new("sudo")
                        .args([
                            "-n",
                            "-u",
                            ls_user.as_str(),
                            "kill",
                            "-KILL",
                            &pid.to_string(),
                        ])
                        .stdout(Stdio::null())
                        .stderr(Stdio::null())
                        .status();
                } else {
                    // Fallback: try with root sudo, then cleanup
                    info!("sudo -u kill failed, trying fallback cleanup");
                    cleanup_orphaned_ls();
                }
            } else {
                // No PID recorded, try blanket cleanup
                cleanup_orphaned_ls();
            }
        } else {
            let _ = self.child.kill();
            let _ = self.child.wait();
        }
    }
}

impl Drop for StandaloneLS {
    fn drop(&mut self) {
        self.kill();
    }
}
