use crate::db::{ConnectionPool, DbError, Row}; use crate::message_broker::MessageBroker; use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::{Duration, Instant}; use super::base::{BaseWorker, Worker, WorkerState}; pub struct ValueRecalculationWorker { base: BaseWorker, } // Produktwissen / Produktions-Logs const QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER: &str = r#" UPDATE falukant_data.knowledge k SET knowledge = LEAST(100, k.knowledge + 1) FROM falukant_data.character c JOIN falukant_log.production p ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' WHERE c.id = k.character_id AND c.user_id = 18 AND k.product_id = 10; "#; const QUERY_DELETE_OLD_PRODUCTIONS: &str = r#" DELETE FROM falukant_log.production flp WHERE DATE(flp.production_timestamp) < CURRENT_DATE; "#; const QUERY_GET_PRODUCERS_LAST_DAY: &str = r#" SELECT p.producer_id FROM falukant_log.production p WHERE DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' GROUP BY producer_id; "#; // Regionale Verkaufspreise const QUERY_UPDATE_REGION_SELL_PRICE: &str = r#" UPDATE falukant_data.town_product_worth tpw SET worth_percent = GREATEST( 0, LEAST( CASE WHEN s.quantity > avg_sells THEN tpw.worth_percent - 1 WHEN s.quantity < avg_sells THEN tpw.worth_percent + 1 ELSE tpw.worth_percent END, 100 ) ) FROM ( SELECT region_id, product_id, quantity, (SELECT AVG(quantity) FROM falukant_log.sell avs WHERE avs.product_id = s.product_id) AS avg_sells FROM falukant_log.sell s WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' ) s WHERE tpw.region_id = s.region_id AND tpw.product_id = s.product_id; "#; const QUERY_DELETE_REGION_SELL_PRICE: &str = r#" DELETE FROM falukant_log.sell s WHERE DATE(s.sell_timestamp) < CURRENT_DATE; "#; const QUERY_GET_SELL_REGIONS: &str = r#" SELECT s.region_id FROM falukant_log.sell s WHERE DATE(s.sell_timestamp) = CURRENT_DATE - INTERVAL '1 day' GROUP BY region_id; "#; // Ehen / Beziehungen const QUERY_SET_MARRIAGES_BY_PARTY: &str = r#" WITH updated_relations AS ( UPDATE falukant_data.relationship AS rel SET relationship_type_id = ( SELECT id FROM falukant_type.relationship AS rt WHERE rt.tr = 'married' ) WHERE rel.id IN ( SELECT rel2.id FROM falukant_data.party AS p JOIN falukant_type.party AS pt ON pt.id = p.party_type_id AND pt.tr = 'wedding' JOIN falukant_data.falukant_user AS fu ON fu.id = p.falukant_user_id JOIN falukant_data.character AS c ON c.user_id = fu.id JOIN falukant_data.relationship AS rel2 ON rel2.character1_id = c.id OR rel2.character2_id = c.id JOIN falukant_type.relationship AS rt2 ON rt2.id = rel2.relationship_type_id AND rt2.tr = 'engaged' WHERE p.created_at <= NOW() - INTERVAL '1 day' ) RETURNING character1_id, character2_id ) SELECT c1.user_id AS character1_user, c2.user_id AS character2_user FROM updated_relations AS ur JOIN falukant_data.character AS c1 ON c1.id = ur.character1_id JOIN falukant_data.character AS c2 ON c2.id = ur.character2_id; "#; // Lernen / Studium const QUERY_GET_STUDYINGS_TO_EXECUTE: &str = r#" SELECT l.id, l.associated_falukant_user_id, l.associated_learning_character_id, l.learn_all_products, l.learning_recipient_id, l.product_id, lr.tr FROM falukant_data.learning l JOIN falukant_type.learn_recipient lr ON lr.id = l.learning_recipient_id WHERE l.learning_is_executed = FALSE AND l.created_at + INTERVAL '1 day' < NOW(); "#; const QUERY_GET_OWN_CHARACTER_ID: &str = r#" SELECT id FROM falukant_data.character c WHERE c.user_id = $1; "#; const QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE: &str = r#" UPDATE falukant_data.knowledge k SET knowledge = LEAST(100, k.knowledge + $1) WHERE k.character_id = $2 AND k.product_id = $3; "#; const QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE: &str = r#" UPDATE falukant_data.knowledge k SET knowledge = LEAST(100, k.knowledge + $1) WHERE k.character_id = $2; "#; const QUERY_SET_LEARNING_DONE: &str = r#" UPDATE falukant_data.learning SET learning_is_executed = TRUE WHERE id = $1; "#; impl ValueRecalculationWorker { pub fn new(pool: ConnectionPool, broker: MessageBroker) -> Self { Self { base: BaseWorker::new("ValueRecalculationWorker", pool, broker), } } fn run_loop(pool: ConnectionPool, broker: MessageBroker, state: Arc) { // Wir nutzen hier einfach Intervall-Logik (täglich / halbtäglich), // statt exakte Uhrzeiten nachzubilden – Verhalten ist funktional ähnlich. let mut last_product = None; let mut last_sell_price = None; loop { if !state.running_worker.load(Ordering::Relaxed) { break; } let now = Instant::now(); // Produktwissen einmal täglich if should_run_interval(last_product, now, Duration::from_secs(24 * 3600)) { if let Err(err) = Self::calculate_product_knowledge_inner(&pool, &broker) { eprintln!("[ValueRecalculationWorker] Fehler in calculateProductKnowledge: {err}"); } last_product = Some(now); } // Regionale Verkaufspreise einmal täglich (gegen Mittag) if should_run_interval(last_sell_price, now, Duration::from_secs(24 * 3600)) { if let Err(err) = Self::calculate_regional_sell_price_inner(&pool, &broker) { eprintln!("[ValueRecalculationWorker] Fehler in calculateRegionalSellPrice: {err}"); } last_sell_price = Some(now); } // Ehen & Studium bei jedem Durchlauf if let Err(err) = Self::calculate_marriages_inner(&pool, &broker) { eprintln!("[ValueRecalculationWorker] Fehler in calculateMarriages: {err}"); } if let Err(err) = Self::calculate_studying_inner(&pool, &broker) { eprintln!("[ValueRecalculationWorker] Fehler in calculateStudying: {err}"); } // 60-Sekunden-Wartezeit in kurze Scheiben aufteilen, damit ein Shutdown // (running_worker = false) schnell greift. const SLICE_MS: u64 = 500; let total_ms = 60_000; let mut slept = 0; while slept < total_ms { if !state.running_worker.load(Ordering::Relaxed) { break; } let remaining = total_ms - slept; let slice = SLICE_MS.min(remaining); std::thread::sleep(Duration::from_millis(slice)); slept += slice; } } } fn calculate_product_knowledge_inner( pool: &ConnectionPool, broker: &MessageBroker, ) -> Result<(), DbError> { let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare( "update_product_knowledge_user", QUERY_UPDATE_PRODUCT_KNOWLEDGE_USER, )?; conn.execute("update_product_knowledge_user", &[])?; conn.prepare("get_producers_last_day", QUERY_GET_PRODUCERS_LAST_DAY)?; let users = conn.execute("get_producers_last_day", &[])?; for row in users { if let Some(user_id) = row.get("producer_id").and_then(|v| v.parse::().ok()) { let message = format!(r#"{{"event":"price_update","user_id":{}}}"#, user_id); broker.publish(message); } } conn.prepare("delete_old_productions", QUERY_DELETE_OLD_PRODUCTIONS)?; conn.execute("delete_old_productions", &[])?; Ok(()) } fn calculate_regional_sell_price_inner( pool: &ConnectionPool, broker: &MessageBroker, ) -> Result<(), DbError> { let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare("update_region_sell_price", QUERY_UPDATE_REGION_SELL_PRICE)?; conn.execute("update_region_sell_price", &[])?; conn.prepare("get_sell_regions", QUERY_GET_SELL_REGIONS)?; let regions = conn.execute("get_sell_regions", &[])?; for row in regions { if let Some(region_id) = row.get("region_id").and_then(|v| v.parse::().ok()) { let message = format!(r#"{{"event":"price_update","region_id":{}}}"#, region_id); broker.publish(message); } } conn.prepare("delete_region_sell_price", QUERY_DELETE_REGION_SELL_PRICE)?; conn.execute("delete_region_sell_price", &[])?; Ok(()) } fn calculate_marriages_inner( pool: &ConnectionPool, broker: &MessageBroker, ) -> Result<(), DbError> { let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare("set_marriages_by_party", QUERY_SET_MARRIAGES_BY_PARTY)?; let rows = conn.execute("set_marriages_by_party", &[])?; for row in rows { if let Some(uid) = row.get("character1_user").and_then(|v| v.parse::().ok()) { let msg = format!(r#"{{"event":"relationship_changed","user_id":{}}}"#, uid); broker.publish(msg); } if let Some(uid) = row.get("character2_user").and_then(|v| v.parse::().ok()) { let msg = format!(r#"{{"event":"relationship_changed","user_id":{}}}"#, uid); broker.publish(msg); } } Ok(()) } fn calculate_studying_inner( pool: &ConnectionPool, broker: &MessageBroker, ) -> Result<(), DbError> { let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare( "get_studyings_to_execute", QUERY_GET_STUDYINGS_TO_EXECUTE, )?; conn.prepare("set_learning_done", QUERY_SET_LEARNING_DONE)?; let studies = conn.execute("get_studyings_to_execute", &[])?; for study in studies { let tr = study.get("tr").cloned().unwrap_or_default(); match tr.as_str() { "self" => Self::calculate_studying_self(pool, broker, &study)?, "children" | "director" => { Self::calculate_studying_for_associated_character( pool, broker, &study, )? } _ => {} } if let Some(id) = study.get("id").and_then(|v| v.parse::().ok()) { conn.execute("set_learning_done", &[&id])?; } } Ok(()) } fn calculate_studying_self( pool: &ConnectionPool, broker: &MessageBroker, entry: &Row, ) -> Result<(), DbError> { let falukant_user_id = match entry .get("associated_falukant_user_id") .and_then(|v| v.parse::().ok()) { Some(id) => id, None => return Ok(()), }; let (learn_all, product_id) = study_scope(entry); let character_id = Self::get_own_character_id(pool, falukant_user_id)?; if let Some(cid) = character_id { Self::calculate_studying_character( pool, broker, cid, learn_all, product_id, parse_i32(entry, "learning_recipient_id", -1), )?; } Ok(()) } fn calculate_studying_for_associated_character( pool: &ConnectionPool, broker: &MessageBroker, entry: &Row, ) -> Result<(), DbError> { let character_id = parse_i32(entry, "associated_learning_character_id", -1); if character_id < 0 { return Ok(()); } let (learn_all, product_id) = study_scope(entry); let recipient_id = parse_i32(entry, "learning_recipient_id", -1); Self::calculate_studying_character( pool, broker, character_id, learn_all, product_id, recipient_id, ) } fn get_own_character_id( pool: &ConnectionPool, falukant_user_id: i32, ) -> Result, DbError> { let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; conn.prepare("get_own_character_id", QUERY_GET_OWN_CHARACTER_ID)?; let rows = conn.execute("get_own_character_id", &[&falukant_user_id])?; Ok(rows .get(0) .and_then(|r| r.get("id")) .and_then(|v| v.parse::().ok())) } fn calculate_studying_character( pool: &ConnectionPool, broker: &MessageBroker, character_id: i32, learn_all: bool, product_id: Option, falukant_user_id: i32, ) -> Result<(), DbError> { let mut conn = pool .get() .map_err(|e| DbError::new(format!("DB-Verbindung fehlgeschlagen: {e}")))?; if learn_all { conn.prepare( "increase_all_products_knowledge", QUERY_INCREASE_ALL_PRODUCTS_KNOWLEDGE, )?; conn.execute( "increase_all_products_knowledge", &[&1_i32, &character_id], )?; } else if let Some(pid) = product_id { conn.prepare( "increase_one_product_knowledge", QUERY_INCREASE_ONE_PRODUCT_KNOWLEDGE, )?; conn.execute( "increase_one_product_knowledge", &[&5_i32, &character_id, &pid], )?; } let message = format!(r#"{{"event":"knowledge_updated","user_id":{}}}"#, falukant_user_id); broker.publish(message); Ok(()) } } impl Worker for ValueRecalculationWorker { fn start_worker_thread(&mut self) { let pool = self.base.pool.clone(); let broker = self.base.broker.clone(); self.base .start_worker_with_loop(move |state: Arc| { ValueRecalculationWorker::run_loop(pool.clone(), broker.clone(), state); }); } fn stop_worker_thread(&mut self) { self.base.stop_worker(); } fn enable_watchdog(&mut self) { self.base.start_watchdog(); } } fn should_run_interval( last_run: Option, now: Instant, interval: Duration, ) -> bool { match last_run { None => true, Some(prev) => now.saturating_duration_since(prev) >= interval, } } fn parse_i32(row: &Row, key: &str, default: i32) -> i32 { row.get(key) .and_then(|v| v.parse::().ok()) .unwrap_or(default) } fn study_scope(entry: &Row) -> (bool, Option) { let learn_all_flag = entry.get("learn_all_products").map(|v| v == "t").unwrap_or(false); let product_id_str = entry.get("product_id").cloned().unwrap_or_default(); if learn_all_flag || product_id_str.is_empty() { (true, None) } else { let pid = product_id_str.parse::().ok(); match pid { Some(id) => (false, Some(id)), None => (true, None), } } }