//! Configuration — "the table" (spec §11). A single serde-loadable
//! [`CombatConfig`] holds every tunable; **nothing balance-relevant is
//! hardcoded**. It compiles once into [`Rules`], the integer-fixed-point form the
//! engine actually runs on (f64 is confined to this parse/compile boundary, §10).
//!
//! Editing config is the primary balancing act; only structural rule changes
//! require a recompile.

use crate::ability::{compile as compile_ability, Ability, AbilitySpec, CostModel};
use crate::actor::{RegenParams, StaggerParams};
use crate::con::{ConAnchor, ConCurve};
use crate::fixed::{mul_div_round, Milli, UNIT};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// Regen tunables (§3).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RegenConfig {
    /// Fraction of normal regen while dazed. `5_000` == half.
    pub dazed_regen_bp: i64,
    /// The non-zero floor (units/sec) so an actor can always claw back toward
    /// affording an action — no soft-lock (§3/§6).
    pub floor_per_sec: i64,
}

impl Default for RegenConfig {
    fn default() -> Self {
        Self { dazed_regen_bp: 5_000, floor_per_sec: 1 }
    }
}

/// Stagger cascade tunables (§4) plus the damage-taken fatigue share (§3).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StaggerConfig {
    /// `hit/capacity` above this is at least a daze. `10_000` == 1.0×.
    pub daze_threshold_bp: i64,
    /// `hit/capacity` at/above this is unconscious. `15_000` == 1.5×.
    pub unconscious_threshold_bp: i64,
    /// Daze duration in ticks.
    pub daze_duration: u32,
    /// Daze granted when a recovery revives an unconscious actor (ticks).
    pub revive_daze: u32,
    /// Fatigue share of every hit taken (§3), bp of the landed damage.
    pub fatigue_ratio_bp: i64,
}

impl Default for StaggerConfig {
    fn default() -> Self {
        Self {
            daze_threshold_bp: 10_000,
            unconscious_threshold_bp: 15_000,
            daze_duration: 8,
            revive_daze: 12,
            fatigue_ratio_bp: 2_000,
        }
    }
}

/// Competency → effectiveness coupling (§9).
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CompetencyConfig {
    /// Effective-level bonus per competency point, bp. `1_000` == +0.1 level/pt
    /// (so 50 competency ≈ +5 effective levels).
    pub level_per_point_bp: i64,
}

impl Default for CompetencyConfig {
    fn default() -> Self {
        Self { level_per_point_bp: 1_000 }
    }
}

/// Fizzle model (§9): chance an ability fails to fire (paid for nothing).
/// `chance_bp = clamp(base + max(0, difficulty − competency) × per_gap, 0, max)`.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FizzleConfig {
    /// Floor fizzle chance even at/above competency (bp).
    pub base_bp: i64,
    /// Added fizzle per point that difficulty exceeds competency (bp).
    pub per_gap_bp: i64,
    /// Hard ceiling on fizzle chance (bp).
    pub max_bp: i64,
}

impl Default for FizzleConfig {
    fn default() -> Self {
        Self { base_bp: 200, per_gap_bp: 150, max_bp: 9_000 }
    }
}

/// The whole tunable surface (§11), serde-loadable from JSON.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CombatConfig {
    /// Fixed tick rate (ticks/second, §10).
    #[serde(default = "default_tick_rate")]
    pub tick_rate: u32,
    /// Global cooldown (ticks): the minimum gap between an actor's actions — the
    /// genre's swing timer (§10 "attack delay"). Bounds how fast *any* policy can
    /// act, so a button-masher can't out-DPS the intended pace. `0` disables it.
    #[serde(default)]
    pub global_cooldown: u32,
    /// ± spread on every landed fill hit, bp. `1_500` == ±15%. The genre's damage
    /// range; also the symmetry-breaker that turns a deterministic mirror match
    /// from a coin-flip-of-draws into smooth win-rate curves.
    #[serde(default)]
    pub damage_variance_bp: i64,
    #[serde(default)]
    pub regen: RegenConfig,
    #[serde(default)]
    pub stagger: StaggerConfig,
    #[serde(default)]
    pub competency: CompetencyConfig,
    #[serde(default)]
    pub fizzle: FizzleConfig,
    #[serde(default)]
    pub cost_model: CostModel,
    /// Con curve anchors (§7). Empty ⇒ the spec's default curve.
    #[serde(default)]
    pub con_curve: Vec<ConAnchor>,
    /// The ability library (§6) — data, not code.
    #[serde(default)]
    pub abilities: Vec<AbilitySpec>,
    /// Optional named archetypes the sim can reference when assembling scenarios.
    #[serde(default)]
    pub stat_blocks: BTreeMap<String, crate::actor::StatBlock>,
}

fn default_tick_rate() -> u32 {
    10
}

impl Default for CombatConfig {
    fn default() -> Self {
        Self {
            tick_rate: 10,
            global_cooldown: 0,
            damage_variance_bp: 0,
            regen: RegenConfig::default(),
            stagger: StaggerConfig::default(),
            competency: CompetencyConfig::default(),
            fizzle: FizzleConfig::default(),
            cost_model: CostModel::default(),
            con_curve: ConCurve::default_anchors(),
            abilities: Vec::new(),
            stat_blocks: BTreeMap::new(),
        }
    }
}

