//! Seeded population of a baked floor: which rooms hold monsters, what they
//! are, and where the loot sits. This is the roguelike half of the design —
//! the floor *geometry* is fixed (it was baked to pixel art ahead of time),
//! but every run rolls its own inhabitants from the run seed.
//!
//! All randomness flows through `combat_core::rng::Rng` forks keyed by floor,
//! so placement is bit-reproducible per (run_seed, floor) and adding a roll to
//! one floor never reshuffles another.

use crate::manifest::Manifest;
use combat_core::config::CombatConfig;
use combat_core::rng::Rng;
use std::collections::{BTreeMap, BTreeSet};

/// Tunables that aren't combat-table balance (those live in config.json).
/// Basis points are of the player's max vigor unless noted.
pub mod balance {
    /// Fill regained per room stepped into — walking is catching your breath.
    pub const STEP_FILL_BP: i64 = 1200;
    /// Fatigue recovered per room stepped into — slow attrition relief so a
    /// deep run wears you down without death-spiralling.
    pub const STEP_FATIGUE_BP: i64 = 350;
    /// Vigor draught: restores fill on the spot (out of combat).
    pub const DRAUGHT_FILL_BP: i64 = 4500;
    /// Stamina tonic: clears fatigue, raising the ceiling back up.
    pub const TONIC_FATIGUE_BP: i64 = 5000;
    /// Breaking off a fight costs ceiling — you escape, but it marks you.
    pub const FLEE_FATIGUE_BP: i64 = 800;
    /// Each relic permanently widens the pool (displayed units).
    pub const RELIC_MAX_VIGOR: i64 = 15;
    pub const START_DRAUGHTS: i32 = 2;
    pub const START_TONICS: i32 = 1;
    /// Monster levels shift with depth; the player gains a level per floor
    /// descended, holding a hero's edge over trash — depth pressure comes
    /// from nastier kits and packs, not stat inflation.
    pub const FLOOR_LEVEL_DELTA: [i32; 3] = [-2, -1, 0];
}

/// A monster standing in a room, resolved against the combat config at
/// placement time so views don't need a config lookup.
#[derive(Clone, Debug)]
pub struct Spawn {
    pub archetype: String,
    pub name: String,
    pub level: i32,
}

#[derive(Clone, Debug)]
pub struct Treasure {
    pub gold: i64,
    /// `"draught" | "tonic" | "relic"` — or None for plain coin.
    pub item: Option<&'static str>,
}

/// A populated floor: baked geometry + this run's inhabitants.
#[derive(Debug)]
pub struct Floor {
    pub manifest: Manifest,
    pub exit_room: usize,
    pub monsters: BTreeMap<usize, Vec<Spawn>>,
    pub treasure: BTreeMap<usize, Treasure>,
    pub visited: BTreeSet<usize>,
    pub has_boss: bool,
}

/// Chance (bp) that a room of this theme houses monsters. The entrance
/// (threshold) is always safe ground; dens are almost never empty.
fn monster_chance_bp(theme: Option<&str>) -> i64 {
    match theme.unwrap_or("stone") {
        "threshold" => 0,
        "den" => 8000,
        "crypt" => 6000,
        "throne" => 6000,
        "forge" => 5500,
        "vault" => 4500,
        "library" => 4500,
        _ => 5000, // stone, hall, cistern
    }
}

/// Chance (bp) that a room of this theme holds treasure.
fn treasure_chance_bp(theme: Option<&str>) -> i64 {
    match theme.unwrap_or("stone") {
        "threshold" => 0,
        "vault" => 9000,
        "throne" => 7500,
        "library" => 6000,
        "crypt" => 4500,
        "den" => 2500,
        _ => 3000,
    }
}

/// Weighted archetype pools per floor index. Weights are relative ints.
fn pool(floor_idx: usize) -> &'static [(&'static str, i32)] {
    match floor_idx {
        // Floor 1 is vermin and bones — the curse-slinging wraith debuts deeper.
        0 => &[("rat", 55), ("skeleton", 45)],
        1 => &[("skeleton", 30), ("wraith", 30), ("troll", 30), ("rat", 10)],
        _ => &[
            ("troll", 30),
            ("wraith", 30),
            ("golem", 25),
            ("skeleton", 15),
        ],
    }
}

