//! Real StemeDB storage for pattern aggregates.
//!
//! This module provides the real implementation of `PatternAggregateStore` that
//! queries StemeDB for community patterns. Pattern aggregates are stored as
//! assertions with special metadata encoding aggregation statistics.
//!
//! # Storage Schema
//!
//! Pattern aggregates are stored as assertions with:
//! - **Subject**: `community://pattern/{anon_hash}` (content-addressed deduplication)
//! - **Predicate**: `"pattern_aggregate"`
//! - **Object**: The aggregated value (Text/Number/Boolean from CommunityObjectValue)
//! - **Source Metadata**: JSON encoding of `{ "subject": "...", "predicate": "...", "project_count": N, "observation_count": M, "first_seen": T1, "last_seen": T2 }`
//!
//! This design enables:
//! 1. Fast lookup by predicate index ("pattern_aggregate")
//! 2. Deduplication via content-addressed subject
//! 3. Rich metadata for promotion decisions
//! 4. Natural fit with existing Episteme storage

use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use stemedb_core::types::{Assertion, ObjectValue};
use stemedb_storage::{GenericPredicateIndexStore, HybridStore, KVStore, PredicateIndexStore};
use tracing::{debug, instrument, warn};

use super::types::{CommunityObjectValue, PatternAggregate};
use crate::corpus::PatternAggregateStore;
use crate::AphoriaError;

/// Metadata stored in assertion.source_metadata for pattern aggregates.
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PatternAggregateMetadata {
    /// Original wildcarded subject (e.g., "code://rust/*/tls/cert")
    subject: String,
    /// Original predicate
    predicate: String,
    /// Number of distinct projects reporting this pattern
    project_count: u64,
    /// Total number of observations
    observation_count: u64,
    /// Unix timestamp of first observation
    first_seen: u64,
    /// Unix timestamp of most recent observation
    last_seen: u64,
}

/// Real StemeDB storage for pattern aggregates.
///
/// Queries assertions with predicate "pattern_aggregate" and decodes
/// metadata to reconstruct PatternAggregate instances.
pub struct StemeDBPatternStore {
    /// KV store for loading assertions by hash
    kv_store: Arc<HybridStore>,
    /// Predicate index store for querying by "pattern_aggregate"
    predicate_index: Arc<GenericPredicateIndexStore<Arc<HybridStore>>>,
}

impl StemeDBPatternStore {
    /// Create a new pattern store backed by StemeDB.
    pub fn new(
        kv_store: Arc<HybridStore>,
        predicate_index: Arc<GenericPredicateIndexStore<Arc<HybridStore>>>,
    ) -> Self {
        Self { kv_store, predicate_index }
    }

    /// Load assertion by hash from KV store.
    async fn load_assertion(&self, hash: &[u8; 32]) -> Result<Option<Assertion>, AphoriaError> {
        let bytes = self.kv_store.get(hash).await.map_err(|e| {
            AphoriaError::Storage(format!("Failed to load assertion {}: {}", hex::encode(hash), e))
        })?;

        let Some(bytes) = bytes else {
            return Ok(None);
        };

        let assertion = stemedb_core::serde::deserialize::<Assertion>(&bytes).map_err(|e| {
            AphoriaError::Storage(format!(
                "Failed to deserialize assertion {}: {}",
                hex::encode(hash),
                e
            ))
        })?;

        Ok(Some(assertion))
    }

    /// Decode PatternAggregate from assertion.
    fn decode_pattern(&self, assertion: &Assertion) -> Result<PatternAggregate, AphoriaError> {
        // Decode metadata from source_metadata field
        let metadata_bytes = assertion.source_metadata.as_ref().ok_or_else(|| {
            AphoriaError::Storage(format!(
                "Pattern aggregate assertion {} missing source_metadata",
                assertion.subject
            ))
        })?;

        let metadata: PatternAggregateMetadata =
            serde_json::from_slice(metadata_bytes).map_err(|e| {
                AphoriaError::Storage(format!("Failed to decode pattern aggregate metadata: {}", e))
            })?;

        // Convert ObjectValue to CommunityObjectValue
        let value = match &assertion.object {
            ObjectValue::Text(s) => CommunityObjectValue::Text(s.clone()),
            ObjectValue::Number(n) => CommunityObjectValue::Number(*n),
            ObjectValue::Boolean(b) => CommunityObjectValue::Boolean(*b),
            ObjectValue::Reference(r) => {
                // References converted to hex strings
                CommunityObjectValue::Text(hex::encode(r))
            }
        };

        Ok(PatternAggregate {
            subject: metadata.subject,
            predicate: metadata.predicate,
            value,
            project_count: metadata.project_count,
            observation_count: metadata.observation_count,
            first_seen: metadata.first_seen,
            last_seen: metadata.last_seen,
        })
    }

