use super::monthly::{resolve_monthly_evidence, MonthlyCompleteness, MonthlyF107Evidence};
use super::record::{explicit_record, F107Kind, F107Record};
use super::store::F107Store;
use crate::SolarFluxUnits;
use chrono::NaiveDate;
use std::sync::Arc;
use tempoch::{Time, UTC};
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum SolarActivitySource {
Explicit(SolarFluxUnits),
Dataset(Arc<F107Store>),
#[default]
Automatic,
}
impl PartialEq for SolarActivitySource {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Explicit(a), Self::Explicit(b)) => a == b,
(Self::Automatic, Self::Automatic) => true,
(Self::Dataset(a), Self::Dataset(b)) => {
a.dataset_id == b.dataset_id
&& a.snapshot_id == b.snapshot_id
&& a.checksum_sha256 == b.checksum_sha256
}
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ResolvedSolarActivity {
pub value: SolarFluxUnits,
pub record: F107Record,
pub dataset_id: Option<String>,
pub snapshot_id: Option<String>,
pub checksum_sha256: Option<String>,
pub requested_date: NaiveDate,
pub resolution_step: &'static str,
pub monthly_completeness: Option<MonthlyCompleteness>,
pub observed_days: Option<u32>,
pub forecast_days: Option<u32>,
pub total_days: Option<u32>,
}
impl ResolvedSolarActivity {
pub fn provenance_fragment(&self) -> String {
format!(
"F10.7 {} sfu kind={} provider={} product={} requested_date={} observation_date={} forecast_issued_at={} dataset={} snapshot={} checksum={} resolution={} monthly_completeness={} observed_days={:?} forecast_days={:?} total_days={:?} uncertainty_sfu={:?} range=[{:?}, {:?}]; measured F10.7 does not make Airglow site-calibrated",
self.value.value(),
self.record.kind.as_str(),
self.record.provider,
self.record.product,
self.requested_date,
self.record.observation_date.as_deref().unwrap_or("n/a"),
self.record
.forecast_issued_at_utc
.as_deref()
.unwrap_or("n/a"),
self.dataset_id.as_deref().unwrap_or("n/a"),
self.snapshot_id.as_deref().unwrap_or("n/a"),
self.checksum_sha256.as_deref().unwrap_or("n/a"),
self.resolution_step,
self.monthly_completeness
.map(MonthlyCompleteness::as_str)
.unwrap_or("n/a"),
self.observed_days,
self.forecast_days,
self.total_days,
self.record.uncertainty_sfu,
self.record.range_low_sfu,
self.record.range_high_sfu,
)
}
pub fn is_degraded_planning_input(&self) -> bool {
matches!(self.record.kind, F107Kind::Forecast | F107Kind::Climatology)
|| matches!(
self.monthly_completeness,
Some(
MonthlyCompleteness::CompleteForecast
| MonthlyCompleteness::ProvisionalObservedPlusForecast
| MonthlyCompleteness::OfficialMonthlyPrediction
| MonthlyCompleteness::Climatology
)
)
}
}
pub fn resolve_f107(
requested_time: Time<UTC>,
source: &SolarActivitySource,
) -> crate::Result<ResolvedSolarActivity> {
let requested_date = utc_calendar_date(requested_time);
let requested_at = requested_time
.to_chrono()
.expect("F10.7 resolution requires a chrono-representable UTC instant")
.naive_utc();
match source {
SolarActivitySource::Explicit(flux) => {
validate_explicit_flux(*flux)?;
Ok(ResolvedSolarActivity {
value: *flux,
record: explicit_record(requested_date, flux.value()),
dataset_id: None,
snapshot_id: None,
checksum_sha256: None,
requested_date,
resolution_step: "explicit-override",
monthly_completeness: None,
observed_days: None,
forecast_days: None,
total_days: None,
})
}
SolarActivitySource::Dataset(store) => {
resolve_from_store(requested_date, requested_at, store)
}
SolarActivitySource::Automatic => {
let store = super::bundled::bundled_f107_store()?;
resolve_from_store(requested_date, requested_at, store)
}
}
}
fn validate_explicit_flux(flux: SolarFluxUnits) -> crate::Result<()> {
let value = flux.value();
if !value.is_finite() || value <= 0.0 {
return Err(crate::error::NsbError::OutOfRange(format!(
"explicit F10.7 must be finite and positive, got {value}"
)));
}
Ok(())
}
fn resolve_from_store(
requested_date: NaiveDate,
requested_at: chrono::NaiveDateTime,
store: &F107Store,
) -> crate::Result<ResolvedSolarActivity> {
let evidence =
resolve_monthly_evidence(store, requested_date, requested_at).map_err(|message| {
crate::error::NsbError::DataParse {
file: "f107_store",
message,
}
})?;
Ok(from_evidence(store, requested_date, evidence))
}
fn from_evidence(
store: &F107Store,
requested_date: NaiveDate,
evidence: MonthlyF107Evidence,
) -> ResolvedSolarActivity {
ResolvedSolarActivity {
value: SolarFluxUnits::new(evidence.value_sfu),
record: evidence.record,
dataset_id: Some(store.dataset_id.clone()),
snapshot_id: Some(store.snapshot_id.clone()),
checksum_sha256: store.checksum_sha256.clone(),
requested_date,
resolution_step: evidence.resolution_step,
monthly_completeness: Some(evidence.method),
observed_days: Some(evidence.observed_days),
forecast_days: Some(evidence.forecast_days),
total_days: Some(evidence.total_days),
}
}
pub(crate) fn utc_calendar_date(time: Time<UTC>) -> NaiveDate {
time.to_chrono()
.expect("F10.7 resolution requires a chrono-representable UTC instant")
.date_naive()
}