use crate::extractors::GameId; use axum::{extract::State, Json}; use rand::{Rng, SeedableRng}; use serde_json::{json, Value}; use crate::{ app::AppState, error::AppResult, models::season::{PROMOTION_PTS, RELEGATION_PTS, SEASON_LENGTH}, services::{club as club_svc, profile as profile_svc, season as season_svc}, }; pub async fn get_division(State(state): State, game: GameId) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let _club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let season = season_svc::get_or_create(&state.pool, &profile.id).await?; Ok(Json(json!({ "division": season.division, "season_number": season.season_number, "season_points": season.season_points, "matches_played": season.matches_played, "matches_remaining": season.matches_remaining(), "season_length": SEASON_LENGTH, "promotion_pts": PROMOTION_PTS, "relegation_pts": RELEGATION_PTS, "pts_above_safe": season.pts_above_safe(), "promotion_achievable": season.promotion_achievable(), "can_be_relegated": season.can_be_relegated(), "record": { "wins": season.wins, "draws": season.draws, "losses": season.losses, }, "pts_for_promotion": season.pts_for_promotion(), "started_at": season.started_at, }))) } pub async fn get_division_history( State(state): State, game: GameId, ) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let history = season_svc::get_history(&state.pool, &profile.id).await?; Ok(Json(json!({ "history": history, "total": history.len() }))) } pub async fn get_division_leaderboard( State(state): State, game: GameId, ) -> AppResult> { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; let season = season_svc::get_or_create(&state.pool, &profile.id).await?; // Seed from division + season_number so the NPC table is stable within a season let seed = (season.division as u64) * 1000 + season.season_number as u64; let mut rng = rand::rngs::SmallRng::seed_from_u64(seed); const NPC_NAMES: &[&str] = &[ "Riverside FC", "City Athletic", "County United", "Valley Rangers", "Harbor Town FC", "Mountside City", "Lakewood Athletic", "Eastbrook United", "Westfield Rovers", "Northgate FC", "Southport Athletic", "Ironbridge City", "Milldale United", "Hillcrest Rangers", "Bayside FC", "Thornfield Athletic", "Greenhill United", "Coldwater City", "Redbury Rangers", "Ashdown FC", ]; // Pick 9 NPC names without repetition using the seeded RNG let mut name_indices: Vec = (0..NPC_NAMES.len()).collect(); name_indices.sort_by_key(|&_i| rng.gen::()); let npc_names: Vec<&str> = name_indices[..9].iter().map(|&i| NPC_NAMES[i]).collect(); // Generate NPC records: clubs in top of table have more wins, bottom have more losses let matches_played = season.matches_played; let npc_matches = matches_played.max(1); // NPC clubs play same number of matches as player let mut table: Vec = npc_names .iter() .enumerate() .map(|(idx, name)| { // Quality bias: index 0-2 = stronger, 6-8 = weaker let quality: f64 = 1.0 - (idx as f64 / 8.0); // 1.0 → 0.0 let expected_win_rate = 0.2 + quality * 0.6; // 0.2–0.8 let wins = (npc_matches as f64 * expected_win_rate * (0.8 + rng.gen::() * 0.4)) as i64; let losses = (npc_matches as f64 * (1.0 - expected_win_rate) * (0.8 + rng.gen::() * 0.4)) as i64; let draws = (npc_matches - wins - losses).max(0); let pts = wins * 3 + draws; json!({ "club_name": name, "wins": wins, "draws": draws, "losses": losses, "pts": pts, "matches_played": npc_matches, "is_player": false, }) }) .collect(); // Add player club row let player_pts = season.wins * 3 + season.draws; table.push(json!({ "club_name": club.name, "wins": season.wins, "draws": season.draws, "losses": season.losses, "pts": player_pts, "matches_played": season.matches_played, "is_player": true, })); // Sort by pts desc, then wins desc table.sort_by(|a, b| { let pts_a = a["pts"].as_i64().unwrap_or(0); let pts_b = b["pts"].as_i64().unwrap_or(0); pts_b.cmp(&pts_a).then_with(|| { let w_b = b["wins"].as_i64().unwrap_or(0); let w_a = a["wins"].as_i64().unwrap_or(0); w_b.cmp(&w_a) }) }); // Add position numbers let table_with_pos: Vec = table .into_iter() .enumerate() .map(|(i, mut entry)| { entry["position"] = json!(i as i64 + 1); entry }) .collect(); Ok(Json(json!({ "leaderboard": table_with_pos, "division": season.division, "season_number": season.season_number, "promotion_threshold": 3, // top 3 promote "relegation_threshold": 8, // bottom 2 relegate }))) }