use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use vti_common::error::AppError;
use super::pcs::{HiddenVettingConfig, HiddenVettingEvent};
use crate::server::AppState;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventRequestRecord {
pub tier: String,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub requested_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventState {
Pending,
Approved,
}
impl EventState {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Approved => "approved",
}
}
}
fn request_key(event_id: &str, member_did: &str) -> Vec<u8> {
let mut key = group_prefix(event_id);
key.extend_from_slice(member_did.as_bytes());
key
}
fn group_prefix(event_id: &str) -> Vec<u8> {
format!("pcs-event:{}:{event_id}:", event_id.len()).into_bytes()
}
pub async fn request(
state: &AppState,
config: &HiddenVettingConfig,
member_did: &str,
event_id: &str,
tier: &str,
window: (NaiveDate, NaiveDate),
now: DateTime<Utc>,
) -> Result<(EventState, usize, HiddenVettingEvent), AppError> {
if !super::vetter_eligible(state, member_did, "vetter", now).await? {
return Err(AppError::Forbidden(format!(
"{member_did} holds no live vetter grant in this community"
)));
}
let event = config
.events
.iter()
.find(|e| e.event_id == event_id)
.ok_or_else(|| {
AppError::NotFound(format!("this community is running no event `{event_id}`"))
})?
.clone();
if event.tier(tier).is_none() {
return Err(AppError::Validation(format!(
"`{tier}` is not a tier `{event_id}` publishes"
)));
}
if now.date_naive() > event.closes_after() {
return Err(AppError::Conflict(format!(
"event `{event_id}` closed after {}",
event.closes_after()
)));
}
let (from, until) = window;
if until < from || from < event.start_date || until > event.end_date {
return Err(AppError::Validation(format!(
"a window of {from}..={until} is not inside `{event_id}`, which runs {}..={}",
event.start_date, event.end_date
)));
}
let written = state
.vetting_pcs_issue_ks
.insert_if_absent(
request_key(event_id, member_did),
&EventRequestRecord {
tier: tier.to_string(),
start_date: from,
end_date: until,
requested_at: now,
},
)
.await?;
if !written {
return Err(AppError::Conflict(format!(
"{member_did} has already asked to be in `{event_id}`"
)));
}
let size = group_size(state, event_id).await?;
let state_now = match gate(state, config, member_did, &event.label(), now).await {
Ok(()) => EventState::Approved,
Err(_) => EventState::Pending,
};
Ok((state_now, size, event))
}
pub async fn group_size(state: &AppState, event_id: &str) -> Result<usize, AppError> {
Ok(state
.vetting_pcs_issue_ks
.prefix_keys(group_prefix(event_id))
.await?
.len())
}
pub async fn has_asked(
state: &AppState,
event_id: &str,
member_did: &str,
) -> Result<bool, AppError> {
Ok(state
.vetting_pcs_issue_ks
.get::<EventRequestRecord>(request_key(event_id, member_did))
.await?
.is_some())
}
#[must_use]
pub fn event_of<'a>(
config: &'a HiddenVettingConfig,
label: &str,
) -> Option<&'a HiddenVettingEvent> {
let id = label.strip_prefix("token/event/")?;
config.events.iter().find(|e| e.event_id == id)
}
pub async fn gate(
state: &AppState,
config: &HiddenVettingConfig,
member_did: &str,
label: &str,
now: DateTime<Utc>,
) -> Result<(), AppError> {
let Some(event) = event_of(config, label) else {
return Ok(());
};
let today = now.date_naive();
if today > event.closes_after() {
return Err(AppError::Forbidden(format!(
"event `{}` closed after {}",
event.event_id,
event.closes_after()
)));
}
let Some(approver) = event.approved_by.as_deref() else {
return Err(AppError::Forbidden(format!(
"event `{}` has not been approved",
event.event_id
)));
};
if state
.vetting_pcs_issue_ks
.get::<EventRequestRecord>(request_key(&event.event_id, approver))
.await?
.is_some()
{
return Err(AppError::Forbidden(format!(
"event `{}` was approved by a vetter in its own group",
event.event_id
)));
}
if state
.vetting_pcs_issue_ks
.get::<EventRequestRecord>(request_key(&event.event_id, member_did))
.await?
.is_none()
{
return Err(AppError::Forbidden(format!(
"{member_did} did not ask to be in event `{}`",
event.event_id
)));
}
let size = group_size(state, &event.event_id).await?;
if size < event.group_floor {
return Err(AppError::Forbidden(format!(
"event `{}` has {size} vetter(s) and this community's floor is {}",
event.event_id, event.group_floor
)));
}
Ok(())
}
pub async fn quota(
state: &AppState,
config: &HiddenVettingConfig,
member_did: &str,
label: &str,
) -> Result<usize, AppError> {
let Some(event) = event_of(config, label) else {
return Ok(config.drip_per_tick);
};
let Some(row) = state
.vetting_pcs_issue_ks
.get::<EventRequestRecord>(request_key(&event.event_id, member_did))
.await?
else {
return Ok(config.drip_per_tick);
};
Ok(event
.tier(&row.tier)
.map_or(config.drip_per_tick, |t| t.drip_per_tick))
}