// r8r: rust port of n8n.io
// Initial author: Michael Cochran <mcochran@cochranblock.org>
// Built for: Tanner Woodrum, intended cofounder
// License: Unlicense (public domain dedication, see UNLICENSE)
//
// Stress test: boot r8r serve in-process, drive it with a real
// headless Chromium via exopack::devtools (chromiumoxide). Captures a
// screenshot of the canvas, scrapes console errors, hammers the API
// with N concurrent /api/run requests, and reports.

use r8r::core::credentials::CredentialStore;
use r8r::core::types::NodeItem;
use r8r::core::workflow::Workflow;
use r8r::engine::execute;
#[allow(unused_imports)]
use r8r::nodes as _;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let port = pick_port().unwrap_or(18888);
    let base = format!("http://127.0.0.1:{}", port);
    println!("[stress] booting r8r serve on {}", base);
    let server_handle = tokio::spawn(async move {
        let _ = boot_server(port).await;
    });
    tokio::time::sleep(std::time::Duration::from_secs(2)).await;

    println!("[stress] === phase 1: API smoke ===");
    let info: serde_json::Value = reqwest::get(format!("{}/api/info", base)).await.unwrap().json().await.unwrap();
    println!("  /api/info: {}", info);
    let nodes: serde_json::Value = reqwest::get(format!("{}/api/nodes", base)).await.unwrap().json().await.unwrap();
    let count = nodes.get("count").and_then(|v| v.as_u64()).unwrap_or(0);
    println!("  /api/nodes: {} nodes", count);

    println!("[stress] === phase 2: concurrent /api/run hammer ===");
    let trivial = std::fs::read_to_string("examples/trivial.toml").unwrap();
    let n_concurrent = 50;
    let started = std::time::Instant::now();
    let mut joins = Vec::with_capacity(n_concurrent);
    for _ in 0..n_concurrent {
        let base = base.clone();
        let body = trivial.clone();
        joins.push(tokio::spawn(async move {
            let r = reqwest::Client::new()
                .post(format!("{}/api/run", base))
                .header("content-type", "application/toml")
                .body(body)
                .send().await.ok()?;
            r.json::<serde_json::Value>().await.ok()
        }));
    }
    let mut ok_count = 0usize;
    let mut fail_count = 0usize;
    for j in joins {
        match j.await {
            Ok(Some(v)) if v.get("ok").and_then(|x| x.as_bool()) == Some(true) => ok_count += 1,
            _ => fail_count += 1,
        }
    }
    let elapsed = started.elapsed();
    println!("  {} workflow runs in {:?} ({} OK, {} FAIL, {:.1}/s)",
        n_concurrent, elapsed, ok_count, fail_count, n_concurrent as f64 / elapsed.as_secs_f64());

    println!("[stress] === phase 3: chromiumoxide visual ===");
    let out_dir = std::path::PathBuf::from("/tmp/r8r-stress");
    let pages: &[(&str, &str)] = &[
        ("canvas-leptos", "/"),
        ("canvas-classic", "/classic"),
    ];
    match exopack::devtools::f75(&base, pages, &out_dir).await {
        Ok(true) => println!("  ✓ screenshots written to {:?}", out_dir),
        Ok(false) => println!("  ⚠ screenshot capture had failures"),
        Err(e) => println!("  ✗ {}", e),
    }

    println!("[stress] === phase 4: console errors ===");
    let paths: &[&str] = &["/", "/classic"];
    match exopack::devtools::f74(&base, paths).await {
        Ok(errors) if errors.is_empty() => println!("  ✓ no console errors on canvas pages"),
        Ok(errors) => {
            println!("  ⚠ {} console errors:", errors.len());
            for e in errors.iter().take(20) { println!("     {}", e); }
        }
        Err(e) => println!("  ✗ devtools: {}", e),
    }

    println!("[stress] done. Server kept alive for inspection at {}", base);
    println!("  (kill with: pkill -f r8r-stress)");
    server_handle.abort();
    let _ = server_handle.await;
}

fn pick_port() -> Option<u16> {
    std::net::TcpListener::bind("127.0.0.1:0").ok()
        .and_then(|l| l.local_addr().ok().map(|a| a.port()))
}

async fn boot_server(port: u16) -> anyhow::Result<()> {
    use axum::body::Body;
    use axum::extract::Path;
    use axum::http::{header, HeaderValue, StatusCode};
    use axum::response::{Html, IntoResponse, Response};
    use axum::routing::{get, post};
    use axum::{Json, Router};
    use include_dir::{include_dir, Dir};

    static CANVAS_DIST: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/static-canvas");

    async fn canvas_index() -> Response {
        match CANVAS_DIST.get_file("index.html") {
            Some(f) => {
                let mut resp = Response::new(Body::from(f.contents()));
                resp.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8"));
                resp
            }
            None => Html("<p>canvas not built</p>").into_response(),
        }
    }
    async fn classic_index() -> Html<&'static str> { Html(include_str!("../../static/index.html")) }
    async fn canvas_asset(Path(path): Path<String>) -> Response {
        match CANVAS_DIST.get_file(&path) {
            Some(f) => {
                let mime = mime_guess::from_path(&path).first_or_octet_stream();
                let mut resp = Response::new(Body::from(f.contents()));
                resp.headers_mut().insert(
                    header::CONTENT_TYPE,
                    HeaderValue::from_str(mime.as_ref()).unwrap_or(HeaderValue::from_static("application/octet-stream")),
                );
                resp
            }
            None => (StatusCode::NOT_FOUND, format!("not found: {}", path)).into_response(),
        }
    }
    async fn info_handler() -> Json<serde_json::Value> {
        let r = r8r::engine::Registry::global();
        Json(serde_json::json!({
            "version": env!("CARGO_PKG_VERSION"),
            "registered_nodes": r.count(),
        }))
    }
    async fn list_nodes() -> Json<serde_json::Value> {
        let r = r8r::engine::Registry::global();
        let names: Vec<&str> = r.names();
        let metas: Vec<serde_json::Value> = names.iter().filter_map(|n| {
            r.lookup(n).map(|node| {
                let m = node.metadata();
                serde_json::json!({
                    "slug": m.name,
                    "display_name": m.display_name,
                    "group": m.group,
                    "description": m.description,
                    "inputs": m.inputs,
                    "outputs": m.outputs,
                    "version": m.version,
                })
            })
        }).collect();
        Json(serde_json::json!({ "count": metas.len(), "nodes": metas }))
    }
    async fn run_workflow(toml_body: String) -> impl IntoResponse {
        let wf = match Workflow::from_toml_str(&toml_body) {
            Ok(w) => w,
            Err(e) => return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": e.to_string() }))),
        };
        let creds = CredentialStore::default();
        match execute(&wf, &creds, vec![NodeItem::default()]).await {
            Ok(state) => (StatusCode::OK, Json(serde_json::json!({
                "ok": state.error.is_none(),
                "error": state.error,
                "last_node": state.last_node_executed,
                "nodes_run": state.run_data.len(),
            }))),
            Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))),
        }
    }

    let app = Router::new()
        .route("/", get(canvas_index))
        .route("/classic", get(classic_index))
        .route("/favicon.ico", get(|| async { StatusCode::NO_CONTENT }))
        .route("/canvas/*path", get(canvas_asset))
        .route("/api/info", get(info_handler))
        .route("/api/nodes", get(list_nodes))
        .route("/api/run", post(run_workflow));
    let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await?;
    axum::serve(listener, app).await?;
    Ok(())
}
