//! combat-wasm — a `wasm-bindgen` bridge that drives [`combat_core`] from a
//! browser front-end (the real-time consumer the spec designs for, §10/§14).
//!
//! The engine is unchanged: the player is a [`HumanController`] whose intent the
//! UI pushes between ticks, monsters run a small [`MonsterAI`], and the front-end
//! advances one tick per wall-clock frame via [`Game::tick`]. State crosses the
//! boundary as JSON strings (parsed with `JSON.parse` in JS) to keep the surface
//! tiny and dependency-light.

use combat_core::ability::Ability;
use combat_core::actor::{compile_actor, Actor, ActorId, StaggerState};
use combat_core::config::{CombatConfig, Rules};
use combat_core::controller::{Action, Controller, HumanController, HumanHandle, View};
use combat_core::effect::EffectKind;
use combat_core::engine::{Encounter, EncounterOptions};
use combat_core::event::{Event, Outcome};
use combat_core::rng::Rng;
use serde::Serialize;
use wasm_bindgen::prelude::*;

/// `1000` milli-units == 1 displayed unit.
fn to_units(milli: i64) -> f64 {
    milli as f64 / 1000.0
}

// --------------------------------------------------------------------------- //
// Monster AI — flavor brain so each archetype uses its signature kit.
// --------------------------------------------------------------------------- //

/// A simple but characterful enemy brain: fire the most expensive ability that's
/// usable right now (a monster's signature move — the wraith's curse, the golem's
/// heavy strike), otherwise auto-attack the player. Lives here, not in core, so
/// the engine stays policy-free.
#[derive(Debug, Default)]
struct MonsterAI;

impl MonsterAI {
    fn usable(view: &View, ab: &Ability) -> bool {
        ab.cost.fill <= view.me.vigor && !view.me.on_cooldown(&ab.id, view.tick)
    }
}

impl Controller for MonsterAI {
    fn decide(&mut self, view: &View, rng: &mut Rng) -> Action {
        let Some(target) = view.weakest_enemy() else {
            return Action::Idle;
        };

        // Signature move: priciest usable hostile ability that isn't the free
        // auto-attack (cost.fill == 0). Used on a coin-flip rather than on every
        // cooldown, so a monster's big hits are spaced out into a readable duel
        // instead of a relentless burst.
        let use_signature = rng.chance_bp(5500);
        let signature = view
            .me
            .loadout
            .iter()
            .filter(|a| a.is_offensive() && a.cost.fill > 0 && Self::usable(view, a))
            .max_by_key(|a| a.cost.fill);
        if let (true, Some(ab)) = (use_signature, signature) {
            // Friendly-tagged effects (a self-buff) target self; here all are
            // hostile, so aim at the player.
            return Action::Use { ability_id: ab.id.clone(), targets: vec![target.id] };
        }

        // Else swing: cheapest offensive ability.
        let auto = view
            .me
            .loadout
            .iter()
            .filter(|a| a.is_offensive())
            .min_by_key(|a| a.cost.fill);
        match auto {
            Some(ab) if Self::usable(view, ab) => {
                Action::Use { ability_id: ab.id.clone(), targets: vec![target.id] }
            }
            _ => Action::Idle,
        }
    }

    fn label(&self) -> &str {
        "monster"
    }
}

// --------------------------------------------------------------------------- //
// Serializable views handed to JS.
// --------------------------------------------------------------------------- //

#[derive(Serialize)]
struct CastView {
    ability: String,
    remaining: u64,
}

#[derive(Serialize)]
struct CooldownView {
    id: String,
    remaining: u64,
}

#[derive(Serialize)]
struct ActorView {
    id: ActorId,
    name: String,
    team: u8,
    level: i32,
    vigor: f64,
    cap: f64,
    fatigue: f64,
    max_vigor: f64,
    stagger: String,
    casting: Option<CastView>,
    statuses: Vec<String>,
    cooldowns: Vec<CooldownView>,
}

#[derive(Serialize)]
#[serde(tag = "t", rename_all = "snake_case")]
enum EvView {
    Hit { src: ActorId, tgt: ActorId, dmg: f64, outcome: String },
    Ceiling { src: ActorId, tgt: ActorId, fatigue: f64 },
    Recovery { tgt: ActorId, heal: f64, revived: bool },
    Cast { actor: ActorId, ability: String },
    Fizzle { actor: ActorId, ability: String },
    Interrupt { actor: ActorId, ability: String },
    Death { actor: ActorId },
}

#[derive(Serialize)]
struct StateView {
    tick: u64,
    outcome: Option<String>,
    actors: Vec<ActorView>,
    events: Vec<EvView>,
}

#[derive(Serialize)]
struct AbilityView {
    id: String,
    name: String,
    school: String,
    fill_cost: f64,
    fatigue_cost: f64,
    cast_time: u32,
    cooldown: u32,
    kind: String,
    /// Rough magnitude as % of a target's pool (for a tooltip), 0 if N/A.
    magnitude_pct: f64,
}

// --------------------------------------------------------------------------- //
// The game.
// --------------------------------------------------------------------------- //

