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

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

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route(
            "/api/projects/:project_id/edges",
            post(create_edge).get(list_edges),
        )
        .route(
            "/api/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(|_| {
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        Json(ApiResponse::err("db error")),
                    )
                })?;
                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(|_| {
                    (
                        StatusCode::INTERNAL_SERVER_ERROR,
                        Json(ApiResponse::err("db error")),
                    )
                })?;
                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(_) = state.db.insert_edge(&edge) {
        return (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ApiResponse::err("db error")),
        );
    }
    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(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    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(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    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(_) => {
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ApiResponse::err("db error")),
            )
        }
    };
    let _ = state.tx.send(format!("edge_updated:{pid}"));
    (StatusCode::OK, Json(ApiResponse::ok(edge)))
}
