use std::sync::Arc;

use anyhow::Result;
use tracing::{debug, info};

use crate::db::KuzuGraph;

use super::types::{Concept, ConceptCategory, KBSyncResult, KnowledgeEntry};

/// Converts knowledge base entries into KuzuGraph nodes and edges.
///
/// The assimilator performs two operations:
/// 1. Creates `Knowledge` nodes for each entry
/// 2. Extracts `Concept` nodes using heuristic patterns and links them
pub struct KBAssimilator {
    kuzu: Arc<KuzuGraph>,
}

impl KBAssimilator {
    pub fn new(kuzu: Arc<KuzuGraph>) -> Self {
        Self { kuzu }
    }

    /// Assimilate a batch of knowledge entries into the graph.
    pub async fn assimilate(
        &self,
        entries: &[KnowledgeEntry],
        source: &str,
    ) -> Result<KBSyncResult> {
        let mut nodes_created = 0usize;
        let mut edges_created = 0usize;
        let mut all_concepts: Vec<Concept> = Vec::new();

        for entry in entries {
            // 1. Create/merge Knowledge node.
            self.kuzu.add_knowledge_node(entry).await?;
            nodes_created += 1;

            // 2. Extract concepts from content (cap at 10 per entry to prevent graph bloat).
            let concepts = Self::extract_concepts(entry);
            for concept in concepts.iter().take(10) {
                self.kuzu.add_concept_node(concept).await?;
                nodes_created += 1;

                // 3. Link Knowledge → Concept via DEFINES_REQUIREMENT.
                self.kuzu
                    .add_knowledge_concept_edge(&entry.id, &concept.id)
                    .await?;
                edges_created += 1;

                // 4. Try to link Concept → File nodes based on name matching
                //    (cap at 5 files per concept to prevent fan-out explosion).
                let matching_files = self.kuzu.find_files_matching_concept(&concept.name).await?;
                for file_path in matching_files.iter().take(5) {
                    self.kuzu
                        .add_concept_file_edge(&concept.id, file_path)
                        .await?;
                    edges_created += 1;
                }
            }

            // 5. Create INFORMS edges from Knowledge directly to matching Files.
            let title_words: Vec<&str> = entry.title.split_whitespace()
                .filter(|w| w.len() > 3)
                .take(3)
                .collect();
            for word in &title_words {
                let matching_files = self.kuzu.find_files_matching_concept(word).await?;
                for file_path in matching_files.iter().take(5) {
                    self.kuzu.add_knowledge_file_edge(&entry.id, file_path).await?;
                    edges_created += 1;
                }
            }

            all_concepts.extend(concepts);
        }

        info!(
            entries = entries.len(),
            concepts = all_concepts.len(),
            nodes = nodes_created,
            edges = edges_created,
            "Assimilation complete"
        );

        Ok(KBSyncResult {
            entries_fetched: entries.len(),
            concepts_extracted: all_concepts.len(),
            graph_nodes_created: nodes_created,
            graph_edges_created: edges_created,
            source: source.to_string(),
        })
    }

    /// Extract concepts from a knowledge entry using heuristic patterns.
    ///
    /// Patterns:
    /// - "must ..."/"shall ..." → Requirement
    /// - "should ..."/"consider ..." → Constraint
    /// - "pattern"/"convention" → Pattern
    /// - Section headers (`# ...`) → Domain
    pub fn extract_concepts(entry: &KnowledgeEntry) -> Vec<Concept> {
        let mut concepts = Vec::new();
        let lines: Vec<&str> = entry.content.lines().collect();

        for (i, line) in lines.iter().enumerate() {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }

            let lower = trimmed.to_lowercase();

            // Requirement: "must ..." or "shall ..."
            if lower.starts_with("must ") || lower.starts_with("shall ") || lower.contains(" must ") || lower.contains(" shall ") {
                let concept_id = format!("concept-{}-{}", entry.id, i);
                concepts.push(Concept {
                    id: concept_id,
                    name: Self::extract_concept_name(trimmed),
                    description: trimmed.to_string(),
                    category: ConceptCategory::Requirement,
                    source_entries: vec![entry.id.clone()],
                });
                continue;
            }

            // Constraint: "should ..." or "consider ..."
            if lower.starts_with("should ") || lower.starts_with("consider ") || lower.contains(" should ") {
                let concept_id = format!("concept-{}-{}", entry.id, i);
                concepts.push(Concept {
                    id: concept_id,
                    name: Self::extract_concept_name(trimmed),
                    description: trimmed.to_string(),
                    category: ConceptCategory::Constraint,
                    source_entries: vec![entry.id.clone()],
                });
                continue;
            }

            // Pattern: mentions of "pattern" or "convention"
            if lower.contains("pattern") || lower.contains("convention") {
                let concept_id = format!("concept-{}-{}", entry.id, i);
                concepts.push(Concept {
                    id: concept_id,
                    name: Self::extract_concept_name(trimmed),
                    description: trimmed.to_string(),
                    category: ConceptCategory::Pattern,
                    source_entries: vec![entry.id.clone()],
                });
                continue;
            }

            // Domain: section headers
            if trimmed.starts_with("# ") || trimmed.starts_with("## ") {
                let header = trimmed.trim_start_matches('#').trim();
                if !header.is_empty() {
                    let concept_id = format!("concept-{}-{}", entry.id, i);
                    concepts.push(Concept {
                        id: concept_id,
                        name: header.to_string(),
                        description: format!("Domain topic: {}", header),
                        category: ConceptCategory::Domain,
                        source_entries: vec![entry.id.clone()],
                    });
                }
            }
        }

