//! Multi-container web pipeline demonstrating bridge networking and container linking.
//!
//! Spawns three containers on a bridge network:
//!
//! ```text
//! store (httpd :9000)  →  processor (wget → transform → httpd :8080)  →  frontend (wget, prints)
//! ```
//!
//! - **store**: Serves raw inventory data over HTTP 9000 via busybox `httpd`
//! - **processor**: Links to `store`, fetches via `wget`, transforms to HTML, serves via `httpd` on 8080
//! - **frontend**: Links to `processor`, fetches the HTML report via `wget`, prints it
//!
//! # Running
//!
//! Build the alpine rootfs first:
//! ```bash
//! ./build-rootfs-docker.sh    # or ./build-rootfs-tarball.sh
//! ```
//!
//! Then run (requires root):
//! ```bash
//! sudo -E cargo run --example web_pipeline
//! ```

use pelagos::container::{Command, Namespace, Stdio};
use pelagos::network::NetworkMode;
use std::env;

const ALPINE_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";

// ---------------------------------------------------------------------------
// Container state helpers — the library's `with_link()` resolves container
// names via `/run/pelagos/containers/{name}/state.json`.  The CLI writes these
// automatically, but since we're using the library API directly we need to
// create and clean up these files ourselves.
// ---------------------------------------------------------------------------

fn register_container(name: &str, pid: i32, ip: &str, rootfs: &str) {
    let dir = format!("/run/pelagos/containers/{}", name);
    std::fs::create_dir_all(&dir).expect("create container state dir");
    let state = serde_json::json!({
        "name": name,
        "rootfs": rootfs,
        "status": "running",
        "pid": pid,
        "watcher_pid": 0,
        "started_at": "2026-01-01T00:00:00Z",
        "exit_code": null,
        "command": ["/bin/sh"],
        "bridge_ip": ip,
    });
    let path = format!("{}/state.json", dir);
    std::fs::write(&path, serde_json::to_string_pretty(&state).unwrap())
        .expect("write container state");
}

fn unregister_container(name: &str) {
    let dir = format!("/run/pelagos/containers/{}", name);
    let _ = std::fs::remove_dir_all(&dir);
}

// ---------------------------------------------------------------------------
// Shell scripts — all networking uses busybox `nc` (no httpd in this rootfs)
// ---------------------------------------------------------------------------

/// Store: serve raw inventory data over HTTP 9000 via busybox httpd.
const STORE_SCRIPT: &str = r#"
mkdir -p /www
echo "apple=5,banana=12,cherry=3" > /www/index.html
exec httpd -f -p 9000 -h /www
"#;

