use std::collections::{BTreeSet, HashMap};
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;
use vti_common::error::AppError;
use crate::join::{JoinStatus, get_vetting_facts, list_join_requests};
use crate::members::storage::get_member;
use crate::server::AppState;
use crate::vetting::revocation;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub enum RevocationReviewState {
NoAdmission,
NeedsReview,
}
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct VettingRevocationRow {
pub issuer: String,
pub statement_id: String,
pub statement_digest_multibase: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub recorded_at: DateTime<Utc>,
pub review_state: RevocationReviewState,
pub affected_join_requests: Vec<Uuid>,
pub affected_members: Vec<String>,
}
pub(crate) async fn revocation_rows(
state: &AppState,
) -> Result<Vec<VettingRevocationRow>, AppError> {
let mut counted_by: HashMap<(String, String), Vec<(Uuid, String)>> = HashMap::new();
for request in list_join_requests(&state.join_requests_ks).await? {
if request.status != JoinStatus::Approved {
continue;
}
let Some(stored) = get_vetting_facts(&state.join_requests_ks, request.id).await? else {
continue;
};
for statement in stored.facts.statements.iter().filter(|s| s.counted) {
if let (Some(issuer), Some(id)) = (&statement.issuer, &statement.id) {
counted_by
.entry((issuer.clone(), id.clone()))
.or_default()
.push((request.id, request.applicant_did.clone()));
}
}
}
let mut rows = Vec::new();
for notice in revocation::list_notices(&state.vetting_revocations_ks).await? {
let affected = counted_by
.get(&(notice.issuer.clone(), notice.statement_id.clone()))
.cloned()
.unwrap_or_default();
let mut members = BTreeSet::new();
for (_, applicant) in &affected {
if get_member(&state.members_ks, applicant)
.await?
.is_some_and(|m| m.removed_at.is_none())
{
members.insert(applicant.clone());
}
}
rows.push(VettingRevocationRow {
review_state: if members.is_empty() {
RevocationReviewState::NoAdmission
} else {
RevocationReviewState::NeedsReview
},
affected_join_requests: affected.iter().map(|(id, _)| *id).collect(),
affected_members: members.into_iter().collect(),
reason: notice.reason.map(|r| r.to_string()),
issuer: notice.issuer,
statement_id: notice.statement_id,
statement_digest_multibase: notice.statement_digest_multibase,
recorded_at: notice.recorded_at,
});
}
Ok(rows)
}