use axum::{
extract::{Path, Query, State},
response::Json,
Extension,
};
use serde::Deserialize;
use crate::ai::{
AITask, Contribution, ContributionStatus, ListContributionsResponse, Priority,
ReviewContributionRequest, SubmitContributionRequest, SubmitContributionResponse,
};
use crate::error::DbError;
use crate::server::auth::Claims;
use crate::server::handlers::AppState;
#[derive(Debug, Deserialize)]
pub struct ListContributionsQuery {
pub status: Option<String>,
pub requester: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
pub async fn submit_contribution_handler(
State(state): State<AppState>,
Path(db_name): Path<String>,
Extension(claims): Extension<Claims>,
Json(request): Json<SubmitContributionRequest>,
) -> Result<Json<SubmitContributionResponse>, DbError> {
let username = claims.sub;
let db = state.storage.get_database(&db_name)?;
if db.get_collection("_ai_contributions").is_err() {
db.create_collection("_ai_contributions".to_string(), None)?;
}
if db.get_collection("_ai_tasks").is_err() {
db.create_collection("_ai_tasks".to_string(), None)?;
}
let contribution = Contribution::new(
request.contribution_type,
request.description.clone(),
username,
request.context.clone(),
);
let contribution_id = contribution.id.clone();
let coll = db.get_collection("_ai_contributions")?;
let doc_value = serde_json::to_value(&contribution)
.map_err(|e| DbError::InternalError(format!("Serialization error: {}", e)))?;
coll.insert(doc_value)?;
let priority = request
.context
.as_ref()
.map(|c| match c.priority {
Priority::Critical => 100,
Priority::High => 75,
Priority::Medium => 50,
Priority::Low => 25,
})
.unwrap_or(50);
let task = AITask::analyze(contribution_id.clone(), priority);
let tasks_coll = db.get_collection("_ai_tasks")?;
let task_value = serde_json::to_value(&task)
.map_err(|e| DbError::InternalError(format!("Serialization error: {}", e)))?;
tasks_coll.insert(task_value)?;
Ok(Json(SubmitContributionResponse {
id: contribution_id,
status: "success".to_string(),
message: "Contribution submitted successfully. AI analysis task queued.".to_string(),
}))
}
pub async fn list_contributions_handler(
State(state): State<AppState>,
Path(db_name): Path<String>,
Query(query): Query<ListContributionsQuery>,
) -> Result<Json<ListContributionsResponse>, DbError> {
let db = state.storage.get_database(&db_name)?;
if db.get_collection("_ai_contributions").is_err() {
return Ok(Json(ListContributionsResponse {
contributions: Vec::new(),
total: 0,
}));
}
let coll = db.get_collection("_ai_contributions")?;
let mut contributions = Vec::new();
for doc in coll.scan(None) {
let contribution: Contribution = serde_json::from_value(doc.to_value())
.map_err(|_| DbError::InternalError("Corrupted contribution data".to_string()))?;
if let Some(ref status_filter) = query.status {
let status_str = contribution.status.to_string();
if status_str != *status_filter {
continue;
}
}
if let Some(ref requester_filter) = query.requester {
if contribution.requester != *requester_filter {
continue;
}
}
contributions.push(contribution);
}
contributions.sort_by(|a, b| {
b.context
.priority
.cmp(&a.context.priority)
.then_with(|| a.created_at.cmp(&b.created_at))
});
let total = contributions.len();
let offset = query.offset.unwrap_or(0);
let limit = query.limit.unwrap_or(100);
let contributions: Vec<Contribution> =
contributions.into_iter().skip(offset).take(limit).collect();
Ok(Json(ListContributionsResponse {
contributions,
total,
}))
}
pub async fn get_contribution_handler(
State(state): State<AppState>,
Path((db_name, contribution_id)): Path<(String, String)>,
) -> Result<Json<Contribution>, DbError> {
let db = state.storage.get_database(&db_name)?;
let coll = db.get_collection("_ai_contributions")?;
let doc = coll.get(&contribution_id)?;
let contribution: Contribution = serde_json::from_value(doc.to_value())
.map_err(|_| DbError::InternalError("Corrupted contribution data".to_string()))?;
Ok(Json(contribution))
}
pub async fn approve_contribution_handler(
State(state): State<AppState>,
Path((db_name, contribution_id)): Path<(String, String)>,
Json(request): Json<ReviewContributionRequest>,
) -> Result<Json<Contribution>, DbError> {
let db = state.storage.get_database(&db_name)?;
let coll = db.get_collection("_ai_contributions")?;
let doc = coll.get(&contribution_id)?;
let mut contribution: Contribution = serde_json::from_value(doc.to_value())
.map_err(|e| DbError::InternalError(format!("Corrupted contribution data: {}", e)))?;
match contribution.status {
ContributionStatus::Submitted
| ContributionStatus::Analyzing
| ContributionStatus::Review => {}
_ => {
return Err(DbError::BadRequest(format!(
"Cannot approve contribution in status {}",
contribution.status
)));
}
}
contribution.status = ContributionStatus::Approved;
if let Some(ref feedback) = request.feedback {
contribution.feedback = Some(format!("Approved by admin: {}", feedback));
}
contribution.updated_at = chrono::Utc::now();
let doc_value = serde_json::to_value(&contribution)
.map_err(|e| DbError::InternalError(format!("Serialization error: {}", e)))?;
coll.update(&contribution_id, doc_value)?;
Ok(Json(contribution))
}
pub async fn reject_contribution_handler(
State(state): State<AppState>,
Path((db_name, contribution_id)): Path<(String, String)>,
Json(request): Json<ReviewContributionRequest>,
) -> Result<Json<Contribution>, DbError> {
let db = state.storage.get_database(&db_name)?;
let coll = db.get_collection("_ai_contributions")?;
let doc = coll.get(&contribution_id)?;
let mut contribution: Contribution = serde_json::from_value(doc.to_value())
.map_err(|e| DbError::InternalError(format!("Corrupted contribution data: {}", e)))?;
match contribution.status {
ContributionStatus::Submitted
| ContributionStatus::Analyzing
| ContributionStatus::Review => {}
_ => {
return Err(DbError::BadRequest(format!(
"Cannot reject contribution in status {}",
contribution.status
)));
}
}
contribution.status = ContributionStatus::Rejected;
if let Some(ref feedback) = request.feedback {
contribution.feedback = Some(format!("Rejected by admin: {}", feedback));
}
contribution.updated_at = chrono::Utc::now();
let doc_value = serde_json::to_value(&contribution)
.map_err(|e| DbError::InternalError(format!("Serialization error: {}", e)))?;
coll.update(&contribution_id, doc_value)?;
Ok(Json(contribution))
}
pub async fn cancel_contribution_handler(
State(state): State<AppState>,
Path((db_name, contribution_id)): Path<(String, String)>,
Extension(claims): Extension<Claims>,
) -> Result<Json<Contribution>, DbError> {
let db = state.storage.get_database(&db_name)?;
let coll = db.get_collection("_ai_contributions")?;
let doc = coll.get(&contribution_id)?;
let mut contribution: Contribution = serde_json::from_value(doc.to_value())
.map_err(|e| DbError::InternalError(format!("Corrupted contribution data: {}", e)))?;
if contribution.requester != claims.sub {
return Err(DbError::Forbidden(
"You can only cancel your own contributions".to_string(),
));
}
match contribution.status {
ContributionStatus::Submitted
| ContributionStatus::Analyzing
| ContributionStatus::Generating => {}
_ => {
return Err(DbError::BadRequest(format!(
"Cannot cancel contribution in status {}",
contribution.status
)));
}
}
contribution.status = ContributionStatus::Cancelled;
contribution.updated_at = chrono::Utc::now();
let doc_value = serde_json::to_value(&contribution)
.map_err(|e| DbError::InternalError(format!("Serialization error: {}", e)))?;
coll.update(&contribution_id, doc_value)?;
Ok(Json(contribution))
}