use std::collections::HashMap;
use std::path::Path;
use std::process::Command;

use super::database::Database;

/// A bundled shared library detected inside an AppImage.
#[derive(Debug, Clone)]
pub struct BundledLibrary {
    pub soname: String,
    pub detected_name: Option<String>,
    pub detected_version: Option<String>,
    pub file_path: String,
    pub file_size: u64,
}

/// A CVE match found for a bundled library.
#[derive(Debug, Clone)]
pub struct CveMatch {
    pub cve_id: String,
    pub severity: String,
    pub cvss_score: Option<f64>,
    pub summary: String,
    pub affected_versions: Option<String>,
    pub fixed_version: Option<String>,
}

/// Result of a security scan for a single AppImage.
#[derive(Debug, Clone)]
pub struct SecurityScanResult {
    pub appimage_id: i64,
    pub libraries: Vec<BundledLibrary>,
    pub cve_matches: Vec<(BundledLibrary, Vec<CveMatch>)>,
    pub critical_count: usize,
    pub high_count: usize,
    pub medium_count: usize,
    pub low_count: usize,
}

impl SecurityScanResult {
    pub fn total_cves(&self) -> usize {
        self.critical_count + self.high_count + self.medium_count + self.low_count
    }
}

// --- Library name to CPE product mapping ---

/// Map a shared library soname to a known product name for CVE lookup.
fn soname_to_product(soname: &str) -> Option<(&'static str, &'static str)> {
    // Returns (ecosystem, package_name) for OSV API query
    let lower = soname.to_lowercase();

    // OpenSSL / LibreSSL
    if lower.starts_with("libssl") || lower.starts_with("libcrypto") {
        return Some(("OSS-Fuzz", "openssl"));
    }
    // curl
    if lower.starts_with("libcurl") {
        return Some(("OSS-Fuzz", "curl"));
    }
    // zlib
    if lower.starts_with("libz.so") {
        return Some(("OSS-Fuzz", "zlib"));
    }
    // libpng
    if lower.starts_with("libpng") {
        return Some(("OSS-Fuzz", "libpng"));
    }
    // libjpeg
    if lower.starts_with("libjpeg") || lower.starts_with("libturbojpeg") {
        return Some(("OSS-Fuzz", "libjpeg-turbo"));
    }
    // libwebp
    if lower.starts_with("libwebp") || lower.starts_with("libsharpyuv") {
        return Some(("OSS-Fuzz", "libwebp"));
    }
    // SQLite
    if lower.starts_with("libsqlite3") {
        return Some(("OSS-Fuzz", "sqlite3"));
    }
    // libxml2
    if lower.starts_with("libxml2") {
        return Some(("OSS-Fuzz", "libxml2"));
    }
    // libxslt
    if lower.starts_with("libxslt") || lower.starts_with("libexslt") {
        return Some(("OSS-Fuzz", "libxslt"));
    }
    // GnuTLS
    if lower.starts_with("libgnutls") {
        return Some(("OSS-Fuzz", "gnutls"));
    }
    // FFmpeg
    if lower.starts_with("libavcodec") || lower.starts_with("libavformat")
        || lower.starts_with("libavutil") || lower.starts_with("libswscale")
        || lower.starts_with("libswresample") || lower.starts_with("libavfilter")
    {
        return Some(("OSS-Fuzz", "ffmpeg"));
    }
    // GLib
    if lower.starts_with("libglib-2") || lower.starts_with("libgio-2") || lower.starts_with("libgobject-2") {
        return Some(("OSS-Fuzz", "glib"));
    }
    // freetype
    if lower.starts_with("libfreetype") {
        return Some(("OSS-Fuzz", "freetype2"));
    }
    // harfbuzz
    if lower.starts_with("libharfbuzz") {
        return Some(("OSS-Fuzz", "harfbuzz"));
    }
    // fontconfig
    if lower.starts_with("libfontconfig") {
        return Some(("OSS-Fuzz", "fontconfig"));
    }
    // expat
    if lower.starts_with("libexpat") {
        return Some(("OSS-Fuzz", "expat"));
    }
    // libtiff
    if lower.starts_with("libtiff") {
        return Some(("OSS-Fuzz", "libtiff"));
    }

    None
}

