//! Gap storage for the Research Agent.
//!
//! Persists detected gaps with tracking metadata to enable research triggering
//! when gaps are seen across multiple projects.

use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use tracing::{debug, info, instrument, warn};

use super::Gap;
use crate::{current_timestamp, AphoriaError};

/// A stored gap record with tracking metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GapRecord {
    /// Unique key for this gap (topic::predicate).
    pub key: String,

    /// The topic extracted from concept paths.
    pub topic: String,

    /// The predicate.
    pub predicate: String,

    /// Number of distinct projects that have reported this gap.
    pub project_count: u32,

    /// Set of project identifiers that reported this gap.
    pub projects: Vec<String>,

    /// Unix timestamp when this gap was first seen.
    pub first_seen: u64,

    /// Unix timestamp when this gap was last seen.
    pub last_seen: u64,

    /// Whether research has been attempted for this gap.
    pub research_attempted: bool,

    /// Whether research was successful.
    pub research_successful: bool,

    /// Unix timestamp when research was last attempted.
    pub research_timestamp: Option<u64>,

    /// Sample concept paths where this gap was detected.
    pub sample_paths: Vec<String>,

    /// Sample descriptions from claims that triggered this gap.
    pub sample_descriptions: Vec<String>,
}

impl GapRecord {
    /// Create a new gap record from a detected gap.
    pub fn new(gap: &Gap, project_id: &str) -> Self {
        let now = current_timestamp();

        Self {
            key: gap.key(),
            topic: gap.topic.clone(),
            predicate: gap.predicate.clone(),
            project_count: 1,
            projects: vec![project_id.to_string()],
            first_seen: now,
            last_seen: now,
            research_attempted: false,
            research_successful: false,
            research_timestamp: None,
            sample_paths: vec![gap.concept_path.clone()],
            sample_descriptions: vec![gap.description.clone()],
        }
    }

    /// Update the record with a new sighting from a project.
    pub fn record_sighting(&mut self, gap: &Gap, project_id: &str) {
        self.last_seen = current_timestamp();

        // Add project if not already tracked
        if !self.projects.contains(&project_id.to_string()) {
            self.projects.push(project_id.to_string());
            self.project_count = self.projects.len() as u32;
        }

        // Add sample path if we have room (max 10 samples)
        if self.sample_paths.len() < 10 && !self.sample_paths.contains(&gap.concept_path) {
            self.sample_paths.push(gap.concept_path.clone());
        }

        // Add sample description if we have room
        if self.sample_descriptions.len() < 5
            && !self.sample_descriptions.contains(&gap.description)
        {
            self.sample_descriptions.push(gap.description.clone());
        }
    }

    /// Mark research as attempted.
    pub fn mark_research_attempted(&mut self, successful: bool) {
        self.research_attempted = true;
        self.research_successful = successful;
        self.research_timestamp = Some(current_timestamp());
    }

    /// Check if this gap is eligible for research.
    ///
    /// A gap is eligible if:
    /// - It has been seen in at least `threshold` projects
    /// - Research hasn't been successfully completed
    /// - Not attempted within the last 24 hours
    pub fn is_eligible_for_research(&self, threshold: u32) -> bool {
        if self.project_count < threshold {
            return false;
        }

        if self.research_successful {
            return false;
        }

        // If research was attempted, check if enough time has passed (24 hours)
        if let Some(ts) = self.research_timestamp {
            let now = current_timestamp();
            let one_day = 24 * 60 * 60;
            if now - ts < one_day {
                return false;
            }
        }

        true
    }
}

/// Persistent store for gap records.
pub struct GapStore {
    /// Path to the gap store file.
    store_path: PathBuf,

    /// In-memory cache of gap records.
    records: HashMap<String, GapRecord>,

    /// Whether the store has been modified since last save.
    dirty: bool,
}

impl GapStore {
    /// Open or create a gap store at the given path.
    #[instrument(skip_all, fields(path = %store_path.display()))]
    pub fn open(store_path: &Path) -> Result<Self, AphoriaError> {
        let store_path = store_path.to_path_buf();

        // Create parent directories if needed
        if let Some(parent) = store_path.parent() {
            fs::create_dir_all(parent)?;
        }

        // Load existing records if file exists
        let records = if store_path.exists() {
            let content = fs::read_to_string(&store_path)?;
            match serde_json::from_str::<HashMap<String, GapRecord>>(&content) {
                Ok(records) => {
                    info!(count = records.len(), "Loaded gap records");
                    records
                }
                Err(e) => {
                    warn!(error = %e, "Failed to parse gap store, starting fresh");
                    HashMap::new()
                }
            }
        } else {
            debug!("Gap store doesn't exist, creating new");
            HashMap::new()
        };

        Ok(Self { store_path, records, dirty: false })
    }

    /// Record gaps detected in a project.
    #[instrument(skip(self, gaps), fields(gap_count = gaps.len()))]
    pub fn record_gaps(&mut self, gaps: &[Gap], project_id: &str) {
        for gap in gaps {
            let key = gap.key();

            if let Some(record) = self.records.get_mut(&key) {
                record.record_sighting(gap, project_id);
            } else {
                self.records.insert(key.clone(), GapRecord::new(gap, project_id));
            }

            self.dirty = true;
        }

        debug!(recorded = gaps.len(), total_gaps = self.records.len(), "Recorded gaps");
    }

    /// Get gaps eligible for research.
    pub fn get_research_candidates(&self, threshold: u32) -> Vec<&GapRecord> {
        self.records.values().filter(|r| r.is_eligible_for_research(threshold)).collect()
    }

    /// Get a gap record by key.
    pub fn get(&self, key: &str) -> Option<&GapRecord> {
        self.records.get(key)
    }

