//! Crate entity domain model

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use super::version::SemanticVersion;

/// Crate flags
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CrateFlags {
    /// Whether this crate is a library
    pub is_lib: bool,
    /// Whether this crate has binaries
    pub has_bin: bool,
    /// Whether this crate should be published
    pub publish: bool,
}

/// A Cargo crate in a workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateEntity {
    /// Crate name
    pub name: String,
    /// Path to the crate directory
    pub path: PathBuf,
    /// Current version of the crate
    pub version: SemanticVersion,
    /// Whether this crate is published to crates.io
    pub published: bool,
    /// Dependencies of this crate
    pub dependencies: Vec<Dependency>,
    /// Crate flags
    #[serde(flatten)]
    pub flags: CrateFlags,
    /// Crate features
    pub features: Vec<String>,
}

impl CrateEntity {
    /// Create a new crate entity
    #[must_use]
    pub const fn new(
        name: String,
        path: PathBuf,
        version: SemanticVersion,
        published: bool,
    ) -> Self {
        Self {
            name,
            path,
            version,
            published,
            dependencies: Vec::new(),
            flags: CrateFlags {
                is_lib: false,
                has_bin: false,
                publish: true,
            },
            features: Vec::new(),
        }
    }

    /// Check if this crate is a library
    #[must_use]
    pub const fn is_lib(&self) -> bool {
        self.flags.is_lib
    }

    /// Check if this crate has binaries
    #[must_use]
    pub const fn has_bin(&self) -> bool {
        self.flags.has_bin
    }

    /// Check if this crate should be published
    #[must_use]
    pub const fn should_publish(&self) -> bool {
        self.flags.publish
    }

    /// Get the crate name
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Get the crate version
    #[must_use]
    pub const fn version(&self) -> &SemanticVersion {
        &self.version
    }

    /// Get the crate path
    #[must_use]
    pub const fn path(&self) -> &PathBuf {
        &self.path
    }

    /// Check if this crate depends on another crate
    #[must_use]
    pub fn depends_on(&self, crate_name: &str) -> bool {
        self.dependencies.iter().any(|d| d.name == crate_name)
    }

    /// Get workspace dependencies
    #[must_use]
    pub fn workspace_dependencies(&self) -> Vec<&Dependency> {
        self.dependencies
            .iter()
            .filter(|d| d.is_workspace())
            .collect()
    }

    /// Get external dependencies
    #[must_use]
    pub fn external_dependencies(&self) -> Vec<&Dependency> {
        self.dependencies
            .iter()
            .filter(|d| !d.is_workspace())
            .collect()
    }
}

impl PartialEq for CrateEntity {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Eq for CrateEntity {}

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

/// Dependency kind
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum DependencyKind {
    /// Regular runtime dependency
    #[default]
    Runtime,
    /// Development dependency
    Dev,
    /// Build dependency
    Build,
}

/// Dependency flags
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DependencyFlags {
    /// Whether this is a workspace dependency
    pub is_workspace: bool,
    /// Dependency kind
    pub kind: DependencyKind,
    /// Whether this dependency is optional
    pub is_optional: bool,
}

impl DependencyFlags {
    /// Create a new default flags
    #[must_use]
    pub const fn new() -> Self {
        Self {
            is_workspace: false,
            kind: DependencyKind::Runtime,
            is_optional: false,
        }
    }

    /// Check if this is a dev dependency
    #[must_use]
    pub fn is_dev(&self) -> bool {
        self.kind == DependencyKind::Dev
    }

    /// Check if this is a build dependency
    #[must_use]
    pub fn is_build(&self) -> bool {
        self.kind == DependencyKind::Build
    }

    /// Set as dev dependency
    #[must_use]
    pub const fn with_dev(mut self) -> Self {
        self.kind = DependencyKind::Dev;
        self
    }

    /// Set as build dependency
    #[must_use]
    pub const fn with_build(mut self) -> Self {
        self.kind = DependencyKind::Build;
        self
    }
}

/// A dependency of a crate
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
    /// Dependency name
    pub name: String,
    /// Version requirement
    pub version_req: Option<String>,
    /// Dependency flags
    #[serde(flatten)]
    pub flags: DependencyFlags,
    /// Features enabled on this dependency
    pub features: Vec<String>,
    /// Registry for this dependency (if not crates.io)
    pub registry: Option<String>,
}

impl Dependency {
    /// Create a new dependency
    #[must_use]
    pub const fn new(name: String) -> Self {
        Self {
            name,
            version_req: None,
            flags: DependencyFlags::new(),
            features: Vec::new(),
            registry: None,
        }
    }

    /// Get the dependency name
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Check if this is a workspace dependency
    #[must_use]
    pub const fn is_workspace(&self) -> bool {
        self.flags.is_workspace
    }

