use std::fmt;
use serde::{Deserialize, Serialize};
use super::policy::{InterstitialKind, InterstitialRoute, InterstitialSeverity};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PageSignatureEvidence {
pub host: Option<String>,
pub status_code: Option<u16>,
pub matched_url_patterns: Vec<String>,
pub matched_body_markers: Vec<String>,
pub matched_headers: Vec<String>,
pub queue_position: Option<u32>,
pub vendor_hint: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouterDecision {
pub kind: InterstitialKind,
pub severity: InterstitialSeverity,
pub route: InterstitialRoute,
pub reason: String,
pub evidence: PageSignatureEvidence,
pub classified_at_unix_ms: u64,
}
impl RouterDecision {
#[must_use]
pub fn new(
kind: InterstitialKind,
route: InterstitialRoute,
reason: impl Into<String>,
evidence: PageSignatureEvidence,
) -> Self {
let severity = InterstitialSeverity::for_kind(kind);
let classified_at_unix_ms = unix_epoch_ms();
Self {
kind,
severity,
route,
reason: reason.into(),
evidence,
classified_at_unix_ms,
}
}
#[must_use]
pub const fn with_timestamp(mut self, unix_ms: u64) -> Self {
self.classified_at_unix_ms = unix_ms;
self
}
#[must_use]
pub const fn kind(&self) -> InterstitialKind {
self.kind
}
#[must_use]
pub const fn severity(&self) -> InterstitialSeverity {
self.severity
}
#[must_use]
pub const fn route(&self) -> &InterstitialRoute {
&self.route
}
#[must_use]
pub const fn evidence(&self) -> &PageSignatureEvidence {
&self.evidence
}
#[must_use]
pub fn reason(&self) -> &str {
&self.reason
}
#[must_use]
pub const fn is_classified(&self) -> bool {
!matches!(self.kind, InterstitialKind::Transient)
}
#[must_use]
pub const fn is_terminal(&self) -> bool {
matches!(self.severity, InterstitialSeverity::Terminal)
}
#[must_use]
pub const fn requires_solve(&self) -> bool {
matches!(self.severity, InterstitialSeverity::RequiresSolve)
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(self.severity, InterstitialSeverity::Retryable)
}
pub fn log(&self) {
if self.is_classified() {
tracing::info!(
target: "stygian::interstitial_router",
kind = self.kind.label(),
severity = self.severity.label(),
route = self.route.label(),
host = self.evidence.host.as_deref().unwrap_or(""),
status_code = self.evidence.status_code.unwrap_or(0),
queue_position = self.evidence.queue_position.unwrap_or(0),
vendor_hint = self.evidence.vendor_hint.as_deref().unwrap_or(""),
matched_url_patterns = self.evidence.matched_url_patterns.len(),
matched_body_markers = self.evidence.matched_body_markers.len(),
matched_headers = self.evidence.matched_headers.len(),
classified_at_unix_ms = self.classified_at_unix_ms,
"interstitial routing decision",
);
} else {
tracing::debug!(
target: "stygian::interstitial_router",
kind = self.kind.label(),
severity = self.severity.label(),
route = self.route.label(),
host = self.evidence.host.as_deref().unwrap_or(""),
status_code = self.evidence.status_code.unwrap_or(0),
"interstitial routing decision (transient)",
);
}
}
}
impl fmt::Display for RouterDecision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"interstitial(kind={}, severity={}, route={}, reason={})",
self.kind.label(),
self.severity.label(),
self.route.label(),
self.reason,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RouterDecisionLog {
pub signature: super::PageSignature,
pub decision: RouterDecision,
}
impl RouterDecisionLog {
#[must_use]
pub const fn new(signature: super::PageSignature, decision: RouterDecision) -> Self {
Self {
signature,
decision,
}
}
}
#[must_use]
pub fn unix_epoch_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(std::time::Duration::ZERO, |d| d)
.as_millis()
.try_into()
.unwrap_or(u64::MAX)
}