//! Software Bill of Materials (SBOM) generation and management
//!
//! Supports multiple SBOM generation methods:
//! - cargo metadata (always available, built-in)
//! - cargo-tree (built-in, provides dependency tree)
//! - cargo-sbom (optional, generates SPDX/CycloneDX format)

use crate::config::SbomFormat;
use crate::error::Result;
use crate::LifecycleError;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
use std::process::Command;

/// SBOM Component (dependency)
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct SbomComponent {
    /// Component name
    pub name: String,
    /// Component version
    pub version: String,
    /// Dependency type (direct, transitive)
    pub dependency_type: String,
}

impl std::hash::Hash for SbomComponent {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state);
        self.version.hash(state);
    }
}

/// SBOM generation method metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SbomGenerationInfo {
    /// Method used (cargo-metadata, cargo-tree, cargo-sbom)
    pub generation_method: String,
    /// Formats generated (spdx, cyclonedx, or both)
    pub formats_generated: Vec<String>,
    /// Whether alternative methods were attempted
    pub alternative_methods_tried: Vec<String>,
    /// Any warnings from generation
    pub warnings: Vec<String>,
}

/// Software Bill of Materials
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Sbom {
    /// SBOM spec version
    pub spec_version: String,
    /// Generated timestamp
    pub generation_date: String,
    /// Project name
    pub project: String,
    /// All components (dependencies)
    pub components: Vec<SbomComponent>,
    /// Direct dependencies count
    pub direct_dependencies: usize,
    /// Transitive dependencies count
    pub transitive_dependencies: usize,
    /// Generation metadata
    #[serde(skip_serializing_if = "Option::is_none")]
    pub generation_info: Option<SbomGenerationInfo>,
}

impl Sbom {
    /// Generate SBOM using multiple methods and aggregate results
    ///
    /// Attempts to generate SBOM using the following methods in order:
    /// 1. cargo metadata (always available, fast, accurate)
    /// 2. cargo-tree (built-in, validates dependency hierarchy)
    /// 3. cargo-sbom (if available, generates SPDX/CycloneDX format)
    ///
    /// Gracefully handles missing tools and uses cargo metadata as fallback.
    /// The `format` parameter controls which SBOM formats to generate (SPDX, CycloneDX, or both).
    pub fn generate(project_path: &Path, project_name: &str, format: SbomFormat) -> Result<Self> {
        let mut warnings = Vec::new();
        let mut alternative_methods_tried = Vec::new();
        let mut formats_generated = Vec::new();
        let mut primary_method = "cargo-metadata".to_string();

        // Step 1: Primary method - cargo metadata (always available)
        let mut sbom = Self::generate_from_metadata(project_path, project_name)?;

        // Step 2: Try cargo-tree for validation and additional metadata
        match Self::generate_from_cargo_tree(project_path, project_name) {
            Ok(tree_components) => {
                alternative_methods_tried.push("cargo-tree".to_string());
                // Validate and enhance with tree data
                sbom = Self::merge_with_tree_data(sbom, tree_components, &mut warnings);
            }
            Err(e) => {
                warnings.push(format!("cargo-tree not available: {}", e));
            }
        }

        // Step 3: Try cargo-sbom if available (advanced SBOM format)
        let mut use_cargo_sbom = false;
        match Self::generate_from_cargo_sbom(project_path, project_name) {
            Ok(sbom_data) => {
                alternative_methods_tried.push("cargo-sbom".to_string());
                primary_method = "cargo-sbom".to_string();
                use_cargo_sbom = true;
                // If cargo-sbom succeeds, use its more standardized format
                sbom = sbom_data;
            }
            Err(e) => {
                warnings.push(format!("cargo-sbom not available or failed: {}", e));
            }
        }

        // Determine formats generated based on configuration
        match format {
            SbomFormat::Spdx => {
                formats_generated.push("spdx".to_string());
                if use_cargo_sbom {
                    warnings.push("Using cargo-sbom for SPDX generation".to_string());
                }
            }
            SbomFormat::CycloneDx => {
                formats_generated.push("cyclonedx".to_string());
                if use_cargo_sbom {
                    warnings.push("Using cargo-sbom for CycloneDX generation".to_string());
                }
            }
            SbomFormat::Both => {
                formats_generated.push("spdx".to_string());
                formats_generated.push("cyclonedx".to_string());
                if use_cargo_sbom {
                    warnings.push(
                        "Using cargo-sbom for both SPDX and CycloneDX generation".to_string(),
                    );
                }
            }
        }

        // Return metadata-based SBOM with generation info
        sbom.generation_info = Some(SbomGenerationInfo {
            generation_method: primary_method,
            formats_generated,
            alternative_methods_tried,
            warnings,
        });

        Ok(sbom)
    }

