//! Pure scoring formulas.
//!
//! Every function in this module is a stateless mathematical formula: it takes
//! numeric inputs and returns a score. No I/O, no struct methods, no ledger
//! access. This makes the formulas independently testable and trivially
//! verifiable against their source definitions.

/// Additive weight applied to the creator-interaction boost.
///
/// When a user has interacted with a creator, items from that creator receive
/// an additive score boost of `interaction_weight * INTERACTION_BOOST_WEIGHT`
/// before normalization. 0.3 is a tuning constant that keeps interaction
/// boosts meaningful without overwhelming the base signal score.
pub(super) const INTERACTION_BOOST_WEIGHT: f64 = 0.3;

/// Hot: `log10(max(upvotes - downvotes, 1)) / (age_hours + 2)^gravity`
pub(super) fn hot_score(views: f64, age_hours: f64, gravity: f64) -> f64 {
    views.max(1.0).log10() / (age_hours + 2.0).powf(gravity)
}

/// Trending: weighted sum of view and share velocity.
pub(super) fn trending_score(view_velocity: f64, share_velocity: f64) -> f64 {
    2.0f64.mul_add(share_velocity, view_velocity)
}

/// Controversial: `(pos * neg) / (pos + neg)^2`
pub(super) fn controversial_score(pos: f64, neg: f64) -> f64 {
    let denom = (pos + neg).powi(2);
    if denom < f64::EPSILON {
        0.0
    } else {
        (pos * neg) / denom
    }
}

/// Hidden gems: `quality / log10(view_count + 10)`
pub(super) fn hidden_gems_score(quality: f64, view_count: f64) -> f64 {
    quality / (view_count + 10.0).log10()
}

/// Shuffle: deterministic hash of entity ID for stable random ordering.
pub(super) fn shuffle_score(entity_id: u64) -> f64 {
    let hash = blake3::hash(&entity_id.to_le_bytes());
    let bytes = hash.as_bytes();
    // First 8 bytes as u64, normalized to [0, 1].
    let arr: [u8; 8] = [
        bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
    ];
    let v = u64::from_le_bytes(arr);
    #[allow(clippy::cast_precision_loss)]
    let score = v as f64 / u64::MAX as f64;
    score
}

/// Haversine great-circle distance in kilometres.
///
/// Computes the shortest-path distance between two points on the Earth's
/// surface given their latitude and longitude in decimal degrees.
#[must_use]
pub fn haversine_km(lat1: f64, lng1: f64, lat2: f64, lng2: f64) -> f64 {
    const R: f64 = 6371.0; // Earth's mean radius in km
    let dlat = (lat2 - lat1).to_radians();
    let dlng = (lng2 - lng1).to_radians();
    let a = (lat1.to_radians().cos() * lat2.to_radians().cos())
        .mul_add((dlng / 2.0).sin().powi(2), (dlat / 2.0).sin().powi(2));
    2.0 * R * a.sqrt().asin()
}

// -- Tests --------------------------------------------------------------------

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
    use super::*;

    #[test]
    fn score_hot_decays_older_candidates() {
        // hot_score = log10(max(views, 1)) / (age_hours + 2)^gravity.
        // Older content (more hours) scores lower than newer with equal views.
        let gravity = 1.8_f64;
        let score_new = hot_score(50.0, 2.0, gravity);
        let score_old = hot_score(50.0, 48.0, gravity);
        assert!(
            score_new > score_old,
            "newer content should score higher than older content with the same view count"
        );
    }

    #[test]
    fn score_trending_uses_velocity() {
        // trending_score = view_velocity + 2.0 * share_velocity.
        // Positive velocity inputs yield a positive score.
        let score = trending_score(2.0, 1.0);
        assert!(score > 0.0);
        assert_eq!(trending_score(0.0, 0.0), 0.0);
    }

    #[test]
    fn haversine_same_point_is_zero() {
        let d = haversine_km(40.7128, -74.0060, 40.7128, -74.0060);
        assert!(d.abs() < 1e-6, "same point should be 0 km, got {d}");
    }

    #[test]
    fn haversine_nyc_to_london() {
        // NYC (40.7128, -74.0060) to London (51.5074, -0.1278)
        // Expected: ~5570 km
        let d = haversine_km(40.7128, -74.0060, 51.5074, -0.1278);
        assert!(
            (5500.0..5650.0).contains(&d),
            "NYC to London should be ~5570 km, got {d}"
        );
    }

    #[test]
    fn haversine_antipodes() {
        // North pole (90, 0) to south pole (-90, 0) = half circumference ~20015 km
        let d = haversine_km(90.0, 0.0, -90.0, 0.0);
        assert!(
            (20_000.0..20_040.0).contains(&d),
            "pole to pole should be ~20015 km, got {d}"
        );
    }
}
