//! The effect library (spec §6.1): the small, data-driven vocabulary every
//! ability is composed from. Adding an effect is a new enum variant + its
//! handling in the engine's apply step — a library entry, not an engine rewrite.
//!
//! Effects target *either* the fill or the ceiling. The ceiling-attacking ones
//! (`CeilingDamage`, `RegenSabotage`, `FatigueAmp`) are the most on-thesis
//! content the system has: they attack the real life total instead of the
//! visible bar.
//!
//! Magnitudes are authored as **basis points of the target's `max_vigor`** (so
//! content scales with whoever it hits) except mitigation amounts, which are
//! basis points of *reduction*. `cost_fill` is the resource (displayed units)
//! this effect contributes to its ability's composed cost.

use crate::mitigation::MitigationStage;
use serde::{Deserialize, Serialize};

/// Whether an effect helps its target (route to self/ally) or harms it (route to
/// an enemy). Derived from the kind so targeting can be inferred without extra
/// authoring.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Valence {
    Hostile,
    Friendly,
}

/// One effect, tagged by kind with its per-kind parameters. `duration` is in
/// ticks; `0`/absent means instant.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum EffectKind {
    /// Damage to the fill bar, as bp of target `max_vigor`, routed through con +
    /// the mitigation pipeline before it lands.
    FillDamage { magnitude_bp: i64 },
    /// Direct fatigue gain on the target (lowers their cap) — a curse on the real
    /// life total. bp of target `max_vigor`. Does **not** go through fill
    /// mitigation; con effectiveness still gates whether it lands (§7).
    CeilingDamage { magnitude_bp: i64 },
    /// Lower the target's fatigue (raise their cap), bp of target `max_vigor`.
    /// The inverse of `CeilingDamage`; friendly.
    Recovery { magnitude_bp: i64 },
    /// Reduce the target's regen by `reduce_bp` for `duration` ticks.
    RegenSabotage { reduce_bp: i64, duration: u32 },
    /// Multiply the target's fatigue-per-spend by `(1 + amp_bp)` for `duration`.
    FatigueAmp { amp_bp: i64, duration: u32 },
    /// Drain fill over time: `per_tick_bp` of target `max_vigor` each tick for
    /// `duration` ticks. Each tick routes through mitigation.
    Dot { per_tick_bp: i64, duration: u32 },
    /// Restore fill over time: `per_tick_bp` of target `max_vigor` per tick.
    Hot { per_tick_bp: i64, duration: u32 },
    /// Add mitigation at `stage` for `duration` ticks (friendly). A `block`
    /// ability is a short, strong `ActiveDefense` buff — defenses are just timed
    /// mitigation, so the reactive-timing meta-game needs no separate concept.
    MitigationBuff { stage: MitigationStage, amount_bp: i64, duration: u32 },
    /// Strip mitigation at `stage` for `duration` ticks (hostile). Negative
    /// reduction at the `Buff` stage reads as a vulnerability/sunder.
    MitigationDebuff { stage: MitigationStage, amount_bp: i64, duration: u32 },
}

impl EffectKind {
    /// Who this effect is meant for. Drives target selection in the engine.
    pub fn valence(&self) -> Valence {
        match self {
            EffectKind::Recovery { .. }
            | EffectKind::Hot { .. }
            | EffectKind::MitigationBuff { .. } => Valence::Friendly,
            EffectKind::FillDamage { .. }
            | EffectKind::CeilingDamage { .. }
            | EffectKind::RegenSabotage { .. }
            | EffectKind::FatigueAmp { .. }
            | EffectKind::Dot { .. }
            | EffectKind::MitigationDebuff { .. } => Valence::Hostile,
        }
    }

    /// Whether this effect resolves instantly (vs. installing a timed status).
    pub fn is_instant(&self) -> bool {
        matches!(
            self,
            EffectKind::FillDamage { .. }
                | EffectKind::CeilingDamage { .. }
                | EffectKind::Recovery { .. }
        )
    }
}

/// An authored effect: what it does (`kind`) plus what it adds to its ability's
/// composed fill cost (`cost_fill`, in displayed units — "more effects ⇒ more
/// expensive", §6).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EffectSpec {
    #[serde(flatten)]
    pub kind: EffectKind,
    #[serde(default)]
    pub cost_fill: i64,
}

impl EffectSpec {
    pub fn new(kind: EffectKind, cost_fill: i64) -> Self {
        Self { kind, cost_fill }
    }

    pub fn valence(&self) -> Valence {
        self.kind.valence()
    }
}

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

    #[test]
    fn valence_is_derived_from_kind() {
        assert_eq!(EffectKind::FillDamage { magnitude_bp: 1000 }.valence(), Valence::Hostile);
        assert_eq!(EffectKind::Recovery { magnitude_bp: 500 }.valence(), Valence::Friendly);
        assert_eq!(
            EffectKind::MitigationBuff {
                stage: MitigationStage::ActiveDefense,
                amount_bp: 8000,
                duration: 2
            }
            .valence(),
            Valence::Friendly
        );
    }

    #[test]
    fn instant_vs_timed_classification() {
        assert!(EffectKind::FillDamage { magnitude_bp: 1000 }.is_instant());
        assert!(!EffectKind::Dot { per_tick_bp: 100, duration: 5 }.is_instant());
    }

    #[test]
    fn effect_spec_roundtrips_json() {
        let e = EffectSpec::new(EffectKind::FillDamage { magnitude_bp: 1000 }, 5);
        let j = serde_json::to_string(&e).unwrap();
        let back: EffectSpec = serde_json::from_str(&j).unwrap();
        assert_eq!(e, back);
        // flattened tag means the json is one flat object.
        assert!(j.contains("\"kind\":\"fill_damage\""));
        assert!(j.contains("\"magnitude_bp\":1000"));
    }
}