/// A live battle the browser drives one tick at a time.
#[wasm_bindgen]
pub struct Game {
    enc: Encounter,
    human: HumanHandle,
    rules: Rules,
    player_id: ActorId,
}

#[wasm_bindgen]
impl Game {
    /// Build a battle: the player (an archetype id like `"duelist"`) on team 0
    /// against `enemies` (a comma-separated list of archetype ids) on team 1.
    /// `config_json` is the combat `CombatConfig`.
    #[wasm_bindgen(constructor)]
    pub fn new(config_json: &str, player: &str, enemies: &str, seed: u64) -> Result<Game, JsValue> {
        let config: CombatConfig = serde_json::from_str(config_json)
            .map_err(|e| JsValue::from_str(&format!("bad config json: {e}")))?;
        let rules = config.compile();

        let build = |archetype: &str, id: ActorId, team: u8| -> Result<Actor, JsValue> {
            let sb = config
                .stat_blocks
                .get(archetype)
                .ok_or_else(|| JsValue::from_str(&format!("unknown archetype {archetype:?}")))?;
            let mut sb = sb.clone();
            sb.team = team;
            let (loadout, missing) = rules.resolve_loadout(&sb.loadout);
            if !missing.is_empty() {
                return Err(JsValue::from_str(&format!(
                    "{archetype} loadout references unknown abilities: {missing:?}"
                )));
            }
            Ok(compile_actor(id, &sb, loadout, rules.tick_rate))
        };

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

        // Player is actor 0, team 0.
        let player_actor = build(player, 0, 0)?;
        actors.push(player_actor);
        let human = HumanController::new();
        let handle = human.handle();
        // Default the auto-attack target to the first enemy (id 1).
        handle.borrow_mut().target = Some(1);
        controllers.push(Box::new(human));

        // Enemies are team 1, ids 1..
        let enemy_ids: Vec<&str> = enemies.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
        if enemy_ids.is_empty() {
            return Err(JsValue::from_str("no enemies specified"));
        }
        for (i, name) in enemy_ids.iter().enumerate() {
            let id = (i + 1) as ActorId;
            actors.push(build(name, id, 1)?);
            controllers.push(Box::new(MonsterAI));
        }

        let opts = EncounterOptions { max_ticks: 100_000, log_events: true, sample_every: 0 };
        let enc = Encounter::new(actors, controllers, rules.clone(), seed, opts);

        Ok(Game { enc, human: handle, rules, player_id: 0 })
    }

    /// Advance one tick. Returns the new state as JSON, including any events that
    /// fired this tick and an `outcome` once the fight resolves.
    pub fn tick(&mut self) -> String {
        let outcome = self.enc.tick_once();
        let events = self.enc.drain_events();
        self.state_with(outcome, &events)
    }

    /// Current state as JSON without advancing (for the initial render).
    pub fn state(&self) -> String {
        self.state_with(None, &[])
    }

    /// The player's slotted abilities as JSON, for building the action bar.
    pub fn abilities(&self) -> String {
        let me = &self.enc.actors[self.player_id as usize];
        let views: Vec<AbilityView> = me.loadout.iter().map(ability_view).collect();
        serde_json::to_string(&views).unwrap_or_else(|_| "[]".into())
    }

    /// The player's actor id.
    #[wasm_bindgen(js_name = playerId)]
    pub fn player_id(&self) -> u32 {
        self.player_id
    }

    /// Queue an ability for the next decision. `target` < 0 picks a sensible
    /// default (self for support, the current target otherwise).
    #[wasm_bindgen(js_name = useAbility)]
    pub fn use_ability(&mut self, ability_id: &str, target: i32) {
        let Some(ability) = self.rules.abilities.get(ability_id).cloned() else {
            return;
        };
        let targets = if ability.is_support() {
            vec![self.player_id]
        } else {
            let t = if target >= 0 {
                target as ActorId
            } else {
                self.human.borrow().target.unwrap_or(1)
            };
            vec![t]
        };
        self.human.borrow_mut().queued = Some(Action::Use {
            ability_id: ability_id.to_string(),
            targets,
        });
    }

    /// Set the passive auto-attack target.
    #[wasm_bindgen(js_name = setTarget)]
    pub fn set_target(&mut self, target: u32) {
        self.human.borrow_mut().target = Some(target);
    }

    /// Toggle whether the player auto-attacks when no ability is queued.
    #[wasm_bindgen(js_name = setAutoAttack)]
    pub fn set_auto_attack(&mut self, on: bool) {
        self.human.borrow_mut().auto_attack = on;
    }

    fn state_with(&self, outcome: Option<Outcome>, events: &[Event]) -> String {
        let actors = self.enc.actors.iter().map(|a| actor_view(a, self.enc.current_tick())).collect();
        let view = StateView {
            tick: self.enc.current_tick(),
            outcome: outcome.map(|o| self.outcome_label(o)),
            actors,
            events: events.iter().filter_map(ev_view).collect(),
        };
        serde_json::to_string(&view).unwrap_or_else(|_| "{}".into())
    }