    /// Check if this is a dev dependency
    #[must_use]
    pub fn is_dev(&self) -> bool {
        self.flags.is_dev()
    }

    /// Check if this is a build dependency
    #[must_use]
    pub fn is_build(&self) -> bool {
        self.flags.is_build()
    }

    /// Check if this dependency is optional
    #[must_use]
    pub const fn is_optional(&self) -> bool {
        self.flags.is_optional
    }
}

/// A set of crates forming a workspace
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workspace {
    /// Path to the workspace root
    pub root: PathBuf,
    /// All crates in the workspace
    pub crates: Vec<CrateEntity>,
    /// Workspace version (if shared versioning is used)
    pub workspace_version: Option<SemanticVersion>,
}

impl Workspace {
    /// Create a new workspace
    #[must_use]
    pub const fn new(root: PathBuf) -> Self {
        Self {
            root,
            crates: Vec::new(),
            workspace_version: None,
        }
    }

    /// Add a crate to the workspace
    pub fn add_crate(&mut self, krate: CrateEntity) {
        if !self.crates.contains(&krate) {
            self.crates.push(krate);
        }
    }

    /// Get a crate by name
    #[must_use]
    pub fn get_crate(&self, name: &str) -> Option<&CrateEntity> {
        self.crates.iter().find(|c| c.name() == name)
    }

    /// Get a mutable reference to a crate by name
    pub fn get_crate_mut(&mut self, name: &str) -> Option<&mut CrateEntity> {
        self.crates.iter_mut().find(|c| c.name() == name)
    }

    /// Get all library crates
    #[must_use]
    pub fn lib_crates(&self) -> Vec<&CrateEntity> {
        self.crates.iter().filter(|c| c.is_lib()).collect()
    }

    /// Get all publishable crates
    #[must_use]
    pub fn publishable_crates(&self) -> Vec<&CrateEntity> {
        self.crates.iter().filter(|c| c.should_publish()).collect()
    }

    /// Get unpublished crates
    #[must_use]
    pub fn unpublished_crates(&self) -> Vec<&CrateEntity> {
        self.crates
            .iter()
            .filter(|c| !c.published && c.should_publish())
            .collect()
    }

    /// Calculate topological order for publishing
    ///
    /// # Errors
    ///
    /// Returns `WorkspaceCycleError` if a dependency cycle is detected in the workspace
    pub fn publish_order(&self) -> Result<Vec<&CrateEntity>, WorkspaceCycleError> {
        use petgraph::algo::toposort;
        use petgraph::graph::DiGraph;

        let mut graph = DiGraph::new();
        let mut indices = std::collections::HashMap::new();

        // Add nodes
        for krate in &self.crates {
            let idx = graph.add_node(krate.name.clone());
            indices.insert(krate.name.clone(), idx);
        }

        // Add edges for workspace dependencies
        for krate in &self.crates {
            if let Some(&from_idx) = indices.get(&krate.name) {
                for dep in krate.workspace_dependencies() {
                    if let Some(&to_idx) = indices.get(&dep.name) {
                        graph.add_edge(from_idx, to_idx, ());
                    }
                }
            }
        }

        // Topological sort
        toposort(&graph, None).map_or(Err(WorkspaceCycleError), |order| {
            let mut crates = Vec::new();
            for idx in order {
                let name = &graph[idx];
                if let Some(krate) = self.get_crate(name) {
                    crates.push(krate);
                }
            }
            Ok(crates)
        })
    }

    /// Get crates that need to be published
    #[must_use]
    pub fn crates_to_publish(&self) -> Vec<&CrateEntity> {
        self.publishable_crates()
            .into_iter()
            .filter(|c| !c.published)
            .collect()
    }
}

/// Error when a dependency cycle is detected
#[derive(Debug, Clone, thiserror::Error)]
#[error("Dependency cycle detected in workspace")]
pub struct WorkspaceCycleError;

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

    #[test]
    fn test_workspace_add_crate() {
        let mut workspace = Workspace::new(PathBuf::from("/test"));
        let krate = CrateEntity::new(
            "test-crate".to_string(),
            PathBuf::from("/test/test-crate"),
            SemanticVersion::parse("1.0.0").unwrap(),
            false,
        );
        workspace.add_crate(krate);
        assert_eq!(workspace.crates.len(), 1);
        assert!(workspace.get_crate("test-crate").is_some());
    }

    #[test]
    fn test_crate_depends_on() {
        let mut krate = CrateEntity::new(
            "test-crate".to_string(),
            PathBuf::from("/test"),
            SemanticVersion::parse("1.0.0").unwrap(),
            false,
        );
        krate.dependencies.push(Dependency::new("dep1".to_string()));
        assert!(krate.depends_on("dep1"));
        assert!(!krate.depends_on("dep2"));
    }
}
