use mongodb::bson::doc;
use rmcp::{model::*, ErrorData as McpError};
use schemars::JsonSchema;
use serde::Deserialize;

use crate::database::Database;

const MAX_LIMIT: i64 = 200;
const DEFAULT_LIMIT: i64 = 50;

fn cap_limit(limit: Option<i64>) -> i64 {
    limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT)
}

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

    #[test]
    fn cap_limit_default() {
        assert_eq!(cap_limit(None), DEFAULT_LIMIT);
    }

    #[test]
    fn cap_limit_clamps_high() {
        assert_eq!(cap_limit(Some(999)), MAX_LIMIT);
    }

    #[test]
    fn cap_limit_clamps_low() {
        assert_eq!(cap_limit(Some(0)), 1);
        assert_eq!(cap_limit(Some(-5)), 1);
    }

    #[test]
    fn cap_limit_normal() {
        assert_eq!(cap_limit(Some(75)), 75);
    }

    #[test]
    fn list_sbom_params_deserialize() {
        let json = serde_json::json!({
            "repo_id": "repo1",
            "has_vulns": true,
            "package_manager": "npm",
            "license": "MIT",
            "limit": 30
        });
        let params: ListSbomPackagesParams = serde_json::from_value(json).unwrap();
        assert_eq!(params.repo_id.as_deref(), Some("repo1"));
        assert_eq!(params.has_vulns, Some(true));
        assert_eq!(params.package_manager.as_deref(), Some("npm"));
        assert_eq!(params.license.as_deref(), Some("MIT"));
        assert_eq!(params.limit, Some(30));
    }

    #[test]
    fn list_sbom_params_all_optional() {
        let params: ListSbomPackagesParams = serde_json::from_value(serde_json::json!({})).unwrap();
        assert!(params.repo_id.is_none());
        assert!(params.has_vulns.is_none());
        assert!(params.package_manager.is_none());
        assert!(params.license.is_none());
        assert!(params.limit.is_none());
    }

    #[test]
    fn sbom_vuln_report_params_deserialize() {
        let json = serde_json::json!({ "repo_id": "my-repo" });
        let params: SbomVulnReportParams = serde_json::from_value(json).unwrap();
        assert_eq!(params.repo_id, "my-repo");
    }
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListSbomPackagesParams {
    /// Filter by repository ID
    pub repo_id: Option<String>,
    /// Only show packages with known vulnerabilities
    pub has_vulns: Option<bool>,
    /// Filter by package manager (e.g. npm, cargo, pip)
    pub package_manager: Option<String>,
    /// Filter by license (e.g. MIT, Apache-2.0)
    pub license: Option<String>,
    /// Maximum number of results (default 50, max 200)
    pub limit: Option<i64>,
}

pub async fn list_sbom_packages(
    db: &Database,
    params: ListSbomPackagesParams,
) -> Result<CallToolResult, McpError> {
    let mut filter = doc! {};
    if let Some(ref repo_id) = params.repo_id {
        filter.insert("repo_id", repo_id);
    }
    if let Some(ref pm) = params.package_manager {
        filter.insert("package_manager", pm);
    }
    if let Some(ref license) = params.license {
        filter.insert("license", license);
    }
    if params.has_vulns == Some(true) {
        filter.insert("known_vulnerabilities.0", doc! { "$exists": true });
    }

    let limit = cap_limit(params.limit);

    let mut cursor = db
        .sbom_entries()
        .find(filter)
        .sort(doc! { "name": 1 })
        .limit(limit)
        .await
        .map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;

    let mut results = Vec::new();
    while cursor
        .advance()
        .await
        .map_err(|e| McpError::internal_error(format!("cursor error: {e}"), None))?
    {
        let entry = cursor
            .deserialize_current()
            .map_err(|e| McpError::internal_error(format!("deserialize error: {e}"), None))?;
        results.push(entry);
    }

    let json = serde_json::to_string_pretty(&results)
        .map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;

    Ok(CallToolResult::success(vec![Content::text(json)]))
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct SbomVulnReportParams {
    /// Repository ID to generate vulnerability report for
    pub repo_id: String,
}

pub async fn sbom_vuln_report(
    db: &Database,
    params: SbomVulnReportParams,
) -> Result<CallToolResult, McpError> {
    let filter = doc! {
        "repo_id": &params.repo_id,
        "known_vulnerabilities.0": { "$exists": true },
    };

    let mut cursor = db
        .sbom_entries()
        .find(filter)
        .sort(doc! { "name": 1 })
        .await
        .map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;

    let mut vulnerable_packages = Vec::new();
    let mut total_vulns = 0u64;

    while cursor
        .advance()
        .await
        .map_err(|e| McpError::internal_error(format!("cursor error: {e}"), None))?
    {
        let entry = cursor
            .deserialize_current()
            .map_err(|e| McpError::internal_error(format!("deserialize error: {e}"), None))?;
        total_vulns += entry.known_vulnerabilities.len() as u64;
        vulnerable_packages.push(serde_json::json!({
            "name": entry.name,
            "version": entry.version,
            "package_manager": entry.package_manager,
            "license": entry.license,
            "vulnerabilities": entry.known_vulnerabilities,
        }));
    }

    let report = serde_json::json!({
        "repo_id": params.repo_id,
        "vulnerable_packages_count": vulnerable_packages.len(),
        "total_vulnerabilities": total_vulns,
        "packages": vulnerable_packages,
    });

    let json = serde_json::to_string_pretty(&report)
        .map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;

    Ok(CallToolResult::success(vec![Content::text(json)]))
}
