//! Dependency version extractor.
//!
//! Checks for dependencies with known vulnerabilities by comparing
//! installed versions against advisory databases.

use regex::Regex;
use stemedb_core::types::ObjectValue;

use super::Extractor;
use crate::types::{ExtractedClaim, Language};

/// Extractor for vulnerable dependency versions.
///
/// Note: This is a simplified version that detects common patterns.
/// A full implementation would integrate with RustSec, npm audit, etc.
pub struct DepVersionsExtractor {
    /// Cargo.toml dependency patterns
    cargo_dep: Regex,
    /// package.json dependency patterns
    npm_dep: Regex,
    /// go.mod dependency patterns
    go_dep: Regex,
    /// requirements.txt patterns
    pip_dep: Regex,
}

impl Default for DepVersionsExtractor {
    fn default() -> Self {
        Self::new()
    }
}

impl DepVersionsExtractor {
    /// Create a new dependency version extractor.
    ///
    /// # Panics
    /// Panics if any regex pattern is invalid (programmer error).
    #[allow(clippy::expect_used)]
    pub fn new() -> Self {
        Self {
            // Matches: package = "1.0.0" or package = { version = "1.0.0" }
            cargo_dep: Regex::new(
                r#"^([a-zA-Z0-9_-]+)\s*=\s*(?:"([^"]+)"|.*version\s*=\s*"([^"]+)")"#,
            )
            .expect("valid regex"),
            // Matches: "package": "^1.0.0"
            npm_dep: Regex::new(r#""([^"]+)":\s*"([~^]?[\d.]+[^"]*)""#).expect("valid regex"),
            // Matches: module/path v1.0.0
            go_dep: Regex::new(r"^\s*([a-zA-Z0-9./_-]+)\s+(v[\d.]+(?:-[a-zA-Z0-9.]+)?)")
                .expect("valid regex"),
            // Matches: package==1.0.0 or package>=1.0.0
            pip_dep: Regex::new(r"^([a-zA-Z0-9_-]+)(?:==|>=|<=|~=|!=)?([\d.]+(?:\.[a-zA-Z0-9]+)?)")
                .expect("valid regex"),
        }
    }

    fn extract_cargo(
        &self,
        path_segments: &[String],
        content: &str,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();
        let mut in_dependencies = false;

        for (line_idx, line) in content.lines().enumerate() {
            let trimmed = line.trim();

            // Track dependency sections
            if trimmed.starts_with("[dependencies")
                || trimmed.starts_with("[dev-dependencies")
                || trimmed.starts_with("[build-dependencies")
            {
                in_dependencies = true;
                continue;
            }

            // Exit dependency section when we hit a new section
            if trimmed.starts_with('[') {
                in_dependencies = false;
                continue;
            }

            // Only extract if we're in a dependencies section
            if in_dependencies {
                if let Some(captures) = self.cargo_dep.captures(line) {
                    let package = captures.get(1).map(|m| m.as_str()).unwrap_or("");
                    let version =
                        captures.get(2).or(captures.get(3)).map(|m| m.as_str()).unwrap_or("");

                    if !package.is_empty() && !version.is_empty() && version != "*" {
                        // Record the dependency for potential advisory lookup
                        let mut concept_path = path_segments.to_vec();
                        concept_path.push("dep".to_string());
                        concept_path.push(package.to_string());
                        concept_path.push("version".to_string());

                        claims.push(ExtractedClaim {
                            concept_path: format!("code://{}", concept_path.join("/")),
                            predicate: "installed_version".to_string(),
                            value: ObjectValue::Text(version.to_string()),
                            file: file.to_string(),
                            line: line_idx + 1,
                            matched_text: line.trim().to_string(),
                            confidence: 1.0,
                            description: format!("Dependency {} at version {}", package, version),
                        });
                    }
                }
            }
        }

        claims
    }

    fn extract_npm(
        &self,
        path_segments: &[String],
        content: &str,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(captures) = self.npm_dep.captures(line) {
                let package = captures.get(1).map(|m| m.as_str()).unwrap_or("");
                let version = captures.get(2).map(|m| m.as_str()).unwrap_or("");

                // Skip npm metadata fields
                if package.starts_with('@')
                    || [
                        "name",
                        "version",
                        "description",
                        "main",
                        "scripts",
                        "devDependencies",
                        "dependencies",
                        "peerDependencies",
                    ]
                    .contains(&package)
                {
                    continue;
                }

                if !package.is_empty() && !version.is_empty() {
                    let mut concept_path = path_segments.to_vec();
                    concept_path.push("dep".to_string());
                    concept_path.push(package.to_string());
                    concept_path.push("version".to_string());

                    claims.push(ExtractedClaim {
                        concept_path: format!("code://{}", concept_path.join("/")),
                        predicate: "installed_version".to_string(),
                        value: ObjectValue::Text(version.to_string()),
                        file: file.to_string(),
                        line: line_idx + 1,
                        matched_text: line.trim().to_string(),
                        confidence: 1.0,
                        description: format!("Dependency {} at version {}", package, version),
                    });
                }
            }
        }

        claims
    }

