use std::collections::BTreeMap;
use std::path::Path;
use chrono::NaiveDate;
use super::super::error::OutputError;
use super::codec::{
deserialize_checkpoint_manifest, deserialize_stage_basis, deserialize_stage_cuts,
deserialize_stage_states, read_sorted_bin_files, serialize_checkpoint_manifest,
serialize_stage_basis, serialize_stage_cuts, serialize_stage_states,
};
use super::records::{
CheckpointManifest, ENTITY_SLOT_DELIVERY_DATE_SENTINEL, OwnedPolicyBasisRecord,
PolicyBasisRecord, PolicyCheckpoint, StageCutsPayload, StageCutsReadResult, StageStatesPayload,
StageStatesReadResult, StateFamily,
};
fn is_well_formed_delivery_date(delivery_date: i32) -> bool {
if delivery_date == ENTITY_SLOT_DELIVERY_DATE_SENTINEL {
return true;
}
let year = delivery_date / 10_000;
let month = (delivery_date / 100) % 100;
let day = delivery_date % 100;
let (Ok(month), Ok(day)) = (u32::try_from(month), u32::try_from(day)) else {
return false;
};
NaiveDate::from_ymd_opt(year, month, day).is_some()
}
fn check_transit_bucket_monotonicity(pool: &StageCutsReadResult) -> Result<(), OutputError> {
let mut by_entity: BTreeMap<i32, Vec<(u32, i32)>> = BTreeMap::new();
for slot in &pool.entity_manifest {
if slot.family() == Some(StateFamily::HydroTransitBucket)
&& slot.delivery_date != ENTITY_SLOT_DELIVERY_DATE_SENTINEL
{
by_entity
.entry(slot.entity_id)
.or_default()
.push((slot.subindex, slot.delivery_date));
}
}
for dates in by_entity.values_mut() {
dates.sort_by_key(|&(subindex, _)| subindex);
for pair in dates.windows(2) {
let (prev_subindex, prev_date) = pair[0];
let (subindex, date) = pair[1];
if date < prev_date {
let pool_id = pool.stage_id;
return Err(OutputError::serialization(
"policy_checkpoint_dates",
format!(
"pool {pool_id} subindex {subindex} carries delivery_date {date}, \
earlier than subindex {prev_subindex}'s {prev_date}"
),
));
}
}
}
Ok(())
}
fn validate_checkpoint_dates(checkpoint: &PolicyCheckpoint) -> Result<(), OutputError> {
for pool in &checkpoint.stage_cuts {
for slot in &pool.entity_manifest {
if !is_well_formed_delivery_date(slot.delivery_date) {
return Err(OutputError::serialization(
"policy_checkpoint_dates",
format!(
"pool {} subindex {} carries malformed delivery_date {}",
pool.stage_id, slot.subindex, slot.delivery_date
),
));
}
}
check_transit_bucket_monotonicity(pool)?;
}
Ok(())
}
fn bin_file_name(id: u32) -> String {
format!("{id:03}.bin")
}
pub fn write_policy_checkpoint(
path: &Path,
stage_cuts: &[StageCutsPayload<'_>],
stage_bases: &[PolicyBasisRecord<'_>],
metadata: &CheckpointManifest,
stage_states: &[StageStatesPayload<'_>],
) -> Result<(), OutputError> {
let cuts_dir = path.join("cuts");
std::fs::create_dir_all(&cuts_dir).map_err(|e| OutputError::io(&cuts_dir, e))?;
let basis_dir = path.join("basis");
std::fs::create_dir_all(&basis_dir).map_err(|e| OutputError::io(&basis_dir, e))?;
for payload in stage_cuts {
let file_path = cuts_dir.join(bin_file_name(payload.stage_id));
let buf = serialize_stage_cuts(payload);
std::fs::write(&file_path, &buf).map_err(|e| OutputError::io(&file_path, e))?;
}
for record in stage_bases {
let file_path = basis_dir.join(bin_file_name(record.stage_id));
let buf = serialize_stage_basis(record);
std::fs::write(&file_path, &buf).map_err(|e| OutputError::io(&file_path, e))?;
}
if !stage_states.is_empty() {
let states_dir = path.join("states");
std::fs::create_dir_all(&states_dir).map_err(|e| OutputError::io(&states_dir, e))?;
for payload in stage_states {
let file_path = states_dir.join(bin_file_name(payload.stage_id));
let buf = serialize_stage_states(payload);
std::fs::write(&file_path, &buf).map_err(|e| OutputError::io(&file_path, e))?;
}
}
let manifest_buf = serialize_checkpoint_manifest(metadata);
let manifest_path = path.join("manifest.bin");
std::fs::write(&manifest_path, &manifest_buf)
.map_err(|e| OutputError::io(&manifest_path, e))?;
Ok(())
}
pub fn read_policy_checkpoint(path: &Path) -> Result<PolicyCheckpoint, OutputError> {
let manifest_path = path.join("manifest.bin");
let manifest_bytes =
std::fs::read(&manifest_path).map_err(|e| OutputError::io(&manifest_path, e))?;
let metadata = deserialize_checkpoint_manifest(&manifest_bytes)?;
let cuts_dir = path.join("cuts");
let mut stage_cuts: Vec<StageCutsReadResult> =
read_sorted_bin_files(&cuts_dir, "stage_cuts", deserialize_stage_cuts)?;
stage_cuts.sort_by_key(|r| r.stage_id);
let basis_dir = path.join("basis");
let mut stage_bases: Vec<OwnedPolicyBasisRecord> =
read_sorted_bin_files(&basis_dir, "stage_basis", deserialize_stage_basis)?;
stage_bases.sort_by_key(|r| r.stage_id);
let states_dir = path.join("states");
let stage_states: Vec<StageStatesReadResult> = if states_dir.is_dir() {
let mut ss = read_sorted_bin_files(&states_dir, "stage_states", deserialize_stage_states)?;
ss.sort_by_key(|r| r.stage_id);
ss
} else {
Vec::new()
};
let checkpoint = PolicyCheckpoint {
metadata,
stage_cuts,
stage_bases,
stage_states,
};
validate_checkpoint_dates(&checkpoint)?;
Ok(checkpoint)
}