    /// Get a mutable gap record by key.
    pub fn get_mut(&mut self, key: &str) -> Option<&mut GapRecord> {
        self.dirty = true;
        self.records.get_mut(key)
    }

    /// Get all gap records.
    pub fn all_records(&self) -> impl Iterator<Item = &GapRecord> {
        self.records.values()
    }

    /// Get count of gaps.
    pub fn len(&self) -> usize {
        self.records.len()
    }

    /// Check if empty.
    pub fn is_empty(&self) -> bool {
        self.records.is_empty()
    }

    /// Get gaps by minimum project count.
    pub fn gaps_by_project_count(&self, min_count: u32) -> Vec<&GapRecord> {
        self.records.values().filter(|r| r.project_count >= min_count).collect()
    }

    /// Save the store to disk.
    #[instrument(skip(self))]
    pub fn save(&mut self) -> Result<(), AphoriaError> {
        if !self.dirty {
            debug!("Store not dirty, skipping save");
            return Ok(());
        }

        let content = serde_json::to_string_pretty(&self.records)
            .map_err(|e| AphoriaError::Storage(format!("Failed to serialize gap store: {}", e)))?;

        // Write atomically via temp file
        let temp_path = self.store_path.with_extension("tmp");
        fs::write(&temp_path, &content)?;
        fs::rename(&temp_path, &self.store_path)?;

        self.dirty = false;
        info!(gaps = self.records.len(), "Saved gap store");

        Ok(())
    }

    /// Prune old gaps that haven't been seen recently.
    #[instrument(skip(self), fields(max_age_days))]
    pub fn prune_old_gaps(&mut self, max_age_days: u64) {
        let now = current_timestamp();
        let max_age_secs = max_age_days * 24 * 60 * 60;

        let before_count = self.records.len();

        self.records.retain(|_, record| {
            // Keep if seen recently
            if now - record.last_seen < max_age_secs {
                return true;
            }

            // Keep if research was successful
            if record.research_successful {
                return true;
            }

            false
        });

        let pruned = before_count - self.records.len();
        if pruned > 0 {
            self.dirty = true;
            info!(pruned, remaining = self.records.len(), "Pruned old gaps");
        }
    }
}

impl Drop for GapStore {
    fn drop(&mut self) {
        if self.dirty {
            if let Err(e) = self.save() {
                tracing::error!(error = %e, "Failed to save gap store on drop");
            }
        }
    }
}

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

    fn make_gap(topic: &str, predicate: &str) -> Gap {
        Gap {
            concept_path: format!("code://rust/test/{}", topic),
            predicate: predicate.to_string(),
            topic: topic.to_string(),
            source_file: "test.rs".to_string(),
            source_line: 1,
            description: format!("Test gap for {}", topic),
            confidence: 0.9,
        }
    }

    #[test]
    fn test_gap_record_creation() {
        let gap = make_gap("redis/max_memory", "config_value");
        let record = GapRecord::new(&gap, "project1");

        assert_eq!(record.key, "redis/max_memory::config_value");
        assert_eq!(record.project_count, 1);
        assert!(!record.research_attempted);
    }

    #[test]
    fn test_gap_record_sighting() {
        let gap = make_gap("redis/max_memory", "config_value");
        let mut record = GapRecord::new(&gap, "project1");

        // Record from same project - shouldn't increase count
        record.record_sighting(&gap, "project1");
        assert_eq!(record.project_count, 1);

        // Record from new project - should increase count
        record.record_sighting(&gap, "project2");
        assert_eq!(record.project_count, 2);
    }

    #[test]
    fn test_gap_research_eligibility() {
        let gap = make_gap("redis/max_memory", "config_value");
        let mut record = GapRecord::new(&gap, "project1");

        // Not eligible with threshold 3 (only 1 project)
        assert!(!record.is_eligible_for_research(3));

        // Add more projects
        record.record_sighting(&gap, "project2");
        record.record_sighting(&gap, "project3");
        assert_eq!(record.project_count, 3);

        // Now eligible
        assert!(record.is_eligible_for_research(3));

        // Mark as successful - no longer eligible
        record.mark_research_attempted(true);
        assert!(!record.is_eligible_for_research(3));
    }

    #[test]
    fn test_gap_store_persistence() {
        let temp_dir = TempDir::new().unwrap();
        let store_path = temp_dir.path().join("gaps.json");

        // Create and populate store
        {
            let mut store = GapStore::open(&store_path).unwrap();
            let gap = make_gap("redis/max_memory", "config_value");
            store.record_gaps(&[gap], "project1");
            store.save().unwrap();
        }

        // Reopen and verify
        {
            let store = GapStore::open(&store_path).unwrap();
            assert_eq!(store.len(), 1);
            let record = store.get("redis/max_memory::config_value").unwrap();
            assert_eq!(record.project_count, 1);
        }
    }

    #[test]
    fn test_gap_store_research_candidates() {
        let temp_dir = TempDir::new().unwrap();
        let store_path = temp_dir.path().join("gaps.json");

        let mut store = GapStore::open(&store_path).unwrap();

        // Add gap seen in 3 projects
        let gap1 = make_gap("redis/max_memory", "config_value");
        store.record_gaps(std::slice::from_ref(&gap1), "project1");
        store.record_gaps(std::slice::from_ref(&gap1), "project2");
        store.record_gaps(std::slice::from_ref(&gap1), "project3");

        // Add gap seen in only 1 project
        let gap2 = make_gap("kafka/retention", "config_value");
        store.record_gaps(&[gap2], "project1");

        // With threshold 3, only first gap should be candidate
        let candidates = store.get_research_candidates(3);
        assert_eq!(candidates.len(), 1);
        assert_eq!(candidates[0].topic, "redis/max_memory");
    }
}
