//! The run: REIKHELM: DESCENT's whole game state machine.
//!
//! A run is three baked floors deep. The player occupies a (room, facing) on
//! the current floor and moves room-to-room over the manifest's nav graph —
//! exactly the moves the pre-baked atlas has frames for. Stepping into an
//! occupied room hands control to an embedded `combat_core::Encounter`; the
//! player's vigor and fatigue persist across fights as basis points of their
//! pool, so a run is one long attrition curve punctuated by duels. The deepest
//! room of the last floor holds the boss; killing it wins the run. Death is
//! permanent — that's what makes the drink-or-descend choices real.

use crate::manifest::{Dir, Manifest};
use crate::spawn::{balance, build_floor, Floor, Spawn};
use crate::views;
use combat_core::actor::compile_actor;
use combat_core::config::{CombatConfig, Rules};
use combat_core::controller::{Action, Controller, HumanController, HumanHandle};
use combat_core::engine::{Encounter, EncounterOptions};
use combat_core::event::Outcome;
use combat_core::rng::Rng;
use serde::Serialize;

const BP_ONE: i64 = 10_000;

/// splitmix64 — derives per-fight encounter seeds from run coordinates so a
/// re-fought room (after fleeing) rolls fresh variance.
fn mix(mut z: u64) -> u64 {
    z = z.wrapping_add(0x9E37_79B9_7F4A_7C15);
    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
    z ^ (z >> 31)
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Mode {
    Explore,
    Combat,
    Dead,
    Victory,
}

impl Mode {
    pub fn label(self) -> &'static str {
        match self {
            Mode::Explore => "explore",
            Mode::Combat => "combat",
            Mode::Dead => "dead",
            Mode::Victory => "victory",
        }
    }
}

/// What just happened, drained into every state payload for the UI to narrate.
#[derive(Serialize, Clone, Debug)]
#[serde(tag = "t", rename_all = "snake_case")]
pub enum GameEvent {
    Moved { room: usize },
    Bump,
    Encounter { monsters: Vec<String> },
    Loot { gold: i64, item: Option<String> },
    ItemUsed { kind: String, ok: bool },
    Rested,
    WinFight { gold: i64, drops: Vec<String> },
    Fled,
    Descend { floor: usize },
    Relic { total: i32 },
    PlayerDied { by: Option<String> },
    Victory,
}

#[derive(Default, Serialize, Clone, Debug)]
pub struct RunStats {
    pub fights: u32,
    pub kills: u32,
    pub gold_looted: i64,
    pub rooms_explored: u32,
    pub flees: u32,
    pub potions_used: u32,
    pub combat_ticks: u64,
    pub deepest_floor: usize,
}

/// The hero, between fights. Vigor/fatigue live as bp of max so they survive
/// pool-size changes (a relic heals proportionally — finding one feels good).
#[derive(Debug)]
pub struct Player {
    pub archetype: String,
    pub vigor_bp: i64,
    pub fatigue_bp: i64,
    pub relics: i32,
    pub gold: i64,
    pub draughts: i32,
    pub tonics: i32,
    pub auto_attack: bool,
}

impl Player {
    fn cap_bp(&self) -> i64 {
        (BP_ONE - self.fatigue_bp).max(0)
    }

    fn clamp(&mut self) {
        self.fatigue_bp = self.fatigue_bp.clamp(0, BP_ONE);
        self.vigor_bp = self.vigor_bp.clamp(0, self.cap_bp());
    }
}

/// A live battle: the encounter plus the player's intent handle and where it
/// is happening (so victory knows which room to clear).
pub struct Battle {
    pub enc: Encounter,
    pub human: HumanHandle,
    pub room: usize,
}

pub struct GameRun {
    pub config: CombatConfig,
    pub rules: Rules,
    pub run_seed: u64,
    pub floors: Vec<Floor>,
    pub floor_idx: usize,
    pub room: usize,
    pub prev_room: usize,
    pub facing: Dir,
    pub mode: Mode,
    pub battle: Option<Battle>,
    pub player: Player,
    pub stats: RunStats,
    pub events: Vec<GameEvent>,
    /// Combat events drained on the most recent `combat_tick`, for floaters.
    pub combat_events: Vec<combat_core::event::Event>,
    /// Set on the tick a fight resolves so the final frame still renders the
    /// battle (death animations, last hit floater) before `battle` drops.
    pub last_outcome: Option<String>,
}

