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

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

pub(crate) fn router() -> Router<AppState> {
    Router::new()
        .route(
            "/api/v1/projects/:project_id/credentials",
            get(list_credentials).post(create_credential),
        )
        .route(
            "/api/v1/projects/:project_id/credentials/:credential_id",
            put(update_credential).delete(delete_credential),
        )
}

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

async fn create_credential(
    State(state): State<AppState>,
    Path(project_id): Path<String>,
    Json(payload): Json<CreateCredentialRequest>,
) -> Result<(StatusCode, Json<ApiResponse<Credential>>), StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;

    // Validate label and secret are non-empty
    if payload.label.trim().is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }
    if payload.secret.trim().is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }

    // Validate source_node_id if provided
    if let Some(ref node_id) = payload.source_node_id {
        let nid = *node_id;
        let exists = state
            .db
            .node_exists(pid, nid)
            .map_err(|err| crate::db_error_status("credentials.create.node_exists", "node", err))?;
        if !exists {
            return Err(StatusCode::BAD_REQUEST);
        }
    }

    let cred = state.db.create_credential(pid, &payload).map_err(|err| {
        crate::db_error_status("credentials.create_credential", "credential", err)
    })?;
    Ok((StatusCode::CREATED, Json(ApiResponse::ok(cred))))
}

async fn update_credential(
    State(state): State<AppState>,
    Path((project_id, credential_id)): Path<(String, String)>,
    Json(payload): Json<UpdateCredentialRequest>,
) -> Result<Json<ApiResponse<Credential>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let cid = crate::parse_uuid(&credential_id)?;
    let cred = state
        .db
        .update_credential(pid, cid, &payload)
        .map_err(|err| crate::db_error_status("credentials.update_credential", "credential", err))?
        .ok_or(StatusCode::NOT_FOUND)?;
    Ok(Json(ApiResponse::ok(cred)))
}

async fn delete_credential(
    State(state): State<AppState>,
    Path((project_id, credential_id)): Path<(String, String)>,
) -> Result<Json<ApiResponse<()>>, StatusCode> {
    let pid = crate::resolve_project_id(&state.db, &project_id).await?;
    let cid = crate::parse_uuid(&credential_id)?;
    state.db.delete_credential(pid, cid).map_err(|err| {
        crate::db_error_status("credentials.delete_credential", "credential", err)
    })?;
    Ok(Json(ApiResponse::ok(())))
}

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

    async fn spawn_app() -> (String, Uuid, Uuid) {
        let db_path =
            std::env::temp_dir().join(format!("theseus-creds-test-{}.db", Uuid::new_v4()));
        let db = Box::leak(Box::new(crate::db::Database::open(&db_path).unwrap()));
        let project = db
            .create_project(&CreateProjectRequest {
                name: "Creds".into(),
            })
            .unwrap();
        let node = Node {
            id: Uuid::new_v4(),
            project_id: project.id,
            label: "source".into(),
            node_type: NodeType::Credential,
            position: None,
            properties: serde_json::json!({}),
            created_at: Utc::now(),
        };
        db.insert_node(&node).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}"), project.id, node.id)
    }

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

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

        let create = client
            .post(format!("{base}/api/v1/projects/{pid}/credentials"))
            .json(&serde_json::json!({
                "label":"admin password",
                "credential_type":"password",
                "username":"admin",
                "secret":"p@ss",
                "source_node_id":node_id,
                "status":"unverified",
                "notes":"from config"
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(create.status(), StatusCode::CREATED);
        let create_body = json(create).await;
        assert_eq!(create_body["success"], true);
        assert_eq!(create_body["data"]["label"], "admin password");
        let cred_id = create_body["data"]["id"].as_str().unwrap().to_string();

        let invalid_source = client
            .post(format!("{base}/api/v1/projects/{pid}/credentials"))
            .json(&serde_json::json!({
                "label":"bad",
                "credential_type":"token",
                "username":null,
                "secret":"x",
                "source_node_id":Uuid::new_v4(),
                "status":"unverified",
                "notes":null
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(invalid_source.status(), StatusCode::BAD_REQUEST);
        assert_eq!(json(invalid_source).await["success"], false);

        let list = client
            .get(format!("{base}/api/v1/projects/{pid}/credentials"))
            .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}/credentials/{cred_id}"))
            .json(&serde_json::json!({
                "label":"admin password verified",
                "credential_type":"password",
                "username":"root",
                "secret":"new",
                "source_node_id":null,
                "status":"verified",
                "notes":"checked"
            }))
            .send()
            .await
            .unwrap();
        assert_eq!(update.status(), StatusCode::OK);
        let update_body = json(update).await;
        assert_eq!(update_body["data"]["label"], "admin password verified");
        assert_eq!(update_body["data"]["status"], "verified");

        let missing = client
            .put(format!(
                "{base}/api/v1/projects/{pid}/credentials/{}",
                Uuid::new_v4()
            ))
            .json(&serde_json::json!({"label":"missing"}))
            .send()
            .await
            .unwrap();
        assert_eq!(missing.status(), StatusCode::NOT_FOUND);

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