//! LS process discovery — finding, inspecting, and managing LS processes.

use super::{paths, MainLSConfig};
use crate::platform;
use crate::proto::wire::extract_proto_string;
use std::net::TcpListener;
use std::process::{Command, Stdio};
use tracing::info;

/// Discover only the extension_server_port and csrf_token from the running main LS.
///
/// This does NOT discover the HTTPS port — we don't need to talk to the real LS,
/// only steal its extension server connection info.
pub fn discover_main_ls_config() -> Result<MainLSConfig, String> {
    let pid = find_main_ls_pid()?;

    let cmdline = std::fs::read(format!("/proc/{pid}/cmdline"))
        .map_err(|e| format!("Can't read cmdline for PID {pid}: {e}"))?;
    let args: Vec<&[u8]> = cmdline.split(|&b| b == 0).collect();

    let mut csrf = String::new();
    let mut ext_port = String::new();

    for (i, arg) in args.iter().enumerate() {
        if let Ok(s) = std::str::from_utf8(arg) {
            match s {
                "--csrf_token" | "-csrf_token" => {
                    if let Some(next) = args.get(i + 1) {
                        if let Ok(val) = std::str::from_utf8(next) {
                            csrf = val.to_string();
                        }
                    }
                }
                "--extension_server_port" | "-extension_server_port" => {
                    if let Some(next) = args.get(i + 1) {
                        if let Ok(val) = std::str::from_utf8(next) {
                            ext_port = val.to_string();
                        }
                    }
                }
                _ => {}
            }
        }
    }

    if csrf.is_empty() {
        return Err("Could not find CSRF token from main LS".to_string());
    }
    if ext_port.is_empty() {
        return Err("Could not find extension_server_port from main LS".to_string());
    }

    info!(
        pid,
        ext_port,
        csrf_len = csrf.len(),
        "Discovered main LS config"
    );

    Ok(MainLSConfig {
        extension_server_port: ext_port,
        csrf,
    })
}

/// Find the PID of the main (real) LS process.
///
/// Checks `/proc/<pid>/exe` to ensure we find the actual LS binary,
/// not bash scripts that happen to mention `language_server_linux` in their args.
pub(super) fn find_main_ls_pid() -> Result<String, String> {
    let proc = std::path::Path::new("/proc");
    if !proc.exists() {
        return Err("No /proc filesystem".to_string());
    }

    let entries = std::fs::read_dir(proc).map_err(|e| format!("Cannot read /proc: {e}"))?;

    for entry in entries.flatten() {
        let name = entry.file_name();
        let name_str = name.to_string_lossy();
        // Only numeric dirs (PIDs)
        if !name_str.chars().all(|c| c.is_ascii_digit()) {
            continue;
        }
        let exe_link = entry.path().join("exe");
        if let Ok(target) = std::fs::read_link(&exe_link) {
            let target_str = target.to_string_lossy().to_string();
            let target_clean = target_str.trim_end_matches(" (deleted)");
            // Must be the actual LS binary, not a bash script
            if target_clean.contains("language_server_linux")
                || target_clean.contains("language_server_darwin")
                || target_clean.contains("language_server_windows")
                || target_clean.contains("antigravity-language-server")
            {
                return Ok(name_str.to_string());
            }
        }
    }

    Err("No main LS process found — Antigravity must be running".to_string())
}

/// Find a free TCP port by binding to port 0.
pub(super) fn find_free_port() -> Result<u16, String> {
    let listener =
        TcpListener::bind("127.0.0.1:0").map_err(|e| format!("Failed to bind for port: {e}"))?;
    listener
        .local_addr()
        .map(|a| a.port())
        .map_err(|e| format!("Failed to get port: {e}"))
}

/// Check if the dedicated LS system user exists (Linux only).
pub(super) fn has_ls_user() -> bool {
    platform::supports_uid_isolation()
}

/// Find the PID of a language_server process owned by a specific user.
///
/// Used to discover the actual LS process after sudo spawns it as a different user.
pub(super) fn find_ls_pid_for_user(user: &str) -> Result<u32, String> {
    let output = Command::new("pgrep")
        .args(["-u", user, "-f", "language_server"])
        .output()
        .map_err(|e| format!("pgrep failed: {e}"))?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    stdout
        .lines()
        .next()
        .and_then(|line| line.trim().parse::<u32>().ok())
        .ok_or_else(|| format!("No LS process found for user {user}"))
}