/// Extract a human-readable library name from the soname.
fn soname_to_name(soname: &str) -> String {
    // Strip .so and version suffix
    if let Some(pos) = soname.find(".so") {
        let base = &soname[..pos];
        // Strip "lib" prefix
        if let Some(name) = base.strip_prefix("lib") {
            return name.to_string();
        }
        return base.to_string();
    }
    soname.to_string()
}

/// Try to detect library version from soname suffix.
fn version_from_soname(soname: &str) -> Option<String> {
    // Pattern: libfoo.so.X.Y.Z
    if let Some(pos) = soname.find(".so.") {
        let ver_part = &soname[pos + 4..];
        if !ver_part.is_empty() && ver_part.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
            return Some(ver_part.to_string());
        }
    }
    None
}

// --- Library inventory extraction ---

/// Extract the list of shared libraries bundled inside an AppImage.
pub fn inventory_bundled_libraries(appimage_path: &Path) -> Vec<BundledLibrary> {
    // Get squashfs offset via binary scan (never execute the AppImage -
    // some apps like Affinity have custom AppRun scripts that ignore flags)
    let offset = match crate::core::inspector::find_squashfs_offset_for(appimage_path) {
        Some(o) => o.to_string(),
        None => return Vec::new(),
    };

    // Use unsquashfs to list all files with details
    let output = Command::new("unsquashfs")
        .args(["-o", &offset, "-ll", "-no-progress"])
        .arg(appimage_path)
        .output();

    let listing = match output {
        Ok(out) if out.status.success() => {
            String::from_utf8_lossy(&out.stdout).to_string()
        }
        _ => return Vec::new(),
    };

    let mut libraries = Vec::new();
    let mut seen = std::collections::HashSet::new();

    for line in listing.lines() {
        // unsquashfs -ll format: "-rwxr-xr-x user/group       12345 2024-01-15 10:30 squashfs-root/usr/lib/libfoo.so.1"
        // We want lines containing .so files
        if !line.contains(".so") {
            continue;
        }

        // Extract the file path (last field)
        let parts: Vec<&str> = line.split_whitespace().collect();
        if parts.len() < 6 {
            continue;
        }

        let file_path = parts[parts.len() - 1];

        // Must be in a lib-like directory or be a .so file
        if !file_path.contains(".so") {
            continue;
        }

        // Extract just the filename
        let filename = file_path.rsplit('/').next().unwrap_or(file_path);

        // Skip non-library files that happen to contain .so in name
        if !filename.contains(".so") {
            continue;
        }

        // Skip symlinks (they have -> in the line)
        if line.contains(" -> ") {
            continue;
        }

        // Skip if we already have this soname
        let soname = filename.to_string();
        if !seen.insert(soname.clone()) {
            continue;
        }

        // Parse file size from the listing
        let file_size: u64 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);

        let detected_name = soname_to_product(&soname)
            .map(|(_, name)| name.to_string())
            .or_else(|| Some(soname_to_name(&soname)));

        let detected_version = version_from_soname(&soname);

        libraries.push(BundledLibrary {
            soname,
            detected_name,
            detected_version,
            file_path: file_path.to_string(),
            file_size,
        });
    }

    libraries
}

/// Try to detect version strings from the binary data of a library.
/// This scans .rodata sections for version patterns.
pub fn detect_version_from_binary(
    appimage_path: &Path,
    lib_file_path: &str,
) -> Option<String> {
    // Get squashfs offset via binary scan (never execute the AppImage)
    let offset = crate::core::inspector::find_squashfs_offset_for(appimage_path)?
        .to_string();

    // Extract the specific library to a temp file
    let temp_dir = tempfile::tempdir().ok()?;
    let extract_output = Command::new("unsquashfs")
        .args(["-o", &offset, "-f", "-d"])
        .arg(temp_dir.path())
        .arg("-no-progress")
        .arg(appimage_path)
        .arg(lib_file_path.trim_start_matches("squashfs-root/"))
        .output()
        .ok()?;

    if !extract_output.status.success() {
        return None;
    }

    // Find the extracted file
    let extracted = temp_dir.path().join(
        lib_file_path.trim_start_matches("squashfs-root/")
    );

    if !extracted.exists() {
        return None;
    }

    // Use strings to find version patterns
    let strings_output = Command::new("strings")
        .arg(&extracted)
        .output()
        .ok()?;

    if !strings_output.status.success() {
        return None;
    }

    let strings = String::from_utf8_lossy(&strings_output.stdout);

    // Look for common version patterns
    for line in strings.lines() {
        let line = line.trim();

        // OpenSSL: "OpenSSL 1.1.1k  25 Mar 2021"
        if line.starts_with("OpenSSL ") && line.len() < 60 {
            if let Some(ver) = line.strip_prefix("OpenSSL ") {
                let ver_part = ver.split_whitespace().next()?;
                return Some(ver_part.to_string());
            }
        }

        // curl: "libcurl/7.81.0"
        if line.starts_with("libcurl/") {
            if let Some(ver) = line.strip_prefix("libcurl/") {
                let ver_part = ver.split_whitespace().next()?;
                return Some(ver_part.to_string());
            }
        }

        // SQLite: "3.39.4"
        if line.starts_with("3.") && line.len() < 20 && line.chars().all(|c| c.is_ascii_digit() || c == '.') {
            // Check it looks like a SQLite version
            let parts: Vec<&str> = line.split('.').collect();
            if parts.len() >= 3 {
                return Some(line.to_string());
            }
        }
    }

    None
}