    /// Generate SBOM using cargo metadata (primary method, always available)
    fn generate_from_metadata(project_path: &Path, project_name: &str) -> Result<Self> {
        let output = Command::new("cargo")
            .args(["metadata", "--format-version", "1"])
            .current_dir(project_path)
            .output()
            .map_err(|e| LifecycleError::custom(format!("Failed to run cargo metadata: {}", e)))?;

        if !output.status.success() {
            return Err(LifecycleError::custom(
                "cargo metadata failed - ensure you're in a Rust project directory",
            ));
        }

        let metadata: CargoMetadata = serde_json::from_slice(&output.stdout).map_err(|e| {
            LifecycleError::custom(format!("Failed to parse cargo metadata: {}", e))
        })?;

        // Extract root package dependencies
        let root_package = metadata
            .resolve
            .root
            .as_ref()
            .ok_or_else(|| LifecycleError::custom("No root package found"))?;

        let mut components = Vec::new();
        let mut direct_deps = 0;
        let mut transitive_deps = 0;

        // Process all packages
        for package in &metadata.packages {
            // Skip the root package itself
            if Some(&package.id) == metadata.resolve.root.as_ref() {
                continue;
            }

            let is_direct = metadata
                .resolve
                .nodes
                .iter()
                .find(|n| n.id == *root_package)
                .map(|n| n.deps.iter().any(|d| d.pkg.contains(&package.id)))
                .unwrap_or(false);

            components.push(SbomComponent {
                name: package.name.clone(),
                version: package.version.to_string(),
                dependency_type: if is_direct {
                    direct_deps += 1;
                    "direct".to_string()
                } else {
                    transitive_deps += 1;
                    "transitive".to_string()
                },
            });
        }

        // Sort components by name
        components.sort_by(|a, b| a.name.cmp(&b.name));

        Ok(Sbom {
            spec_version: "1.4".to_string(),
            generation_date: chrono::Utc::now().to_rfc3339(),
            project: project_name.to_string(),
            components,
            direct_dependencies: direct_deps,
            transitive_dependencies: transitive_deps,
            generation_info: None,
        })
    }

    /// Generate dependency tree using cargo-tree
    ///
    /// cargo-tree is always available (built-in command) and provides
    /// a human-readable dependency tree. Returns parsed components.
    fn generate_from_cargo_tree(
        project_path: &Path,
        _project_name: &str,
    ) -> Result<Vec<SbomComponent>> {
        let output = Command::new("cargo")
            .args(["tree", "--depth", "999", "--format", "{p}"])
            .current_dir(project_path)
            .output()
            .map_err(|e| LifecycleError::custom(format!("Failed to run cargo-tree: {}", e)))?;

        if !output.status.success() {
            return Err(LifecycleError::custom("cargo tree failed"));
        }

        let tree_output = String::from_utf8_lossy(&output.stdout);
        let mut components = Vec::new();
        let mut seen = HashSet::new();

        for line in tree_output.lines() {
            // Parse lines like "package-name v1.2.3"
            if let Some(caps) = parse_cargo_tree_line(line) {
                let component = SbomComponent {
                    name: caps.0,
                    version: caps.1,
                    dependency_type: "transitive".to_string(), // Tree includes all
                };

                if seen.insert((component.name.clone(), component.version.clone())) {
                    components.push(component);
                }
            }
        }

        Ok(components)
    }

