use axum::{
    extract::{Multipart, Path, Query, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use chrono::Utc;
use std::fs;
use std::io::Write;
use uuid::Uuid;

use crate::types::*;
use crate::AppState;

const VALID_VERDICTS: &[&str] = &["pending", "success", "failed", "in_progress"];
const VALID_EVIDENCE: &[&str] = &["confirmed", "unverified", "hypothesis"];

fn is_valid_custom_type(s: &str) -> bool {
    !s.is_empty()
        && s.len() <= 30
        && s.chars()
            .next()
            .map(|c| c.is_ascii_lowercase())
            .unwrap_or(false)
        && s.chars()
            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route(
            "/api/v1/projects/:project_id/nodes",
            post(create_node).get(list_nodes),
        )
        .route(
            "/api/v1/projects/:project_id/nodes/:node_id",
            get(get_node).put(update_node).delete(delete_node),
        )
        .route(
            "/api/v1/projects/:project_id/nodes/:node_id/upload",
            post(upload_screenshot),
        )
        .route("/api/v1/projects/:project_id/nodes/search", get(search_nodes))
}

async fn create_node(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Json(payload): Json<CreateNodeRequest>,
) -> (StatusCode, Json<ApiResponse<Node>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };

    // Validate label
    if payload.label.trim().is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err("label cannot be empty")),
        );
    }
    // Validate node_type
    if let NodeType::Custom(ref s) = payload.node_type {
        if !is_valid_custom_type(s) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid node_type '{}', allowed: host, service, vulnerability, credential, finding, note, entry_point, attack_step, impact, objective, or custom [a-z][a-z0-9_]{{0,29}}", s
                ))),
            );
        }
    }
    // Validate properties is Object
    if !payload.properties.is_object() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err(
                "properties must be a JSON object, not array/string/null/number",
            )),
        );
    }

    // Rule 1: non-entry nodes must have `after` (no isolated nodes)
    let existing = match state.db.list_nodes(pid) {
        Ok(n) => n,
        Err(err) => return crate::db_error_response("nodes.create_node.list_nodes", "node", err),
    };
    if !existing.is_empty() && payload.after.is_none() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err(
                "孤立节点：项目已有节点，新节点必须通过 `after` 指定前驱节点并连线。用法: --after <NODE_ID> --edge-label \"描述\"",
            )),
        );
    }

    // Rule 2: content must use full URLs, not bare paths
    if let Some(content) = payload.properties.get("content").and_then(|v| v.as_str()) {
        for line in content.lines() {
            // Skip lines that already contain a full URL (scheme://host)
            if line.contains("://") {
                continue;
            }
            // Check each whitespace-separated token for bare paths
            for word in line.split_whitespace() {
                if word.starts_with('/')
                    && word.len() > 1
                    && word.chars().nth(1).map_or(false, |c| c.is_alphabetic())
                {
                    return (
                        StatusCode::BAD_REQUEST,
                        Json(ApiResponse::err(&format!(
                            "裸路径检测：content 中包含裸路径 '{}'，必须使用完整绝对 URL（如 http://host{}）",
                            word, word
                        ))),
                    );
                }
            }
        }
    }

    let node = Node {
        id: Uuid::new_v4(),
        project_id: pid,
        label: payload.label.clone(),
        node_type: payload.node_type.clone(),
        position: payload.position.clone(),
        properties: payload.properties.clone(),
        created_at: Utc::now(),
    };

    if let Err(err) = state.db.insert_node(&node) {
        return crate::db_error_response("nodes.insert_node", "node", err);
    }

    // Auto-create edge if `after` is specified
    if let Some(after_raw) = &payload.after {
        let after_id = match crate::resolve_node_id(state.db, pid, after_raw) {
            Ok(id) => id,
            Err(StatusCode::NOT_FOUND) => {
                return (
                    StatusCode::BAD_REQUEST,
                    Json(ApiResponse::err("after: node not found")),
                )
            }
            Err(StatusCode::BAD_REQUEST) => {
                return (
                    StatusCode::BAD_REQUEST,
                    Json(ApiResponse::err("after: ambiguous or too short node ID")),
                )
            }
            Err(e) => {
                return (
                    e,
                    Json(ApiResponse::err("failed to resolve predecessor node")),
                )
            }
        };
        let edge = Edge {
            id: Uuid::new_v4(),
            project_id: pid,
            source_node_id: after_id,
            target_node_id: node.id,
            label: payload.edge_label.clone(),
            created_at: Utc::now(),
        };
        if let Err(err) = state.db.insert_edge(&edge) {
            return crate::db_error_response("nodes.create_node.insert_after_edge", "edge", err);
        }
    }

    let _ = state.tx.send(format!("node_created:{pid}"));
    (StatusCode::CREATED, Json(ApiResponse::ok(node)))
}

