//! Native no-KVM backend experiments.
//!
//! This module starts with the process/descriptor model that the kbox Chromium
//! proof showed is mandatory: file-descriptor state is per process, while open
//! file descriptions and their host-backed shadows are shared by reference
//! across fork/clone, dup, dup2, and exec. Keeping this pure lets us test the
//! hardest invariants on every host before wiring syscall interception and LKL.

mod dispatch;
mod fd;
mod memory;
mod restore;
mod runtime;
mod shadow;
mod snapshot;
mod supervisor;
mod tracee;

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::collections::BTreeMap;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::path::Path;

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) use runtime::{
    restore_runtime_snapshot_with_replacements, KboxlikeRuntimeRestoreOptions,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) use supervisor::{
    capture_live_ptrace_rootfs_exec_snapshot_after_sigstop_request,
    kill_and_reap_live_process_group, KboxlikeRootfsExecConfig,
};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) use tracee::{LinuxStoppedTraceeReplacementFactory, PtraceDetachRestoredTraceeResumer};

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const KBOXLIVE_PROXY_ENV_KEYS: &[&str] = &[
    "HTTP_PROXY",
    "HTTPS_PROXY",
    "ALL_PROXY",
    "NO_PROXY",
    "http_proxy",
    "https_proxy",
    "all_proxy",
    "no_proxy",
    "npm_config_proxy",
    "npm_config_https_proxy",
    "npm_config_noproxy",
];
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const KBOXLIVE_PROXY_ENV_ALIASES: &[(&str, &str)] = &[
    ("HTTP_PROXY", "http_proxy"),
    ("HTTPS_PROXY", "https_proxy"),
    ("ALL_PROXY", "all_proxy"),
    ("NO_PROXY", "no_proxy"),
];

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const KBOXLIVE_GUEST_CA_BUNDLE: &str = "/etc/ssl/certs/ca-certificates.crt";
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const KBOXLIVE_GUEST_CA_SOURCE_DIRS: &[&str] = &[
    // Debian/Ubuntu/Alpine update-ca-certificates includes local *.crt files
    // from this directory when regenerating ca-certificates.crt.
    "/usr/local/share/ca-certificates",
    // RHEL/Fedora update-ca-trust consumes local anchors from here.
    "/etc/pki/ca-trust/source/anchors",
];
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const KBOXLIVE_CA_ENV_KEYS: &[&str] = &[
    "SSL_CERT_FILE",
    "CURL_CA_BUNDLE",
    "GIT_SSL_CAINFO",
    "REQUESTS_CA_BUNDLE",
    "NODE_EXTRA_CA_CERTS",
    "NPM_CONFIG_CAFILE",
    "npm_config_cafile",
];

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn prepare_rootfs_network_config(rootfs: &Path) -> std::io::Result<()> {
    if is_host_root(rootfs) {
        return Ok(());
    }
    let etc = rootfs.join("etc");
    std::fs::create_dir_all(&etc)?;
    if let Some(text) = host_resolv_conf_for_kboxlike() {
        atomic_write(&etc.join("resolv.conf"), text.as_bytes())?;
    }
    ensure_localhost_hosts_entry(&etc.join("hosts"))?;
    inject_host_ca_bundle(rootfs)?;
    write_apt_network_config(rootfs)?;
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn add_host_proxy_env_if_absent(env: &mut BTreeMap<String, String>) {
    for (upper, lower) in KBOXLIVE_PROXY_ENV_ALIASES {
        if let Some(value) = proxy_alias_value(env, upper, lower) {
            env.entry((*upper).to_owned())
                .or_insert_with(|| value.clone());
            env.entry((*lower).to_owned()).or_insert(value);
        }
    }
    for key in KBOXLIVE_PROXY_ENV_KEYS {
        if env.contains_key(*key) {
            continue;
        }
        if let Ok(value) = std::env::var(key) {
            if !value.is_empty() {
                env.insert((*key).to_owned(), value);
            }
        }
    }
    if has_host_ca_source() {
        for key in KBOXLIVE_CA_ENV_KEYS {
            env.entry((*key).to_owned())
                .or_insert_with(|| KBOXLIVE_GUEST_CA_BUNDLE.to_owned());
        }
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn proxy_alias_value(env: &BTreeMap<String, String>, upper: &str, lower: &str) -> Option<String> {
    env.get(lower)
        .filter(|value| !value.is_empty())
        .or_else(|| env.get(upper).filter(|value| !value.is_empty()))
        .cloned()
        .or_else(|| nonempty_env(lower))
        .or_else(|| nonempty_env(upper))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn merge_host_proxy_env_if_absent(env: &mut Vec<(String, String)>) {
    let mut merged = env.iter().cloned().collect::<BTreeMap<_, _>>();
    add_host_proxy_env_if_absent(&mut merged);
    *env = merged.into_iter().collect();
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn is_host_root(path: &Path) -> bool {
    path == Path::new("/")
        || path
            .canonicalize()
            .map(|canonical| canonical == Path::new("/"))
            .unwrap_or(false)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn host_resolv_conf_for_kboxlike() -> Option<String> {
    for path in ["/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"] {
        let Ok(text) = std::fs::read_to_string(path) else {
            continue;
        };
        if let Some(text) = sanitize_kboxlike_resolver(&text) {
            return Some(text);
        }
    }
    Some(
        "# generated by supermachine for kboxlike networking\nnameserver 1.1.1.1\noptions timeout:2 attempts:2 single-request no-aaaa\n"
            .to_owned(),
    )
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sanitize_kboxlike_resolver(text: &str) -> Option<String> {
    let nameserver = text.lines().find_map(|line| {
        let mut fields = line.split_whitespace();
        if !matches!(fields.next(), Some("nameserver")) {
            return None;
        }
        let addr = fields.next()?;
        if addr.eq_ignore_ascii_case("localhost") {
            return None;
        }
        Some(addr.to_owned())
    })?;

    Some(format!(
        "# generated by supermachine for kboxlike networking\nnameserver {nameserver}\noptions timeout:2 attempts:2 single-request no-aaaa\n"
    ))
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ensure_localhost_hosts_entry(path: &Path) -> std::io::Result<()> {
    let existing = std::fs::read_to_string(path).unwrap_or_default();
    if existing
        .lines()
        .any(|line| line.split_whitespace().any(|field| field == "localhost"))
    {
        return Ok(());
    }
    let mut next = existing;
    if !next.is_empty() && !next.ends_with('\n') {
        next.push('\n');
    }
    next.push_str("127.0.0.1\tlocalhost\n::1\tlocalhost\n");
    atomic_write(path, next.as_bytes())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn inject_host_ca_bundle(rootfs: &Path) -> std::io::Result<()> {
    let host_pems = collect_host_ca_pems()?;
    if host_pems.is_empty() {
        return Ok(());
    }

    install_host_ca_sources(rootfs, &host_pems)?;
    let guest_bundle = rootfs.join(KBOXLIVE_GUEST_CA_BUNDLE.trim_start_matches('/'));
    let existing = std::fs::read_to_string(&guest_bundle).unwrap_or_default();
    let mut merged = existing;
    for pem in host_pems {
        if !merged.contains(&pem) {
            if !merged.is_empty() && !merged.ends_with('\n') {
                merged.push('\n');
            }
            merged.push_str(&pem);
            if !merged.ends_with('\n') {
                merged.push('\n');
            }
        }
    }
    atomic_write(&guest_bundle, merged.as_bytes())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn install_host_ca_sources(rootfs: &Path, host_pems: &[String]) -> std::io::Result<()> {
    for dir in KBOXLIVE_GUEST_CA_SOURCE_DIRS {
        let target_dir = rootfs.join(dir.trim_start_matches('/'));
        std::fs::create_dir_all(&target_dir)?;
        for (idx, pem) in host_pems.iter().enumerate() {
            let path = target_dir.join(format!("supermachine-host-ca-{idx:03}.crt"));
            if std::fs::read_to_string(&path).ok().as_deref() == Some(pem.as_str()) {
                continue;
            }
            atomic_write(&path, pem.as_bytes())?;
        }
    }
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_apt_network_config(rootfs: &Path) -> std::io::Result<()> {
    let mut lines = Vec::new();
    if has_host_ca_source() {
        lines.push(format!(
            "Acquire::https::CaInfo \"{}\";",
            apt_conf_escape(KBOXLIVE_GUEST_CA_BUNDLE)
        ));
    }

    let no_proxy = nonempty_env("no_proxy").or_else(|| nonempty_env("NO_PROXY"));
    let no_proxy_all = no_proxy
        .as_deref()
        .map(no_proxy_matches_all)
        .unwrap_or(false);
    if !no_proxy_all {
        if let Some(proxy) = nonempty_env("http_proxy").or_else(|| nonempty_env("HTTP_PROXY")) {
            lines.push(format!(
                "Acquire::http::Proxy \"{}\";",
                apt_conf_escape(&proxy)
            ));
        }
        if let Some(proxy) = nonempty_env("https_proxy")
            .or_else(|| nonempty_env("HTTPS_PROXY"))
            .or_else(|| nonempty_env("http_proxy"))
            .or_else(|| nonempty_env("HTTP_PROXY"))
        {
            lines.push(format!(
                "Acquire::https::Proxy \"{}\";",
                apt_conf_escape(&proxy)
            ));
        }
        if let Some(no_proxy) = no_proxy {
            for host in apt_no_proxy_hosts(&no_proxy) {
                lines.push(format!(
                    "Acquire::http::Proxy::{} \"DIRECT\";",
                    apt_conf_escape(&host)
                ));
                lines.push(format!(
                    "Acquire::https::Proxy::{} \"DIRECT\";",
                    apt_conf_escape(&host)
                ));
            }
        }
    }

    let path = rootfs.join("etc/apt/apt.conf.d/99supermachine-network");
    if lines.is_empty() {
        let _ = std::fs::remove_file(path);
        return Ok(());
    }
    let mut text = String::from("// generated by supermachine for host proxy/CA parity\n");
    for line in lines {
        text.push_str(&line);
        text.push('\n');
    }
    atomic_write(&path, text.as_bytes())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn nonempty_env(key: &str) -> Option<String> {
    std::env::var(key).ok().filter(|value| !value.is_empty())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn no_proxy_matches_all(value: &str) -> bool {
    value.split(',').any(|part| part.trim() == "*")
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn apt_no_proxy_hosts(value: &str) -> Vec<String> {
    let mut hosts = Vec::new();
    for part in value.split(',') {
        let mut host = part.trim();
        if host.is_empty() || host == "*" {
            continue;
        }
        if let Some(stripped) = host.strip_prefix('.') {
            host = stripped;
        }
        if host.starts_with('[') {
            if let Some(end) = host.find(']') {
                host = &host[1..end];
            }
        } else if let Some((without_port, port)) = host.rsplit_once(':') {
            if !without_port.contains(':') && port.chars().all(|ch| ch.is_ascii_digit()) {
                host = without_port;
            }
        }
        if host.is_empty()
            || host.contains('/')
            || !host
                .chars()
                .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-'))
        {
            continue;
        }
        hosts.push(host.to_owned());
    }
    hosts.sort();
    hosts.dedup();
    hosts
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn apt_conf_escape(value: &str) -> String {
    value.replace('\\', "\\\\").replace('"', "\\\"")
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn has_host_ca_source() -> bool {
    host_ca_candidate_files()
        .into_iter()
        .any(|path| path.is_file())
        || std::env::var_os("SSL_CERT_DIR")
            .map(|dir| Path::new(&dir).is_dir())
            .unwrap_or(false)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn collect_host_ca_pems() -> std::io::Result<Vec<String>> {
    let mut out = Vec::new();
    for path in host_ca_candidate_files() {
        collect_ca_pems_from_file(&path, &mut out)?;
    }
    if let Some(dir) = std::env::var_os("SSL_CERT_DIR") {
        collect_ca_pems_from_dir(Path::new(&dir), &mut out)?;
    }
    out.sort();
    out.dedup();
    Ok(out)
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn host_ca_candidate_files() -> Vec<std::path::PathBuf> {
    let mut paths = Vec::new();
    if let Some(path) = std::env::var_os("SSL_CERT_FILE") {
        paths.push(path.into());
    }
    for path in [
        "/etc/ssl/certs/ca-certificates.crt",
        "/etc/pki/tls/certs/ca-bundle.crt",
        "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
        "/etc/ssl/cert.pem",
    ] {
        paths.push(path.into());
    }
    paths
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn collect_ca_pems_from_dir(path: &Path, out: &mut Vec<String>) -> std::io::Result<()> {
    let Ok(entries) = std::fs::read_dir(path) else {
        return Ok(());
    };
    for entry in entries {
        let entry = entry?;
        let file_type = entry.file_type()?;
        if file_type.is_file() || file_type.is_symlink() {
            collect_ca_pems_from_file(&entry.path(), out)?;
        }
    }
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn collect_ca_pems_from_file(path: &Path, out: &mut Vec<String>) -> std::io::Result<()> {
    let Ok(text) = std::fs::read_to_string(path) else {
        return Ok(());
    };
    out.extend(extract_pem_certificates(&text));
    Ok(())
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn extract_pem_certificates(text: &str) -> Vec<String> {
    const BEGIN: &str = "-----BEGIN CERTIFICATE-----";
    const END: &str = "-----END CERTIFICATE-----";

    let mut certs = Vec::new();
    let mut rest = text;
    while let Some(begin) = rest.find(BEGIN) {
        rest = &rest[begin..];
        let Some(end) = rest.find(END) else {
            break;
        };
        let pem_end = end + END.len();
        certs.push(format!("{}\n", rest[..pem_end].trim()));
        rest = &rest[pem_end..];
    }
    certs
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn atomic_write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
    let parent = path.parent().ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent")
    })?;
    std::fs::create_dir_all(parent)?;
    let tmp = parent.join(format!(
        ".{}.{}.tmp",
        path.file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("supermachine"),
        std::process::id()
    ));
    std::fs::write(&tmp, bytes)?;
    if let Err(err) = std::fs::rename(&tmp, path) {
        let _ = std::fs::remove_file(&tmp);
        return Err(err);
    }
    Ok(())
}

#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod tests {
    use std::collections::BTreeMap;

    #[test]
    fn kboxlike_resolver_keeps_docker_loopback_dns() {
        let out = super::sanitize_kboxlike_resolver("nameserver 127.0.0.11\noptions ndots:0\n")
            .expect("resolver");
        assert!(out.contains("nameserver 127.0.0.11"));
        assert!(out.contains("single-request no-aaaa"));
    }

    #[test]
    fn kboxlike_resolver_skips_localhost_name() {
        assert!(super::sanitize_kboxlike_resolver("nameserver localhost\n").is_none());
    }

    #[test]
    fn kboxlike_network_prep_skips_host_root() {
        assert!(super::is_host_root(std::path::Path::new("/")));
    }

    #[test]
    fn kboxlike_extracts_pem_certificates_from_bundle() {
        let certs = super::extract_pem_certificates(
            "noise\n-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\nnoise\n\
             -----BEGIN CERTIFICATE-----\ndef\n-----END CERTIFICATE-----",
        );
        assert_eq!(certs.len(), 2);
        assert!(certs[0].contains("abc"));
        assert!(certs[1].contains("def"));
    }

    #[test]
    fn kboxlike_installs_host_ca_sources_for_distro_regeneration() {
        let root = std::env::temp_dir().join(format!(
            "supermachine-kboxlike-ca-sources-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&root);
        let pem = "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n".to_string();

        super::install_host_ca_sources(&root, &[pem.clone()]).expect("install ca sources");

        for dir in super::KBOXLIVE_GUEST_CA_SOURCE_DIRS {
            let path = root
                .join(dir.trim_start_matches('/'))
                .join("supermachine-host-ca-000.crt");
            assert_eq!(std::fs::read_to_string(path).unwrap(), pem);
        }
        let _ = std::fs::remove_dir_all(root);
    }

    #[test]
    fn kboxlike_proxy_alias_uses_existing_uppercase_value() {
        let mut env = BTreeMap::new();
        env.insert(
            "HTTPS_PROXY".to_string(),
            "http://proxy.local:8080".to_string(),
        );
        super::add_host_proxy_env_if_absent(&mut env);
        assert_eq!(
            env.get("https_proxy").map(String::as_str),
            Some("http://proxy.local:8080")
        );
    }

    #[test]
    fn kboxlike_apt_no_proxy_hosts_normalize_common_forms() {
        assert_eq!(
            super::apt_no_proxy_hosts(
                "localhost,.example.com,registry.npmjs.org:443,10.0.0.0/8,[::1]"
            ),
            vec![
                "example.com".to_string(),
                "localhost".to_string(),
                "registry.npmjs.org".to_string()
            ]
        );
    }

    #[test]
    fn kboxlike_apt_conf_escape_quotes() {
        assert_eq!(
            super::apt_conf_escape("http://proxy/\"x\""),
            "http://proxy/\\\"x\\\""
        );
    }
}
