//! The con curve (spec §7): a single level-delta → magnitude-multiplier mapping,
//! reused in both directions.
//!
//! - **Incoming damage**: `delta = attacker_level − defender_level`.
//! - **Outgoing effect effectiveness / resist**: `delta = caster_effective_level
//!   − target_level`, where `caster_effective_level` folds in competency (§9).
//!
//! Represented as a small **anchor table** with clamped linear interpolation in
//! integer space (basis points). Anchors come from config, so the whole feel of
//! over/under-con fights is a table you edit, not code.
//!
//! ## The "double duty" caveat (§7)
//! Both callers feed a *delta*, but their domains differ: raw level delta vs. a
//! competency-inflated effective delta that can run wider. [`ConCurve`] clamps
//! outside its anchor range and exposes [`ConCurve::covers`] so a caller can
//! detect when a delta falls past the tuned region. If competency inflation ever
//! pushes effective deltas past where the damage-tuned curve behaves, split this
//! into two `ConCurve`s — the call sites already pass their own delta, so that
//! split is a config change plus one field, not a rewrite.

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

/// One `(delta, multiplier)` anchor. `mult_bp` is basis points (`10_000` == 1.0×).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConAnchor {
    pub delta: i32,
    pub mult_bp: i64,
}

/// A monotone-ish anchor table mapping level delta to a magnitude multiplier.
///
/// Anchors are kept sorted by `delta`. Between anchors the multiplier is linearly
/// interpolated; outside the first/last anchor it is clamped to the endpoint.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConCurve {
    /// Sorted (ascending `delta`) anchor points. Always non-empty after
    /// [`ConCurve::new`].
    anchors: Vec<ConAnchor>,
}

impl ConCurve {
    /// Build from anchors, sorting by delta and de-duplicating exact-delta
    /// collisions (last wins). Falls back to an even-con identity if empty so a
    /// degenerate config can never produce a panicking or zero-length curve.
    pub fn new(mut anchors: Vec<ConAnchor>) -> Self {
        anchors.sort_by_key(|a| a.delta);
        anchors.dedup_by_key(|a| a.delta);
        if anchors.is_empty() {
            anchors.push(ConAnchor { delta: 0, mult_bp: BP_ONE });
        }
        Self { anchors }
    }

    /// The default illustrative curve from the spec (§7): even-con `1.0×`,
    /// `+5 ≈ 3.0×`, `−5 ≈ 0.2×`, monotone, with a couple of intermediate anchors
    /// so the interpolation isn't a straight line through the extremes.
    pub fn default_anchors() -> Vec<ConAnchor> {
        vec![
            ConAnchor { delta: -8, mult_bp: 200 },    // 0.02× — hopelessly under-con
            ConAnchor { delta: -5, mult_bp: 2_000 },  // 0.20×
            ConAnchor { delta: -2, mult_bp: 6_000 },  // 0.60×
            ConAnchor { delta: 0, mult_bp: 10_000 },  // 1.00× — even con
            ConAnchor { delta: 2, mult_bp: 16_000 },  // 1.60×
            ConAnchor { delta: 5, mult_bp: 30_000 },  // 3.00×
            ConAnchor { delta: 8, mult_bp: 50_000 },  // 5.00× — brutally over-con
        ]
    }

    /// Multiplier in basis points for a given level `delta`, clamped to the
    /// endpoint multipliers outside the anchored range.
    pub fn multiplier_bp(&self, delta: i32) -> i64 {
        let first = self.anchors.first().expect("ConCurve is never empty");
        let last = self.anchors.last().expect("ConCurve is never empty");
        if delta <= first.delta {
            return first.mult_bp;
        }
        if delta >= last.delta {
            return last.mult_bp;
        }
        // Find the bracketing pair [lo, hi] with lo.delta <= delta < hi.delta.
        // Anchor counts are tiny (single digits), so a linear scan is clearest.
        for pair in self.anchors.windows(2) {
            let lo = pair[0];
            let hi = pair[1];
            if delta >= lo.delta && delta < hi.delta {
                let span = (hi.delta - lo.delta) as i64; // > 0 (deltas are unique & sorted)
                let into = (delta - lo.delta) as i64;
                let rise = hi.mult_bp - lo.mult_bp;
                // lo.mult + rise * into / span, rounded half-to-even.
                return lo.mult_bp + crate::fixed::mul_div_round(rise, into, span);
            }
        }
        // Unreachable given the clamps above, but stay total.
        last.mult_bp
    }

    /// Whether `delta` falls inside the anchored (tuned) range. A caller using a
    /// competency-inflated delta can check this to log/escalate when it leaves
    /// the region the curve was tuned for (the §7 caveat).
    pub fn covers(&self, delta: i32) -> bool {
        let first = self.anchors.first().expect("ConCurve is never empty");
        let last = self.anchors.last().expect("ConCurve is never empty");
        (first.delta..=last.delta).contains(&delta)
    }
}

impl Default for ConCurve {
    fn default() -> Self {
        ConCurve::new(ConCurve::default_anchors())
    }
}

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

    #[test]
    fn anchors_are_exact() {
        let c = ConCurve::default();
        assert_eq!(c.multiplier_bp(0), 10_000);
        assert_eq!(c.multiplier_bp(5), 30_000);
        assert_eq!(c.multiplier_bp(-5), 2_000);
    }

    #[test]
    fn clamps_outside_range() {
        let c = ConCurve::default();
        assert_eq!(c.multiplier_bp(-100), 200); // floor anchor
        assert_eq!(c.multiplier_bp(100), 50_000); // ceil anchor
        assert!(!c.covers(-100));
        assert!(c.covers(0));
    }

    #[test]
    fn interpolates_between_anchors() {
        let c = ConCurve::default();
        // delta=1 sits halfway between 0 (10_000) and 2 (16_000) -> 13_000.
        assert_eq!(c.multiplier_bp(1), 13_000);
        // delta=3 between 2 (16_000) and 5 (30_000): 16000 + 14000*1/3 = 20666.67
        // round half-to-even of 20666.666... -> 20667.
        assert_eq!(c.multiplier_bp(3), 20_667);
    }

    #[test]
    fn monotone_nondecreasing() {
        let c = ConCurve::default();
        let mut prev = i64::MIN;
        for d in -10..=10 {
            let m = c.multiplier_bp(d);
            assert!(m >= prev, "curve dipped at delta {d}: {m} < {prev}");
            prev = m;
        }
    }

    #[test]
    fn unsorted_input_is_normalized() {
        let c = ConCurve::new(vec![
            ConAnchor { delta: 5, mult_bp: 30_000 },
            ConAnchor { delta: 0, mult_bp: 10_000 },
            ConAnchor { delta: -5, mult_bp: 2_000 },
        ]);
        assert_eq!(c.multiplier_bp(0), 10_000);
        assert_eq!(c.multiplier_bp(5), 30_000);
    }
}