impl GameRun {
    pub fn new(
        config_json: &str,
        manifests_json: &str,
        archetype: &str,
        run_seed: u64,
    ) -> Result<GameRun, String> {
        let config: CombatConfig =
            serde_json::from_str(config_json).map_err(|e| format!("bad config json: {e}"))?;
        if !config.stat_blocks.contains_key(archetype) {
            return Err(format!("unknown archetype {archetype:?}"));
        }
        let rules = config.compile();

        let manifests: Vec<serde_json::Value> =
            serde_json::from_str(manifests_json).map_err(|e| format!("bad manifests json: {e}"))?;
        if manifests.is_empty() {
            return Err("no floor manifests".to_string());
        }

        let root = Rng::from_seed(run_seed);
        let mut floors = Vec::new();
        let last = manifests.len() - 1;
        for (i, m) in manifests.iter().enumerate() {
            let manifest = Manifest::parse(&m.to_string())?;
            let mut rng = root.fork(&format!("floor{i}:spawn"));
            floors.push(build_floor(manifest, i, i == last, &config, &mut rng));
        }

        let start = floors[0].manifest.start_room;
        let facing = first_open_facing(&floors[0], start);
        Ok(GameRun {
            config,
            rules,
            run_seed,
            floors,
            floor_idx: 0,
            room: start,
            prev_room: start,
            facing,
            mode: Mode::Explore,
            battle: None,
            player: Player {
                archetype: archetype.to_string(),
                vigor_bp: BP_ONE,
                fatigue_bp: 0,
                relics: 0,
                gold: 0,
                draughts: balance::START_DRAUGHTS,
                tonics: balance::START_TONICS,
                auto_attack: true,
            },
            stats: RunStats::default(),
            events: Vec::new(),
            combat_events: Vec::new(),
            last_outcome: None,
        })
    }

    pub fn floor(&self) -> &Floor {
        &self.floors[self.floor_idx]
    }

    fn floor_mut(&mut self) -> &mut Floor {
        &mut self.floors[self.floor_idx]
    }

    /// The player's effective level: hardened by one per floor descended.
    pub fn player_level(&self) -> i32 {
        let base = self.config.stat_blocks[&self.player.archetype].level;
        base + self.floor_idx as i32
    }

    /// The player's pool size: archetype base widened by relics.
    pub fn player_max_vigor(&self) -> i64 {
        let base = self.config.stat_blocks[&self.player.archetype].max_vigor;
        base + self.player.relics as i64 * balance::RELIC_MAX_VIGOR
    }

    // ----------------------------------------------------------------- //
    // Exploration
    // ----------------------------------------------------------------- //

    pub fn turn(&mut self, delta: i32) {
        if self.mode != Mode::Explore {
            return;
        }
        self.facing = self.facing.turned(delta);
    }

    pub fn step(&mut self, forward: bool) {
        if self.mode != Mode::Explore {
            return;
        }
        let dir = if forward {
            self.facing
        } else {
            self.facing.opposite()
        };
        let target = {
            let by_id = self.floor().manifest.by_id();
            by_id.get(&self.room).and_then(|r| r.nav.get(dir))
        };
        let Some(target) = target else {
            self.events.push(GameEvent::Bump);
            return;
        };

        self.prev_room = self.room;
        self.room = target;
        if self.floor_mut().visited.insert(target) {
            self.stats.rooms_explored += 1;
        }
        self.events.push(GameEvent::Moved { room: target });

        // Walking is rest: catch some breath, shed a little weariness.
        self.player.vigor_bp += balance::STEP_FILL_BP;
        self.player.fatigue_bp -= balance::STEP_FATIGUE_BP;
        self.player.clamp();

        if let Some(t) = self.floor_mut().treasure.remove(&target) {
            self.player.gold += t.gold;
            self.stats.gold_looted += t.gold;
            match t.item {
                Some("draught") => self.player.draughts += 1,
                Some("tonic") => self.player.tonics += 1,
                Some("relic") => {
                    self.player.relics += 1;
                    self.events.push(GameEvent::Relic {
                        total: self.player.relics,
                    });
                }
                _ => {}
            }
            self.events.push(GameEvent::Loot {
                gold: t.gold,
                item: t.item.map(|s| s.to_string()),
            });
        }

        if self.floor().monsters.contains_key(&target) {
            self.start_battle(target);
        }
    }

    pub fn descend(&mut self) {
        if self.mode != Mode::Explore || self.room != self.floor().exit_room {
            return;
        }
        // The last floor's exit holds the boss, so standing here in Explore
        // mode means it is already dead — but descend past it is meaningless.
        if self.floor_idx + 1 >= self.floors.len() {
            return;
        }
        self.floor_idx += 1;
        self.stats.deepest_floor = self.stats.deepest_floor.max(self.floor_idx);
        self.room = self.floor().manifest.start_room;
        self.prev_room = self.room;
        self.facing = first_open_facing(self.floor(), self.room);
        let here = self.room;
        self.floor_mut().visited.insert(here);
        self.events.push(GameEvent::Descend {
            floor: self.floor_idx,
        });
    }

