use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
    Json, Router,
};
use std::collections::HashMap;

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

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route("/api/v1/projects", post(create_project).get(list_projects))
        .route("/api/v1/projects/stats", get(project_stats))
        .route(
            "/api/v1/projects/:project_id",
            get(get_project).delete(delete_project),
        )
}

async fn create_project(
    State(state): State<AppState>,
    Json(payload): Json<CreateProjectRequest>,
) -> Result<impl IntoResponse, StatusCode> {
    let name = payload.name.trim();
    if name.is_empty() {
        return Ok((
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::<Project>::err(
                "Project name must not be empty",
            )),
        ));
    }
    if name.len() > 200 {
        return Ok((
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::<Project>::err(
                "Project name must not exceed 200 characters",
            )),
        ));
    }
    if name.contains('\n') || name.contains('\r') {
        return Ok((
            StatusCode::BAD_REQUEST,
            Json(ApiResponse::<Project>::err(
                "Project name must not contain newlines",
            )),
        ));
    }
    if state
        .db
        .find_project_by_name(name)
        .map_err(|err| crate::db_error_status("projects.find_project_by_name", "project", err))?
        .is_some()
    {
        return Ok((
            StatusCode::CONFLICT,
            Json(ApiResponse::<Project>::err(&format!(
                "Project name already exists: {name}"
            ))),
        ));
    }
    let project = state
        .db
        .create_project(&CreateProjectRequest {
            name: name.to_string(),
        })
        .map_err(|err| crate::db_error_status("projects.create_project", "project", err))?;
    Ok((StatusCode::CREATED, Json(ApiResponse::ok(project))))
}

async fn list_projects(
    State(state): State<AppState>,
) -> Result<Json<ApiResponse<Vec<Project>>>, StatusCode> {
    let projects = state
        .db
        .list_projects()
        .map_err(|err| crate::db_error_status("projects.list_projects", "project", err))?;
    Ok(Json(ApiResponse::ok(projects)))
}

async fn project_stats(
    State(state): State<AppState>,
) -> Result<Json<ApiResponse<HashMap<String, ProjectStats>>>, StatusCode> {
    let projects = state.db.list_projects().map_err(|err| {
        crate::db_error_status("projects.project_stats.list_projects", "project", err)
    })?;
    let mut stats = HashMap::new();
    for p in &projects {
        let node_count = state.db.count_nodes_for_project(p.id).unwrap_or(0);
        let edge_count = state.db.count_edges_for_project(p.id).unwrap_or(0);
        stats.insert(
            p.id.to_string(),
            ProjectStats {
                node_count,
                edge_count,
            },
        );
    }
    Ok(Json(ApiResponse::ok(stats)))
}

async fn get_project(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
) -> Result<Json<ApiResponse<Project>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let project = state
        .db
        .get_project(pid)
        .map_err(|err| crate::db_error_status("projects.get_project", "project", err))?
        .ok_or(StatusCode::NOT_FOUND)?;
    Ok(Json(ApiResponse::ok(project)))
}

async fn delete_project(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
) -> Result<Json<ApiResponse<()>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let deleted = state
        .db
        .delete_project(pid)
        .map_err(|err| crate::db_error_status("projects.delete_project", "project", err))?;
    if deleted {
        Ok(Json(ApiResponse::ok(())))
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}

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

    async fn spawn_app() -> String {
        let db_path =
            std::env::temp_dir().join(format!("theseus-projects-test-{}.db", uuid::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 = 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}")
    }

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

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

        let resp = client
            .post(format!("{base}/api/v1/projects"))
            .json(&serde_json::json!({"name":" Eng A "}))
            .send()
            .await
            .unwrap();
        assert_eq!(resp.status(), StatusCode::CREATED);
        let body = json(resp).await;
        assert_eq!(body["success"], true);
        assert_eq!(body["data"]["name"], "Eng A");
        assert!(body["error"].is_null());
        let project_id = body["data"]["id"].as_str().unwrap().to_string();

        let duplicate = client
            .post(format!("{base}/api/v1/projects"))
            .json(&serde_json::json!({"name":"Eng A"}))
            .send()
            .await
            .unwrap();
        assert_eq!(duplicate.status(), StatusCode::CONFLICT);
        assert_eq!(json(duplicate).await["success"], false);

        let invalid = client
            .post(format!("{base}/api/v1/projects"))
            .json(&serde_json::json!({"name":"\n"}))
            .send()
            .await
            .unwrap();
        assert_eq!(invalid.status(), StatusCode::BAD_REQUEST);
        assert_eq!(json(invalid).await["success"], false);

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

        let stats = client
            .get(format!("{base}/api/v1/projects/stats"))
            .send()
            .await
            .unwrap();
        assert_eq!(stats.status(), StatusCode::OK);
        assert_eq!(json(stats).await["data"][&project_id]["node_count"], 0);

        let short_id = &project_id[..6];
        let get = client
            .get(format!("{base}/api/v1/projects/{short_id}"))
            .send()
            .await
            .unwrap();
        assert_eq!(get.status(), StatusCode::OK);
        assert_eq!(json(get).await["data"]["id"], project_id);

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

        let missing = client
            .get(format!("{base}/api/v1/projects/{project_id}"))
            .send()
            .await
            .unwrap();
        assert_eq!(missing.status(), StatusCode::NOT_FOUND);
        assert_eq!(json(missing).await["success"], false);
    }
}
