//! Semantic Checker implementation
//!
//! Provides pre-ingest checking for duplicate detection and conflict identification.

use crate::storage::DocumentStore;
use crate::types::MemoryNode;
use crate::vector::hnsw::HNSWIndex;

use super::types::{
    CheckResult, DuplicateCandidate, ConflictCandidate,
    DuplicateReason, ConflictType, SemanticCheckerConfig, SemanticError,
};

/// Semantic Checker for pre-ingest validation
///
/// Performs semantic deduplication and conflict detection before
/// data is committed to the database.
pub struct SemanticChecker {
    /// Configuration
    config: SemanticCheckerConfig,
}

impl SemanticChecker {
    /// Create a new SemanticChecker with default configuration
    pub fn new() -> Self {
        Self {
            config: SemanticCheckerConfig::default(),
        }
    }

    /// Create a new SemanticChecker with custom configuration
    pub fn with_config(config: SemanticCheckerConfig) -> Self {
        Self { config }
    }

    /// Get the current configuration
    pub fn config(&self) -> &SemanticCheckerConfig {
        &self.config
    }

    /// Update the configuration
    pub fn set_config(&mut self, config: SemanticCheckerConfig) {
        self.config = config;
    }

    /// Perform pre-ingest check on a new node
    ///
    /// This is the main entry point for semantic checking. It:
    /// 1. Searches for similar vectors in the HNSW index
    /// 2. Filters candidates by similarity threshold
    /// 3. Optionally refines with cross-encoder scoring
    /// 4. Checks for logical conflicts if enabled
    ///
    /// # Arguments
    /// * `new_node` - The node to be inserted
    /// * `hnsw` - Reference to the HNSW index
    /// * `docs` - Reference to the document store
    ///
    /// # Returns
    /// * `Ok(CheckResult)` - The result of the semantic check
    /// * `Err(SemanticError)` - If an error occurred during checking
    pub fn pre_ingest_check(
        &self,
        new_node: &MemoryNode,
        hnsw: &HNSWIndex,
        docs: &dyn crate::storage::backend::StorageBackend,
    ) -> Result<CheckResult, SemanticError> {
        // Check if we have an embedding
        let embedding = new_node.embedding.as_ref()
            .ok_or(SemanticError::NoEmbedding)?;

        // Skip check if both detections are disabled
        if !self.config.detect_duplicates && !self.config.detect_conflicts {
            return Ok(CheckResult::Safe);
        }

        // Step 1: Vector search for nearest neighbors
        let neighbors = self.search_neighbors(embedding, hnsw)?;

        if neighbors.is_empty() {
            return Ok(CheckResult::Safe);
        }

        // Step 2: Filter by similarity threshold and get full nodes
        let candidates = self.filter_candidates(&neighbors, docs);

        if candidates.is_empty() {
            return Ok(CheckResult::Safe);
        }

        // Step 3: Check for duplicates
        let mut duplicates = Vec::new();
        let mut conflicts = Vec::new();

        if self.config.detect_duplicates {
            duplicates = self.detect_duplicates(new_node, &candidates);
        }

        // Step 4: Check for conflicts
        if self.config.detect_conflicts {
            conflicts = self.detect_conflicts(new_node, &candidates);
        }

        // Step 5: Build result
        match (duplicates.is_empty(), conflicts.is_empty()) {
            (true, true) => Ok(CheckResult::Safe),
            (false, true) => Ok(CheckResult::DuplicateSuspect(duplicates)),
            (true, false) => Ok(CheckResult::ConflictSuspect(conflicts)),
            (false, false) => Ok(CheckResult::Mixed { duplicates, conflicts }),
        }
    }

    /// Search for nearest neighbors in the HNSW index
    fn search_neighbors(
        &self,
        embedding: &[f32],
        hnsw: &HNSWIndex,
    ) -> Result<Vec<(String, f32)>, SemanticError> {
        // Use the HNSW search_with_ids method which returns (external_id, distance)
        let results = hnsw.search_with_ids(embedding, self.config.top_k)
            .map_err(|e| SemanticError::IndexError(e.to_string()))?;

        // Convert distance to similarity
        // For cosine distance: similarity = 1 - distance
        let neighbors: Vec<(String, f32)> = results
            .into_iter()
            .map(|(id, distance)| {
                let similarity = 1.0 - distance;
                (id, similarity)
            })
            .collect();

        Ok(neighbors)
    }

    /// Filter candidates by similarity threshold and retrieve full nodes
    fn filter_candidates(
        &self,
        neighbors: &[(String, f32)],
        docs: &dyn crate::storage::backend::StorageBackend,
    ) -> Vec<(MemoryNode, f32)> {
        neighbors
            .iter()
            .filter(|(_, sim)| *sim >= self.config.similarity_threshold)
            .filter_map(|(id, sim)| {
                docs.get(id).ok().flatten().map(|node| (node, *sim))
            })
            .collect()
    }

