use rusqlite::{params, Connection}; use serde::Serialize; use crate::common::format_population; pub fn cmd_sync_wiki(conn: &Connection, system_id: Option<&str>, wiki_dir: &str) { // Get list of systems to sync let system_ids: Vec = if let Some(sid) = system_id { vec![sid.to_string()] } else { let mut stmt = conn .prepare("SELECT DISTINCT system_id FROM bodies ORDER BY system_id") .unwrap(); stmt.query_map([], |row| row.get::<_, String>(0)) .unwrap() .filter_map(|r| r.ok()) .collect() }; let mut synced = 0; for sid in &system_ids { let sid_slug = sid.replace(' ', "-"); let wiki_path = std::path::Path::new(wiki_dir) .join(&sid_slug) .join("index.md"); if !wiki_path.exists() { eprintln!("skip: {} — no wiki page at {}", sid, wiki_path.display()); continue; } // Query bodies let mut body_stmt = conn .prepare( "SELECT body_id, proper_name, body_type, orbit_index, parent_body_id, inhabited, population, mass_class, atmosphere, surface_gravity, orbital_period_days, rotation_period_hours, planet_class, hydrosphere, economic_role, settlement_pattern, industrial_corridor FROM bodies WHERE system_id = ?1 ORDER BY CASE WHEN parent_body_id IS NULL THEN orbit_index ELSE 1000 + orbit_index END", ) .unwrap(); struct Body { id: String, name: Option, btype: String, orbit: i32, parent: Option, inhabited: bool, population: i64, mass_class: Option, atmosphere: Option, gravity: Option, orbital_days: Option, rotation_hours: Option, biome: Option, hydro: Option, econ: Option, settlement: Option, industrial: Option, } let bodies: Vec = body_stmt .query_map(params![sid], |row| { Ok(Body { id: row.get(0)?, name: row.get(1)?, btype: row.get(2)?, orbit: row.get::<_, Option>(3)?.unwrap_or(0), parent: row.get(4)?, inhabited: row.get::<_, i32>(5)? != 0, population: row.get::<_, Option>(6)?.unwrap_or(0), mass_class: row.get(7)?, atmosphere: row.get(8)?, gravity: row.get(9)?, orbital_days: row.get(10)?, rotation_hours: row.get(11)?, biome: row.get(12)?, hydro: row.get(13)?, econ: row.get(14)?, settlement: row.get(15)?, industrial: row.get(16)?, }) }) .unwrap() .filter_map(|r| r.ok()) .collect(); // Query stations let mut station_stmt = conn .prepare( "SELECT station_id, proper_name, orbits_body_id, station_type, population, economic_role, governance_type, docking_class, has_gate_infrastructure, district_count FROM stations WHERE system_id = ?1 ORDER BY station_id", ) .unwrap(); struct Station { id: String, name: Option, orbits: Option, stype: String, population: i64, econ: Option, governance: Option, docking: Option, gate: bool, districts: i32, } let stations: Vec = station_stmt .query_map(params![sid], |row| { Ok(Station { id: row.get(0)?, name: row.get(1)?, orbits: row.get(2)?, stype: row.get(3)?, population: row.get::<_, Option>(4)?.unwrap_or(0), econ: row.get(5)?, governance: row.get(6)?, docking: row.get(7)?, gate: row.get::<_, Option>(8)?.unwrap_or(0) != 0, districts: row.get::<_, Option>(9)?.unwrap_or(1), }) }) .unwrap() .filter_map(|r| r.ok()) .collect(); if bodies.is_empty() && stations.is_empty() { continue; } // Build the Celestial Bodies section let mut section = String::new(); section.push_str("## Celestial Bodies\n"); section .push_str("\n\n"); // Bodies table if !bodies.is_empty() { section.push_str("| Orbit | ID | Name | Type | Inhabited | Pop | Mass | Gravity | Year (d) | Day (h) | Atmo | Biome | Hydro | Economy | Settlement | Industrial |\n"); section.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n"); // First pass: top-level bodies (no parent) for b in &bodies { if b.parent.is_some() { continue; } let name = b.name.as_deref().unwrap_or("—"); let pop = if b.population > 0 { format_population(b.population) } else { "—".to_string() }; let grav = b .gravity .map(|g| format!("{:.2}g", g)) .unwrap_or_else(|| "—".to_string()); let year = b .orbital_days .map(|d| format!("{:.0}", d)) .unwrap_or_else(|| "—".to_string()); let day = b .rotation_hours .map(|h| format!("{:.1}", h)) .unwrap_or_else(|| "—".to_string()); section.push_str(&format!( "| {} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", b.orbit, b.id, name, b.btype, if b.inhabited { "yes" } else { "no" }, pop, b.mass_class.as_deref().unwrap_or("—"), grav, year, day, b.atmosphere.as_deref().unwrap_or("—"), b.biome.as_deref().unwrap_or("—"), b.hydro.as_deref().unwrap_or("—"), b.econ.as_deref().unwrap_or("—"), b.settlement.as_deref().unwrap_or("—"), b.industrial.as_deref().unwrap_or("—"), )); // Child bodies (moons) for m in &bodies { if m.parent.as_deref() == Some(&b.id) { let mname = m.name.as_deref().unwrap_or("—"); let mpop = if m.population > 0 { format_population(m.population) } else { "—".to_string() }; let mgrav = m .gravity .map(|g| format!("{:.2}g", g)) .unwrap_or_else(|| "—".to_string()); let myear = m .orbital_days .map(|d| format!("{:.0}", d)) .unwrap_or_else(|| "—".to_string()); let mday = m .rotation_hours .map(|h| format!("{:.1}", h)) .unwrap_or_else(|| "—".to_string()); section.push_str(&format!( "| ↳ {}.{} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", b.orbit, m.orbit, m.id, mname, m.btype, if m.inhabited { "yes" } else { "no" }, mpop, m.mass_class.as_deref().unwrap_or("—"), mgrav, myear, mday, m.atmosphere.as_deref().unwrap_or("—"), m.biome.as_deref().unwrap_or("—"), m.hydro.as_deref().unwrap_or("—"), m.econ.as_deref().unwrap_or("—"), m.settlement.as_deref().unwrap_or("—"), m.industrial.as_deref().unwrap_or("—"), )); } } } section.push('\n'); } // Stations table if !stations.is_empty() { section.push_str("### Stations & Facilities\n\n"); section.push_str("| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts |\n"); section.push_str("|---|---|---|---|---|---|---|---|---|---|\n"); for s in &stations { let name = s.name.as_deref().unwrap_or("—"); let orbits = s.orbits.as_deref().unwrap_or("—"); let pop = if s.population > 0 { format_population(s.population) } else { "—".to_string() }; section.push_str(&format!( "| `{}` | {} | {} | `{}` | {} | {} | {} | {} | {} | {} |\n", s.id, name, s.stype, orbits, pop, s.econ.as_deref().unwrap_or("—"), s.governance.as_deref().unwrap_or("—"), s.docking.as_deref().unwrap_or("—"), if s.gate { "yes" } else { "no" }, s.districts, )); } section.push('\n'); } // Read current wiki content let content = std::fs::read_to_string(&wiki_path).unwrap(); // Replace or insert the Celestial Bodies section let marker_start = "## Celestial Bodies"; let new_content = if let Some(start_pos) = content.find(marker_start) { // Find the next ## heading after the section (or end of file) let after = &content[start_pos + marker_start.len()..]; let end_offset = after .find("\n## ") .map(|p| start_pos + marker_start.len() + p + 1) .unwrap_or(content.len()); format!( "{}{}\n{}", &content[..start_pos], section.trim_end(), &content[end_offset..] ) } else { // Insert before ## Topology if it exists, otherwise before end of file if let Some(topo_pos) = content.find("\n## Topology") { let insert_pos = topo_pos + 1; // after the newline format!( "{}{}\n\n{}", &content[..insert_pos], section.trim_end(), &content[insert_pos..] ) } else { // No topology section — append at end format!("{}\n{}", content.trim_end(), section) } }; std::fs::write(&wiki_path, new_content).unwrap(); eprintln!("synced: {} → {}", sid, wiki_path.display()); synced += 1; } #[derive(Serialize)] struct SyncResult { systems_synced: usize, } println!( "{}", serde_json::to_string_pretty(&SyncResult { systems_synced: synced }) .unwrap() ); }