//! netup — configuración de red mínima Rust-nativa para el userland de hammer (Etapa C).
//!
//! Reemplaza el `ip` de busybox + la IP estática hardcodeada del driver de rebuild por un binario
//! propio: levanta la interfaz, negocia un lease DHCPv4 y aplica IP/ruta/DNS vía netlink — todo
//! síncrono, sin tokio, dependiendo sólo de libc (ABI estable del kernel). Como hammerd/arje, es
//! código hammer-propio: no hay un cliente DHCP Rust "maduro" para adoptar al estilo ripgrep.
//!
//! Uso: `netup [interfaz]`  (sin argumento, autodetecta la primera interfaz no-loopback).

mod dhcp;
mod netlink;

use std::process::ExitCode;

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(e) => {
            eprintln!("netup: {e}");
            ExitCode::FAILURE
        }
    }
}

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let iface = match std::env::args().nth(1) {
        Some(a) => a,
        None => detect_iface().ok_or("no encuentro ninguna interfaz no-loopback en /sys/class/net")?,
    };
    let ifindex = read_sysfs_int(&iface, "ifindex")? as i32;
    let mac = read_mac(&iface)?;
    println!("netup: interfaz {iface} (index {ifindex}, mac {})", fmt_mac(mac));

    let mut nl = netlink::Netlink::open()?;
    nl.link_up(ifindex)?;
    // Esperar el carrier (virtio/e1000 lo levantan en ms, pero damos margen).
    wait_carrier(&iface, std::time::Duration::from_secs(5));

    let lease = dhcp::run(&iface, mac)?;
    println!(
        "netup: lease {} /{} gw {} dns {} ({}s) de {}",
        fmt_ip(lease.ip),
        lease.prefix,
        lease.router.map(fmt_ip).unwrap_or_else(|| "-".into()),
        if lease.dns.is_empty() { "-".into() } else { lease.dns.iter().map(|d| fmt_ip(*d)).collect::<Vec<_>>().join(",") },
        lease.lease_secs,
        fmt_ip(lease.server),
    );

    nl.add_addr(ifindex, lease.ip, lease.prefix)?;
    if let Some(gw) = lease.router {
        nl.add_default_route(ifindex, gw)?;
    }
    if !lease.dns.is_empty() {
        write_resolv_conf(&lease.dns)?;
    }
    println!("netup: red configurada en {iface}");
    Ok(())
}

/// Primera interfaz de /sys/class/net que no sea loopback (type 772 = ARPHRD_LOOPBACK).
fn detect_iface() -> Option<String> {
    let mut names: Vec<String> = std::fs::read_dir("/sys/class/net")
        .ok()?
        .filter_map(|e| e.ok())
        .map(|e| e.file_name().to_string_lossy().into_owned())
        .filter(|n| n != "lo")
        .filter(|n| read_sysfs_int(n, "type").map(|t| t != 772).unwrap_or(true))
        .collect();
    names.sort(); // determinismo (eth0 < eth1)
    names.into_iter().next()
}

fn read_sysfs_int(iface: &str, attr: &str) -> Result<i64, Box<dyn std::error::Error>> {
    let s = std::fs::read_to_string(format!("/sys/class/net/{iface}/{attr}"))?;
    Ok(s.trim().parse()?)
}

fn read_mac(iface: &str) -> Result<[u8; 6], Box<dyn std::error::Error>> {
    let s = std::fs::read_to_string(format!("/sys/class/net/{iface}/address"))?;
    let bytes: Vec<u8> = s
        .trim()
        .split(':')
        .filter_map(|h| u8::from_str_radix(h, 16).ok())
        .collect();
    if bytes.len() != 6 {
        return Err(format!("MAC inesperada para {iface}: {}", s.trim()).into());
    }
    Ok([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]])
}

/// Espera hasta `timeout` a que /sys/class/net/<if>/carrier sea 1 (link físico/virtio listo).
fn wait_carrier(iface: &str, timeout: std::time::Duration) {
    let start = std::time::Instant::now();
    while start.elapsed() < timeout {
        if let Ok(c) = std::fs::read_to_string(format!("/sys/class/net/{iface}/carrier")) {
            if c.trim() == "1" {
                return;
            }
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
}

fn write_resolv_conf(dns: &[[u8; 4]]) -> std::io::Result<()> {
    let body: String = dns.iter().map(|d| format!("nameserver {}\n", fmt_ip(*d))).collect();
    std::fs::write("/etc/resolv.conf", body)
}

fn fmt_ip(ip: [u8; 4]) -> String {
    format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
}

fn fmt_mac(m: [u8; 6]) -> String {
    format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5])
}
