// 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)
//
// `r8r serve` — axum HTTP API + canvas SPA host. v0.1 ships a
// stub canvas (single HTML page with vanilla JS calling the API).
// The Leptos build replaces /index.html with a WASM bundle in a follow-up.

use 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");
use r8r::core::credentials::CredentialStore;
use r8r::core::types::NodeItem;
use r8r::core::workflow::Workflow;
use r8r::engine::{execute, Registry};
use serde_json::{json, Value};
use std::net::SocketAddr;
use std::path::PathBuf;

pub async fn run(port: u16, credentials_path: String) -> Result<()> {
    let creds_path = expand_tilde(&credentials_path);
    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/nodes/:slug", get(node_meta))
        .route("/api/run", post(move |body: String| run_workflow(body, creds_path.clone())));

    let addr = SocketAddr::from(([0, 0, 0, 0], port));
    let listener = tokio::net::TcpListener::bind(addr).await?;
    println!("r8r serve: http://{}", listener.local_addr()?);
    println!("  GET  /                 → Leptos canvas (drag-drop graph editor)");
    println!("  GET  /classic          → vanilla-JS fallback canvas");
    println!("  GET  /canvas/<asset>   → WASM/JS bundle assets");
    println!("  GET  /api/info         → version + node count");
    println!("  GET  /api/nodes        → list every registered node slug");
    println!("  GET  /api/nodes/:slug  → metadata for a single node");
    println!("  POST /api/run         ← workflow.toml body, returns run state JSON");
    axum::serve(listener, app).await?;
    Ok(())
}

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 — run <code>trunk build --release</code> in crates/r8r-canvas</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<Value> {
    let r = Registry::global();
    Json(json!({
        "version": env!("CARGO_PKG_VERSION"),
        "registered_nodes": r.count(),
    }))
}

async fn list_nodes() -> Json<Value> {
    let r = Registry::global();
    let names: Vec<&str> = r.names();
    let metas: Vec<Value> = names.iter().filter_map(|n| {
        r.lookup(n).map(|node| {
            let m = node.metadata();
            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(json!({ "count": metas.len(), "nodes": metas }))
}

async fn node_meta(Path(slug): Path<String>) -> Result<Json<Value>, (StatusCode, String)> {
    let r = Registry::global();
    match r.lookup(&slug) {
        Some(node) => Ok(Json(serde_json::to_value(node.metadata()).unwrap_or(Value::Null))),
        None => Err((StatusCode::NOT_FOUND, format!("unknown node `{}`", slug))),
    }
}

async fn run_workflow(toml_body: String, creds_path: PathBuf) -> impl IntoResponse {
    let wf = match Workflow::from_toml_str(&toml_body) {
        Ok(w) => w,
        Err(e) => return (StatusCode::BAD_REQUEST, Json(json!({ "error": e.to_string() }))),
    };
    let creds = if creds_path.exists() {
        CredentialStore::from_toml_file(&creds_path).unwrap_or_default()
    } else {
        CredentialStore::default()
    };
    match execute(&wf, &creds, vec![NodeItem::default()]).await {
        Ok(state) => {
            let runs: Value = state.run_data.iter().map(|(name, runs)| {
                let last = runs.last();
                (name.clone(), json!({
                    "status": last.map(|r| format!("{:?}", r.status)).unwrap_or_else(|| "Unknown".into()),
                    "items": last.map(|r| r.items.clone()).unwrap_or_default(),
                    "execution_time_ms": last.map(|r| r.execution_time_ms).unwrap_or(0),
                    "error": last.and_then(|r| r.error.clone()),
                }))
            }).collect::<serde_json::Map<_, _>>().into();
            (StatusCode::OK, Json(json!({
                "ok": state.error.is_none(),
                "error": state.error,
                "last_node": state.last_node_executed,
                "runs": runs,
            })))
        }
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))),
    }
}

fn expand_tilde(s: &str) -> PathBuf {
    if let Some(rest) = s.strip_prefix("~/") {
        if let Ok(home) = std::env::var("HOME") {
            return PathBuf::from(home).join(rest);
        }
    }
    PathBuf::from(s)
}