/// Kill any orphaned standalone LS processes from previous runs.
///
/// This handles the case where the proxy crashed or was killed without
/// properly cleaning up the sudo-spawned LS process.
///
/// Key insight: the sudoers rule allows running commands AS antigravity-ls
/// (`ALL=(antigravity-ls) NOPASSWD: ALL`). A process can send signals to
/// other processes with the same UID, so we run `kill` as antigravity-ls
/// rather than as root.
pub(super) fn cleanup_orphaned_ls() {
    if !has_ls_user() {
        return;
    }

    let ls_user = &paths().ls_user;

    // Find all LS processes owned by the LS user
    let output = Command::new("pgrep")
        .args(["-u", ls_user.as_str(), "-f", "language_server"])
        .output();

    let pids: Vec<u32> = match output {
        Ok(out) => String::from_utf8_lossy(&out.stdout)
            .lines()
            .filter_map(|l| l.trim().parse().ok())
            .collect(),
        Err(_) => return,
    };

    if pids.is_empty() {
        return;
    }

    info!(
        count = pids.len(),
        ?pids,
        "Cleaning up orphaned standalone LS processes"
    );

    // Kill each PID by running `kill` AS the antigravity-ls user.
    // This works because same-UID processes can signal each other,
    // and the sudoers rule allows ALL commands as antigravity-ls.
    for pid in &pids {
        let ok = 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 {
            info!(pid, "Killed orphaned LS process");
        } else {
            // Fallback: try as root (needs separate sudoers entry)
            let _ = Command::new("sudo")
                .args(["-n", "kill", "-TERM", &pid.to_string()])
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .status();
        }
    }

    // Wait for graceful exit
    std::thread::sleep(std::time::Duration::from_millis(500));

    // Force-kill any survivors
    let still_alive = Command::new("pgrep")
        .args(["-u", ls_user.as_str(), "-f", "language_server"])
        .output()
        .map(|o| !o.stdout.is_empty())
        .unwrap_or(false);

    if still_alive {
        info!("Orphaned LS still alive, force killing");
        for pid in &pids {
            let _ = Command::new("sudo")
                .args([
                    "-n",
                    "-u",
                    ls_user.as_str(),
                    "kill",
                    "-KILL",
                    &pid.to_string(),
                ])
                .stdout(Stdio::null())
                .stderr(Stdio::null())
                .status();
        }
        std::thread::sleep(std::time::Duration::from_millis(300));

        // Final check
        let still_alive = Command::new("pgrep")
            .args(["-u", ls_user.as_str(), "-f", "language_server"])
            .output()
            .map(|o| !o.stdout.is_empty())
            .unwrap_or(false);

        if still_alive {
            eprintln!("\n  \x1b[1;31m⚠ Cannot kill orphaned LS process\x1b[0m");
            eprintln!(
                "  Run: \x1b[1msudo pkill -u {} -f language_server\x1b[0m\n",
                ls_user
            );
        }
    } else {
        info!("Orphaned LS processes cleaned up");
    }
}

/// Read OAuth token state directly from Antigravity's state.vscdb.
///
/// The DB stores the exact Topic proto bytes under key `antigravityUnifiedStateSync.oauthToken`.
/// This includes access_token + refresh_token + expiry, allowing the LS to auto-refresh.
/// Returns (access_token, topic_proto_bytes) or None if unavailable.
pub(super) fn read_oauth_from_state_db() -> Option<(String, Vec<u8>)> {
    use base64::Engine;

    let db_path = paths().state_db_path;

    // Check the DB file exists
    if !std::path::Path::new(&db_path).exists() {
        return None;
    }

    // Read the Topic proto (base64-encoded in the DB)
    let output = std::process::Command::new("sqlite3")
        .args([
            &db_path,
            "SELECT value FROM ItemTable WHERE key='antigravityUnifiedStateSync.oauthToken'",
        ])
        .output()
        .ok()?;

    if !output.status.success() {
        return None;
    }

    let b64_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if b64_str.is_empty() {
        return None;
    }

    // Decode the base64 to get the raw Topic proto bytes
    let topic_bytes = base64::engine::general_purpose::STANDARD
        .decode(&b64_str)
        .ok()?;

    if topic_bytes.is_empty() {
        return None;
    }

    // Extract the access_token from the OAuthTokenInfo inside the Topic proto.
    // The inner value (Row.value) is also base64, containing a serialized OAuthTokenInfo.
    // For the access_token (used by GetSecretValue), we can read it from the authStatus.
    let access_token = read_access_token_from_auth_status(&db_path)
        .or_else(|| extract_access_token_from_topic(&topic_bytes))
        .unwrap_or_default();

    Some((access_token, topic_bytes))
}

/// Read the current access token from `antigravityAuthStatus` in state.vscdb.
/// This JSON object has an `apiKey` field with the latest access token.
fn read_access_token_from_auth_status(db_path: &str) -> Option<String> {
    let output = std::process::Command::new("sqlite3")
        .args([
            db_path,
            "SELECT value FROM ItemTable WHERE key='antigravityAuthStatus'",
        ])
        .output()
        .ok()?;

    if !output.status.success() {
        return None;
    }

    let json_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
    // Simple extraction: find "apiKey":"..." pattern
    let marker = "\"apiKey\":\"";
    let start = json_str.find(marker)? + marker.len();
    let end = json_str[start..].find('"')? + start;
    let api_key = &json_str[start..end];
    if api_key.starts_with("ya29.") {
        Some(api_key.to_string())
    } else {
        None
    }
}

/// Extract access_token from the Topic proto bytes by finding the inner
/// base64-encoded OAuthTokenInfo and decoding its first string field.
fn extract_access_token_from_topic(topic_bytes: &[u8]) -> Option<String> {
    use base64::Engine;

    let as_str = String::from_utf8_lossy(topic_bytes);
    for segment in as_str.split(|c: char| !c.is_alphanumeric() && c != '+' && c != '/' && c != '=')
    {
        if segment.len() > 50 {
            if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(segment) {
                // Use shared proto decoder
                if let Some(token) = extract_proto_string(&decoded, 1) {
                    if token.starts_with("ya29.") {
                        return Some(token);
                    }
                }
            }
        }
    }
    None
}