    /// Get pattern by exact (subject, predicate, value) match.
    ///
    /// Used for aggregation to check if a pattern already exists before
    /// creating a new one or updating existing counts.
    pub async fn get_pattern_by_spv(
        &self,
        subject: &str,
        predicate: &str,
        value: &CommunityObjectValue,
    ) -> Result<Option<PatternAggregate>, AphoriaError> {
        // Compute content-addressed subject using the same logic as add_pattern
        let mut hasher = blake3::Hasher::new();
        hasher.update(subject.as_bytes());
        hasher.update(b":");
        hasher.update(predicate.as_bytes());
        hasher.update(b":");
        // Hash the value based on its type
        match value {
            CommunityObjectValue::Text(s) => {
                hasher.update(s.as_bytes());
            }
            CommunityObjectValue::Number(n) => {
                hasher.update(&n.to_le_bytes());
            }
            CommunityObjectValue::Boolean(b) => {
                hasher.update(&[if *b { 1 } else { 0 }]);
            }
        }
        let anon_hash = hasher.finalize();
        let assertion_subject = format!("community://pattern/{}", hex::encode(anon_hash.as_bytes()));

        // Query all pattern_aggregate assertions to find matching subject
        let hashes = self.predicate_index.get_by_predicate("pattern_aggregate").await.map_err(|e| {
            AphoriaError::Storage(format!("Failed to query pattern_aggregate predicate index: {}", e))
        })?;

        for hash in &hashes {
            if let Ok(Some(assertion)) = self.load_assertion(hash).await {
                if assertion.subject == assertion_subject {
                    return self.decode_pattern(&assertion).map(Some);
                }
            }
        }

        Ok(None)
    }

    /// Check if pattern aggregate includes a specific project.
    ///
    /// For MVP, we always return false which causes project_count to
    /// increment on every scan. Future implementation should maintain
    /// a separate index of project_hash → pattern mappings for accurate
    /// deduplication.
    pub async fn has_project(
        &self,
        _pattern: &PatternAggregate,
        _project_hash: &str,
    ) -> Result<bool, AphoriaError> {
        // TODO: Implement proper project tracking
        // For now, accept over-counting as safe default
        Ok(false)
    }

    /// Update existing pattern aggregate.
    ///
    /// Since patterns use content-addressed subjects, this is effectively
    /// the same as add_pattern - the new version overwrites the old.
    pub async fn update_pattern(
        &self,
        pattern: &PatternAggregate,
    ) -> Result<(), AphoriaError> {
        // Reuse add_pattern logic - content-addressed subject means update = overwrite
        let aggregator = PatternAggregator::new(self.kv_store.clone(), self.predicate_index.clone());
        aggregator.add_pattern(pattern).await?;
        Ok(())
    }
}

impl PatternAggregateStore for StemeDBPatternStore {
    #[instrument(skip(self), fields(min_projects, limit))]
    fn get_popular_patterns(
        &self,
        min_projects: u64,
        limit: usize,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<PatternAggregate>, AphoriaError>> + Send + '_>>
    {
        Box::pin(async move {
            debug!(min_projects, limit, "Querying popular patterns from StemeDB");

            // Query all pattern_aggregate assertions from predicate index
            let hashes =
                self.predicate_index.get_by_predicate("pattern_aggregate").await.map_err(|e| {
                    AphoriaError::Storage(format!(
                        "Failed to query pattern_aggregate predicate index: {}",
                        e
                    ))
                })?;

            debug!(total_patterns = hashes.len(), "Found pattern aggregate assertions");

            // Load and decode assertions
            let mut patterns = Vec::new();
            for hash in &hashes {
                match self.load_assertion(hash).await {
                    Ok(Some(assertion)) => match self.decode_pattern(&assertion) {
                        Ok(pattern) => {
                            // Filter by min_projects
                            if pattern.project_count >= min_projects {
                                patterns.push(pattern);
                            }
                        }
                        Err(e) => {
                            warn!(hash = %hex::encode(hash), error = %e, "Failed to decode pattern");
                        }
                    },
                    Ok(None) => {
                        warn!(hash = %hex::encode(hash), "Pattern assertion not found in KV store");
                    }
                    Err(e) => {
                        warn!(hash = %hex::encode(hash), error = %e, "Failed to load pattern");
                    }
                }
            }

            // Sort by project_count descending (most popular first)
            patterns.sort_by(|a, b| b.project_count.cmp(&a.project_count));

            // Apply limit
            patterns.truncate(limit);

            debug!(matched_patterns = patterns.len(), "Returning popular patterns");
            Ok(patterns)
        })
    }

    #[instrument(skip(self))]
    fn get_total_projects(
        &self,
    ) -> Pin<Box<dyn Future<Output = Result<u64, AphoriaError>> + Send + '_>> {
        Box::pin(async move {
            debug!("Querying total unique projects");

            // Query all pattern_aggregate assertions
            let hashes =
                self.predicate_index.get_by_predicate("pattern_aggregate").await.map_err(|e| {
                    AphoriaError::Storage(format!(
                        "Failed to query pattern_aggregate predicate index: {}",
                        e
                    ))
                })?;

            // Track unique project IDs by decoding patterns and taking max project_count
            // Note: This is a heuristic - the real count would require tracking project hashes
            // For MVP, we use the maximum project_count seen across all patterns as a proxy
            let mut max_projects = 0u64;

            for hash in &hashes {
                if let Ok(Some(assertion)) = self.load_assertion(hash).await {
                    if let Ok(pattern) = self.decode_pattern(&assertion) {
                        max_projects = max_projects.max(pattern.project_count);
                    }
                }
            }

            debug!(total_projects = max_projects, "Estimated total projects");
            Ok(max_projects)
        })
    }
}

