use std::collections::HashMap;

use serde::{Deserialize, Serialize};

/// Supported knowledge base backends.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum KBSource {
    ChromaDB,
    Pinecone,
    Weaviate,
    Custom(String),
}

impl std::str::FromStr for KBSource {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "chromadb" | "chroma" => Ok(KBSource::ChromaDB),
            "pinecone" => Ok(KBSource::Pinecone),
            "weaviate" => Ok(KBSource::Weaviate),
            other => Ok(KBSource::Custom(other.to_string())),
        }
    }
}

impl std::fmt::Display for KBSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            KBSource::ChromaDB => write!(f, "ChromaDB"),
            KBSource::Pinecone => write!(f, "Pinecone"),
            KBSource::Weaviate => write!(f, "Weaviate"),
            KBSource::Custom(name) => write!(f, "{}", name),
        }
    }
}

/// Configuration for connecting to an external knowledge base MCP server.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KBServerConfig {
    pub source: KBSource,
    /// Unique name for this KB connection (used as registry key).
    pub name: String,
    /// Command to spawn the MCP server process.
    pub command: String,
    /// Arguments to the command.
    pub args: Vec<String>,
    /// Environment variables to set.
    pub env_vars: Vec<(String, String)>,
    /// Default collection to query when none is specified.
    pub collection: Option<String>,
    /// Glob pattern to match multiple collections (e.g., `requirements-*`).
    pub collection_pattern: Option<String>,
}

/// A single entry retrieved from a knowledge base.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeEntry {
    pub id: String,
    pub source: String,
    pub collection: String,
    pub title: String,
    pub content: String,
    pub metadata: HashMap<String, String>,
    pub relevance_score: f64,
    pub retrieved_at: i64,
}

/// A semantic concept extracted from knowledge base entries.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Concept {
    pub id: String,
    pub name: String,
    pub description: String,
    pub category: ConceptCategory,
    pub source_entries: Vec<String>,
}

/// Category of an extracted concept.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ConceptCategory {
    Requirement,
    DesignDecision,
    Constraint,
    Pattern,
    Domain,
}

impl std::fmt::Display for ConceptCategory {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ConceptCategory::Requirement => write!(f, "Requirement"),
            ConceptCategory::DesignDecision => write!(f, "DesignDecision"),
            ConceptCategory::Constraint => write!(f, "Constraint"),
            ConceptCategory::Pattern => write!(f, "Pattern"),
            ConceptCategory::Domain => write!(f, "Domain"),
        }
    }
}

impl std::str::FromStr for ConceptCategory {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "requirement" => Ok(ConceptCategory::Requirement),
            "designdecision" | "design_decision" => Ok(ConceptCategory::DesignDecision),
            "constraint" => Ok(ConceptCategory::Constraint),
            "pattern" => Ok(ConceptCategory::Pattern),
            "domain" => Ok(ConceptCategory::Domain),
            other => Err(format!("Unknown concept category: '{}'", other)),
        }
    }
}

/// Result of querying a knowledge base.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KBQueryResult {
    pub entries: Vec<KnowledgeEntry>,
    pub source: String,
    pub collection: String,
    pub query: String,
    pub total_results: usize,
}

/// Result of syncing (query + assimilation) a knowledge base.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KBSyncResult {
    pub entries_fetched: usize,
    pub concepts_extracted: usize,
    pub graph_nodes_created: usize,
    pub graph_edges_created: usize,
    pub source: String,
}

/// Per-source sync status for display in extension UIs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KBSourceStatus {
    pub name: String,
    pub synced: bool,
    pub knowledge_count: usize,
}

// ===========================================================================
// Tests
// ===========================================================================

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

    #[test]
    fn test_kb_source_from_str() {
        assert_eq!("chromadb".parse::<KBSource>().unwrap(), KBSource::ChromaDB);
        assert_eq!("chroma".parse::<KBSource>().unwrap(), KBSource::ChromaDB);
        assert_eq!("Pinecone".parse::<KBSource>().unwrap(), KBSource::Pinecone);
        assert_eq!("weaviate".parse::<KBSource>().unwrap(), KBSource::Weaviate);
        assert_eq!(
            "custom-db".parse::<KBSource>().unwrap(),
            KBSource::Custom("custom-db".into())
        );
    }

    #[test]
    fn test_kb_source_display() {
        assert_eq!(KBSource::ChromaDB.to_string(), "ChromaDB");
        assert_eq!(KBSource::Pinecone.to_string(), "Pinecone");
        assert_eq!(KBSource::Weaviate.to_string(), "Weaviate");
        assert_eq!(KBSource::Custom("MyDB".into()).to_string(), "MyDB");
    }

    #[test]
    fn test_concept_category_roundtrip() {
        let categories = vec![
            ConceptCategory::Requirement,
            ConceptCategory::DesignDecision,
            ConceptCategory::Constraint,
            ConceptCategory::Pattern,
            ConceptCategory::Domain,
        ];
        for cat in &categories {
            let s = cat.to_string();
            let parsed: ConceptCategory = s.parse().unwrap();
            assert_eq!(&parsed, cat);
        }
    }

    #[test]
    fn test_knowledge_entry_serialization() {
        let entry = KnowledgeEntry {
            id: "kb-1".into(),
            source: "ChromaDB".into(),
            collection: "design-docs".into(),
            title: "Auth Design".into(),
            content: "Use JWT tokens".into(),
            metadata: HashMap::from([("author".into(), "alice".into())]),
            relevance_score: 0.85,
            retrieved_at: 1707300600,
        };

        let json = serde_json::to_string(&entry).unwrap();
        let parsed: KnowledgeEntry = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.id, "kb-1");
        assert_eq!(parsed.relevance_score, 0.85);
        assert_eq!(parsed.metadata.get("author").unwrap(), "alice");
    }

    #[test]
    fn test_concept_serialization() {
        let concept = Concept {
            id: "concept-1".into(),
            name: "JWT Authentication".into(),
            description: "Must use JWT for auth".into(),
            category: ConceptCategory::Requirement,
            source_entries: vec!["kb-1".into()],
        };

        let json = serde_json::to_string(&concept).unwrap();
        let parsed: Concept = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.id, "concept-1");
        assert_eq!(parsed.category, ConceptCategory::Requirement);
    }

    #[test]
    fn test_kb_query_result_serialization() {
        let result = KBQueryResult {
            entries: vec![],
            source: "ChromaDB".into(),
            collection: "docs".into(),
            query: "auth".into(),
            total_results: 0,
        };

        let json = serde_json::to_string(&result).unwrap();
        let parsed: KBQueryResult = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.source, "ChromaDB");
        assert_eq!(parsed.total_results, 0);
    }

    #[test]
    fn test_kb_sync_result_serialization() {
        let result = KBSyncResult {
            entries_fetched: 10,
            concepts_extracted: 5,
            graph_nodes_created: 15,
            graph_edges_created: 8,
            source: "ChromaDB".into(),
        };

        let json = serde_json::to_string(&result).unwrap();
        let parsed: KBSyncResult = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.entries_fetched, 10);
        assert_eq!(parsed.concepts_extracted, 5);
    }
}