// --- CVE checking via OSV API ---

/// Query the OSV.dev API for vulnerabilities affecting a specific package version.
fn query_osv(ecosystem: &str, package: &str, version: &str) -> Vec<CveMatch> {
    let body = serde_json::json!({
        "version": version,
        "package": {
            "name": package,
            "ecosystem": ecosystem
        }
    });

    let result = ureq::post("https://api.osv.dev/v1/query")
        .send_json(&body);

    let mut response = match result {
        Ok(r) => r,
        Err(e) => {
            log::debug!("OSV query failed for {}/{}: {}", ecosystem, package, e);
            return Vec::new();
        }
    };

    let json: serde_json::Value = match response.body_mut().read_json() {
        Ok(j) => j,
        Err(_) => return Vec::new(),
    };

    let mut matches = Vec::new();

    if let Some(vulns) = json.get("vulns").and_then(|v| v.as_array()) {
        for vuln in vulns {
            let vuln_id = vuln.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();

            // Skip non-CVE entries unless they reference a CVE
            let cve_id = if vuln_id.starts_with("CVE-") {
                vuln_id.clone()
            } else {
                // Check aliases for a CVE
                vuln.get("aliases")
                    .and_then(|a| a.as_array())
                    .and_then(|aliases| {
                        aliases.iter()
                            .filter_map(|a| a.as_str())
                            .find(|a| a.starts_with("CVE-"))
                            .map(|s| s.to_string())
                    })
                    .unwrap_or(vuln_id)
            };

            let summary = vuln.get("summary")
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            // Extract severity from database_specific or severity array
            let (severity, cvss_score) = extract_severity(vuln);

            // Extract fixed version
            let fixed_version = extract_fixed_version(vuln);

            matches.push(CveMatch {
                cve_id,
                severity,
                cvss_score,
                summary,
                affected_versions: None,
                fixed_version,
            });
        }
    }

    matches
}

fn extract_severity(vuln: &serde_json::Value) -> (String, Option<f64>) {
    // Try severity array first
    if let Some(severities) = vuln.get("severity").and_then(|s| s.as_array()) {
        for sev in severities {
            if let Some(score_str) = sev.get("score").and_then(|s| s.as_str()) {
                // CVSS vector string - extract score
                if score_str.starts_with("CVSS:") {
                    // Parse CVSS score from vector if available
                    // For now, just classify by the type
                    let score_type = sev.get("type").and_then(|t| t.as_str()).unwrap_or("");
                    if score_type == "CVSS_V3" || score_type == "CVSS_V4" {
                        // Try to extract numerical score from database_specific
                        if let Some(db_spec) = vuln.get("database_specific") {
                            if let Some(score) = db_spec.get("severity").and_then(|s| s.as_str()) {
                                return (score.to_uppercase(), None);
                            }
                        }
                    }
                }
            }
        }
    }

    // Try database_specific
    if let Some(db_spec) = vuln.get("database_specific") {
        if let Some(severity) = db_spec.get("severity").and_then(|s| s.as_str()) {
            return (severity.to_uppercase(), None);
        }
    }

    // Default: classify as MEDIUM if we have a CVE but can't determine severity
    ("MEDIUM".to_string(), None)
}