#[derive(Debug, serde::Deserialize)]
struct NodeFilterParams {
    node_type: Option<String>,
    verdict: Option<String>,
    evidence: Option<String>,
}

async fn list_nodes(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Query(params): Query<NodeFilterParams>,
) -> Result<Json<ApiResponse<Vec<Node>>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nodes = state
        .db
        .list_nodes(pid)
        .map_err(|err| crate::db_error_status("nodes.list_nodes", "node", err))?;

    let filtered: Vec<Node> = nodes
        .into_iter()
        .filter(|node| {
            if let Some(ref t) = params.node_type {
                if node.node_type.to_string() != *t {
                    return false;
                }
            }
            if let Some(ref v) = params.verdict {
                if node
                    .properties
                    .get("verdict")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    != v.as_str()
                {
                    return false;
                }
            }
            if let Some(ref e) = params.evidence {
                if node
                    .properties
                    .get("evidence")
                    .and_then(|e| e.as_str())
                    .unwrap_or("")
                    != e.as_str()
                {
                    return false;
                }
            }
            true
        })
        .collect();

    Ok(Json(ApiResponse::ok(filtered)))
}

async fn get_node(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
) -> Result<Json<ApiResponse<Node>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nid = crate::parse_uuid(&node_id)?;
    let node = state
        .db
        .get_node(pid, nid)
        .map_err(|err| crate::db_error_status("nodes.get_node", "node", err))?
        .ok_or(StatusCode::NOT_FOUND)?;
    Ok(Json(ApiResponse::ok(node)))
}

async fn update_node(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
    Json(payload): Json<UpdateNodeRequest>,
) -> (StatusCode, Json<ApiResponse<Node>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };
    let nid = match crate::parse_uuid(&node_id) {
        Ok(id) => id,
        Err(s) => return (s, Json(ApiResponse::err("invalid node_id"))),
    };

    // Validate label if provided
    if let Some(ref label) = payload.label {
        if label.trim().is_empty() {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("label cannot be empty")),
            );
        }
    }
    // Validate node_type if provided
    if let Some(NodeType::Custom(ref s)) = payload.node_type {
        if !is_valid_custom_type(s) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid node_type '{}', allowed: host, service, vulnerability, credential, finding, note, entry_point, attack_step, impact, objective, or custom [a-z][a-z0-9_]{{0,29}}", s
                ))),
            );
        }
    }
    // Validate properties is Object if provided
    if let Some(ref props) = payload.properties {
        if !props.is_object() {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(
                    "properties must be a JSON object, not array/string/null/number",
                )),
            );
        }
    }
    // Validate verdict if provided
    if let Some(ref v) = payload.set_verdict {
        if !VALID_VERDICTS.contains(&v.as_str()) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid verdict '{}', allowed: {}",
                    v,
                    VALID_VERDICTS.join(", ")
                ))),
            );
        }
    }
    // Validate evidence if provided
    if let Some(ref e) = payload.set_evidence {
        if !VALID_EVIDENCE.contains(&e.as_str()) {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(&format!(
                    "invalid evidence '{}', allowed: {}",
                    e,
                    VALID_EVIDENCE.join(", ")
                ))),
            );
        }
    }

    let node = state.db.update_node(pid, nid, &payload);
    match node {
        Ok(Some(n)) => {
            let _ = state.tx.send(format!("node_updated:{pid}"));
            (StatusCode::OK, Json(ApiResponse::ok(n)))
        }
        Ok(None) => (
            StatusCode::NOT_FOUND,
            Json(ApiResponse::err("node not found")),
        ),
        Err(err) => crate::db_error_response("nodes.update_node", "node", err),
    }
}