    fn extract_go(
        &self,
        path_segments: &[String],
        content: &str,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();
        let mut in_require = false;

        for (line_idx, line) in content.lines().enumerate() {
            // Track require block
            if line.contains("require (") || line.contains("require(") {
                in_require = true;
                continue;
            }
            if in_require && line.contains(')') {
                in_require = false;
                continue;
            }

            if in_require {
                if let Some(captures) = self.go_dep.captures(line) {
                    let package = captures.get(1).map(|m| m.as_str()).unwrap_or("");
                    let version = captures.get(2).map(|m| m.as_str()).unwrap_or("");

                    if !package.is_empty() && !version.is_empty() {
                        // Use last segment of path as package name
                        let short_name = package.rsplit('/').next().unwrap_or(package);

                        let mut concept_path = path_segments.to_vec();
                        concept_path.push("dep".to_string());
                        concept_path.push(short_name.to_string());
                        concept_path.push("version".to_string());

                        claims.push(ExtractedClaim {
                            concept_path: format!("code://{}", concept_path.join("/")),
                            predicate: "installed_version".to_string(),
                            value: ObjectValue::Text(version.to_string()),
                            file: file.to_string(),
                            line: line_idx + 1,
                            matched_text: line.trim().to_string(),
                            confidence: 1.0,
                            description: format!("Dependency {} at version {}", package, version),
                        });
                    }
                }
            }
        }

        claims
    }

    fn extract_pip(
        &self,
        path_segments: &[String],
        content: &str,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        let mut claims = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            let line = line.trim();
            // Skip comments and empty lines
            if line.starts_with('#') || line.is_empty() {
                continue;
            }

            if let Some(captures) = self.pip_dep.captures(line) {
                let package = captures.get(1).map(|m| m.as_str()).unwrap_or("");
                let version = captures.get(2).map(|m| m.as_str()).unwrap_or("");

                if !package.is_empty() && !version.is_empty() {
                    let mut concept_path = path_segments.to_vec();
                    concept_path.push("dep".to_string());
                    concept_path.push(package.to_string());
                    concept_path.push("version".to_string());

                    claims.push(ExtractedClaim {
                        concept_path: format!("code://{}", concept_path.join("/")),
                        predicate: "installed_version".to_string(),
                        value: ObjectValue::Text(version.to_string()),
                        file: file.to_string(),
                        line: line_idx + 1,
                        matched_text: line.to_string(),
                        confidence: 1.0,
                        description: format!("Dependency {} at version {}", package, version),
                    });
                }
            }
        }

        claims
    }
}

impl Extractor for DepVersionsExtractor {
    fn name(&self) -> &str {
        "dep_versions"
    }

    fn languages(&self) -> &[Language] {
        &[Language::CargoManifest, Language::NpmManifest, Language::GoMod, Language::PythonManifest]
    }