    /// Catch your breath where you stand: the same recovery as a step, no
    /// movement. Holding the key is the crawler's campfire.
    pub fn rest(&mut self) {
        if self.mode != Mode::Explore {
            return;
        }
        self.player.vigor_bp += balance::STEP_FILL_BP;
        self.player.fatigue_bp -= balance::STEP_FATIGUE_BP;
        self.player.clamp();
        self.events.push(GameEvent::Rested);
    }

    pub fn use_item(&mut self, kind: &str) {
        // Quaffing mid-melee isn't a thing; Second Wind is the in-combat verb.
        if self.mode != Mode::Explore {
            self.events.push(GameEvent::ItemUsed {
                kind: kind.to_string(),
                ok: false,
            });
            return;
        }
        let ok = match kind {
            "draught" if self.player.draughts > 0 => {
                self.player.draughts -= 1;
                self.player.vigor_bp += balance::DRAUGHT_FILL_BP;
                true
            }
            "tonic" if self.player.tonics > 0 => {
                self.player.tonics -= 1;
                self.player.fatigue_bp -= balance::TONIC_FATIGUE_BP;
                true
            }
            _ => false,
        };
        if ok {
            self.stats.potions_used += 1;
            self.player.clamp();
        }
        self.events.push(GameEvent::ItemUsed {
            kind: kind.to_string(),
            ok,
        });
    }

    // ----------------------------------------------------------------- //
    // Combat
    // ----------------------------------------------------------------- //

    fn start_battle(&mut self, room: usize) {
        let spawns: Vec<Spawn> = self.floor().monsters[&room].clone();
        self.events.push(GameEvent::Encounter {
            monsters: spawns.iter().map(|s| s.name.clone()).collect(),
        });

        // Player actor: archetype block at run-current level/pool/condition.
        let mut psb = self.config.stat_blocks[&self.player.archetype].clone();
        psb.team = 0;
        psb.level = self.player_level();
        psb.max_vigor = self.player_max_vigor();
        psb.start_vigor_bp = self.player.vigor_bp;
        psb.start_fatigue_bp = self.player.fatigue_bp;

        let mut actors = Vec::new();
        let mut controllers: Vec<Box<dyn Controller>> = Vec::new();

        let (loadout, _) = self.rules.resolve_loadout(&psb.loadout);
        actors.push(compile_actor(0, &psb, loadout, self.rules.tick_rate));
        let human = HumanController::new();
        let handle = human.handle();
        {
            let mut h = handle.borrow_mut();
            h.target = Some(1);
            h.auto_attack = self.player.auto_attack;
        }
        controllers.push(Box::new(human));

        for (i, spawn) in spawns.iter().enumerate() {
            let mut sb = self.config.stat_blocks[&spawn.archetype].clone();
            sb.team = 1;
            sb.level = spawn.level;
            let (loadout, _) = self.rules.resolve_loadout(&sb.loadout);
            actors.push(compile_actor(
                (i + 1) as u32,
                &sb,
                loadout,
                self.rules.tick_rate,
            ));
            controllers.push(Box::new(crate::monster_ai::MonsterAI));
        }

        let seed = mix(self.run_seed
            ^ mix((self.floor_idx as u64) << 32 | room as u64)
            ^ mix(self.stats.fights as u64 + 1));
        let opts = EncounterOptions {
            max_ticks: 100_000,
            log_events: true,
            sample_every: 0,
        };
        let enc = Encounter::new(actors, controllers, self.rules.clone(), seed, opts);

        self.stats.fights += 1;
        self.battle = Some(Battle {
            enc,
            human: handle,
            room,
        });
        self.mode = Mode::Combat;
        self.last_outcome = None;
    }

    /// Advance the battle one tick (the front-end calls this at tick rate).
    pub fn combat_tick(&mut self) {
        if self.mode != Mode::Combat {
            return;
        }
        let Some(battle) = self.battle.as_mut() else {
            return;
        };
        let outcome = battle.enc.tick_once();
        self.combat_events = battle.enc.drain_events();
        self.stats.combat_ticks += 1;
        if let Some(o) = outcome {
            self.finish_battle(o);
        }
    }