async fn delete_node(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
    Query(params): Query<std::collections::HashMap<String, String>>,
) -> (StatusCode, Json<ApiResponse<()>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };
    let nid = match crate::parse_uuid(&node_id) {
        Ok(id) => id,
        Err(s) => return (s, Json(ApiResponse::err("invalid node_id"))),
    };

    let edge_count = match state.db.count_edges_for_node(pid, nid) {
        Ok(c) => c,
        Err(err) => return crate::db_error_response("nodes.count_edges_for_node", "edge", err),
    };

    let force = params.get("force").map(|v| v.as_str()) == Some("true");
    if edge_count > 0 && !force {
        return (
            StatusCode::CONFLICT,
            Json(ApiResponse::err(&format!(
                "Cannot delete node: {} edges connected. Delete edges first or use ?force=true",
                edge_count
            ))),
        );
    }

    match state.db.delete_node(pid, nid) {
        Ok(true) => {
            let _ = state.tx.send(format!("node_deleted:{pid}"));
            (StatusCode::OK, Json(ApiResponse::ok(())))
        }
        Ok(false) => (
            StatusCode::NOT_FOUND,
            Json(ApiResponse::err("node not found")),
        ),
        Err(err) => crate::db_error_response("nodes.delete_node", "node", err),
    }
}

// ── Screenshot upload ──

async fn upload_screenshot(
    State(state): State<AppState>,
    Path((project_id, node_id)): Path<(String, String)>,
    mut multipart: Multipart,
) -> Result<impl IntoResponse, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let nid = crate::parse_uuid(&node_id)?;

    // Ensure node exists
    state
        .db
        .get_node(pid, nid)
        .map_err(|err| crate::db_error_status("nodes.upload_screenshot.get_node", "node", err))?
        .ok_or(StatusCode::NOT_FOUND)?;

    // Create screenshots dir
    let dir = format!("screenshots/{pid}");
    fs::create_dir_all(&dir).map_err(|err| {
        let app_err = crate::AppError::internal(
            "filesystem.create_dir_failed",
            "nodes.upload_screenshot.create_dir",
            "Failed to create screenshot directory.",
            "创建截图目录失败。",
        )
        .with_entity("screenshot")
        .with_hint(format!("dir={dir}"))
        .with_source(err.to_string());
        let status = app_err.status;
        app_err.log();
        status
    })?;

    let mut filename = String::new();
    while let Ok(Some(field)) = multipart.next_field().await {
        if let Some(name) = field.file_name() {
            filename = format!("{dir}/{nid}_{}", name);
            let data = field.bytes().await.map_err(|_| StatusCode::BAD_REQUEST)?;
            let mut f = fs::File::create(&filename).map_err(|err| {
                let app_err = crate::AppError::internal(
                    "filesystem.create_file_failed",
                    "nodes.upload_screenshot.create_file",
                    "Failed to create screenshot file.",
                    "创建截图文件失败。",
                )
                .with_entity("screenshot")
                .with_hint(format!("file={filename}"))
                .with_source(err.to_string());
                let status = app_err.status;
                app_err.log();
                status
            })?;
            f.write_all(&data).map_err(|err| {
                let app_err = crate::AppError::internal(
                    "filesystem.write_file_failed",
                    "nodes.upload_screenshot.write_file",
                    "Failed to write screenshot file.",
                    "写入截图文件失败。",
                )
                .with_entity("screenshot")
                .with_hint(format!("file={filename}"))
                .with_source(err.to_string());
                let status = app_err.status;
                app_err.log();
                status
            })?;
        }
    }

    if filename.is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }

    Ok((
        StatusCode::CREATED,
        Json(ApiResponse::ok(serde_json::json!({
            "filename": filename
        }))),
    ))
}

