leankg 0.19.10

Lightweight Knowledge Graph for AI-Assisted Development
use axum::{
    extract::{Query, State},
    Json,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::api::{ApiResponse, ApiState};

#[derive(Serialize)]
pub struct StatusData {
    pub elements: usize,
    pub relationships: usize,
    pub annotations: usize,
    pub files: usize,
    pub functions: usize,
    pub classes: usize,
    pub database: String,
}

pub async fn health() -> Json<ApiResponse<crate::api::HealthResponse>> {
    Json(ApiResponse::success(crate::api::HealthResponse {
        status: "ok".to_string(),
        version: env!("CARGO_PKG_VERSION").to_string(),
    }))
}

pub async fn api_status(
    State(state): State<ApiState>,
) -> Result<Json<ApiResponse<StatusData>>, &'static str> {
    let mut element_count = 0usize;
    let mut relationship_count = 0usize;
    let mut annotation_count = 0usize;
    let mut files_count = 0usize;
    let mut functions_count = 0usize;
    let mut classes_count = 0usize;

    if let Ok(graph) = state.get_graph_engine().await {
        if let Ok(elements) = graph.all_elements() {
            element_count = elements.len();
            let unique_files: std::collections::HashSet<_> =
                elements.iter().map(|e| e.file_path.clone()).collect();
            files_count = unique_files.len();
            functions_count = elements
                .iter()
                .filter(|x| x.element_type == "function")
                .count();
            classes_count = elements
                .iter()
                .filter(|x| x.element_type == "class" || x.element_type == "struct")
                .count();
        }
        if let Ok(relns) = graph.all_relationships() {
            relationship_count = relns.len();
        }
        if let Ok(anns) = graph.all_annotations() {
            annotation_count = anns.len();
        }
    }

    Ok(Json(ApiResponse::success(StatusData {
        elements: element_count,
        relationships: relationship_count,
        annotations: annotation_count,
        files: files_count,
        functions: functions_count,
        classes: classes_count,
        database: state.db_path.to_string_lossy().to_string(),
    })))
}

#[derive(Deserialize)]
pub struct SearchQuery {
    pub q: String,
    #[serde(default = "default_limit")]
    pub limit: usize,
}

fn default_limit() -> usize {
    20
}

#[derive(Serialize)]
pub struct SearchResult {
    pub elements: Vec<SearchElement>,
}

#[derive(Serialize)]
pub struct SearchElement {
    pub qualified_name: String,
    pub name: String,
    pub element_type: String,
    pub file_path: String,
    pub line_start: usize,
    pub line_end: usize,
}

pub async fn api_search(
    State(state): State<ApiState>,
    Query(query): Query<SearchQuery>,
) -> Result<Json<ApiResponse<SearchResult>>, &'static str> {
    if query.q.is_empty() {
        return Err("Query parameter 'q' is required");
    }

    let graph = match state.get_graph_engine().await {
        Ok(g) => g,
        Err(_) => return Err("Failed to get graph engine"),
    };

    let search_results = graph
        .search_by_name(&query.q)
        .map_err(|_| "Search failed")?;

    let elements: Vec<SearchElement> = search_results
        .into_iter()
        .take(query.limit)
        .map(|e| SearchElement {
            qualified_name: e.qualified_name,
            name: e.name,
            element_type: e.element_type,
            file_path: e.file_path,
            line_start: e.line_start as usize,
            line_end: e.line_end as usize,
        })
        .collect();

    Ok(Json(ApiResponse::success(SearchResult { elements })))
}

pub async fn api_v2_status(State(state): State<ApiState>) -> Json<ApiResponse<Value>> {
    let graph = match state.get_graph_engine().await {
        Ok(g) => g,
        Err(e) => return Json(ApiResponse::error(&e.to_string())),
    };
    let elements = graph.all_elements().unwrap_or_default();
    let rels = graph.all_relationships().unwrap_or_default();
    Json(ApiResponse::success(json!({
        "total_elements": elements.len(),
        "total_relationships": rels.len(),
        "service": state.db_path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown"),
        "version": env!("CARGO_PKG_VERSION"),
    })))
}

pub async fn api_v2_service_context(
    State(state): State<ApiState>,
    Query(params): Query<std::collections::HashMap<String, String>>,
) -> Json<ApiResponse<Value>> {
    let graph = match state.get_graph_engine().await {
        Ok(g) => g,
        Err(e) => return Json(ApiResponse::error(&e.to_string())),
    };
    let service = params.get("service").map(|s| s.as_str()).unwrap_or("");
    let env = params
        .get("env")
        .map(|s| s.as_str())
        .unwrap_or("production");
    match graph.get_service_context(service, env) {
        Ok(ctx) => Json(ApiResponse::success(
            serde_json::to_value(&ctx).unwrap_or_default(),
        )),
        Err(e) => Json(ApiResponse::error(&e)),
    }
}

pub async fn api_v2_incidents(
    State(state): State<ApiState>,
    Query(params): Query<std::collections::HashMap<String, String>>,
) -> Json<ApiResponse<Value>> {
    let db = match state.get_db() {
        Ok(db) => db,
        Err(e) => return Json(ApiResponse::error(&e.to_string())),
    };
    let service = params.get("service").map(|s| s.as_str());
    let pattern = params.get("pattern").map(|s| s.as_str());
    let env = params
        .get("env")
        .map(|s| s.as_str())
        .unwrap_or("production");
    let limit = params
        .get("limit")
        .and_then(|v| v.parse::<usize>().ok())
        .unwrap_or(10);
    match crate::db::query_incidents(&db, service, pattern, Some(env), limit) {
        Ok(incidents) => Json(ApiResponse::success(
            serde_json::to_value(&incidents).unwrap_or_default(),
        )),
        Err(e) => Json(ApiResponse::error(&e.to_string())),
    }
}

pub async fn api_v2_env_diff(
    State(state): State<ApiState>,
    Query(params): Query<std::collections::HashMap<String, String>>,
) -> Json<ApiResponse<Value>> {
    let graph = match state.get_graph_engine().await {
        Ok(g) => g,
        Err(e) => return Json(ApiResponse::error(&e.to_string())),
    };
    let service = params.get("service").map(|s| s.as_str()).unwrap_or("");
    match graph.find_env_conflicts(service) {
        Ok(conflicts) => Json(ApiResponse::success(
            serde_json::to_value(&conflicts).unwrap_or_default(),
        )),
        Err(e) => Json(ApiResponse::error(&e)),
    }
}

pub async fn api_v2_health(State(_state): State<ApiState>) -> Json<ApiResponse<Value>> {
    Json(ApiResponse::success(json!({
        "status": "healthy",
        "version": env!("CARGO_PKG_VERSION"),
    })))
}