/// Service for aggregating observations into pattern aggregates.
///
/// This handles the "write path" - taking raw observations from scans
/// and updating pattern aggregate assertions in StemeDB.
pub struct PatternAggregator {
    kv_store: Arc<HybridStore>,
    predicate_index: Arc<GenericPredicateIndexStore<Arc<HybridStore>>>,
}

impl PatternAggregator {
    /// Create a new pattern aggregator.
    pub fn new(
        kv_store: Arc<HybridStore>,
        predicate_index: Arc<GenericPredicateIndexStore<Arc<HybridStore>>>,
    ) -> Self {
        Self { kv_store, predicate_index }
    }

    /// Add a single pattern aggregate to storage.
    ///
    /// Creates an assertion with predicate "pattern_aggregate" and stores it in StemeDB.
    /// Uses content-addressed subject for deduplication.
    ///
    /// # Arguments
    ///
    /// * `pattern` - The pattern aggregate to store
    ///
    /// # Returns
    ///
    /// The hash of the created assertion.
    #[instrument(skip(self), fields(subject = %pattern.subject, predicate = %pattern.predicate))]
    pub async fn add_pattern(&self, pattern: &PatternAggregate) -> Result<[u8; 32], AphoriaError> {
        debug!("Adding pattern aggregate to storage");

        // Compute content-addressed subject for deduplication using blake3
        let mut hasher = blake3::Hasher::new();
        hasher.update(pattern.subject.as_bytes());
        hasher.update(b":");
        hasher.update(pattern.predicate.as_bytes());
        hasher.update(b":");
        // Hash the value based on its type
        match &pattern.value {
            CommunityObjectValue::Text(s) => {
                hasher.update(s.as_bytes());
            }
            CommunityObjectValue::Number(n) => {
                hasher.update(&n.to_le_bytes());
            }
            CommunityObjectValue::Boolean(b) => {
                hasher.update(&[if *b { 1 } else { 0 }]);
            }
        }
        let anon_hash = hasher.finalize();
        let subject = format!("community://pattern/{}", hex::encode(anon_hash.as_bytes()));

        // Encode metadata as JSON
        let metadata = PatternAggregateMetadata {
            subject: pattern.subject.clone(),
            predicate: pattern.predicate.clone(),
            project_count: pattern.project_count,
            observation_count: pattern.observation_count,
            first_seen: pattern.first_seen,
            last_seen: pattern.last_seen,
        };

        let metadata_bytes = serde_json::to_vec(&metadata).map_err(|e| {
            AphoriaError::Storage(format!("Failed to encode pattern metadata: {}", e))
        })?;

        // Convert value to ObjectValue
        let object_value: ObjectValue = pattern.value.clone().into();

        // Compute source hash
        let mut source_hasher = blake3::Hasher::new();
        source_hasher.update(subject.as_bytes());
        source_hasher.update(b"pattern_aggregate");
        let source_hash = *source_hasher.finalize().as_bytes();

        // Create assertion (unsigned bootstrap patterns)
        let assertion = stemedb_core::types::Assertion {
            subject,
            predicate: "pattern_aggregate".to_string(),
            object: object_value,
            parent_hash: None,
            source_hash,
            source_class: stemedb_core::types::SourceClass::Observational,
            visual_hash: None,
            epoch: None,
            source_metadata: Some(metadata_bytes),
            lifecycle: stemedb_core::types::LifecycleStage::Approved,
            signatures: vec![], // Bootstrap patterns are unsigned (no signing key available)
            confidence: 1.0,    // Pattern aggregates are high confidence
            timestamp: pattern.last_seen,
            hlc_timestamp: stemedb_core::types::HlcTimestamp::default(),
            vector: None,
        };

        // Serialize assertion
        let assertion_bytes = stemedb_core::serde::serialize(&assertion).map_err(|e| {
            AphoriaError::Storage(format!("Failed to serialize pattern assertion: {}", e))
        })?;

        // Compute assertion hash using blake3
        let assertion_hash = {
            let mut h = blake3::Hasher::new();
            h.update(&assertion_bytes);
            *h.finalize().as_bytes()
        };

        // Store in KV store
        self.kv_store.put(&assertion_hash, &assertion_bytes).await.map_err(|e| {
            AphoriaError::Storage(format!(
                "Failed to store pattern assertion {}: {}",
                hex::encode(assertion_hash),
                e
            ))
        })?;

        // Add to predicate index
        self.predicate_index
            .add_to_predicate_index(&assertion.predicate, &assertion_hash)
            .await
            .map_err(|e| {
                AphoriaError::Storage(format!(
                    "Failed to index pattern assertion {}: {}",
                    hex::encode(assertion_hash),
                    e
                ))
            })?;

        debug!(hash = %hex::encode(assertion_hash), "Stored pattern aggregate");
        Ok(assertion_hash)
    }

