//! The mitigation pipeline (spec §8): one ordered place where "how much of this
//! hit actually lands" is decided, so new sources are additive contributors and
//! never reworks.
//!
//! Order is fixed: **Armor → Con → Active defense → Stance → Buff/debuff**.
//! Armor / defenses / stances / buffs are *reductions* (a fraction passes
//! through). Con is a *multiplier* drawn from the con curve (§7) — it can
//! amplify (over-con) as well as shrink (under-con). The pipeline reports each
//! stage's contribution because you cannot tune the con curve, the balance
//! lever, from the final `landed` number alone (spec §12).

use crate::fixed::{apply_bp, Milli, BP_ONE};
use serde::{Deserialize, Serialize};

/// The ordered stages of the pipeline. `ActiveDefense`, `Stance`, and `Buff` are
/// the stages that timed/temporary effects feed into.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MitigationStage {
    Armor,
    Con,
    ActiveDefense,
    Stance,
    Buff,
}

/// Per-stage reduction/multiplier inputs for a single hit, gathered by the
/// engine from the defender's stat block + active statuses.
///
/// All `*_reduce_bp` are "fraction removed" in basis points (`10_000` == removes
/// everything). `con_mult_bp` is a straight multiplier (`10_000` == ×1.0,
/// `30_000` == ×3.0). `buff_reduce_bp` may be **negative** — a vulnerability
/// debuff that *amplifies* incoming damage.
#[derive(Clone, Copy, Debug)]
pub struct MitigationInputs {
    pub armor_reduce_bp: i64,
    pub con_mult_bp: i64,
    pub active_defense_reduce_bp: i64,
    pub stance_reduce_bp: i64,
    pub buff_reduce_bp: i64,
}

/// The landed damage plus the per-stage breakdown (each is `before − after` at
/// that stage, so they sum — with sign — to `pre_mitigation − landed`). The con
/// delta is negative when over-con amplifies the hit.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct MitigationResult {
    pub pre_mitigation: Milli,
    pub landed: Milli,
    pub armor_delta: Milli,
    pub con_delta: Milli,
    pub defense_delta: Milli,
    pub stance_delta: Milli,
    pub buff_delta: Milli,
}

/// Run a pre-mitigation magnitude through the full ordered pipeline.
///
/// Each reduction multiplies the running value by `(1 − reduce)`, clamped so a
/// single reduction stage can null a hit but never make it negative. Con
/// multiplies (and may amplify). The final `landed` is floored at zero.
pub fn run(pre_mitigation: Milli, inp: MitigationInputs) -> MitigationResult {
    let mut cur = pre_mitigation;

    // Armor — passive attrition reduction.
    let after_armor = reduce(cur, inp.armor_reduce_bp);
    let armor_delta = cur - after_armor;
    cur = after_armor;

    // Con — level-delta multiplier (the amplify-capable stage).
    let after_con = apply_bp(cur, inp.con_mult_bp.max(0));
    let con_delta = cur - after_con; // negative when amplified
    cur = after_con;

    // Active defense — block/parry/evade windows.
    let after_def = reduce(cur, inp.active_defense_reduce_bp);
    let defense_delta = cur - after_def;
    cur = after_def;

    // Stance — toggled mitigation trade.
    let after_stance = reduce(cur, inp.stance_reduce_bp);
    let stance_delta = cur - after_stance;
    cur = after_stance;

    // Buff/debuff — temporary contributors. Negative reduce amplifies.
    let after_buff = reduce_signed(cur, inp.buff_reduce_bp);
    let buff_delta = cur - after_buff;
    cur = after_buff.max(0);

    MitigationResult {
        pre_mitigation,
        landed: cur,
        armor_delta,
        con_delta,
        defense_delta,
        stance_delta,
        buff_delta,
    }
}

/// Apply a non-negative reduction (`reduce_bp` clamped to `[0, BP_ONE]`).
#[inline]
fn reduce(amount: Milli, reduce_bp: i64) -> Milli {
    let keep = BP_ONE - reduce_bp.clamp(0, BP_ONE);
    apply_bp(amount, keep)
}

/// Apply a signed reduction: positive shrinks (clamped to full negation),
/// negative amplifies (a vulnerability debuff). `keep = 1 − reduce` is clamped
/// at the low end to 0 (can't go negative) but may exceed 1 to amplify.
#[inline]
fn reduce_signed(amount: Milli, reduce_bp: i64) -> Milli {
    let keep = (BP_ONE - reduce_bp).max(0);
    apply_bp(amount, keep)
}

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

    fn no_mit(con_mult_bp: i64) -> MitigationInputs {
        MitigationInputs {
            armor_reduce_bp: 0,
            con_mult_bp,
            active_defense_reduce_bp: 0,
            stance_reduce_bp: 0,
            buff_reduce_bp: 0,
        }
    }

    #[test]
    fn even_con_no_mitigation_is_identity() {
        let r = run(10_000, no_mit(BP_ONE));
        assert_eq!(r.landed, 10_000);
        assert_eq!(r.con_delta, 0);
    }

    #[test]
    fn over_con_amplifies_via_con_stage() {
        let r = run(10_000, no_mit(30_000)); // ×3.0
        assert_eq!(r.landed, 30_000);
        assert_eq!(r.con_delta, -20_000); // amplification reads as negative mitigation
    }

    #[test]
    fn armor_then_con_order_matters() {
        // 50% armor, then ×2.0 con: 10000 -> 5000 -> 10000.
        let inp = MitigationInputs { armor_reduce_bp: 5_000, ..no_mit(20_000) };
        let r = run(10_000, inp);
        assert_eq!(r.armor_delta, 5_000);
        assert_eq!(r.landed, 10_000);
    }

    #[test]
    fn active_defense_can_negate() {
        let inp = MitigationInputs { active_defense_reduce_bp: 10_000, ..no_mit(BP_ONE) };
        let r = run(10_000, inp);
        assert_eq!(r.landed, 0);
        assert_eq!(r.defense_delta, 10_000);
    }

    #[test]
    fn vulnerability_debuff_amplifies() {
        // buff_reduce_bp = -5000 => keep 1.5x.
        let inp = MitigationInputs { buff_reduce_bp: -5_000, ..no_mit(BP_ONE) };
        let r = run(10_000, inp);
        assert_eq!(r.landed, 15_000);
        assert_eq!(r.buff_delta, -5_000);
    }

    #[test]
    fn deltas_reconcile_with_landed() {
        let inp = MitigationInputs {
            armor_reduce_bp: 2_000,
            con_mult_bp: 16_000,
            active_defense_reduce_bp: 3_000,
            stance_reduce_bp: 1_000,
            buff_reduce_bp: -1_000,
        };
        let r = run(12_345, inp);
        let sum = r.armor_delta + r.con_delta + r.defense_delta + r.stance_delta + r.buff_delta;
        assert_eq!(r.pre_mitigation - r.landed, sum);
    }
}
