use crate::core::pipeline::generate_manifest;
use crate::data::repository::{ISnapshotProvider, hydrate_snapshot};
use crate::data::swap::RegistryState;
use bistun_core::{
CapabilityManifest, Direction, LmsError, LmsRule, MorphType, NormRule, ResolutionMetrics,
SdkState, SegType, SyncMetrics, TraitKey, TraitValue, TransRule,
};
use std::sync::{Arc, RwLock};
use web_time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "async-worker")]
use std::time::Duration;
#[cfg(feature = "async-worker")]
use tokio::time;
use tracing::{error, info};
#[derive(Debug, Clone)]
pub struct LinguisticManager {
state: RegistryState,
status: Arc<RwLock<SdkState>>,
pub metrics: Arc<RwLock<SyncMetrics>>,
pub resolution_metrics: Arc<RwLock<ResolutionMetrics>>,
}
impl Default for LinguisticManager {
fn default() -> Self {
Self::new()
}
}
impl LinguisticManager {
#[must_use]
pub fn new() -> Self {
Self {
state: RegistryState::new(),
status: Arc::new(RwLock::new(SdkState::Bootstrapping)),
metrics: Arc::new(RwLock::new(SyncMetrics::default())),
resolution_metrics: Arc::new(RwLock::new(ResolutionMetrics::default())),
}
}
#[must_use]
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("LMS-OPS: System clock moved backwards")
.as_secs()
}
pub async fn initialize(&self, provider: &impl ISnapshotProvider, public_key_b64: &str) {
self.metrics.write().expect("LMS-OPS: Sync metrics lock poisoned").last_attempted_sync =
Self::now_secs();
match hydrate_snapshot(provider, public_key_b64).await {
Ok(store) => {
self.state.swap_registry(store);
*self.status.write().expect("LMS-OPS: Status lock poisoned") = SdkState::Ready;
self.metrics
.write()
.expect("LMS-OPS: Sync metrics lock poisoned")
.last_successful_sync = Self::now_secs();
info!("LinguisticManager initialized successfully.");
}
Err(e) => {
*self.status.write().expect("LMS-OPS: Status lock poisoned") = SdkState::Degraded;
self.metrics
.write()
.expect("LMS-OPS: Sync metrics lock poisoned")
.sync_error_count += 1;
error!(
"LinguisticManager failed to initialize. Triggering Circuit Breaker. Reason: {e}"
);
}
}
}
pub async fn force_sync(
&self,
provider: &impl ISnapshotProvider,
public_key_b64: &str,
) -> Result<(), String> {
self.metrics.write().expect("LMS-OPS: Sync metrics lock poisoned").last_attempted_sync =
Self::now_secs();
match hydrate_snapshot(provider, public_key_b64).await {
Ok(store) => {
self.state.swap_registry(store);
let mut s = self.status.write().expect("LMS-OPS: Status lock poisoned");
if *s != SdkState::Ready {
*s = SdkState::Ready;
}
self.metrics
.write()
.expect("LMS-OPS: Sync metrics lock poisoned")
.last_successful_sync = Self::now_secs();
info!("LMS-OPS: Real-time force_sync successful. Registry hot-swapped.");
Ok(())
}
Err(e) => {
self.metrics
.write()
.expect("LMS-OPS: Sync metrics lock poisoned")
.sync_error_count += 1;
error!("LMS-OPS: Real-time force_sync failed: {}", e);
Err(e.to_string())
}
}
}
#[cfg(feature = "async-worker")]
pub fn spawn_background_sync<P>(&self, interval_secs: u64, provider: P, public_key_b64: String)
where
P: ISnapshotProvider + 'static,
{
let state = self.state.clone();
let status = self.status.clone();
let metrics = self.metrics.clone();
tokio::spawn(async move {
info!("Background sync worker started (Interval: {interval_secs}s)");
let mut interval_timer = time::interval(Duration::from_secs(interval_secs));
loop {
interval_timer.tick().await;
match hydrate_snapshot(&provider, &public_key_b64).await {
Ok(store) => {
state.swap_registry(store);
let mut s =
status.write().expect("LMS-OPS: Background status lock poisoned");
if *s != SdkState::Ready {
*s = SdkState::Ready;
}
info!("Background sync successful. Registry hot-swapped.");
}
Err(e) => {
metrics
.write()
.expect("LMS-OPS: Background metrics lock poisoned")
.sync_error_count += 1;
error!(
"Background sync failed. Retaining current registry state. Reason: {e}"
);
}
}
}
});
}
#[must_use]
pub fn status(&self) -> SdkState {
*self.status.read().expect("LMS-OPS: Status lock poisoned")
}
#[must_use]
pub fn metrics(&self) -> SyncMetrics {
self.metrics.read().expect("LMS-OPS: Metrics lock poisoned").clone()
}
#[must_use]
pub fn resolution_metrics(&self) -> ResolutionMetrics {
self.resolution_metrics.read().expect("LMS-OPS: Resolution metrics lock poisoned").clone()
}
pub fn resolve_capabilities(&self, tag: &str) -> Result<CapabilityManifest, LmsError> {
self.resolution_metrics
.write()
.expect("LMS-OPS: Resolution metrics lock poisoned")
.total_manifests_resolved += 1;
if self.status() == SdkState::Degraded {
return Ok(Self::generate_circuit_breaker_manifest());
}
generate_manifest(tag, &self.state)
}
#[must_use]
fn generate_circuit_breaker_manifest() -> CapabilityManifest {
let mut manifest = CapabilityManifest::new("en-US".to_string());
manifest.traits.insert(TraitKey::PrimaryDirection, TraitValue::Direction(Direction::LTR));
manifest.traits.insert(TraitKey::HasBidiElements, TraitValue::Boolean(false));
manifest.traits.insert(TraitKey::RequiresShaping, TraitValue::Boolean(false));
manifest.traits.insert(TraitKey::SegmentationStrategy, TraitValue::SegType(SegType::SPACE));
manifest
.traits
.insert(TraitKey::MorphologyType, TraitValue::MorphType(MorphType::FUSIONAL));
manifest.rules.insert("NORMALIZATION_DEFAULT".to_string(), LmsRule::Norm(NormRule::NFC));
manifest
.rules
.insert("TRANSLITERATION_DEFAULT".to_string(), LmsRule::Trans(TransRule::NONE));
manifest.metadata.insert("registry_version".to_string(), "CIRCUIT_BREAKER".to_string());
manifest.metadata.insert("resolution_path".to_string(), "DEGRADED_FALLBACK".to_string());
manifest.metadata.insert("resolution_time_ms".to_string(), "0.0000".to_string());
manifest.metadata.insert("circuit_breaker".to_string(), "true".to_string());
manifest
}
}
#[cfg(all(test, feature = "simulation"))]
mod tests {
use super::*;
use crate::data::repository::SimulatedSnapshotProvider;
#[test]
fn test_manager_starts_in_bootstrapping_state() {
let manager = LinguisticManager::new();
assert_eq!(manager.status(), SdkState::Bootstrapping);
}
#[tokio::test]
async fn test_manager_initializes_into_ready_state() {
let manager = LinguisticManager::new();
let provider = SimulatedSnapshotProvider::new();
manager.initialize(&provider, &provider.public_key).await;
assert_eq!(manager.status(), SdkState::Ready);
let metrics = manager.metrics();
assert!(metrics.last_successful_sync > 0);
assert_eq!(metrics.sync_error_count, 0);
}
#[tokio::test]
async fn test_manager_delegates_to_dynamic_pipeline() {
let manager = LinguisticManager::new();
let provider = SimulatedSnapshotProvider::new();
manager.initialize(&provider, &provider.public_key).await;
let manifest =
manager.resolve_capabilities("th-TH").expect("LMS-TEST: SDK delegation failed");
assert_eq!(manifest.resolved_locale, "th-TH");
assert!(!manifest.traits.is_empty());
assert_eq!(manager.resolution_metrics().total_manifests_resolved, 1);
}
#[tokio::test]
async fn test_circuit_breaker_intercepts_requests() {
let manager = LinguisticManager::new();
*manager.status.write().expect("LMS-TEST: Status lock poisoned") = SdkState::Degraded;
let manifest =
manager.resolve_capabilities("ar-EG").expect("LMS-TEST: Circuit breaker failed");
assert_eq!(manifest.resolved_locale, "en-US");
assert_eq!(
manifest.metadata.get("registry_version").expect("LMS-TEST: Missing key"),
"CIRCUIT_BREAKER"
);
assert_eq!(
manifest.metadata.get("circuit_breaker").expect("LMS-TEST: Missing key"),
"true"
);
assert!(manifest.rules.contains_key("NORMALIZATION_DEFAULT"));
}
}