    fn finish_battle(&mut self, outcome: Outcome) {
        let battle = self.battle.as_ref().unwrap();
        let room = battle.room;
        let won = matches!(outcome, Outcome::Win { team: 0 });

        // Persist the hero's condition exactly as the fight left it.
        let me = &battle.enc.actors[0];
        let max = me.max_vigor.max(1);
        self.player.vigor_bp = (me.vigor as i128 * BP_ONE as i128 / max as i128) as i64;
        self.player.fatigue_bp = (me.fatigue as i128 * BP_ONE as i128 / max as i128) as i64;
        self.player.clamp();

        if won {
            let slain = self.floor_mut().monsters.remove(&room).unwrap_or_default();
            self.stats.kills += slain.len() as u32;

            // Spoils: gold per kill plus a chance of a potion drop, all from a
            // fight-keyed fork so outcomes don't disturb placement streams.
            let mut rng = Rng::from_seed(self.run_seed).fork(&format!(
                "loot:f{}:r{}:k{}",
                self.floor_idx, room, self.stats.kills
            ));
            let mut gold = 0;
            let mut drops: Vec<String> = Vec::new();
            for s in &slain {
                gold += 6 + s.level as i64 + rng.range(0, 9) as i64;
                if rng.chance_bp(2000) {
                    self.player.draughts += 1;
                    drops.push("draught".to_string());
                } else if rng.chance_bp(1500) {
                    self.player.tonics += 1;
                    drops.push("tonic".to_string());
                }
            }
            self.player.gold += gold;
            self.stats.gold_looted += gold;
            self.events.push(GameEvent::WinFight { gold, drops });
            self.last_outcome = Some("win_player".to_string());

            let boss_down = self.floor().has_boss && room == self.floor().exit_room;
            if boss_down {
                self.mode = Mode::Victory;
                self.events.push(GameEvent::Victory);
            } else {
                self.mode = Mode::Explore;
            }
        } else {
            let by = battle
                .enc
                .actors
                .iter()
                .find(|a| a.team == 1 && a.is_active())
                .map(|a| a.name.clone());
            self.last_outcome = Some("win_enemy".to_string());
            self.mode = Mode::Dead;
            self.events.push(GameEvent::PlayerDied { by });
        }
        // Keep `battle` alive for this one payload (final frame renders the
        // killing blow); the next API call clears it.
    }

    /// Drop a finished battle before handling the next command.
    pub fn settle(&mut self) {
        if self.mode != Mode::Combat && self.last_outcome.is_some() {
            self.battle = None;
        }
    }

    pub fn use_ability(&mut self, ability_id: &str, target: i32) {
        let Some(battle) = self.battle.as_mut() else {
            return;
        };
        if self.mode != Mode::Combat {
            return;
        }
        let Some(ability) = self.rules.abilities.get(ability_id).cloned() else {
            return;
        };
        let targets = if ability.is_support() {
            vec![0]
        } else if target >= 0 {
            vec![target as u32]
        } else {
            vec![battle.human.borrow().target.unwrap_or(1)]
        };
        battle.human.borrow_mut().queued = Some(Action::Use {
            ability_id: ability_id.to_string(),
            targets,
        });
    }

    pub fn set_auto_attack(&mut self, on: bool) {
        self.player.auto_attack = on;
        if let Some(battle) = self.battle.as_mut() {
            battle.human.borrow_mut().auto_attack = on;
        }
    }

    pub fn set_target(&mut self, target: u32) {
        if let Some(battle) = self.battle.as_mut() {
            battle.human.borrow_mut().target = Some(target);
        }
    }

    /// Break off the fight: back out the way you came, paying in ceiling.
    pub fn flee(&mut self) {
        if self.mode != Mode::Combat {
            return;
        }
        let Some(battle) = self.battle.take() else {
            return;
        };

        // Persist condition as the fight stands, then pay the price.
        let me = &battle.enc.actors[0];
        let max = me.max_vigor.max(1);
        self.player.vigor_bp = (me.vigor as i128 * BP_ONE as i128 / max as i128) as i64;
        self.player.fatigue_bp =
            (me.fatigue as i128 * BP_ONE as i128 / max as i128) as i64 + balance::FLEE_FATIGUE_BP;
        self.player.clamp();

        self.stats.flees += 1;
        self.room = self.prev_room;
        self.mode = Mode::Explore;
        self.last_outcome = None;
        self.events.push(GameEvent::Fled);
    }

    // ----------------------------------------------------------------- //
    // State payload
    // ----------------------------------------------------------------- //

    pub fn state_json(&mut self) -> String {
        views::state_json(self)
    }
}

fn first_open_facing(floor: &Floor, room: usize) -> Dir {
    let by_id = floor.manifest.by_id();
    if let Some(r) = by_id.get(&room) {
        for d in crate::manifest::DIRS {
            if r.nav.get(d).is_some() {
                return d;
            }
        }
    }
    Dir::N
}