    /// Batch add multiple pattern aggregates.
    ///
    /// More efficient than calling add_pattern() repeatedly.
    #[instrument(skip(self, patterns), fields(pattern_count = patterns.len()))]
    pub async fn add_patterns(
        &self,
        patterns: &[PatternAggregate],
    ) -> Result<Vec<[u8; 32]>, AphoriaError> {
        debug!("Batch adding pattern aggregates");

        let mut hashes = Vec::with_capacity(patterns.len());
        for pattern in patterns {
            let hash = self.add_pattern(pattern).await?;
            hashes.push(hash);
        }

        debug!(count = hashes.len(), "Batch add complete");
        Ok(hashes)
    }

    /// Aggregate observations into pattern aggregates.
    ///
    /// Groups observations by (subject, predicate, value) and either creates
    /// new pattern aggregates or updates existing ones.
    ///
    /// # Arguments
    ///
    /// * `observations` - Raw observations from scan
    /// * `project_id` - Unique identifier for this project (for deduplication)
    ///
    /// # Returns
    ///
    /// Number of pattern aggregates created or updated.
    #[instrument(skip(self, observations), fields(observation_count = observations.len()))]
    pub async fn aggregate_observations(
        &self,
        observations: &[crate::types::Observation],
        project_id: &str,
    ) -> Result<usize, AphoriaError> {
        debug!(project_id, "Aggregating observations into patterns");

        // Group observations by (subject, predicate, value)
        let mut groups: HashMap<
            (String, String, CommunityObjectValue),
            Vec<&crate::types::Observation>,
        > = HashMap::new();

        for obs in observations {
            // Wildcard the project path for community sharing
            let wildcarded_subject = super::anonymizer::wildcard_project_path(&obs.concept_path);

            // Convert value to CommunityObjectValue
            let value = CommunityObjectValue::from(&obs.value);

            let key = (wildcarded_subject, obs.predicate.clone(), value);
            groups.entry(key).or_default().push(obs);
        }

        debug!(unique_patterns = groups.len(), "Grouped observations into patterns");

        // Create pattern aggregates for each group
        let timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_err(|e| AphoriaError::Storage(format!("Failed to get timestamp: {}", e)))?
            .as_secs();

        let mut count = 0;
        for ((subject, predicate, value), obs_group) in groups {
            let pattern = PatternAggregate {
                subject,
                predicate,
                value,
                project_count: 1, // Single project for now
                observation_count: obs_group.len() as u64,
                first_seen: timestamp,
                last_seen: timestamp,
            };

            self.add_pattern(&pattern).await?;
            count += 1;
        }

        debug!(count, "Created pattern aggregates from observations");
        Ok(count)
    }
}

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

    #[tokio::test]
    async fn test_empty_store() {
        let temp_dir = TempDir::new().expect("tempdir");
        let store_path = temp_dir.path().join("store");
        std::fs::create_dir_all(&store_path).expect("create store dir");

        let hybrid_store = Arc::new(HybridStore::open(&store_path).expect("open hybrid store"));
        let predicate_index = Arc::new(GenericPredicateIndexStore::new(hybrid_store.clone()));

        let store = StemeDBPatternStore::new(hybrid_store, predicate_index);

        // Empty store should return no patterns
        let patterns = store.get_popular_patterns(1, 100).await.expect("get_popular");
        assert_eq!(patterns.len(), 0);

        let total = store.get_total_projects().await.expect("get_total");
        assert_eq!(total, 0);
    }
}
