// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 SenWeaverCoding
// Licensed under the MIT License.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Write;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Finding {

    #[serde(default)]
    pub cve_id: String,

    pub cvss_score: f64,

    pub severity: String,

    pub affected_asset: String,

    pub description: String,

    #[serde(default)]
    pub remediation: String,

    #[serde(default)]
    pub internet_facing: bool,

    #[serde(default = "default_true")]
    pub production: bool,
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnerabilityReport {

    pub scan_date: DateTime<Utc>,

    pub scanner: String,

    pub findings: Vec<Finding>,
}

pub fn effective_priority(finding: &Finding) -> f64 {
    let mut score = finding.cvss_score;
    if finding.internet_facing {
        score += 2.0;
    }
    if finding.production {
        score += 1.0;
    }
    score.min(10.0)
}

pub fn cvss_to_severity(cvss: f64) -> &'static str {
    match cvss {
        s if s >= 9.0 => "critical",
        s if s >= 7.0 => "high",
        s if s >= 4.0 => "medium",
        s if s > 0.0 => "low",
        _ => "informational",
    }
}

pub fn parse_vulnerability_json(json_str: &str) -> anyhow::Result<VulnerabilityReport> {
    let report: VulnerabilityReport = serde_json::from_str(json_str)
        .map_err(|e| anyhow::anyhow!("Failed to parse vulnerability report: {e}"))?;

    for (i, finding) in report.findings.iter().enumerate() {
        if !(0.0..=10.0).contains(&finding.cvss_score) {
            anyhow::bail!(
                "findings[{}].cvss_score must be between 0.0 and 10.0, got {}",
                i,
                finding.cvss_score
            );
        }
    }

    Ok(report)
}

pub fn generate_summary(report: &VulnerabilityReport) -> String {
    if report.findings.is_empty() {
        return format!(
            "Vulnerability scan by {} on {}: No findings.",
            report.scanner,
            report.scan_date.format("%Y-%m-%d")
        );
    }

    let total = report.findings.len();
    let critical = report
        .findings
        .iter()
        .filter(|f| f.severity.eq_ignore_ascii_case("critical"))
        .count();
    let high = report
        .findings
        .iter()
        .filter(|f| f.severity.eq_ignore_ascii_case("high"))
        .count();
    let medium = report
        .findings
        .iter()
        .filter(|f| f.severity.eq_ignore_ascii_case("medium"))
        .count();
    let low = report
        .findings
        .iter()
        .filter(|f| f.severity.eq_ignore_ascii_case("low"))
        .count();
    let informational = report
        .findings
        .iter()
        .filter(|f| f.severity.eq_ignore_ascii_case("informational"))
        .count();

    let mut sorted: Vec<&Finding> = report.findings.iter().collect();
    sorted.sort_by(|a, b| {
        effective_priority(b)
            .partial_cmp(&effective_priority(a))
            .unwrap_or(std::cmp::Ordering::Equal)
    });

    let mut summary = format!(
        "## Vulnerability Scan Summary\n\
         **Scanner:** {} | **Date:** {}\n\
         **Total findings:** {} (Critical: {}, High: {}, Medium: {}, Low: {}, Informational: {})\n\n",
        report.scanner,
        report.scan_date.format("%Y-%m-%d"),
        total,
        critical,
        high,
        medium,
        low,
        informational
    );

    summary.push_str("### Top Findings by Priority\n\n");
    for (i, finding) in sorted.iter().take(10).enumerate() {
        let priority = effective_priority(finding);
        let context = match (finding.internet_facing, finding.production) {
            (true, true) => " [internet-facing, production]",
            (true, false) => " [internet-facing]",
            (false, true) => " [production]",
            (false, false) => "",
        };
        let _ = writeln!(
            summary,
            "{}. **{}** (CVSS: {:.1}, Priority: {:.1}){}\n   Asset: {} | {}",
            i + 1,
            if finding.cve_id.is_empty() {
                "No CVE"
            } else {
                &finding.cve_id
            },
            finding.cvss_score,
            priority,
            context,
            finding.affected_asset,
            finding.description
        );
        if !finding.remediation.is_empty() {
            let _ = writeln!(summary, "   Remediation: {}", finding.remediation);
        }
        summary.push('\n');
    }

    if critical > 0 || high > 0 {
        summary.push_str("### Remediation Recommendations\n\n");
        if critical > 0 {
            let _ = writeln!(
                summary,
                "- **URGENT:** {} critical findings require immediate remediation",
                critical
            );
        }
        if high > 0 {
            let _ = writeln!(
                summary,
                "- **HIGH:** {} high-severity findings should be addressed within 7 days",
                high
            );
        }
        let internet_facing_critical = sorted
            .iter()
            .filter(|f| f.internet_facing && (f.severity == "critical" || f.severity == "high"))
            .count();
        if internet_facing_critical > 0 {
            let _ = writeln!(
                summary,
                "- **PRIORITY:** {} critical/high findings on internet-facing assets",
                internet_facing_critical
            );
        }
    }

    summary
}
