//! Abilities (spec §6): skills and spells are **one thing**. "Kick" = melee
//! delivery + a fill-damage effect. "Fireball" = projectile delivery + a fire
//! (fill-damage) effect with a wind-up. Both resolve through the same composed
//! two-axis cost grid.
//!
//! An [`AbilitySpec`] is authored data; [`Ability`] is its compiled form with an
//! **absolute** [`AbilityCost`] baked in by [`compose_cost`]. The only structural
//! axis separating a skill from a spell is `cast_time` (instant vs. wind-up).

use crate::effect::{EffectSpec, Valence};
use crate::fixed::{apply_bp, units, Milli};
use serde::{Deserialize, Serialize};

/// Delivery / shape — sets targeting and a base cost contribution.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Delivery {
    /// Affects only the caster (recovery, self-buffs).
    SelfCast,
    /// One adjacent target, no wind-up cost premium.
    Touch,
    /// One target in melee — the auto-attack / weapon-skill shape.
    Melee,
    /// One target at range; the canonical spell shape.
    Projectile,
    /// One target, channel-flavored ranged.
    Beam,
    /// Up to `n` targets in an area.
    Aoe { n: u32 },
}

impl Delivery {
    /// Whether this delivery can reach more than one enemy (informational; the
    /// authoritative target count is [`AbilitySpec::targets`]).
    pub fn max_targets(&self) -> u32 {
        match self {
            Delivery::Aoe { n } => *n,
            _ => 1,
        }
    }
}

/// Coefficients turning a spec into a cost — "the table" for cost (spec §11). All
/// base costs are in **displayed units** of fill; fatigue defaults to a ratio of
/// the composed fill unless an ability overrides it.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CostModel {
    pub base_fill_self: i64,
    pub base_fill_touch: i64,
    pub base_fill_melee: i64,
    pub base_fill_projectile: i64,
    pub base_fill_beam: i64,
    pub base_fill_aoe: i64,
    /// Default fatigue ("spend leaves a mark", §3): `fatigue = fill × this_bp`.
    /// Applied when an ability does not author `fatigue_units`.
    pub fatigue_ratio_bp: i64,
    /// Difficulty per unit of composed fill cost (drives fizzle, §9), in bp.
    /// `difficulty = round(fill_units × this_bp / 10_000)` when not authored.
    pub difficulty_per_fill_bp: i64,
}

impl Default for CostModel {
    fn default() -> Self {
        Self {
            base_fill_self: 0,
            base_fill_touch: 1,
            base_fill_melee: 1,
            base_fill_projectile: 2,
            base_fill_beam: 3,
            base_fill_aoe: 4,
            fatigue_ratio_bp: 2_000, // 20% of every point spent becomes fatigue
            difficulty_per_fill_bp: 10_000, // difficulty ≈ fill cost in units
        }
    }
}

impl CostModel {
    fn base_fill(&self, d: Delivery) -> i64 {
        match d {
            Delivery::SelfCast => self.base_fill_self,
            Delivery::Touch => self.base_fill_touch,
            Delivery::Melee => self.base_fill_melee,
            Delivery::Projectile => self.base_fill_projectile,
            Delivery::Beam => self.base_fill_beam,
            Delivery::Aoe { .. } => self.base_fill_aoe,
        }
    }
}

/// An authored, explicit cost in displayed units (used by `cost_override` for
/// shapes the composer can't express cleanly, e.g. auto-attack's `(0, small)`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawCost {
    pub fill: i64,
    pub fatigue: i64,
}

/// The compiled, absolute cost of an ability, in milli-units. `fatigue` is the
/// **net** ceiling change from paying (positive = a mark; recovery's net gain
/// comes from its `Recovery` effect, not from a negative cost here).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct AbilityCost {
    pub fill: Milli,
    pub fatigue: Milli,
}

