//! Rugcheck.xyz integration for LP lock and liquidity risk assessment.
//!
//! Provides types for the `/v1/tokens/{id}/report/summary` endpoint and the
//! 25-point liquidity scoring logic.
//!
//! ## Scoring breakdown (25 pts total)
//!
//! | Sub-category          | Max | Signal source         |
//! |---|---|---|
//! | LP lock %             |  15 | `lp_locked_pct`       |
//! | Rugcheck risk score   |   7 | `score` (raw)         |
//! | No DANGER-level risks |   3 | `risks[].level`       |
//!
//! A locked percentage of exactly 0% triggers a Critical hard stop — the rest
//! of the score is irrelevant when anyone can pull all liquidity at any time.

use anyhow::{Context, Result};
use serde::Deserialize;

const RUGCHECK_API_URL: &str = "https://api.rugcheck.xyz/v1/tokens/{}/report/summary";

// ── API response types ────────────────────────────────────────────────────────

#[derive(Debug, Clone, Deserialize, Default)]
pub struct RugcheckReport {
    pub score: Option<u64>,
    #[serde(rename = "score_normalised")]
    pub score_normalised: Option<u64>,
    #[serde(rename = "lpLockedPct")]
    pub lp_locked_pct: Option<f64>,
    #[serde(rename = "tokenType")]
    pub token_type: Option<String>,
    #[serde(rename = "tokenProgram")]
    pub token_program: Option<String>,
    #[serde(default)]
    pub risks: Vec<RugcheckRisk>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct RugcheckRisk {
    pub name: Option<String>,
    pub level: Option<String>,
    pub description: Option<String>,
    pub score: Option<i64>,
    pub value: Option<String>,
}

// ── Fetch ─────────────────────────────────────────────────────────────────────

/// Fetch a token's rugcheck summary report.
///
/// Uses the public `/v1/tokens/{id}/report/summary` endpoint — no authentication
/// required. Returns a deserialized [`RugcheckReport`].
pub async fn fetch_rugcheck_report(client: &reqwest::Client, mint: &str) -> Result<RugcheckReport> {
    let url = RUGCHECK_API_URL.replace("{}", mint);
    let report = client
        .get(&url)
        .send()
        .await
        .context("HTTP request to rugcheck.xyz failed")?
        .error_for_status()
        .context("rugcheck.xyz returned an error status")?
        .json::<RugcheckReport>()
        .await
        .context("Failed to deserialize rugcheck.xyz response")?;
    Ok(report)
}

// ── Scoring ───────────────────────────────────────────────────────────────────

/// Output of the liquidity scoring calculation.
pub struct LiquidityScoreResult {
    pub score: f64,
    pub reasons: Vec<String>,
    pub warnings: Vec<String>,
    pub positive_signals: Vec<String>,
}

/// Score the liquidity category (0–25 points) from a rugcheck report.
///
/// # Breakdown
/// - **LP lock % (15 pts)**: 0% → hard stop (0 pts); scales up to 15 at ≥95%.
/// - **Rugcheck risk score (7 pts)**: lower raw score = safer.
/// - **No DANGER risks (3 pts)**: counts `risks[]` entries with `level = "DANGER"`.
///
/// When `lp_locked_pct` is 0% this function still returns 0 pts for that
/// sub-category; the actual hard stop is raised separately by
/// [`crate::hard_stops::check_liquidity_hard_stops`].
pub fn score_liquidity(report: &RugcheckReport) -> LiquidityScoreResult {
    let mut score = 0.0;
    let mut reasons = Vec::new();
    let mut warnings = Vec::new();
    let mut positive_signals = Vec::new();

    let lp_pct = report.lp_locked_pct.unwrap_or(0.0);

    // --- Part 1: LP lock % (15 pts) ---
    if lp_pct == 0.0 {
        reasons.push("+0  — LP not locked (0%) — hard stop triggered".to_string());
        warnings.push("⚠ CRITICAL: Liquidity pool not locked — rug pull risk".to_string());
    } else if lp_pct < 25.0 {
        score += 3.0;
        reasons.push(format!("+3  — LP locked {:.1}% (minimal lock)", lp_pct));
        warnings.push(format!(
            "⚠ Only {:.1}% of LP is locked — high rug risk",
            lp_pct
        ));
    } else if lp_pct < 50.0 {
        score += 7.0;
        reasons.push(format!("+7  — LP locked {:.1}% (partial lock)", lp_pct));
        warnings.push(format!("⚠ {:.1}% of LP locked — moderate rug risk", lp_pct));
    } else if lp_pct < 75.0 {
        score += 10.0;
        reasons.push(format!("+10 — LP locked {:.1}% (majority locked)", lp_pct));
        positive_signals.push(format!("✓ {:.1}% of LP locked", lp_pct));
    } else if lp_pct < 95.0 {
        score += 13.0;
        reasons.push(format!("+13 — LP locked {:.1}% (strong lock)", lp_pct));
        positive_signals.push(format!("✓ Strong LP lock: {:.1}% locked", lp_pct));
    } else {
        score += 15.0;
        reasons.push(format!(
            "+15 — LP locked {:.1}% (fully locked/burned)",
            lp_pct
        ));
        positive_signals.push(format!(
            "✓ Excellent LP lock: {:.1}% (effectively permanent)",
            lp_pct
        ));
    }

    // --- Part 2: Rugcheck risk score (7 pts) ---
    match report.score {
        None => {
            reasons.push("+0  — Rugcheck score unavailable".to_string());
            warnings.push("⚠ Rugcheck risk score unavailable".to_string());
        }
        Some(s) if s >= 500 => {
            reasons.push(format!("+0  — Rugcheck score {} (high risk)", s));
            warnings.push(format!(
                "⚠ CRITICAL: Rugcheck risk score {} is very high",
                s
            ));
        }
        Some(s) if s >= 350 => {
            score += 2.0;
            reasons.push(format!("+2  — Rugcheck score {} (elevated risk)", s));
            warnings.push(format!("⚠ Rugcheck risk score {} is elevated", s));
        }
        Some(s) if s >= 200 => {
            score += 5.0;
            reasons.push(format!("+5  — Rugcheck score {} (moderate risk)", s));
        }
        Some(s) => {
            score += 7.0;
            reasons.push(format!("+7  — Rugcheck score {} (low risk)", s));
            positive_signals.push(format!("✓ Low rugcheck risk score ({})", s));
        }
    }

    // --- Part 3: No DANGER-level risks (3 pts) ---
    let danger_count = report
        .risks
        .iter()
        .filter(|r| {
            r.level
                .as_deref()
                .map(|l| l.to_uppercase() == "DANGER")
                .unwrap_or(false)
        })
        .count();

    match danger_count {
        0 => {
            score += 3.0;
            reasons.push("+3  — No DANGER-level risks identified".to_string());
            positive_signals.push("✓ No DANGER-level risks (rugcheck.xyz)".to_string());
        }
        1 => {
            score += 1.0;
            reasons.push("+1  — 1 DANGER-level risk identified".to_string());
            warnings.push("⚠ 1 DANGER-level risk flagged by rugcheck".to_string());
        }
        n => {
            reasons.push(format!("+0  — {} DANGER-level risks identified", n));
            warnings.push(format!(
                "⚠ CRITICAL: {} DANGER-level risks flagged by rugcheck",
                n
            ));
        }
    }

    LiquidityScoreResult {
        score,
        reasons,
        warnings,
        positive_signals,
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

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

    fn report_with_lp(lp_pct: f64) -> RugcheckReport {
        RugcheckReport {
            lp_locked_pct: Some(lp_pct),
            score: Some(100),
            risks: vec![],
            ..Default::default()
        }
    }

    fn danger_risk() -> RugcheckRisk {
        RugcheckRisk {
            name: Some("LP Unlocked".to_string()),
            level: Some("DANGER".to_string()),
            description: None,
            score: Some(500),
            value: None,
        }
    }

    #[test]
    fn test_lp_zero_scores_zero_and_warns() {
        let report = report_with_lp(0.0);
        let result = score_liquidity(&report);
        // LP sub-score = 0, rugcheck = 7, no danger = 3 → total 10
        assert_eq!(result.score, 10.0);
        assert!(result.warnings.iter().any(|w| w.contains("not locked")));
    }

    #[test]
    fn test_lp_minimal_lock() {
        let report = report_with_lp(15.0);
        let result = score_liquidity(&report);
        // LP = 3, rugcheck = 7, no danger = 3 → 13
        assert_eq!(result.score, 13.0);
        assert!(result.warnings.iter().any(|w| w.contains("high rug risk")));
    }

    #[test]
    fn test_lp_partial_lock() {
        let report = report_with_lp(40.0);
        let result = score_liquidity(&report);
        // LP = 7, rugcheck = 7, no danger = 3 → 17
        assert_eq!(result.score, 17.0);
    }

    #[test]
    fn test_lp_majority_locked() {
        let report = report_with_lp(60.0);
        let result = score_liquidity(&report);
        // LP = 10, rugcheck = 7, no danger = 3 → 20
        assert_eq!(result.score, 20.0);
    }

    #[test]
    fn test_lp_strong_lock() {
        let report = report_with_lp(80.0);
        let result = score_liquidity(&report);
        // LP = 13, rugcheck = 7, no danger = 3 → 23
        assert_eq!(result.score, 23.0);
    }

    #[test]
    fn test_lp_fully_locked() {
        let report = report_with_lp(99.0);
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 7, no danger = 3 → 25
        assert_eq!(result.score, 25.0);
        assert!(
            result
                .positive_signals
                .iter()
                .any(|s| s.contains("Excellent"))
        );
    }

    #[test]
    fn test_high_rugcheck_score_penalised() {
        let report = RugcheckReport {
            lp_locked_pct: Some(99.0),
            score: Some(600),
            risks: vec![],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 0, no danger = 3 → 18
        assert_eq!(result.score, 18.0);
        assert!(result.warnings.iter().any(|w| w.contains("very high")));
    }

    #[test]
    fn test_elevated_rugcheck_score() {
        let report = RugcheckReport {
            lp_locked_pct: Some(99.0),
            score: Some(400),
            risks: vec![],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 2, no danger = 3 → 20
        assert_eq!(result.score, 20.0);
    }

    #[test]
    fn test_moderate_rugcheck_score() {
        let report = RugcheckReport {
            lp_locked_pct: Some(99.0),
            score: Some(250),
            risks: vec![],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 5, no danger = 3 → 23
        assert_eq!(result.score, 23.0);
    }

    #[test]
    fn test_missing_rugcheck_score_scores_zero() {
        let report = RugcheckReport {
            lp_locked_pct: Some(99.0),
            score: None,
            risks: vec![],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 0, no danger = 3 → 18
        assert_eq!(result.score, 18.0);
        assert!(result.warnings.iter().any(|w| w.contains("unavailable")));
    }

    #[test]
    fn test_one_danger_risk_gets_one_point() {
        let report = RugcheckReport {
            lp_locked_pct: Some(99.0),
            score: Some(100),
            risks: vec![danger_risk()],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 7, 1 danger = 1 → 23
        assert_eq!(result.score, 23.0);
    }

    #[test]
    fn test_two_danger_risks_scores_zero_for_part3() {
        let report = RugcheckReport {
            lp_locked_pct: Some(99.0),
            score: Some(100),
            risks: vec![danger_risk(), danger_risk()],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 7, 2 danger = 0 → 22
        assert_eq!(result.score, 22.0);
        assert!(
            result
                .warnings
                .iter()
                .any(|w| w.contains("2 DANGER-level risks"))
        );
    }

    #[test]
    fn test_warn_level_risks_do_not_affect_danger_count() {
        let report = RugcheckReport {
            lp_locked_pct: Some(99.0),
            score: Some(100),
            risks: vec![RugcheckRisk {
                name: Some("Something".to_string()),
                level: Some("WARN".to_string()),
                description: None,
                score: Some(100),
                value: None,
            }],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        // LP = 15, rugcheck = 7, no danger = 3 → 25
        assert_eq!(result.score, 25.0);
    }

    #[test]
    fn test_perfect_token_scores_25() {
        let report = RugcheckReport {
            lp_locked_pct: Some(100.0),
            score: Some(50),
            risks: vec![],
            ..Default::default()
        };
        let result = score_liquidity(&report);
        assert_eq!(result.score, 25.0);
        assert!(result.warnings.is_empty());
    }
}