        debug!(
            entry_id = %entry.id,
            concepts = concepts.len(),
            "Extracted concepts from KB entry"
        );

        concepts
    }

    /// Extract a short concept name from a line of text.
    /// Takes the first ~60 chars and strips common prefixes.
    fn extract_concept_name(line: &str) -> String {
        let cleaned = line
            .trim_start_matches("- ")
            .trim_start_matches("* ")
            .trim_start_matches("> ");

        if cleaned.len() <= 60 {
            cleaned.to_string()
        } else {
            format!("{}...", &cleaned[..57])
        }
    }
}

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

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::*;

    fn make_entry(id: &str, content: &str) -> KnowledgeEntry {
        KnowledgeEntry {
            id: id.to_string(),
            source: "test".to_string(),
            collection: "test-col".to_string(),
            title: "Test Entry".to_string(),
            content: content.to_string(),
            metadata: HashMap::new(),
            relevance_score: 0.9,
            retrieved_at: 0,
        }
    }

    #[test]
    fn test_extract_requirement() {
        let entry = make_entry("e1", "All endpoints must use JWT authentication.\nOther text.");
        let concepts = KBAssimilator::extract_concepts(&entry);

        assert_eq!(concepts.len(), 1);
        assert_eq!(concepts[0].category, ConceptCategory::Requirement);
        assert!(concepts[0].description.contains("must use JWT"));
    }

    #[test]
    fn test_extract_shall_requirement() {
        let entry = make_entry("e2", "The system shall support pagination.");
        let concepts = KBAssimilator::extract_concepts(&entry);

        assert_eq!(concepts.len(), 1);
        assert_eq!(concepts[0].category, ConceptCategory::Requirement);
    }

    #[test]
    fn test_extract_constraint() {
        let entry = make_entry("e3", "Developers should use structured logging.\nConsider using tracing crate.");
        let concepts = KBAssimilator::extract_concepts(&entry);

        assert_eq!(concepts.len(), 2);
        assert!(concepts.iter().all(|c| c.category == ConceptCategory::Constraint));
    }

    #[test]
    fn test_extract_pattern() {
        let entry = make_entry("e4", "Use the repository pattern for data access.");
        let concepts = KBAssimilator::extract_concepts(&entry);

        assert_eq!(concepts.len(), 1);
        assert_eq!(concepts[0].category, ConceptCategory::Pattern);
    }

    #[test]
    fn test_extract_domain_from_header() {
        let entry = make_entry("e5", "# Authentication\nDetails about auth.\n## Token Management\nJWT stuff.");
        let concepts = KBAssimilator::extract_concepts(&entry);

        let domain_concepts: Vec<_> = concepts
            .iter()
            .filter(|c| c.category == ConceptCategory::Domain)
            .collect();
        assert_eq!(domain_concepts.len(), 2);
        assert_eq!(domain_concepts[0].name, "Authentication");
        assert_eq!(domain_concepts[1].name, "Token Management");
    }

    #[test]
    fn test_extract_mixed_content() {
        let entry = make_entry(
            "e6",
            "# Architecture\nThe API must validate all inputs.\nDevelopers should follow the builder pattern convention.\nRegular text here.",
        );
        let concepts = KBAssimilator::extract_concepts(&entry);

        assert!(concepts.len() >= 3);
        let categories: Vec<_> = concepts.iter().map(|c| &c.category).collect();
        assert!(categories.contains(&&ConceptCategory::Domain));
        assert!(categories.contains(&&ConceptCategory::Requirement));
        // "builder pattern convention" matches both Pattern (convention) — it picks the first match (should) if "should" is present,
        // otherwise pattern/convention
    }

    #[test]
    fn test_extract_empty_content() {
        let entry = make_entry("e7", "");
        let concepts = KBAssimilator::extract_concepts(&entry);
        assert!(concepts.is_empty());
    }

    #[test]
    fn test_extract_no_matching_content() {
        let entry = make_entry("e8", "This is just regular text.\nNothing special here.");
        let concepts = KBAssimilator::extract_concepts(&entry);
        assert!(concepts.is_empty());
    }

    #[test]
    fn test_concept_name_truncation() {
        let long_line = "x".repeat(100);
        let name = KBAssimilator::extract_concept_name(&long_line);
        assert!(name.len() <= 60);
        assert!(name.ends_with("..."));
    }

    #[test]
    fn test_concept_name_prefix_stripping() {
        assert_eq!(KBAssimilator::extract_concept_name("- list item"), "list item");
        assert_eq!(KBAssimilator::extract_concept_name("* bullet"), "bullet");
        assert_eq!(KBAssimilator::extract_concept_name("> quote"), "quote");
    }

    #[test]
    fn test_concept_ids_are_unique() {
        let entry = make_entry("e9", "Must do A.\nMust do B.\nMust do C.");
        let concepts = KBAssimilator::extract_concepts(&entry);

        let ids: Vec<_> = concepts.iter().map(|c| &c.id).collect();
        let unique: std::collections::HashSet<_> = ids.iter().collect();
        assert_eq!(ids.len(), unique.len(), "All concept IDs should be unique");
    }
}