    /// Generate SBOM using cargo-sbom (advanced format support)
    ///
    /// cargo-sbom generates SPDX or CycloneDX format SBOMs.
    /// This is optional and only used if the tool is installed.
    /// Gracefully falls back to cargo metadata if not available.
    fn generate_from_cargo_sbom(project_path: &Path, project_name: &str) -> Result<Self> {
        // Check if cargo-sbom is available
        let check = Command::new("cargo").args(["sbom", "--version"]).output();

        if check.is_err() || !check.unwrap().status.success() {
            return Err(LifecycleError::custom("cargo-sbom not installed"));
        }

        // Generate SBOM in JSON format
        let output = Command::new("cargo")
            .args(["sbom", "--format", "json"])
            .current_dir(project_path)
            .output()
            .map_err(|e| LifecycleError::custom(format!("Failed to run cargo-sbom: {}", e)))?;

        if !output.status.success() {
            return Err(LifecycleError::custom("cargo-sbom generation failed"));
        }

        // Parse cargo-sbom JSON output
        // cargo-sbom outputs CycloneDX or SPDX JSON format
        let sbom_output = String::from_utf8_lossy(&output.stdout);
        let mut components = Vec::new();
        let mut direct_deps = 0;
        let mut transitive_deps = 0;

        // Parse JSON from cargo-sbom
        if let Ok(sbom_json) = serde_json::from_str::<serde_json::Value>(&sbom_output) {
            // Extract components from SPDX/CycloneDX format
            if let Some(components_arr) = sbom_json.get("components").and_then(|v| v.as_array()) {
                for comp in components_arr {
                    if let (Some(name), Some(version)) = (
                        comp.get("name").and_then(|v| v.as_str()),
                        comp.get("version").and_then(|v| v.as_str()),
                    ) {
                        let dep_type = comp
                            .get("scope")
                            .and_then(|v| v.as_str())
                            .unwrap_or("transitive");

                        let dependency_type = match dep_type {
                            "required" | "direct" => {
                                direct_deps += 1;
                                "direct".to_string()
                            }
                            _ => {
                                transitive_deps += 1;
                                "transitive".to_string()
                            }
                        };

                        components.push(SbomComponent {
                            name: name.to_string(),
                            version: version.to_string(),
                            dependency_type,
                        });
                    }
                }
            }
        } else {
            // Fall back to metadata parsing if JSON parsing fails
            return Self::generate_from_metadata(project_path, project_name);
        }

        components.sort_by(|a, b| a.name.cmp(&b.name));

        Ok(Sbom {
            spec_version: "1.5".to_string(), // cargo-sbom uses 1.5
            generation_date: chrono::Utc::now().to_rfc3339(),
            project: project_name.to_string(),
            components,
            direct_dependencies: direct_deps,
            transitive_dependencies: transitive_deps,
            generation_info: None,
        })
    }

    /// Merge cargo-tree data with metadata-based SBOM
    fn merge_with_tree_data(
        sbom: Self,
        tree_components: Vec<SbomComponent>,
        warnings: &mut Vec<String>,
    ) -> Self {
        // Create a set of components from metadata for comparison
        let metadata_set: HashSet<(String, String)> = sbom
            .components
            .iter()
            .map(|c| (c.name.clone(), c.version.clone()))
            .collect();

        let mut discrepancies = 0;
        for tree_comp in tree_components {
            if !metadata_set.contains(&(tree_comp.name.clone(), tree_comp.version.clone())) {
                discrepancies += 1;
            }
        }

        if discrepancies > 0 {
            warnings.push(format!(
                "Found {} dependency discrepancies between cargo metadata and cargo-tree",
                discrepancies
            ));
        }

        sbom
    }

    /// Save SBOM to file
    pub fn save_to_file(&self, path: &Path) -> Result<()> {
        let json = serde_json::to_string_pretty(self)
            .map_err(|e| LifecycleError::custom(format!("Failed to serialize SBOM: {}", e)))?;
        std::fs::write(path, json)?;
        Ok(())
    }

    /// Load SBOM from file
    pub fn load_from_file(path: &Path) -> Result<Self> {
        let content = std::fs::read_to_string(path)?;
        serde_json::from_str(&content)
            .map_err(|e| LifecycleError::custom(format!("Failed to parse SBOM: {}", e)))
    }
}

/// Cargo metadata structure (minimal)
#[derive(Debug, Deserialize)]
struct CargoMetadata {
    packages: Vec<CargoPackage>,
    resolve: CargoResolve,
}

#[derive(Debug, Deserialize)]
struct CargoPackage {
    id: String,
    name: String,
    version: CargoVersion,
}

#[derive(Debug, Deserialize, Clone)]
struct CargoVersion {
    major: u32,
    minor: u32,
    patch: u32,
}