/// Authored ability data (config). Compiled to [`Ability`] via [`compile`].
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AbilitySpec {
    pub id: String,
    pub name: String,
    /// Competency school this draws on for fizzle/effectiveness (§9).
    pub school: String,
    pub delivery: Delivery,
    #[serde(default)]
    pub effects: Vec<EffectSpec>,
    /// Scalar on effect magnitude *and* cost. bp; `10_000` == ×1.0.
    #[serde(default = "default_potency")]
    pub potency_bp: i64,
    #[serde(default = "default_targets")]
    pub targets: u32,
    /// Wind-up in ticks. `0` == instant (a skill); `>0` == a spell.
    #[serde(default)]
    pub cast_time: u32,
    /// Ticks before this ability is reusable.
    #[serde(default)]
    pub cooldown: u32,
    /// Explicit cost in displayed units; bypasses the composer when set.
    #[serde(default)]
    pub cost_override: Option<RawCost>,
    /// Explicit fatigue in displayed units; overrides only the fatigue column.
    #[serde(default)]
    pub fatigue_units: Option<i64>,
    /// Explicit difficulty (drives fizzle); derived from cost when absent.
    #[serde(default)]
    pub difficulty: Option<i64>,
}

fn default_potency() -> i64 {
    10_000
}
fn default_targets() -> u32 {
    1
}

/// A compiled ability: spec data plus its baked-in absolute cost and difficulty.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Ability {
    pub id: String,
    pub name: String,
    pub school: String,
    pub delivery: Delivery,
    pub effects: Vec<EffectSpec>,
    pub potency_bp: i64,
    pub targets: u32,
    pub cast_time: u32,
    pub cooldown: u32,
    pub cost: AbilityCost,
    pub difficulty: i64,
}

impl Ability {
    /// Whether this ability only ever helps its targets (all effects friendly) —
    /// used by controllers/targeting to route a heal at an ally rather than a foe.
    pub fn is_support(&self) -> bool {
        !self.effects.is_empty()
            && self.effects.iter().all(|e| e.valence() == Valence::Friendly)
    }

    /// Whether this ability has any hostile effect (it attacks someone).
    pub fn is_offensive(&self) -> bool {
        self.effects.iter().any(|e| e.valence() == Valence::Hostile)
    }
}

/// Compile a spec into an [`Ability`], composing its absolute cost.
pub fn compile(spec: &AbilitySpec, model: &CostModel) -> Ability {
    let cost = compose_cost(spec, model);
    let difficulty = spec.difficulty.unwrap_or_else(|| {
        // Derived difficulty ≈ composed fill cost in units, scaled.
        let fill_units = cost.fill / crate::fixed::UNIT;
        crate::fixed::mul_div_round(fill_units, model.difficulty_per_fill_bp, 10_000)
    });
    Ability {
        id: spec.id.clone(),
        name: spec.name.clone(),
        school: spec.school.clone(),
        delivery: spec.delivery,
        effects: spec.effects.clone(),
        potency_bp: spec.potency_bp,
        targets: spec.targets.max(1),
        cast_time: spec.cast_time,
        cooldown: spec.cooldown,
        cost,
        difficulty: difficulty.max(0),
    }
}