    /// Detect potential duplicates among candidates
    fn detect_duplicates(
        &self,
        new_node: &MemoryNode,
        candidates: &[(MemoryNode, f32)],
    ) -> Vec<DuplicateCandidate> {
        let mut duplicates = Vec::new();

        for (candidate, similarity) in candidates {
            // Calculate confidence score
            let confidence = self.calculate_duplicate_confidence(new_node, candidate, *similarity);

            if confidence >= self.config.duplicate_confidence_threshold {
                let reason = self.determine_duplicate_reason(new_node, candidate, *similarity, confidence);
                
                duplicates.push(DuplicateCandidate {
                    id: candidate.id.clone(),
                    content: candidate.content.clone(),
                    similarity: *similarity,
                    confidence,
                    reason,
                });
            }
        }

        // Sort by confidence descending
        duplicates.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal));

        duplicates
    }

    /// Calculate duplicate confidence score
    ///
    /// Combines multiple signals:
    /// - Vector similarity
    /// - Text similarity (Jaccard, edit distance)
    /// - Cross-encoder score (if available)
    fn calculate_duplicate_confidence(
        &self,
        new_node: &MemoryNode,
        candidate: &MemoryNode,
        vector_similarity: f32,
    ) -> f32 {
        // Boost if text content is very similar
        let text_similarity = self.calculate_text_similarity(&new_node.content, &candidate.content);
        
        // Weighted combination
        // Vector similarity: 60%, Text similarity: 40%
        let mut confidence = vector_similarity * 0.6 + text_similarity * 0.4;

        // If cross-encoder is enabled, we would refine here
        // For now, we use a heuristic based on combined scores
        if self.config.use_cross_encoder {
            // Placeholder for cross-encoder integration
            // In a full implementation, this would call the cross-encoder model
            confidence = confidence * 0.9 + text_similarity * 0.1;
        }

        confidence.clamp(0.0, 1.0)
    }

    /// Calculate simple text similarity using normalized Jaccard-like metric
    fn calculate_text_similarity(&self, text_a: &str, text_b: &str) -> f32 {
        // Normalize texts
        let a_lower = text_a.to_lowercase();
        let b_lower = text_b.to_lowercase();

        // Check for exact match
        if a_lower == b_lower {
            return 1.0;
        }

        // Simple word-based Jaccard similarity
        let words_a: std::collections::HashSet<&str> = a_lower.split_whitespace().collect();
        let words_b: std::collections::HashSet<&str> = b_lower.split_whitespace().collect();

        if words_a.is_empty() && words_b.is_empty() {
            return 1.0;
        }

        let intersection = words_a.intersection(&words_b).count();
        let union = words_a.union(&words_b).count();

        if union == 0 {
            return 0.0;
        }

        intersection as f32 / union as f32
    }

    /// Determine the reason for flagging as duplicate
    fn determine_duplicate_reason(
        &self,
        new_node: &MemoryNode,
        candidate: &MemoryNode,
        similarity: f32,
        confidence: f32,
    ) -> DuplicateReason {
        let text_sim = self.calculate_text_similarity(&new_node.content, &candidate.content);

        if text_sim > 0.95 {
            DuplicateReason::TextMatch
        } else if similarity > 0.95 && text_sim < 0.5 {
            // High vector similarity but low text similarity = paraphrase
            DuplicateReason::Paraphrase
        } else if confidence > 0.9 {
            DuplicateReason::SemanticEquivalence
        } else {
            DuplicateReason::HighSimilarity
        }
    }

    /// Detect potential conflicts among candidates
    fn detect_conflicts(
        &self,
        new_node: &MemoryNode,
        candidates: &[(MemoryNode, f32)],
    ) -> Vec<ConflictCandidate> {
        let mut conflicts = Vec::new();

        for (candidate, _similarity) in candidates {
            // Check for various conflict types
            if let Some(conflict) = self.check_conflict(new_node, candidate) {
                if conflict.severity >= self.config.conflict_severity_threshold {
                    conflicts.push(conflict);
                }
            }
        }

        // Sort by severity descending
        conflicts.sort_by(|a, b| b.severity.partial_cmp(&a.severity).unwrap_or(std::cmp::Ordering::Equal));

        conflicts
    }

    /// Check for conflict between two nodes
    fn check_conflict(
        &self,
        new_node: &MemoryNode,
        existing: &MemoryNode,
    ) -> Option<ConflictCandidate> {
        // Check temporal conflict
        if self.config.check_temporal {
            if let Some(temporal_conflict) = self.check_temporal_conflict(new_node, existing) {
                return Some(temporal_conflict);
            }
        }

        // Check for logical contradiction using simple heuristics
        if let Some(contradiction) = self.check_contradiction(new_node, existing) {
            return Some(contradiction);
        }

        None
    }

    /// Check for temporal conflicts
    ///
    /// Temporal conflicts occur when two events are scheduled at the same time
    /// but are mutually exclusive (e.g., being in two places at once)
    fn check_temporal_conflict(
        &self,
        new_node: &MemoryNode,
        existing: &MemoryNode,
    ) -> Option<ConflictCandidate> {
        // Check if timestamps overlap significantly (within same day)
        let time_diff = (new_node.created_at - existing.created_at).abs();
        let one_day_ms = 24 * 60 * 60 * 1000;

        if time_diff > one_day_ms {
            return None;
        }

        // Look for temporal keywords that might indicate conflict
        let new_content = new_node.content.to_lowercase();
        let existing_content = existing.content.to_lowercase();

        // Check for location indicators that might conflict
        let location_keywords = ["在家", "at home", "在公司", "at office", "在上海", "in shanghai", 
                                "在北京", "in beijing", "出差", "traveling", "meeting", "开会"];
        
        let new_locations: Vec<&str> = location_keywords.iter()
            .filter(|kw| new_content.contains(*kw))
            .copied()
            .collect();
        
        let existing_locations: Vec<&str> = location_keywords.iter()
            .filter(|kw| existing_content.contains(*kw))
            .copied()
            .collect();

        // If both mention different locations around the same time, it's a potential conflict
        if !new_locations.is_empty() && !existing_locations.is_empty() {
            let has_different_locations = new_locations.iter()
                .any(|loc| !existing_locations.contains(loc));

            if has_different_locations {
                return Some(ConflictCandidate {
                    id: existing.id.clone(),
                    content: existing.content.clone(),
                    severity: 0.8,
                    conflict_type: ConflictType::Spatial,
                    explanation: Some(format!(
                        "Potential location conflict: '{}' vs '{}'",
                        new_locations.join(", "),
                        existing_locations.join(", ")
                    )),
                });
            }
        }

        None
    }

    /// Check for logical contradictions
    fn check_contradiction(
        &self,
        new_node: &MemoryNode,
        existing: &MemoryNode,
    ) -> Option<ConflictCandidate> {
        let new_content = new_node.content.to_lowercase();
        let existing_content = existing.content.to_lowercase();

        // Check for negation patterns
        let negation_pairs = [
            ("不会", "会"),
            ("不能", "能"),
            ("不是", "是"),
            ("不要", "要"),
            ("没有", "有"),
            ("won't", "will"),
            ("can't", "can"),
            ("don't", "do"),
            ("isn't", "is"),
            ("not", ""),
        ];

        for (neg, pos) in negation_pairs.iter() {
            // Check if new content has negation and existing has positive (or vice versa)
            let new_has_neg = new_content.contains(neg);
            let existing_has_neg = existing_content.contains(neg);
            
            // If one has negation and the other doesn't, check if they share the positive form
            if new_has_neg != existing_has_neg && !pos.is_empty() {
                if new_content.contains(pos) && existing_content.contains(pos) {
                    return Some(ConflictCandidate {
                        id: existing.id.clone(),
                        content: existing.content.clone(),
                        severity: 0.75,
                        conflict_type: ConflictType::Contradiction,
                        explanation: Some("Potential negation-based contradiction detected".to_string()),
                    });
                }
            }
        }

        None
    }

    /// Quick check without full processing
    ///
    /// Returns true if there might be issues that need full checking
    pub fn quick_check(
        &self,
        embedding: &[f32],
        hnsw: &HNSWIndex,
    ) -> Result<bool, SemanticError> {
        let neighbors = self.search_neighbors(embedding, hnsw)?;
        
        // Check if any neighbor exceeds the similarity threshold
        let has_potential_issue = neighbors.iter()
            .any(|(_, sim)| *sim >= self.config.similarity_threshold);

        Ok(has_potential_issue)
    }
}

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

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

    #[test]
    fn test_text_similarity_exact_match() {
        let checker = SemanticChecker::new();
        let sim = checker.calculate_text_similarity("Hello World", "hello world");
        assert_eq!(sim, 1.0);
    }

    #[test]
    fn test_text_similarity_partial() {
        let checker = SemanticChecker::new();
        let sim = checker.calculate_text_similarity("Hello World", "Hello Universe");
        assert!(sim > 0.0 && sim < 1.0);
    }

    #[test]
    fn test_text_similarity_no_match() {
        let checker = SemanticChecker::new();
        let sim = checker.calculate_text_similarity("Hello", "Goodbye");
        assert_eq!(sim, 0.0);
    }

    #[test]
    fn test_default_config() {
        let checker = SemanticChecker::new();
        assert!(checker.config().detect_duplicates);
        assert!(checker.config().detect_conflicts);
        assert_eq!(checker.config().top_k, 10);
    }

    #[test]
    fn test_custom_config() {
        let config = SemanticCheckerConfig::new()
            .with_similarity_threshold(0.9)
            .with_top_k(5);
        let checker = SemanticChecker::with_config(config);
        
        assert_eq!(checker.config().similarity_threshold, 0.9);
        assert_eq!(checker.config().top_k, 5);
    }
}