    fn extract(
        &self,
        path_segments: &[String],
        content: &str,
        language: Language,
        file: &str,
    ) -> Vec<ExtractedClaim> {
        match language {
            Language::CargoManifest => self.extract_cargo(path_segments, content, file),
            Language::NpmManifest => self.extract_npm(path_segments, content, file),
            Language::GoMod => self.extract_go(path_segments, content, file),
            Language::PythonManifest => self.extract_pip(path_segments, content, file),
            _ => Vec::new(),
        }
    }
}

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

    #[test]
    fn test_cargo_dependency_extraction() {
        let extractor = DepVersionsExtractor::new();
        let content = r#"
[dependencies]
tokio = "1.28"
serde = { version = "1.0", features = ["derive"] }
        "#;

        let claims = extractor.extract(
            &["rust".to_string()],
            content,
            Language::CargoManifest,
            "Cargo.toml",
        );

        assert_eq!(claims.len(), 2);
        assert!(claims.iter().any(|c| c.concept_path.contains("tokio")));
        assert!(claims.iter().any(|c| c.concept_path.contains("serde")));
    }

    #[test]
    fn test_npm_dependency_extraction() {
        let extractor = DepVersionsExtractor::new();
        let content = r#"
{
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "4.17.21"
  }
}
        "#;

        let claims =
            extractor.extract(&["js".to_string()], content, Language::NpmManifest, "package.json");

        assert_eq!(claims.len(), 2);
    }

    #[test]
    fn test_go_mod_extraction() {
        let extractor = DepVersionsExtractor::new();
        let content = r#"
module myapp

go 1.21

require (
    github.com/gin-gonic/gin v1.9.0
    golang.org/x/crypto v0.14.0
)
        "#;

        let claims = extractor.extract(&["go".to_string()], content, Language::GoMod, "go.mod");

        assert_eq!(claims.len(), 2);
        assert!(claims.iter().any(|c| c.concept_path.contains("gin")));
        assert!(claims.iter().any(|c| c.concept_path.contains("crypto")));
    }

    #[test]
    fn test_pip_requirements_extraction() {
        let extractor = DepVersionsExtractor::new();
        let content = r#"
# Python requirements
requests==2.28.0
flask>=2.0.0
        "#;

        let claims = extractor.extract(
            &["python".to_string()],
            content,
            Language::PythonManifest,
            "requirements.txt",
        );

        assert_eq!(claims.len(), 2);
    }

    #[test]
    fn test_cargo_ignores_package_metadata() {
        let extractor = DepVersionsExtractor::new();
        let content = r#"
[package]
name = "maxwell-daemon"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "maxwelld"
path = "src/main.rs"

[dependencies]
tokio = "1.28"
        "#;

        let claims = extractor.extract(
            &["rust".to_string(), "maxwell".to_string()],
            content,
            Language::CargoManifest,
            "Cargo.toml",
        );

        // Should only extract the dependency (tokio), not package metadata
        assert_eq!(claims.len(), 1);
        assert!(claims[0].concept_path.contains("tokio"));
        assert!(!claims.iter().any(|c| c.concept_path.contains("name")));
        assert!(!claims.iter().any(|c| c.concept_path.contains("version")
            && c.value == ObjectValue::Text("0.1.0".to_string())));
    }

    #[test]
    fn test_cargo_extracts_from_dev_dependencies() {
        let extractor = DepVersionsExtractor::new();
        let content = r#"
[dependencies]
tokio = "1.28"

[dev-dependencies]
criterion = "0.5"
        "#;

        let claims = extractor.extract(
            &["rust".to_string()],
            content,
            Language::CargoManifest,
            "Cargo.toml",
        );

        // Should extract from both [dependencies] and [dev-dependencies]
        assert_eq!(claims.len(), 2);
        assert!(claims.iter().any(|c| c.concept_path.contains("tokio")));
        assert!(claims.iter().any(|c| c.concept_path.contains("criterion")));
    }
}