/// Processor: fetch from store via wget, transform into an HTML report, serve on 8080.
const PROCESSOR_SCRIPT: &str = r#"
# Wait for store to be ready, then fetch
ATTEMPT=0
RAW=""
while [ $ATTEMPT -lt 10 ]; do
    ATTEMPT=$((ATTEMPT + 1))
    RAW=$(wget -qO- http://store:9000/ 2>/dev/null)
    [ -n "$RAW" ] && break
    sleep 1
done

if [ -z "$RAW" ]; then
    echo "[processor] ERROR: could not reach store" >&2
    exit 1
fi

# Transform "apple=5,banana=12,cherry=3" into an HTML report
ITEMS=$(echo "$RAW" | tr ',' '\n')
TOTAL=0
ROWS=""
for item in $ITEMS; do
    NAME=$(echo "$item" | cut -d= -f1)
    QTY=$(echo "$item" | cut -d= -f2)
    ROWS="${ROWS}<tr><td>${NAME}</td><td>${QTY}</td></tr>"
    TOTAL=$((TOTAL + QTY))
done

mkdir -p /www
cat > /www/index.html <<HTMLEOF
<!DOCTYPE html>
<html>
<head><title>Inventory Report</title></head>
<body>
<h1>Inventory Report</h1>
<p>Generated by the pelagos web pipeline</p>
<table border="1" cellpadding="4">
<tr><th>Item</th><th>Quantity</th></tr>
${ROWS}
<tr><td><strong>Total</strong></td><td><strong>${TOTAL}</strong></td></tr>
</table>
</body>
</html>
HTMLEOF

exec httpd -f -p 8080 -h /www
"#;

/// Frontend: fetch the HTML report from the processor via wget.
const FRONTEND_SCRIPT: &str = r#"
ATTEMPT=0
RESULT=""
while [ $ATTEMPT -lt 10 ]; do
    ATTEMPT=$((ATTEMPT + 1))
    RESULT=$(wget -qO- http://processor:8080/ 2>/dev/null)
    [ -n "$RESULT" ] && break
    sleep 1
done

if [ -n "$RESULT" ]; then
    echo "[frontend] Pipeline complete!"
    echo ""
    echo "$RESULT"
else
    echo "[frontend] ERROR: could not reach processor after 10 attempts"
    exit 1
fi
"#;

fn main() {
    env_logger::init();

    let current_dir = env::current_dir().expect("Failed to get current directory");
    let rootfs = current_dir.join("alpine-rootfs");

    if !rootfs.exists() {
        eprintln!("Error: alpine-rootfs not found!");
        eprintln!("Build it with: ./build-rootfs-docker.sh");
        eprintln!("Or without Docker: ./build-rootfs-tarball.sh");
        std::process::exit(1);
    }

    let rootfs_str = rootfs.to_str().unwrap();

    println!("=== Pelagos Multi-Container Web Pipeline ===\n");
    println!("Architecture:");
    println!("  store (httpd :9000) --> processor (httpd :8080) --> frontend (wget)\n");

    // ------------------------------------------------------------------
    // 1. Spawn the store container
    // ------------------------------------------------------------------
    println!("[main] Starting store container ...");

    let mut store = Command::new("/bin/sh")
        .args(&["-c", STORE_SCRIPT])
        .stdin(Stdio::Null)
        .stdout(Stdio::Null)
        .stderr(Stdio::Null)
        .with_chroot(&rootfs)
        .with_namespaces(Namespace::UTS | Namespace::MOUNT)
        .env("PATH", ALPINE_PATH)
        .with_proc_mount()
        .with_network(NetworkMode::Bridge)
        .with_tmpfs("/www", "size=1m")
        .spawn()
        .expect("Failed to spawn store container");

    let store_ip = store.container_ip().expect("store should have a bridge IP");
    let store_pid = store.pid() as i32;
    println!("[main] store running — PID {} — IP {}", store_pid, store_ip);

    register_container("store", store_pid, &store_ip, rootfs_str);

    // ------------------------------------------------------------------
    // 2. Spawn the processor container (links to store)
    // ------------------------------------------------------------------
    println!("[main] Starting processor container ...");

    let mut processor = Command::new("/bin/sh")
        .args(&["-c", PROCESSOR_SCRIPT])
        .stdin(Stdio::Null)
        .stdout(Stdio::Null)
        .stderr(Stdio::Null)
        .with_chroot(&rootfs)
        .with_namespaces(Namespace::UTS | Namespace::MOUNT)
        .env("PATH", ALPINE_PATH)
        .with_proc_mount()
        .with_network(NetworkMode::Bridge)
        .with_link("store")
        .with_tmpfs("/tmp", "size=1m")
        .with_tmpfs("/www", "size=1m")
        .spawn()
        .expect("Failed to spawn processor container");

    let proc_ip = processor
        .container_ip()
        .expect("processor should have a bridge IP");
    let proc_pid = processor.pid() as i32;
    println!(
        "[main] processor running — PID {} — IP {}",
        proc_pid, proc_ip
    );

    register_container("processor", proc_pid, &proc_ip, rootfs_str);

    // ------------------------------------------------------------------
    // 3. Spawn the frontend container (links to processor)
    // ------------------------------------------------------------------
    println!("[main] Starting frontend container ...\n");

    let mut frontend = Command::new("/bin/sh")
        .args(&["-c", FRONTEND_SCRIPT])
        .stdin(Stdio::Null)
        .stdout(Stdio::Piped)
        .stderr(Stdio::Piped)
        .with_chroot(&rootfs)
        .with_namespaces(Namespace::UTS | Namespace::MOUNT)
        .env("PATH", ALPINE_PATH)
        .with_proc_mount()
        .with_network(NetworkMode::Bridge)
        .with_link("processor")
        .with_tmpfs("/tmp", "size=1m")
        .spawn()
        .expect("Failed to spawn frontend container");

    let front_ip = frontend
        .container_ip()
        .expect("frontend should have a bridge IP");
    println!(
        "[main] frontend running — PID {} — IP {}\n",
        frontend.pid(),
        front_ip
    );

    // ------------------------------------------------------------------
    // 4. Wait for frontend to finish and collect output
    // ------------------------------------------------------------------
    let (front_status, front_stdout, front_stderr) = frontend
        .wait_with_output()
        .expect("Failed to wait for frontend");

    let stdout = String::from_utf8_lossy(&front_stdout);
    let stderr = String::from_utf8_lossy(&front_stderr);

    println!("--- frontend output ---");
    print!("{}", stdout);
    if !stderr.is_empty() {
        println!("--- frontend stderr ---");
        print!("{}", stderr);
    }
    println!("--- end ---\n");

    // ------------------------------------------------------------------
    // 5. Tear down processor and store
    // ------------------------------------------------------------------
    println!("[main] Stopping processor ...");
    unsafe {
        libc::kill(proc_pid, libc::SIGTERM);
    }
    let _ = processor.wait();

    println!("[main] Stopping store ...");
    unsafe {
        libc::kill(store_pid, libc::SIGTERM);
    }
    let _ = store.wait();

    // ------------------------------------------------------------------
    // 6. Clean up state files
    // ------------------------------------------------------------------
    unregister_container("store");
    unregister_container("processor");

    // ------------------------------------------------------------------
    // Summary
    // ------------------------------------------------------------------
    println!("\n=== Pipeline Complete ===\n");
    println!("What this demonstrated:");
    println!("  - Bridge networking (172.19.0.x/24) across 3 containers");
    println!("  - Container linking (name resolution via /etc/hosts)");
    println!("  - HTTP data flow: store -> processor (httpd on port 9000)");
    println!("  - HTTP data flow: processor -> frontend (httpd on port 8080)");
    println!("  - Data transformation: raw inventory CSV -> HTML report");
    println!("  - Automatic network cleanup on container exit");

    if front_status.success() {
        println!("\nAll containers exited successfully!");
    } else {
        println!("\nFrontend exited with code: {:?}", front_status.code());
    }
}