/// The compiled, integer-fixed-point ruleset the engine runs on.
#[derive(Clone, Debug)]
pub struct Rules {
    pub tick_rate: u32,
    pub global_cooldown: u32,
    pub damage_variance_bp: i64,
    pub stagger: StaggerParams,
    pub regen: RegenParams,
    pub revive_daze: u32,
    pub con: ConCurve,
    pub competency_level_per_point_bp: i64,
    pub fizzle: FizzleConfig,
    pub cost_model: CostModel,
    /// Default spend-mark ratio (§3), mirrored from the cost model for clarity.
    pub spend_fatigue_ratio_bp: i64,
    /// Compiled abilities keyed by id (resolution for loadouts).
    pub abilities: BTreeMap<String, Ability>,
}

impl CombatConfig {
    /// Compile config into [`Rules`]: convert units/sec → milli/tick, fold the
    /// con anchors, and compose every ability's absolute cost up front.
    pub fn compile(&self) -> Rules {
        let tick_rate = self.tick_rate.max(1);

        let stagger = StaggerParams {
            daze_threshold_bp: self.stagger.daze_threshold_bp,
            unconscious_threshold_bp: self.stagger.unconscious_threshold_bp,
            daze_duration: self.stagger.daze_duration,
            fatigue_ratio_bp: self.stagger.fatigue_ratio_bp,
        };

        let regen = RegenParams {
            dazed_regen_bp: self.regen.dazed_regen_bp,
            regen_floor: per_sec_to_per_tick(self.regen.floor_per_sec, tick_rate),
        };

        let con = if self.con_curve.is_empty() {
            ConCurve::default()
        } else {
            ConCurve::new(self.con_curve.clone())
        };

        let abilities = self
            .abilities
            .iter()
            .map(|spec| (spec.id.clone(), compile_ability(spec, &self.cost_model)))
            .collect();

        Rules {
            tick_rate,
            global_cooldown: self.global_cooldown,
            damage_variance_bp: self.damage_variance_bp.max(0),
            stagger,
            regen,
            revive_daze: self.stagger.revive_daze,
            con,
            competency_level_per_point_bp: self.competency.level_per_point_bp,
            fizzle: self.fizzle.clone(),
            cost_model: self.cost_model.clone(),
            spend_fatigue_ratio_bp: self.cost_model.fatigue_ratio_bp,
            abilities,
        }
    }
}

impl Rules {
    /// Resolve a loadout of ability ids against the compiled library, dropping
    /// (and reporting) any unknown ids so a typo in config fails loud-ish but the
    /// rest of the actor still builds.
    pub fn resolve_loadout(&self, ids: &[String]) -> (Vec<Ability>, Vec<String>) {
        let mut out = Vec::new();
        let mut missing = Vec::new();
        for id in ids {
            match self.abilities.get(id) {
                Some(a) => out.push(a.clone()),
                None => missing.push(id.clone()),
            }
        }
        (out, missing)
    }

    /// Fizzle chance (bp) for `difficulty` attempted at `competency` (§9).
    /// Trivial abilities (`difficulty <= 0`, e.g. a basic auto-attack) never
    /// fizzle — the base floor only applies to real skills/spells.
    pub fn fizzle_chance_bp(&self, competency: i64, difficulty: i64) -> i64 {
        if difficulty <= 0 {
            return 0;
        }
        let gap = (difficulty - competency).max(0);
        let raw = self.fizzle.base_bp + gap * self.fizzle.per_gap_bp;
        raw.clamp(0, self.fizzle.max_bp)
    }

    /// Con multiplier (bp) for an attacker/caster of `attacker_eff_level` against
    /// `target_level` (§7).
    pub fn con_mult_bp(&self, attacker_eff_level: i32, target_level: i32) -> i64 {
        self.con.multiplier_bp(attacker_eff_level - target_level)
    }
}

/// units/sec → milli/tick: `per_sec × 1000 / tick_rate`, rounded half-to-even.
fn per_sec_to_per_tick(per_sec: i64, tick_rate: u32) -> Milli {
    mul_div_round(per_sec * UNIT, 1, tick_rate.max(1) as i64)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_config_compiles() {
        let cfg = CombatConfig::default();
        let rules = cfg.compile();
        assert_eq!(rules.tick_rate, 10);
        assert_eq!(rules.con.multiplier_bp(0), 10_000);
        // floor 1 unit/sec at 10 tps -> 100 milli/tick.
        assert_eq!(rules.regen.regen_floor, 100);
    }

    #[test]
    fn fizzle_grows_with_difficulty_gap() {
        let rules = CombatConfig::default().compile();
        // competency >= difficulty -> just the base.
        assert_eq!(rules.fizzle_chance_bp(50, 20), 200);
        // a 10-point gap adds 10 * 150 = 1500.
        assert_eq!(rules.fizzle_chance_bp(20, 30), 200 + 1_500);
        // clamps at max.
        assert_eq!(rules.fizzle_chance_bp(0, 1_000), 9_000);
    }

    #[test]
    fn config_roundtrips_json() {
        let cfg = CombatConfig::default();
        let j = serde_json::to_string(&cfg).unwrap();
        let back: CombatConfig = serde_json::from_str(&j).unwrap();
        assert_eq!(back.tick_rate, cfg.tick_rate);
        assert_eq!(back.con_curve.len(), cfg.con_curve.len());
    }

    #[test]
    fn partial_json_fills_defaults() {
        // Only override tick rate; everything else should default.
        let cfg: CombatConfig = serde_json::from_str(r#"{ "tick_rate": 20 }"#).unwrap();
        assert_eq!(cfg.tick_rate, 20);
        assert_eq!(cfg.stagger.unconscious_threshold_bp, 15_000);
    }
}