/// Theme nudges: a crypt leans undead, a den leans beasts. Multiplies the
/// base weight when the archetype suits the room.
fn theme_weight(theme: Option<&str>, archetype: &str) -> i32 {
    match (theme.unwrap_or("stone"), archetype) {
        ("crypt", "skeleton") | ("crypt", "wraith") => 3,
        ("den", "rat") | ("den", "troll") => 3,
        ("vault", "golem") | ("throne", "golem") | ("forge", "golem") => 3,
        _ => 1,
    }
}

fn pick_archetype(rng: &mut Rng, floor_idx: usize, theme: Option<&str>) -> &'static str {
    let weighted: Vec<(&str, i32)> = pool(floor_idx)
        .iter()
        .map(|(a, w)| (*a, w * theme_weight(theme, a)))
        .collect();
    let total: i32 = weighted.iter().map(|(_, w)| w).sum();
    let mut roll = rng.range(0, total);
    for (a, w) in &weighted {
        roll -= w;
        if roll < 0 {
            return a;
        }
    }
    weighted.last().unwrap().0
}

fn spawn_of(config: &CombatConfig, archetype: &str, level_delta: i32) -> Option<Spawn> {
    let sb = config.stat_blocks.get(archetype)?;
    Some(Spawn {
        archetype: archetype.to_string(),
        name: sb.name.clone(),
        level: sb.level + level_delta,
    })
}

/// Populate a baked floor for this run. `rng` must be a fork keyed by the
/// floor so floors roll independently.
pub fn build_floor(
    manifest: Manifest,
    floor_idx: usize,
    is_last: bool,
    config: &CombatConfig,
    rng: &mut Rng,
) -> Floor {
    let exit_room = manifest.exit_room();
    let start = manifest.start_room;
    let delta = *balance::FLOOR_LEVEL_DELTA
        .get(floor_idx)
        .unwrap_or(balance::FLOOR_LEVEL_DELTA.last().unwrap());

    let mut monsters: BTreeMap<usize, Vec<Spawn>> = BTreeMap::new();
    let mut treasure: BTreeMap<usize, Treasure> = BTreeMap::new();

    // Rooms in id order so the roll sequence is stable.
    let mut rooms: Vec<_> = manifest.rooms.clone();
    rooms.sort_by_key(|r| r.id);

    for room in &rooms {
        if room.id == start {
            continue;
        }
        let theme = room.theme.as_deref();

        // The final floor's deepest room belongs to the boss alone.
        if is_last && room.id == exit_room {
            if let Some(boss) = spawn_of(config, "boss", 0) {
                monsters.insert(room.id, vec![boss]);
            }
            continue;
        }

        if rng.chance_bp(monster_chance_bp(theme)) {
            let arch = pick_archetype(rng, floor_idx, theme);
            let mut pack = Vec::new();
            if let Some(s) = spawn_of(config, arch, delta) {
                pack.push(s);
            }
            // Sometimes a straggler joins — small fry so 2v1 stays fair.
            // Never on floor 1: a pack as your opening fight is a noob-killer;
            // the action-economy lesson waits for the Drowned Halls.
            let straggler = if floor_idx < 2 { "rat" } else { "skeleton" };
            if floor_idx > 0 && arch != straggler && rng.chance_bp(2200) {
                if let Some(s) = spawn_of(config, straggler, delta) {
                    pack.push(s);
                }
            }
            if !pack.is_empty() {
                monsters.insert(room.id, pack);
            }
        }

        if rng.chance_bp(treasure_chance_bp(theme)) {
            let gold = 8 + (floor_idx as i64) * 8 + rng.range(0, 13) as i64;
            let roll = rng.range(0, 10_000);
            let item = if roll < 3000 {
                Some("draught")
            } else if roll < 5000 {
                Some("tonic")
            } else if roll < 6200 {
                Some("relic")
            } else {
                None
            };
            treasure.insert(room.id, Treasure { gold, item });
        }
    }

    let mut visited = BTreeSet::new();
    visited.insert(start);

    Floor {
        manifest,
        exit_room,
        monsters,
        treasure,
        visited,
        has_boss: is_last,
    }
}
