use super::database::{CveSummary, Database};
use crate::config::VERSION;

/// Export format for security reports.
#[derive(Debug, Clone, Copy)]
pub enum ReportFormat {
    Json,
    Html,
    Csv,
}

impl ReportFormat {
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "json" => Some(Self::Json),
            "html" => Some(Self::Html),
            "csv" => Some(Self::Csv),
            _ => None,
        }
    }

    pub fn extension(&self) -> &'static str {
        match self {
            Self::Json => "json",
            Self::Html => "html",
            Self::Csv => "csv",
        }
    }
}

/// A single CVE finding in a report.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ReportCveFinding {
    pub cve_id: String,
    pub severity: String,
    pub cvss_score: Option<f64>,
    pub summary: String,
    pub library_name: String,
    pub library_version: String,
    pub fixed_version: Option<String>,
}

/// Per-app entry in a report.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ReportAppEntry {
    pub name: String,
    pub version: Option<String>,
    pub path: String,
    pub libraries_scanned: usize,
    pub cve_summary: ReportCveSummaryData,
    pub findings: Vec<ReportCveFinding>,
}

/// Serializable CVE summary counts.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ReportCveSummaryData {
    pub critical: i64,
    pub high: i64,
    pub medium: i64,
    pub low: i64,
    pub total: i64,
}

impl From<&CveSummary> for ReportCveSummaryData {
    fn from(s: &CveSummary) -> Self {
        Self {
            critical: s.critical,
            high: s.high,
            medium: s.medium,
            low: s.low,
            total: s.total(),
        }
    }
}

/// Complete security report.
#[derive(Debug, Clone, serde::Serialize)]
pub struct SecurityReport {
    pub generated_at: String,
    pub driftwood_version: String,
    pub apps: Vec<ReportAppEntry>,
    pub totals: ReportCveSummaryData,
}

/// Generate a security report from the database.
pub fn build_report(db: &Database, single_app_id: Option<i64>) -> SecurityReport {
    let records = if let Some(id) = single_app_id {
        db.get_appimage_by_id(id).ok().flatten().into_iter().collect()
    } else {
        db.get_all_appimages().unwrap_or_default()
    };

    let mut apps = Vec::new();
    let mut total_summary = CveSummary::default();

    for record in &records {
        let libs = db.get_bundled_libraries(record.id).unwrap_or_default();
        let cve_matches = db.get_cve_matches(record.id).unwrap_or_default();
        let summary = db.get_cve_summary(record.id).unwrap_or_default();

        let findings: Vec<ReportCveFinding> = cve_matches.iter().map(|m| {
            ReportCveFinding {
                cve_id: m.cve_id.clone(),
                severity: m.severity.clone().unwrap_or_default(),
                cvss_score: m.cvss_score,
                summary: m.summary.clone().unwrap_or_default(),
                library_name: m.library_name.clone().unwrap_or_else(|| m.library_soname.clone()),
                library_version: m.library_version.clone().unwrap_or_default(),
                fixed_version: m.fixed_version.clone(),
            }
        }).collect();

        total_summary.critical += summary.critical;
        total_summary.high += summary.high;
        total_summary.medium += summary.medium;
        total_summary.low += summary.low;

        apps.push(ReportAppEntry {
            name: record.app_name.clone().unwrap_or_else(|| record.filename.clone()),
            version: record.app_version.clone(),
            path: record.path.clone(),
            libraries_scanned: libs.len(),
            cve_summary: ReportCveSummaryData::from(&summary),
            findings,
        });
    }

    SecurityReport {
        generated_at: chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(),
        driftwood_version: VERSION.to_string(),
        apps,
        totals: ReportCveSummaryData::from(&total_summary),
    }
}

/// Render the report to JSON.
pub fn render_json(report: &SecurityReport) -> String {
    serde_json::to_string_pretty(report).unwrap_or_else(|_| "{}".to_string())
}

/// Render the report to CSV.
pub fn render_csv(report: &SecurityReport) -> String {
    let mut out = String::from("App,Version,Path,CVE ID,Severity,CVSS,Library,Library Version,Fixed Version,Summary\n");

    for app in &report.apps {
        if app.findings.is_empty() {
            out.push_str(&format!(
                "\"{}\",\"{}\",\"{}\",,,,,,,No CVEs found\n",
                csv_escape(&app.name),
                csv_escape(app.version.as_deref().unwrap_or("")),
                csv_escape(&app.path),
            ));
        } else {
            for f in &app.findings {
                out.push_str(&format!(
                    "\"{}\",\"{}\",\"{}\",\"{}\",\"{}\",{},\"{}\",\"{}\",\"{}\",\"{}\"\n",
                    csv_escape(&app.name),
                    csv_escape(app.version.as_deref().unwrap_or("")),
                    csv_escape(&app.path),
                    csv_escape(&f.cve_id),
                    csv_escape(&f.severity),
                    f.cvss_score.map(|s| format!("{:.1}", s)).unwrap_or_default(),
                    csv_escape(&f.library_name),
                    csv_escape(&f.library_version),
                    csv_escape(f.fixed_version.as_deref().unwrap_or("")),
                    csv_escape(&f.summary),
                ));
            }
        }
    }

    out
}

fn csv_escape(s: &str) -> String {
    s.replace('"', "\"\"")
}