/// Compose an absolute `(fill, fatigue)` cost (spec §6): base delivery cost plus
/// each effect's contribution, scaled by potency, multiplied by target count.
///
/// `fill = (base_delivery + Σ effect.cost_fill) × potency × targets`, then
/// `fatigue = fill × fatigue_ratio` unless authored. An explicit `cost_override`
/// short-circuits the whole thing.
pub fn compose_cost(spec: &AbilitySpec, model: &CostModel) -> AbilityCost {
    if let Some(o) = spec.cost_override {
        return AbilityCost {
            fill: units(o.fill),
            fatigue: units(o.fatigue),
        };
    }

    let targets = spec.targets.max(1) as i64;
    let base_units: i64 = model.base_fill(spec.delivery)
        + spec.effects.iter().map(|e| e.cost_fill).sum::<i64>();

    // Scale by potency in milli-space, then multiply by target count.
    let mut fill = apply_bp(units(base_units), spec.potency_bp);
    fill *= targets;

    let fatigue = match spec.fatigue_units {
        Some(f) => units(f) * targets, // authored fatigue still scales with targets
        None => apply_bp(fill, model.fatigue_ratio_bp),
    };

    AbilityCost { fill, fatigue }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::effect::EffectKind;
    use crate::fixed::UNIT;

    fn fill_dmg(mag: i64, cost: i64) -> EffectSpec {
        EffectSpec::new(EffectKind::FillDamage { magnitude_bp: mag }, cost)
    }

    #[test]
    fn composes_a_simple_skill_cost() {
        let model = CostModel::default();
        let spec = AbilitySpec {
            id: "kick".into(),
            name: "Kick".into(),
            school: "martial".into(),
            delivery: Delivery::Melee,
            effects: vec![fill_dmg(1_000, 4)], // melee base 1 + effect 4 = 5 fill
            potency_bp: 10_000,
            targets: 1,
            cast_time: 0,
            cooldown: 0,
            cost_override: None,
            fatigue_units: None,
            difficulty: None,
        };
        let ab = compile(&spec, &model);
        assert_eq!(ab.cost.fill, 5 * UNIT);
        // 20% fatigue ratio -> 1.0 unit fatigue.
        assert_eq!(ab.cost.fatigue, UNIT);
        assert!(ab.is_offensive());
    }

    #[test]
    fn potency_and_targets_scale_cost() {
        let model = CostModel::default();
        let base = AbilitySpec {
            id: "nuke".into(),
            name: "Nuke".into(),
            school: "invocation".into(),
            delivery: Delivery::Aoe { n: 6 },
            effects: vec![fill_dmg(2_000, 6)], // aoe base 4 + 6 = 10 fill at 1x,1 target
            potency_bp: 20_000,                // ×2 potency
            targets: 6,                        // ×6 targets
            cast_time: 20,
            cooldown: 0,
            cost_override: None,
            fatigue_units: None,
            difficulty: None,
        };
        let ab = compile(&base, &model);
        // (10 units × 2.0) × 6 = 120 units fill.
        assert_eq!(ab.cost.fill, 120 * UNIT);
        // difficulty derived from fill units (120) × 1.0.
        assert_eq!(ab.difficulty, 120);
    }

    #[test]
    fn cost_override_makes_auto_attack_shape() {
        let model = CostModel::default();
        let spec = AbilitySpec {
            id: "swing".into(),
            name: "Swing".into(),
            school: "martial".into(),
            delivery: Delivery::Melee,
            effects: vec![fill_dmg(1_000, 0)],
            potency_bp: 10_000,
            targets: 1,
            cast_time: 0,
            cooldown: 10,
            cost_override: Some(RawCost { fill: 0, fatigue: 1 }), // (0 fill, +small)
            fatigue_units: None,
            difficulty: None,
        };
        let ab = compile(&spec, &model);
        assert_eq!(ab.cost.fill, 0);
        assert_eq!(ab.cost.fatigue, UNIT);
    }

    #[test]
    fn support_ability_is_detected() {
        let model = CostModel::default();
        let spec = AbilitySpec {
            id: "mend".into(),
            name: "Mend".into(),
            school: "restoration".into(),
            delivery: Delivery::SelfCast,
            effects: vec![EffectSpec::new(EffectKind::Recovery { magnitude_bp: 1_500 }, 2)],
            potency_bp: 10_000,
            targets: 1,
            cast_time: 0,
            cooldown: 30,
            cost_override: None,
            fatigue_units: None,
            difficulty: None,
        };
        let ab = compile(&spec, &model);
        assert!(ab.is_support());
        assert!(!ab.is_offensive());
    }
}
