pub mod concept;
pub mod loader;
pub mod procedural;
pub mod query;

// Re-export for convenience
#[allow(unused_imports)]
pub use concept::{ConceptElementType, ConceptMetadata, ConceptNode};
#[allow(unused_imports)]
pub use loader::{
    concept_nodes_to_elements, failure_mode_nodes_to_elements, load_aliases_yaml,
    load_concepts_yaml, load_workflows_yaml, workflow_nodes_to_elements,
    workflow_step_nodes_to_elements,
};
#[allow(unused_imports)]
pub use procedural::{
    FailureModeMetadata, FailureModeNode, ProceduralElementType, WorkflowMetadata, WorkflowNode,
    WorkflowStepMetadata, WorkflowStepNode,
};
#[allow(unused_imports)]
pub use query::{calculate_match_score, OntologyQueryEngine, OntologyStatus};

use serde::{Deserialize, Serialize};

/// Ontology layer types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OntologyLayer {
    Domain,
    Procedural,
}

impl OntologyLayer {
    pub fn as_str(&self) -> &'static str {
        match self {
            OntologyLayer::Domain => "domain",
            OntologyLayer::Procedural => "procedural",
        }
    }
}

/// Stable Global ID for ontology nodes
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OntologyGid {
    pub env: String,
    pub scope: String,
    pub ontology_type: String,
    pub id: String,
    pub version: String,
}

impl OntologyGid {
    /// Parse a GID string like "local:checkout-service:domain_entity:refund:v1"
    pub fn parse(s: &str) -> Option<Self> {
        let parts: Vec<&str> = s.split(':').collect();
        if parts.len() != 5 {
            return None;
        }
        Some(Self {
            env: parts[0].to_string(),
            scope: parts[1].to_string(),
            ontology_type: parts[2].to_string(),
            id: parts[3].to_string(),
            version: parts[4].to_string(),
        })
    }

    /// Format GID string
    pub fn to_gid_string(&self) -> String {
        format!(
            "{}:{}:{}:{}:{}",
            self.env, self.scope, self.ontology_type, self.id, self.version
        )
    }
}

impl std::fmt::Display for OntologyGid {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}:{}:{}:{}:{}",
            self.env, self.scope, self.ontology_type, self.id, self.version
        )
    }
}

/// Alias normalization helper
pub fn normalize_alias(alias: &str) -> String {
    alias
        .to_lowercase()
        .trim()
        .chars()
        .filter(|c| c.is_alphanumeric() || *c == ' ' || *c == '-' || *c == '_')
        .collect::<String>()
}

/// Normalize multiple aliases
pub fn normalize_aliases(aliases: &[String]) -> Vec<String> {
    aliases.iter().map(|a| normalize_alias(a)).collect()
}

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

    #[test]
    fn test_ontology_gid_parse() {
        let gid = OntologyGid::parse("local:checkout-service:domain_entity:refund:v1").unwrap();
        assert_eq!(gid.env, "local");
        assert_eq!(gid.scope, "checkout-service");
        assert_eq!(gid.ontology_type, "domain_entity");
        assert_eq!(gid.id, "refund");
        assert_eq!(gid.version, "v1");
    }

    #[test]
    fn test_ontology_gid_roundtrip() {
        let gid = OntologyGid::parse("local:checkout-service:domain_entity:refund:v1").unwrap();
        assert_eq!(
            gid.to_string(),
            "local:checkout-service:domain_entity:refund:v1"
        );
    }

    #[test]
    fn test_normalize_alias() {
        assert_eq!(normalize_alias("  Refund  "), "refund");
        assert_eq!(normalize_alias("Money Back"), "money back");
        assert_eq!(normalize_alias("charge-back"), "charge-back");
    }

    #[test]
    fn test_calculate_match_score_exact_name() {
        let (score, reason) = calculate_match_score("refund", "Refund", &[], "Money back");
        assert_eq!(score, 1.0);
        assert!(reason.contains("exact name match"));
    }

    #[test]
    fn test_calculate_match_score_name_contains() {
        let (score, reason) = calculate_match_score("refund", "RefundPolicy", &[], "");
        assert_eq!(score, 0.8);
        assert!(reason.contains("name contains"));
    }

    #[test]
    fn test_calculate_match_score_alias_match() {
        let (score, reason) = calculate_match_score(
            "reversal",
            "Refund",
            &["reversal".to_string(), "chargeback".to_string()],
            "",
        );
        assert_eq!(score, 0.9);
        assert!(reason.contains("exact alias match"));
    }

    #[test]
    fn test_calculate_match_score_description() {
        let (score, reason) = calculate_match_score("payment", "Refund", &[], "Money payment back");
        assert_eq!(score, 0.5);
        assert!(reason.contains("description contains"));
    }

    #[test]
    fn test_calculate_match_score_no_match() {
        let (score, reason) = calculate_match_score("xyz", "Refund", &[], "");
        assert_eq!(score, 0.0);
        assert!(reason.is_empty());
    }
}
