use crate::types::{PackageName, Version};
use crate::{AuditError, Result};
use async_trait::async_trait;
use std::path::Path;

pub mod lock;
pub mod pyproject;

/// A dependency parsed from a project file
#[derive(Debug, Clone)]
pub struct ParsedDependency {
    /// Package name
    pub name: PackageName,
    /// Installed or specified version
    pub version: Version,
    /// Whether this is a direct dependency (listed in main dependencies)
    pub is_direct: bool,
    /// Source of the dependency (PyPI, git, path, etc.)
    pub source: DependencySource,
    /// Optional path for path dependencies
    pub path: Option<std::path::PathBuf>,
    /// Type of dependency (main, dev, optional)
    pub dependency_type: DependencyType,
}

/// Source type for dependencies
#[derive(Debug, Clone)]
pub enum DependencySource {
    /// PyPI registry
    Registry,
    /// Git repository
    Git { url: String, rev: Option<String> },
    /// Local path
    Path,
    /// Direct URL
    Url(String),
}

/// Type of dependency
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DependencyType {
    /// Main production dependency
    Main,
    /// Development dependency
    Dev,
    /// Optional dependency
    Optional,
}

/// Trait for parsing different project file formats
#[async_trait]
pub trait ProjectParser: Send + Sync {
    /// Returns the name of this parser (e.g., "uv.lock", "pyproject.toml")
    fn name(&self) -> &'static str;

    /// Check if this parser can handle the given project directory
    fn can_parse(&self, project_path: &Path) -> bool;

    /// Priority for parser selection (lower number = higher priority)
    /// 1 = highest priority (lock files with exact versions)
    /// 5 = lowest priority (basic requirement files)
    fn priority(&self) -> u8;

    /// Parse dependencies from the project directory
    async fn parse_dependencies(
        &self,
        project_path: &Path,
        include_dev: bool,
        include_optional: bool,
        direct_only: bool,
    ) -> Result<Vec<ParsedDependency>>;

    /// Validate the parsed dependencies and return warnings
    fn validate_dependencies(&self, dependencies: &[ParsedDependency]) -> Vec<String> {
        let mut warnings = Vec::new();

        if dependencies.is_empty() {
            warnings.push(
                "No dependencies found. This might indicate an issue with dependency resolution."
                    .to_string(),
            );
            return warnings;
        }

        // Check for placeholder versions
        let placeholder_count = dependencies
            .iter()
            .filter(|dep| dep.version == Version::new([0, 0, 0]))
            .count();

        if placeholder_count > 0 {
            warnings.push(format!(
                "{placeholder_count} dependencies have placeholder versions. Consider using a lock file for accurate version information."
            ));
        }

        warnings
    }
}

/// Registry for managing multiple project parsers
pub struct ParserRegistry {
    parsers: Vec<Box<dyn ProjectParser>>,
}

impl ParserRegistry {
    /// Create a new parser registry with all available parsers
    pub fn new() -> Self {
        let parsers: Vec<Box<dyn ProjectParser>> = vec![
            Box::new(lock::UvLockParser::new()),
            Box::new(pyproject::PyProjectParser::new()),
        ];

        Self { parsers }
    }

    /// Parse dependencies from a project directory using the best available parser
    pub async fn parse_project(
        &self,
        project_path: &Path,
        include_dev: bool,
        include_optional: bool,
        direct_only: bool,
    ) -> Result<(Vec<ParsedDependency>, &'static str)> {
        // Find all compatible parsers
        let mut compatible_parsers: Vec<&Box<dyn ProjectParser>> = self
            .parsers
            .iter()
            .filter(|parser| parser.can_parse(project_path))
            .collect();

        if compatible_parsers.is_empty() {
            return Err(AuditError::NoDependencyInfo);
        }

        // Sort by priority (lower number = higher priority)
        compatible_parsers.sort_by_key(|parser| parser.priority());

        // Use the highest priority parser
        let parser = compatible_parsers[0];
        let parser_name = parser.name();

        tracing::debug!(
            "Using parser: {} for project: {}",
            parser_name,
            project_path.display()
        );

        let dependencies = parser
            .parse_dependencies(project_path, include_dev, include_optional, direct_only)
            .await?;

        Ok((dependencies, parser_name))
    }

    /// Get validation warnings for parsed dependencies
    pub fn validate_dependencies(
        &self,
        dependencies: &[ParsedDependency],
        parser_name: &str,
    ) -> Vec<String> {
        // Find the parser that was used
        if let Some(parser) = self.parsers.iter().find(|p| p.name() == parser_name) {
            parser.validate_dependencies(dependencies)
        } else {
            // Fallback to basic validation
            let mut warnings = Vec::new();
            if dependencies.is_empty() {
                warnings.push("No dependencies found.".to_string());
            }
            warnings
        }
    }
}

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

/// Statistics about parsed dependencies
#[derive(Debug, Clone)]
pub struct DependencyStats {
    pub total_packages: usize,
    pub direct_packages: usize,
    pub transitive_packages: usize,
    pub by_type: std::collections::HashMap<DependencyType, usize>,
    pub by_source: std::collections::HashMap<String, usize>,
}

impl DependencyStats {
    /// Calculate statistics from parsed dependencies
    pub fn from_dependencies(dependencies: &[ParsedDependency]) -> Self {
        let total_packages = dependencies.len();
        let direct_packages = dependencies.iter().filter(|d| d.is_direct).count();
        let transitive_packages = total_packages - direct_packages;

        let mut by_type = std::collections::HashMap::new();
        let mut by_source = std::collections::HashMap::new();

        for dep in dependencies {
            *by_type.entry(dep.dependency_type).or_insert(0) += 1;

            let source_name = match &dep.source {
                DependencySource::Registry => "Registry",
                DependencySource::Git { .. } => "Git",
                DependencySource::Path => "Path",
                DependencySource::Url(_) => "URL",
            };
            *by_source.entry(source_name.to_string()).or_insert(0) += 1;
        }

        Self {
            total_packages,
            direct_packages,
            transitive_packages,
            by_type,
            by_source,
        }
    }
}

impl std::fmt::Display for DependencyStats {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Found {} packages ({} direct, {} transitive)",
            self.total_packages, self.direct_packages, self.transitive_packages
        )?;

        if !self.by_source.is_empty() {
            write!(f, " - Sources: ")?;
            let mut first = true;
            for (source, count) in &self.by_source {
                if !first {
                    write!(f, ", ")?;
                }
                write!(f, "{source}: {count}")?;
                first = false;
            }
        }

        Ok(())
    }
}