async fn search_nodes(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Query(params): Query<std::collections::HashMap<String, String>>,
) -> (StatusCode, Json<ApiResponse<Vec<Node>>>) {
    let pid = match crate::resolve_project_id(&state.db, &project_id).await {
        Ok(p) => p,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid project_id")),
            )
        }
    };
    let query = params.get("q").cloned().unwrap_or_default();
    if query.trim().is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err("search query cannot be empty")),
        );
    }
    let nodes = match state.db.search_nodes(pid, &query) {
        Ok(n) => n,
        Err(err) => return crate::db_error_response("nodes.search_nodes", "node", err),
    };
    (StatusCode::OK, Json(ApiResponse::ok(nodes)))
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::middleware;
    use chrono::Utc;
    use tokio::sync::broadcast;
    use uuid::Uuid;

    async fn spawn_app() -> (String, &'static crate::db::Database) {
        let db_path =
            std::env::temp_dir().join(format!("theseus-nodes-test-{}.db", Uuid::new_v4()));
        let db = Box::leak(Box::new(crate::db::Database::open(&db_path).unwrap()));
        let (tx, _) = broadcast::channel(16);
        let state = crate::AppState {
            db,
            tx,
            config: crate::Config {
                api_key: None,
                deepseek_api_key: None,
                cors_origins: None,
                providers: std::collections::HashMap::new(),
                agent_workers: Vec::new(),
                default_provider: None,
            },
            api_key: None,
            http_client: reqwest::Client::new(),
                provider_registry: None,
        };
        let app = crate::routes::projects::router()
            .merge(router())
            .layer(middleware::from_fn(crate::json_error_middleware))
            .with_state(state);
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
        (format!("http://{addr}"), db)
    }

    fn seed_node(
        project_id: Uuid,
        label: &str,
        node_type: NodeType,
        props: serde_json::Value,
    ) -> Node {
        Node {
            id: Uuid::new_v4(),
            project_id,
            label: label.to_string(),
            node_type,
            position: None,
            properties: props,
            created_at: Utc::now(),
        }
    }

    async fn json(resp: reqwest::Response) -> serde_json::Value {
        resp.json::<serde_json::Value>().await.unwrap()
    }

    #[tokio::test]
    async fn node_crud_filters_and_errors() {
        let (base, db) = spawn_app().await;
        let client = reqwest::Client::new();

        let project = client
            .post(format!("{base}/api/v1/projects"))
            .json(&serde_json::json!({"name":"Nodes"}))
            .send()
            .await
            .unwrap();
        let project_body = json(project).await;
        let pid = project_body["data"]["id"].as_str().unwrap().to_string();
        let project_id = Uuid::parse_str(&pid).unwrap();

        let first = client
            .post(format!("{base}/api/v1/projects/{pid}/nodes"))
            .json(&serde_json::json!({
                "label":"Entry",
                "node_type":"entry_point",
                "properties":{"content":"https://target","verdict":"pending","evidence":"confirmed"}
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(first.status(), StatusCode::CREATED);
        let first_body = json(first).await;
        let first_id = first_body["data"]["id"].as_str().unwrap().to_string();

        let isolated = client
            .post(format!("{base}/api/v1/projects/{pid}/nodes"))
            .json(&serde_json::json!({
                "label":"Second",
                "node_type":"service",
                "properties":{}
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(isolated.status(), StatusCode::BAD_REQUEST);
        assert_eq!(json(isolated).await["success"], false);

        let invalid_props = client
            .post(format!("{base}/api/v1/projects/{pid}/nodes"))
            .json(&serde_json::json!({
                "label":"Bad Props",
                "node_type":"service",
                "after":first_id,
                "properties":[]
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(invalid_props.status(), StatusCode::BAD_REQUEST);

        let created = client
            .post(format!("{base}/api/v1/projects/{pid}/nodes"))
            .json(&serde_json::json!({
                "label":"Nacos",
                "node_type":"service",
                "after":first_id,
                "edge_label":"discovered",
                "properties":{"content":"http://target:8848","verdict":"success","evidence":"confirmed"}
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(created.status(), StatusCode::CREATED);
        let created_body = json(created).await;
        let node_id = created_body["data"]["id"].as_str().unwrap().to_string();

        let list = client
            .get(format!("{base}/api/v1/projects/{pid}/nodes?node_type=service&verdict=success&evidence=confirmed"))
            .send()
            .await
            .unwrap();
        assert_eq!(list.status(), StatusCode::OK);
        assert_eq!(json(list).await["data"].as_array().unwrap().len(), 1);

        let get = client
            .get(format!("{base}/api/v1/projects/{pid}/nodes/{node_id}"))
            .send()
            .await
            .unwrap();
        assert_eq!(get.status(), StatusCode::OK);
        assert_eq!(json(get).await["data"]["label"], "Nacos");

        let update = client
            .put(format!("{base}/api/v1/projects/{pid}/nodes/{node_id}"))
            .json(&serde_json::json!({
                "label":"Nacos 2.2.3",
                "set_verdict":"failed",
                "set_evidence":"hypothesis"
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(update.status(), StatusCode::OK);
        let update_body = json(update).await;
        assert_eq!(update_body["data"]["label"], "Nacos 2.2.3");
        assert_eq!(update_body["data"]["properties"]["verdict"], "failed");

        let bad_update = client
            .put(format!("{base}/api/v1/projects/{pid}/nodes/{node_id}"))
            .json(&serde_json::json!({"set_verdict":"boom"}))
            .send()
            .await
            .unwrap();
        assert_eq!(bad_update.status(), StatusCode::BAD_REQUEST);

        let search = client
            .get(format!("{base}/api/v1/projects/{pid}/nodes/search?q=nacos"))
            .send()
            .await
            .unwrap();
        assert_eq!(search.status(), StatusCode::OK);
        assert_eq!(json(search).await["data"].as_array().unwrap().len(), 1);

        let empty_search = client
            .get(format!("{base}/api/v1/projects/{pid}/nodes/search?q="))
            .send()
            .await
            .unwrap();
        assert_eq!(empty_search.status(), StatusCode::BAD_REQUEST);

        let seeded = seed_node(
            project_id,
            "Parent",
            NodeType::Service,
            serde_json::json!({}),
        );
        let child = seed_node(project_id, "Child", NodeType::Note, serde_json::json!({}));
        db.insert_node(&seeded).unwrap();
        db.insert_node(&child).unwrap();
        db.insert_edge(&Edge {
            id: Uuid::new_v4(),
            project_id,
            source_node_id: seeded.id,
            target_node_id: child.id,
            label: Some("link".into()),
            created_at: Utc::now(),
        })
        .unwrap();

        let conflict_delete = client
            .delete(format!("{base}/api/v1/projects/{pid}/nodes/{}", seeded.id))
            .send()
            .await
            .unwrap();
        assert_eq!(conflict_delete.status(), StatusCode::CONFLICT);

        let forced_delete = client
            .delete(format!(
                "{base}/api/v1/projects/{pid}/nodes/{}?force=true",
                seeded.id
            ))
            .send()
            .await
            .unwrap();
        assert_eq!(forced_delete.status(), StatusCode::OK);

        let missing = client
            .get(format!(
                "{base}/api/v1/projects/{pid}/nodes/{}",
                Uuid::new_v4()
            ))
            .send()
            .await
            .unwrap();
        assert_eq!(missing.status(), StatusCode::NOT_FOUND);
    }
}