    fn outcome_label(&self, o: Outcome) -> String {
        match o {
            Outcome::Win { team } if team == self.enc.actors[self.player_id as usize].team => {
                "win_player".into()
            }
            Outcome::Win { .. } => "win_enemy".into(),
            Outcome::Draw => "draw".into(),
            Outcome::Timeout => "timeout".into(),
        }
    }
}

fn actor_view(a: &Actor, tick: u64) -> ActorView {
    let stagger = match a.stagger {
        StaggerState::Normal => "normal",
        StaggerState::Dazed { .. } => "dazed",
        StaggerState::Unconscious => "unconscious",
        StaggerState::Dead => "dead",
    }
    .to_string();
    let casting = a.casting.as_ref().map(|c| CastView {
        ability: c.ability_id.clone(),
        remaining: c.completes_at.saturating_sub(tick),
    });
    let statuses = a
        .statuses
        .iter()
        .map(|s| status_label(&s.kind).to_string())
        .collect();
    let cooldowns = a
        .cooldowns
        .iter()
        .filter(|(_, &ready)| ready > tick)
        .map(|(id, &ready)| CooldownView { id: id.clone(), remaining: ready - tick })
        .collect();
    ActorView {
        id: a.id,
        name: a.name.clone(),
        team: a.team,
        level: a.level,
        vigor: to_units(a.vigor),
        cap: to_units(a.cap()),
        fatigue: to_units(a.fatigue),
        max_vigor: to_units(a.max_vigor),
        stagger,
        casting,
        statuses,
        cooldowns,
    }
}

fn status_label(kind: &combat_core::actor::StatusKind) -> &'static str {
    use combat_core::actor::StatusKind::*;
    match kind {
        RegenSabotage { .. } => "regen_sabotage",
        FatigueAmp { .. } => "fatigue_amp",
        Dot { .. } => "dot",
        Hot { .. } => "hot",
        Mitigation { .. } => "mitigation",
    }
}

fn ev_view(e: &Event) -> Option<EvView> {
    Some(match e {
        Event::Hit { source, target, landed, outcome, .. } => EvView::Hit {
            src: *source,
            tgt: *target,
            dmg: to_units(*landed),
            outcome: format!("{outcome:?}").to_lowercase(),
        },
        Event::Ceiling { source, target, fatigue_added, .. } => EvView::Ceiling {
            src: *source,
            tgt: *target,
            fatigue: to_units(*fatigue_added),
        },
        Event::Recovery { target, fatigue_healed, revived, .. } => EvView::Recovery {
            tgt: *target,
            heal: to_units(*fatigue_healed),
            revived: *revived,
        },
        Event::CastStarted { actor, ability, .. } => EvView::Cast {
            actor: *actor,
            ability: ability.clone(),
        },
        Event::Fail { actor, ability, reason, .. } => {
            use combat_core::event::FailReason::*;
            match reason {
                CompetencyFizzle => EvView::Fizzle { actor: *actor, ability: ability.clone() },
                Interrupted => EvView::Interrupt { actor: *actor, ability: ability.clone() },
            }
        }
        Event::Death { actor, .. } => EvView::Death { actor: *actor },
        // Action / Blocked / End are not surfaced as floaters.
        _ => return None,
    })
}

fn ability_view(ab: &Ability) -> AbilityView {
    let kind = classify(ab).to_string();
    let magnitude_pct = ab
        .effects
        .iter()
        .filter_map(|e| match e.kind {
            EffectKind::FillDamage { magnitude_bp } => Some(magnitude_bp),
            EffectKind::CeilingDamage { magnitude_bp } => Some(magnitude_bp),
            EffectKind::Recovery { magnitude_bp } => Some(magnitude_bp),
            _ => None,
        })
        .map(|bp| bp as f64 / 100.0) // bp -> percent
        .next()
        .unwrap_or(0.0);
    AbilityView {
        id: ab.id.clone(),
        name: ab.name.clone(),
        school: ab.school.clone(),
        fill_cost: to_units(ab.cost.fill),
        fatigue_cost: to_units(ab.cost.fatigue),
        cast_time: ab.cast_time,
        cooldown: ab.cooldown,
        kind,
        magnitude_pct,
    }
}

/// Classify an ability for the UI (icon / colour).
fn classify(ab: &Ability) -> &'static str {
    if ab.is_support() {
        if ab.effects.iter().any(|e| matches!(e.kind, EffectKind::MitigationBuff { .. })) {
            return "defense";
        }
        return "recovery";
    }
    if ab.effects.iter().any(|e| matches!(e.kind, EffectKind::CeilingDamage { .. })) {
        return "curse";
    }
    if ab.effects.iter().any(|e| matches!(e.kind, EffectKind::RegenSabotage { .. } | EffectKind::FatigueAmp { .. })) {
        return "hex";
    }
    if ab.effects.iter().any(|e| matches!(e.kind, EffectKind::Dot { .. })) {
        return "dot";
    }
    if ab.cost.fill == 0 {
        return "auto";
    }
    if ab.cast_time > 0 {
        return "spell";
    }
    "skill"
}