impl std::fmt::Display for CargoVersion {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
    }
}

#[derive(Debug, Deserialize)]
struct CargoResolve {
    root: Option<String>,
    nodes: Vec<CargoResolveNode>,
}

#[derive(Debug, Deserialize)]
struct CargoResolveNode {
    id: String,
    deps: Vec<CargoDep>,
}

#[derive(Debug, Deserialize)]
struct CargoDep {
    pkg: String,
}

/// Parse a line from cargo tree output
/// Format: "package-name v1.2.3" with optional tree box-drawing characters
/// Returns: (package_name, version) tuple
fn parse_cargo_tree_line(line: &str) -> Option<(String, String)> {
    // Remove tree box-drawing characters and spaces from the beginning
    let trimmed = line
        .trim_start_matches(['│', '├', '└', '─', ' '])
        .trim_start();

    if trimmed.is_empty() {
        return None;
    }

    // Match pattern: name version
    if let Some(space_pos) = trimmed.find(' ') {
        let name = trimmed[..space_pos].to_string();
        let version_part = trimmed[space_pos..].trim();

        // Extract version (remove 'v' prefix and extract semver)
        if let Some(version_str) = version_part.strip_prefix('v') {
            // Take only the version part before any annotations
            if let Some(ver) = version_str.split_whitespace().next() {
                return Some((name, ver.to_string()));
            }
        }
    }

    None
}

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

    #[test]
    fn test_sbom_creation() {
        let sbom = Sbom {
            spec_version: "1.4".to_string(),
            generation_date: "2025-11-13T00:00:00Z".to_string(),
            project: "test-project".to_string(),
            components: vec![SbomComponent {
                name: "serde".to_string(),
                version: "1.0.0".to_string(),
                dependency_type: "direct".to_string(),
            }],
            direct_dependencies: 1,
            transitive_dependencies: 0,
            generation_info: None,
        };

        assert_eq!(sbom.project, "test-project");
        assert_eq!(sbom.components.len(), 1);
    }

    #[test]
    fn test_sbom_serialization() {
        let sbom = Sbom {
            spec_version: "1.4".to_string(),
            generation_date: "2025-11-13T00:00:00Z".to_string(),
            project: "test-project".to_string(),
            components: vec![],
            direct_dependencies: 0,
            transitive_dependencies: 0,
            generation_info: None,
        };

        let json = serde_json::to_string(&sbom).unwrap();
        assert!(json.contains("test-project"));
        assert!(json.contains("1.4"));
    }

    #[test]
    fn test_sbom_with_generation_info() {
        let sbom = Sbom {
            spec_version: "1.4".to_string(),
            generation_date: "2025-11-13T00:00:00Z".to_string(),
            project: "test-project".to_string(),
            components: vec![],
            direct_dependencies: 0,
            transitive_dependencies: 0,
            generation_info: Some(SbomGenerationInfo {
                generation_method: "cargo-metadata".to_string(),
                formats_generated: vec!["spdx".to_string(), "cyclonedx".to_string()],
                alternative_methods_tried: vec!["cargo-tree".to_string()],
                warnings: vec!["cargo-sbom not available".to_string()],
            }),
        };

        assert_eq!(
            sbom.generation_info.as_ref().unwrap().generation_method,
            "cargo-metadata"
        );
        assert_eq!(
            sbom.generation_info
                .as_ref()
                .unwrap()
                .formats_generated
                .len(),
            2
        );
        assert_eq!(
            sbom.generation_info
                .as_ref()
                .unwrap()
                .alternative_methods_tried
                .len(),
            1
        );
    }

    #[test]
    fn test_parse_cargo_tree_line() {
        // Test basic format
        assert_eq!(
            parse_cargo_tree_line("serde v1.0.0"),
            Some(("serde".to_string(), "1.0.0".to_string()))
        );

        // Test with tree characters
        assert_eq!(
            parse_cargo_tree_line("├── tokio v1.35.0"),
            Some(("tokio".to_string(), "1.35.0".to_string()))
        );

        // Test with multiple spaces
        assert_eq!(
            parse_cargo_tree_line("│   └── parking_lot v0.12.1"),
            Some(("parking_lot".to_string(), "0.12.1".to_string()))
        );
    }
}
