use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::{post, put},
    Json, Router,
};

use crate::types::*;
use crate::AppState;
use uuid::Uuid;

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route(
            "/api/v1/projects/:project_id/edges",
            post(create_edge).get(list_edges),
        )
        .route(
            "/api/v1/projects/:project_id/edges/:edge_id",
            put(update_edge).delete(delete_edge),
        )
}

async fn create_edge(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Json(payload): Json<CreateEdgeRequest>,
) -> (StatusCode, Json<ApiResponse<Edge>>) {
    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")),
            )
        }
    };

    fn resolve_node(
        node_id: Option<Uuid>,
        label: Option<String>,
        param_name: &str,
        db: &crate::Database,
        pid: Uuid,
    ) -> Result<Uuid, (StatusCode, Json<ApiResponse<Edge>>)> {
        match (node_id, label) {
            (Some(id), _) => {
                let exists = db.node_exists(pid, id).map_err(|_| {
                    crate::error_response(
                        crate::AppError::internal(
                            "edges.resolve_node_exists_failed",
                            "edges.resolve_node",
                            "Failed to verify whether the referenced node exists.",
                            "检查引用节点是否存在时失败。",
                        )
                        .with_entity("node")
                        .with_hint(format!("param={param_name}, node_id={id}")),
                    )
                })?;
                if !exists {
                    return Err((
                        StatusCode::BAD_REQUEST,
                        Json(ApiResponse::err(format!(
                            "{param_name} node not found: {id}"
                        ))),
                    ));
                }
                Ok(id)
            }
            (None, Some(lbl)) => {
                let matches = db.find_nodes_by_label_fuzzy(pid, &lbl).map_err(|_| {
                    crate::error_response(
                        crate::AppError::internal(
                            "edges.resolve_node_lookup_failed",
                            "edges.resolve_node",
                            "Failed to look up nodes by label.",
                            "按标签查找节点时失败。",
                        )
                        .with_entity("node")
                        .with_hint(format!("param={param_name}, label={lbl}")),
                    )
                })?;
                match matches.len() {
                    0 => Err((
                        StatusCode::BAD_REQUEST,
                        Json(ApiResponse::err(format!(
                            "{param_name} label not found: {lbl}"
                        ))),
                    )),
                    1 => Ok(matches[0].id),
                    _ => {
                        let labels: Vec<String> = matches.iter().map(|n| n.label.clone()).collect();
                        Err((
                            StatusCode::BAD_REQUEST,
                            Json(ApiResponse::err(format!(
                                "{param_name} label ambiguous ({} matches): {}",
                                matches.len(),
                                labels.join(", ")
                            ))),
                        ))
                    }
                }
            }
            (None, None) => Err((
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err(format!(
                    "must provide {param_name}_node_id or {param_name}_label"
                ))),
            )),
        }
    }

    let source_id = match resolve_node(
        payload.source_node_id,
        payload.source_label,
        "source",
        state.db,
        pid,
    ) {
        Ok(id) => id,
        Err((code, json)) => return (code, json),
    };
    let target_id = match resolve_node(
        payload.target_node_id,
        payload.target_label,
        "target",
        state.db,
        pid,
    ) {
        Ok(id) => id,
        Err((code, json)) => return (code, json),
    };

    if source_id == target_id {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err(
                "Self-loop edges not allowed: source and target are the same node",
            )),
        );
    }

    if let Ok(true) = state.db.edge_exists(
        pid,
        source_id,
        target_id,
        payload.label.as_deref().unwrap_or(""),
    ) {
        return (
            StatusCode::CONFLICT,
            Json(ApiResponse::err(
                "Duplicate edge: this connection already exists",
            )),
        );
    }

    let edge = Edge {
        id: uuid::Uuid::new_v4(),
        project_id: pid,
        source_node_id: source_id,
        target_node_id: target_id,
        label: payload.label,
        created_at: chrono::Utc::now(),
    };
    if let Err(err) = state.db.insert_edge(&edge) {
        return crate::db_error_response("edges.insert_edge", "edge", err);
    }
    let _ = state.tx.send(format!("edge_created:{pid}"));
    (StatusCode::CREATED, Json(ApiResponse::ok(edge)))
}

async fn list_edges(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
) -> Result<Json<ApiResponse<Vec<Edge>>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let edges = state
        .db
        .list_edges(pid)
        .map_err(|err| crate::db_error_status("edges.list_edges", "edge", err))?;
    Ok(Json(ApiResponse::ok(edges)))
}