/// Render the report to a standalone HTML document.
pub fn render_html(report: &SecurityReport) -> String {
    let mut html = String::new();

    html.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
    html.push_str("<meta charset=\"UTF-8\">\n");
    html.push_str("<title>Driftwood Security Report</title>\n");
    html.push_str("<style>\n");
    html.push_str("body { font-family: system-ui, -apple-system, sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; color: #333; }\n");
    html.push_str("h1 { border-bottom: 2px solid #333; padding-bottom: 0.3em; }\n");
    html.push_str("h2 { margin-top: 2em; }\n");
    html.push_str("table { border-collapse: collapse; width: 100%; margin: 1em 0; }\n");
    html.push_str("th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }\n");
    html.push_str("th { background: #f5f5f5; }\n");
    html.push_str(".critical { color: #d32f2f; font-weight: bold; }\n");
    html.push_str(".high { color: #e65100; font-weight: bold; }\n");
    html.push_str(".medium { color: #f9a825; }\n");
    html.push_str(".low { color: #666; }\n");
    html.push_str(".summary-box { background: #f5f5f5; border-radius: 8px; padding: 1em; margin: 1em 0; }\n");
    html.push_str("footer { margin-top: 3em; padding-top: 1em; border-top: 1px solid #ddd; font-size: 0.85em; color: #666; }\n");
    html.push_str("</style>\n</head>\n<body>\n");

    html.push_str("<h1>Driftwood Security Report</h1>\n");
    html.push_str(&format!("<p>Generated: {} | Driftwood v{}</p>\n",
        report.generated_at, report.driftwood_version));

    // Summary
    html.push_str("<div class=\"summary-box\">\n");
    html.push_str("<h2>Summary</h2>\n");
    html.push_str(&format!("<p>Apps scanned: {} | Total CVEs: {}</p>\n",
        report.apps.len(), report.totals.total));
    html.push_str(&format!(
        "<p><span class=\"critical\">Critical: {}</span> | <span class=\"high\">High: {}</span> | <span class=\"medium\">Medium: {}</span> | <span class=\"low\">Low: {}</span></p>\n",
        report.totals.critical, report.totals.high, report.totals.medium, report.totals.low));
    html.push_str("</div>\n");

    // Per-app sections
    for app in &report.apps {
        html.push_str(&format!("<h2>{}", html_escape(&app.name)));
        if let Some(ref ver) = app.version {
            html.push_str(&format!(" v{}", html_escape(ver)));
        }
        html.push_str("</h2>\n");
        html.push_str(&format!("<p>Path: <code>{}</code> | Libraries scanned: {}</p>\n",
            html_escape(&app.path), app.libraries_scanned));

        if app.findings.is_empty() {
            html.push_str("<p>No known vulnerabilities found.</p>\n");
            continue;
        }

        html.push_str("<table>\n<tr><th>CVE</th><th>Severity</th><th>CVSS</th><th>Library</th><th>Fixed In</th><th>Summary</th></tr>\n");
        for f in &app.findings {
            let sev_class = html_escape(&f.severity.to_lowercase());
            html.push_str(&format!(
                "<tr><td>{}</td><td class=\"{}\">{}</td><td>{}</td><td>{} {}</td><td>{}</td><td>{}</td></tr>\n",
                html_escape(&f.cve_id),
                sev_class, html_escape(&f.severity),
                f.cvss_score.map(|s| format!("{:.1}", s)).unwrap_or_default(),
                html_escape(&f.library_name), html_escape(&f.library_version),
                html_escape(f.fixed_version.as_deref().unwrap_or("-")),
                html_escape(&f.summary),
            ));
        }
        html.push_str("</table>\n");
    }

    html.push_str("<footer>\n");
    html.push_str("<p>This report was generated by Driftwood using the OSV.dev vulnerability database. ");
    html.push_str("Library detection uses heuristics and may not identify all bundled components. ");
    html.push_str("Results should be treated as advisory, not definitive.</p>\n");
    html.push_str("</footer>\n");
    html.push_str("</body>\n</html>\n");

    html
}

fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
     .replace('<', "&lt;")
     .replace('>', "&gt;")
     .replace('"', "&quot;")
}

/// Render the report in the given format.
pub fn render(report: &SecurityReport, format: ReportFormat) -> String {
    match format {
        ReportFormat::Json => render_json(report),
        ReportFormat::Html => render_html(report),
        ReportFormat::Csv => render_csv(report),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::database::Database;

    #[test]
    fn test_render_json_empty() {
        let db = Database::open_in_memory().unwrap();
        let report = build_report(&db, None);
        let json = render_json(&report);
        assert!(json.contains("\"apps\""));
        assert!(json.contains("\"totals\""));
        assert!(json.contains("\"driftwood_version\""));
    }

    #[test]
    fn test_render_csv_header() {
        let db = Database::open_in_memory().unwrap();
        let report = build_report(&db, None);
        let csv = render_csv(&report);
        assert!(csv.starts_with("App,Version,Path,CVE ID"));
    }

    #[test]
    fn test_render_html_structure() {
        let db = Database::open_in_memory().unwrap();
        let report = build_report(&db, None);
        let html = render_html(&report);
        assert!(html.contains("<!DOCTYPE html>"));
        assert!(html.contains("Driftwood Security Report"));
        assert!(html.contains("</html>"));
    }

    #[test]
    fn test_report_format_from_str() {
        assert!(matches!(ReportFormat::from_str("json"), Some(ReportFormat::Json)));
        assert!(matches!(ReportFormat::from_str("HTML"), Some(ReportFormat::Html)));
        assert!(matches!(ReportFormat::from_str("csv"), Some(ReportFormat::Csv)));
        assert!(ReportFormat::from_str("xml").is_none());
    }

    #[test]
    fn test_csv_escape() {
        assert_eq!(csv_escape("hello \"world\""), "hello \"\"world\"\"");
    }

    #[test]
    fn test_html_escape() {
        assert_eq!(html_escape("<script>&"), "&lt;script&gt;&amp;");
    }
}