fn extract_fixed_version(vuln: &serde_json::Value) -> Option<String> {
    if let Some(affected) = vuln.get("affected").and_then(|a| a.as_array()) {
        for entry in affected {
            if let Some(ranges) = entry.get("ranges").and_then(|r| r.as_array()) {
                for range in ranges {
                    if let Some(events) = range.get("events").and_then(|e| e.as_array()) {
                        for event in events {
                            if let Some(fixed) = event.get("fixed").and_then(|f| f.as_str()) {
                                return Some(fixed.to_string());
                            }
                        }
                    }
                }
            }
        }
    }
    None
}

// --- Main scanning entry point ---

/// Scan a single AppImage for security vulnerabilities.
/// Returns the scan result with all findings.
pub fn scan_appimage(appimage_path: &Path, appimage_id: i64) -> SecurityScanResult {
    let libraries = inventory_bundled_libraries(appimage_path);

    let mut cve_matches: Vec<(BundledLibrary, Vec<CveMatch>)> = Vec::new();
    let mut critical_count = 0;
    let mut high_count = 0;
    let mut medium_count = 0;
    let mut low_count = 0;

    for lib in &libraries {
        // Only query CVEs for libraries we can map to a product
        if let Some((ecosystem, package)) = soname_to_product(&lib.soname) {
            // Use detected version, or try to extract from soname,
            // then fall back to binary analysis for accurate version detection
            let soname_ver = lib.detected_version.clone()
                .or_else(|| version_from_soname(&lib.soname));

            let version_string = soname_ver.or_else(|| {
                detect_version_from_binary(appimage_path, &lib.file_path)
            });

            let version = match version_string.as_deref() {
                Some(v) if !v.is_empty() => v,
                _ => {
                    // Last resort: check system version for comparison logging
                    for sys_pkg in product_to_system_packages(package) {
                        if let Some(sys_ver) = get_system_library_version(sys_pkg) {
                            log::debug!("System {} version: {} (bundled version unknown)", sys_pkg, sys_ver);
                            break;
                        }
                    }
                    continue;
                }
            };

            let matches = query_osv(ecosystem, package, version);

            if !matches.is_empty() {
                for m in &matches {
                    match m.severity.as_str() {
                        "CRITICAL" => critical_count += 1,
                        "HIGH" => high_count += 1,
                        "MEDIUM" => medium_count += 1,
                        "LOW" => low_count += 1,
                        _ => {}
                    }
                }
                cve_matches.push((lib.clone(), matches));
            }
        }
    }

    SecurityScanResult {
        appimage_id,
        libraries,
        cve_matches,
        critical_count,
        high_count,
        medium_count,
        low_count,
    }
}

/// Scan an AppImage and store results in the database.
pub fn scan_and_store(db: &Database, appimage_id: i64, appimage_path: &Path) -> SecurityScanResult {
    let result = scan_appimage(appimage_path, appimage_id);

    // Clear old data
    db.clear_bundled_libraries(appimage_id).ok();
    db.clear_cve_matches(appimage_id).ok();

    // Store library inventory
    let mut lib_id_map: HashMap<String, i64> = HashMap::new();
    for lib in &result.libraries {
        if let Ok(lib_id) = db.insert_bundled_library(
            appimage_id,
            &lib.soname,
            lib.detected_name.as_deref(),
            lib.detected_version.as_deref(),
            Some(&lib.file_path),
            lib.file_size as i64,
        ) {
            lib_id_map.insert(lib.soname.clone(), lib_id);
        }
    }

    // Store CVE matches
    for (lib, matches) in &result.cve_matches {
        if let Some(&lib_id) = lib_id_map.get(&lib.soname) {
            for m in matches {
                db.insert_cve_match(
                    appimage_id,
                    lib_id,
                    &m.cve_id,
                    Some(&m.severity),
                    m.cvss_score,
                    Some(&m.summary),
                    m.affected_versions.as_deref(),
                    m.fixed_version.as_deref(),
                ).ok();
            }
        }
    }

    result
}

/// Batch scan all AppImages in the database.
pub fn batch_scan(db: &Database) -> Vec<SecurityScanResult> {
    let records = match db.get_all_appimages() {
        Ok(r) => r,
        Err(e) => {
            log::error!("Failed to get appimages for security scan: {}", e);
            return Vec::new();
        }
    };

    let mut results = Vec::new();

    for record in &records {
        let path = Path::new(&record.path);
        if !path.exists() {
            continue;
        }

        let result = scan_and_store(db, record.id, path);
        results.push(result);
    }

    results
}