async fn delete_edge(
    State(state): State<AppState>,
    Path((project_id, edge_id)): Path<(String, String)>,
) -> Result<Json<ApiResponse<()>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let eid = crate::resolve_edge_id(state.db, pid, &edge_id)?;
    let deleted = state
        .db
        .delete_edge(pid, eid)
        .map_err(|err| crate::db_error_status("edges.delete_edge", "edge", err))?;
    if !deleted {
        return Err(StatusCode::NOT_FOUND);
    }
    let _ = state.tx.send(format!("edge_deleted:{pid}"));
    Ok(Json(ApiResponse::ok(())))
}

async fn update_edge(
    State(state): State<AppState>,
    Path((project_id, edge_id)): Path<(String, String)>,
    Json(payload): Json<EdgeUpdateRequest>,
) -> (StatusCode, Json<ApiResponse<Edge>>) {
    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 eid = match crate::resolve_edge_id(state.db, pid, &edge_id) {
        Ok(id) => id,
        Err(_) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(ApiResponse::err("invalid edge_id")),
            )
        }
    };

    let label = payload.label.as_deref().unwrap_or("");
    if label.trim().is_empty() {
        return (
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::err("Edge label cannot be empty")),
        );
    }

    let edge = match state.db.update_edge(pid, eid, label) {
        Ok(Some(edge)) => edge,
        Ok(None) => {
            return (
                StatusCode::NOT_FOUND,
                Json(ApiResponse::err(format!("Edge not found: {edge_id}"))),
            )
        }
        Err(err) => return crate::db_error_response("edges.update_edge", "edge", err),
    };
    let _ = state.tx.send(format!("edge_updated:{pid}"));
    (StatusCode::OK, Json(ApiResponse::ok(edge)))
}

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

    async fn spawn_app() -> (String, Uuid, Uuid, Uuid) {
        let db_path =
            std::env::temp_dir().join(format!("theseus-edges-test-{}.db", Uuid::new_v4()));
        let db = Box::leak(Box::new(crate::db::Database::open(&db_path).unwrap()));
        let pid = Uuid::new_v4();
        db.create_project(&CreateProjectRequest {
            name: "Edges".into(),
        })
        .unwrap();
        let project = db.list_projects().unwrap().into_iter().next().unwrap();
        let source = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "src".into(),
            node_type: NodeType::EntryPoint,
            position: None,
            properties: serde_json::json!({}),
            created_at: Utc::now(),
        };
        let target = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "dst".into(),
            node_type: NodeType::Service,
            position: None,
            properties: serde_json::json!({}),
            created_at: Utc::now(),
        };
        db.insert_node(&source).unwrap();
        db.insert_node(&target).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 = 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() });
        let _ = pid;
        (format!("http://{addr}"), project.id, source.id, target.id)
    }

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

    #[tokio::test]
    async fn edge_crud_and_error_statuses() {
        let (base, pid, source_id, target_id) = spawn_app().await;
        let client = reqwest::Client::new();

        let create = client
            .post(format!("{base}/api/v1/projects/{pid}/edges"))
            .json(&serde_json::json!({
                "source_node_id": source_id,
                "target_node_id": target_id,
                "label": "discovers"
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(create.status(), StatusCode::CREATED);
        let create_body = json(create).await;
        let edge_id = create_body["data"]["id"].as_str().unwrap().to_string();
        assert_eq!(create_body["data"]["label"], "discovers");

        let duplicate = client
            .post(format!("{base}/api/v1/projects/{pid}/edges"))
            .json(&serde_json::json!({
                "source_node_id": source_id,
                "target_node_id": target_id,
                "label": "discovers"
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(duplicate.status(), StatusCode::CONFLICT);

        let self_loop = client
            .post(format!("{base}/api/v1/projects/{pid}/edges"))
            .json(&serde_json::json!({
                "source_node_id": source_id,
                "target_node_id": source_id,
                "label": "loop"
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(self_loop.status(), StatusCode::BAD_REQUEST);

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

        let update = client
            .put(format!("{base}/api/v1/projects/{pid}/edges/{edge_id}"))
            .json(&serde_json::json!({"label":"pivots"}))
            .send()
            .await
            .unwrap();
        assert_eq!(update.status(), StatusCode::OK);
        assert_eq!(json(update).await["data"]["label"], "pivots");

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

        let delete = client
            .delete(format!("{base}/api/v1/projects/{pid}/edges/{edge_id}"))
            .send()
            .await
            .unwrap();
        assert_eq!(delete.status(), StatusCode::OK);
        assert_eq!(json(delete).await["success"], true);

        let missing = client
            .delete(format!("{base}/api/v1/projects/{pid}/edges/abcdef"))
            .send()
            .await
            .unwrap();
        assert!(matches!(
            missing.status(),
            StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND
        ));
    }
}
