//! Gap detection for the Research Agent.
//!
//! Detects when extracted code claims have no matching authoritative coverage
//! in the corpus, indicating a potential gap in knowledge.

use std::collections::HashSet;

use tracing::{debug, instrument};

use crate::episteme::ConceptIndex;
use crate::types::ExtractedClaim;

/// A detected gap in authoritative coverage.
#[derive(Debug, Clone)]
pub struct Gap {
    /// The concept path from the code claim (e.g., `code://rust/myapp/redis/max_memory_policy`).
    pub concept_path: String,

    /// The predicate from the claim.
    pub predicate: String,

    /// Normalized topic extracted from the concept path (e.g., `redis/max_memory_policy`).
    pub topic: String,

    /// The source file where the gap was detected.
    pub source_file: String,

    /// Line number in the source file.
    pub source_line: usize,

    /// The original claim description.
    pub description: String,

    /// Confidence of the extraction that led to this gap.
    pub confidence: f32,
}

impl Gap {
    /// Create a gap from an extracted claim.
    pub fn from_claim(claim: &ExtractedClaim) -> Self {
        let topic = extract_topic(&claim.concept_path);

        Self {
            concept_path: claim.concept_path.clone(),
            predicate: claim.predicate.clone(),
            topic,
            source_file: claim.file.clone(),
            source_line: claim.line,
            description: claim.description.clone(),
            confidence: claim.confidence,
        }
    }

    /// Get a unique key for deduplication.
    pub fn key(&self) -> String {
        format!("{}::{}", self.topic, self.predicate)
    }
}

/// Detect gaps in authoritative coverage.
///
/// Compares extracted claims against the authoritative corpus index and
/// identifies claims that have no matching authoritative source.
///
/// # Arguments
///
/// * `claims` - Extracted code claims to check
/// * `index` - Authoritative corpus concept index
///
/// # Returns
///
/// A vector of detected gaps, deduplicated by topic+predicate.
#[instrument(skip(claims, index), fields(claim_count = claims.len()))]
pub fn detect_gaps(claims: &[ExtractedClaim], index: &ConceptIndex) -> Vec<Gap> {
    let mut gaps = Vec::new();
    let mut seen_keys = HashSet::new();

    for claim in claims {
        // Check if there's any authoritative coverage for this claim
        if index.lookup(&claim.concept_path, &claim.predicate).is_none() {
            let gap = Gap::from_claim(claim);
            let key = gap.key();

            // Deduplicate by topic+predicate
            if !seen_keys.contains(&key) {
                debug!(
                    concept_path = %claim.concept_path,
                    predicate = %claim.predicate,
                    topic = %gap.topic,
                    "Gap detected: no authoritative coverage"
                );
                seen_keys.insert(key);
                gaps.push(gap);
            }
        }
    }

    gaps
}

/// Extract a normalized topic from a concept path.
///
/// Takes the last 2 path segments after the scheme.
///
/// # Examples
///
/// - `code://rust/myapp/redis/max_memory_policy` → `redis/max_memory_policy`
/// - `code://go/server/http/read_timeout` → `http/read_timeout`
fn extract_topic(concept_path: &str) -> String {
    // Split on "://" to separate scheme from path
    let path = concept_path.find("://").map(|i| &concept_path[i + 3..]).unwrap_or(concept_path);

    // Get last two non-empty segments
    let segments: Vec<&str> = path.rsplit('/').filter(|s| !s.is_empty()).take(2).collect();

    if segments.len() >= 2 {
        format!("{}/{}", segments[1], segments[0])
    } else if segments.len() == 1 {
        segments[0].to_string()
    } else {
        path.to_string()
    }
}

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

    fn make_claim(concept_path: &str, predicate: &str) -> ExtractedClaim {
        ExtractedClaim {
            concept_path: concept_path.to_string(),
            predicate: predicate.to_string(),
            value: ObjectValue::Boolean(true),
            file: "test.rs".to_string(),
            line: 42,
            matched_text: "test".to_string(),
            confidence: 0.9,
            description: "Test claim".to_string(),
        }
    }

    #[test]
    fn test_extract_topic() {
        assert_eq!(
            extract_topic("code://rust/myapp/redis/max_memory_policy"),
            "redis/max_memory_policy"
        );
        assert_eq!(extract_topic("code://go/server/http/read_timeout"), "http/read_timeout");
        assert_eq!(extract_topic("code://rust/tls/cert_verification"), "tls/cert_verification");
    }

    #[test]
    fn test_gap_key() {
        let claim = make_claim("code://rust/myapp/redis/max_memory_policy", "config_value");
        let gap = Gap::from_claim(&claim);

        assert_eq!(gap.key(), "redis/max_memory_policy::config_value");
    }

    #[test]
    fn test_detect_gaps_empty_index() {
        let claims = vec![
            make_claim("code://rust/myapp/redis/max_memory_policy", "config_value"),
            make_claim("code://rust/myapp/kafka/retention_ms", "config_value"),
        ];

        // Empty index means no coverage
        let index = ConceptIndex::build(&[]);
        let gaps = detect_gaps(&claims, &index);

        assert_eq!(gaps.len(), 2);
        assert!(gaps.iter().any(|g| g.topic == "redis/max_memory_policy"));
        assert!(gaps.iter().any(|g| g.topic == "kafka/retention_ms"));
    }

    #[test]
    fn test_detect_gaps_deduplication() {
        let claims = vec![
            make_claim("code://rust/app1/redis/max_memory_policy", "config_value"),
            make_claim("code://rust/app2/redis/max_memory_policy", "config_value"), // Same topic
            make_claim("code://rust/app3/redis/max_memory_policy", "config_value"), // Same topic
        ];

        let index = ConceptIndex::build(&[]);
        let gaps = detect_gaps(&claims, &index);

        // Should deduplicate to just one gap
        assert_eq!(gaps.len(), 1);
        assert_eq!(gaps[0].topic, "redis/max_memory_policy");
    }
}