/// Get system library version by running dpkg or rpm.
pub fn get_system_library_version(package_name: &str) -> Option<String> {
    // Try dpkg first (Debian/Ubuntu)
    let output = Command::new("dpkg-query")
        .args(["-W", "-f", "${Version}", package_name])
        .output();

    if let Ok(output) = output {
        if output.status.success() {
            let ver = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !ver.is_empty() {
                return Some(ver);
            }
        }
    }

    // Try rpm (Fedora/RHEL)
    let output = Command::new("rpm")
        .args(["-q", "--queryformat", "%{VERSION}", package_name])
        .output();

    if let Ok(output) = output {
        if output.status.success() {
            let ver = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !ver.is_empty() {
                return Some(ver);
            }
        }
    }

    None
}

/// Map a library product name to system package names to check.
pub fn product_to_system_packages(product: &str) -> Vec<&'static str> {
    match product {
        "openssl" => vec!["libssl3", "libssl3t64", "openssl"],
        "curl" => vec!["libcurl4", "libcurl4t64", "curl"],
        "zlib" => vec!["zlib1g", "zlib"],
        "libpng" => vec!["libpng16-16", "libpng16-16t64", "libpng"],
        "libjpeg-turbo" => vec!["libjpeg-turbo8", "libjpeg62-turbo", "libjpeg-turbo"],
        "libwebp" => vec!["libwebp7", "libwebp"],
        "sqlite3" => vec!["libsqlite3-0", "sqlite"],
        "libxml2" => vec!["libxml2", "libxml2"],
        "gnutls" => vec!["libgnutls30", "libgnutls30t64", "gnutls"],
        "ffmpeg" => vec!["libavcodec-extra60", "libavcodec60", "ffmpeg-libs"],
        "freetype2" => vec!["libfreetype6", "libfreetype6t64", "freetype"],
        "harfbuzz" => vec!["libharfbuzz0b", "harfbuzz"],
        "expat" => vec!["libexpat1", "expat"],
        _ => vec![],
    }
}

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

    #[test]
    fn test_soname_to_product() {
        assert_eq!(
            soname_to_product("libssl.so.1.1"),
            Some(("OSS-Fuzz", "openssl"))
        );
        assert_eq!(
            soname_to_product("libcurl.so.4"),
            Some(("OSS-Fuzz", "curl"))
        );
        assert_eq!(
            soname_to_product("libz.so.1"),
            Some(("OSS-Fuzz", "zlib"))
        );
        assert_eq!(
            soname_to_product("libwebp.so.7"),
            Some(("OSS-Fuzz", "libwebp"))
        );
        assert_eq!(soname_to_product("libfoo.so.1"), None);
    }

    #[test]
    fn test_soname_to_name() {
        assert_eq!(soname_to_name("libssl.so.1.1"), "ssl");
        assert_eq!(soname_to_name("libcurl.so.4"), "curl");
        assert_eq!(soname_to_name("libz.so.1"), "z");
    }

    #[test]
    fn test_version_from_soname() {
        assert_eq!(version_from_soname("libssl.so.1.1"), Some("1.1".to_string()));
        assert_eq!(version_from_soname("libz.so.1"), Some("1".to_string()));
        assert_eq!(version_from_soname("libfoo.so"), None);
    }

    #[test]
    fn test_product_to_system_packages() {
        let pkgs = product_to_system_packages("openssl");
        assert!(pkgs.contains(&"libssl3"));
        assert!(!pkgs.is_empty());

        let unknown = product_to_system_packages("unknown_lib");
        assert!(unknown.is_empty());
    }

    #[test]
    fn test_extract_severity_default() {
        let vuln = serde_json::json!({});
        let (severity, score) = extract_severity(&vuln);
        assert_eq!(severity, "MEDIUM");
        assert!(score.is_none());
    }

    #[test]
    fn test_extract_fixed_version() {
        let vuln = serde_json::json!({
            "affected": [{
                "ranges": [{
                    "type": "SEMVER",
                    "events": [
                        {"introduced": "0"},
                        {"fixed": "1.2.3"}
                    ]
                }]
            }]
        });
        assert_eq!(extract_fixed_version(&vuln), Some("1.2.3".to_string()));

        let no_fix = serde_json::json!({});
        assert_eq!(extract_fixed_version(&no_fix), None);
    }
}
