//! Pattern aggregate storage for community corpus contributions.
//!
//! This module provides storage for aggregated scan patterns from the Aphoria
//! community corpus. When projects opt-in to community sharing, their anonymized
//! observations are aggregated here.
//!
//! # Storage Layout
//!
//! | Key Pattern | Value | Purpose |
//! |-------------|-------|---------|
//! | `{subject}\x00PA:{predicate}:{value_hash}` | Serialized PatternAggregate | Pattern metadata |
//! | `{subject}\x00PC:{predicate}:{value_hash}` | u64 (LE) | Distinct project count |
//! | `{subject}\x00PO:{predicate}:{value_hash}` | u64 (LE) | Total observation count |
//! | `{subject}\x00PS:{predicate}:{value_hash}:{project_hash}` | u8 (1) | Project membership |
//!
//! # Design Philosophy
//!
//! - **Project Deduplication:** Each project can only increment the project_count once
//! - **Atomic Counters:** Uses fetch_and_add for race-free counter updates
//! - **Privacy-Preserving:** Only stores hashed values, never raw project names

mod store_impl;

use async_trait::async_trait;
use serde::{Deserialize, Serialize};

use crate::error::Result;

pub use store_impl::GenericPatternAggregateStore;

/// A pattern aggregate from the community corpus.
///
/// This represents community consensus on a particular code pattern.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternAggregate {
    /// The anonymized subject path (with wildcarded project segment).
    pub subject: String,

    /// The predicate (e.g., "enabled", "min_version").
    pub predicate: String,

    /// Hash of the value (for storage lookup).
    pub value_hash: String,

    /// String representation of the value for display.
    pub value_display: String,

    /// Number of distinct projects reporting this pattern.
    pub project_count: u64,

    /// Total number of observations.
    pub observation_count: u64,

    /// Unix timestamp of first observation.
    pub first_seen: u64,

    /// Unix timestamp of most recent observation.
    pub last_seen: u64,
}

/// Specialized storage trait for pattern aggregate operations.
///
/// This trait provides pattern aggregation operations for the community corpus,
/// enabling Aphoria to track which patterns are seen across many projects.
#[async_trait]
pub trait PatternAggregateStore: Send + Sync {
    /// Record an observation from a project.
    ///
    /// This operation:
    /// 1. Checks if this project has already reported this pattern
    /// 2. If not, increments the project count
    /// 3. Always increments the observation count
    /// 4. Updates first_seen/last_seen timestamps
    ///
    /// # Arguments
    /// * `subject` - The anonymized subject path
    /// * `predicate` - The predicate
    /// * `value_hash` - Hash of the value (hex string)
    /// * `value_display` - Display string for the value
    /// * `project_hash` - Hash of the project (hex string, for deduplication)
    /// * `timestamp` - Unix timestamp of the observation
    ///
    /// # Returns
    /// The updated aggregate.
    async fn record_observation(
        &self,
        subject: &str,
        predicate: &str,
        value_hash: &str,
        value_display: &str,
        project_hash: &str,
        timestamp: u64,
    ) -> Result<PatternAggregate>;

    /// Get a pattern aggregate by (subject, predicate, value_hash).
    async fn get_aggregate(
        &self,
        subject: &str,
        predicate: &str,
        value_hash: &str,
    ) -> Result<Option<PatternAggregate>>;

    /// Get patterns with at least `min_projects` reporting.
    ///
    /// Used to retrieve high-confidence patterns that can be used
    /// to adjust default thresholds.
    async fn get_popular_patterns(
        &self,
        min_projects: u64,
        limit: usize,
    ) -> Result<Vec<PatternAggregate>>;

    /// Get all patterns for a subject prefix.
    ///
    /// Used for filtering patterns by domain (e.g., "code://rust/*/tls/").
    async fn get_patterns_for_subject_prefix(
        &self,
        subject_prefix: &str,
        min_projects: u64,
        limit: usize,
    ) -> Result<Vec<PatternAggregate>>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::HybridStore;
    use std::sync::Arc;

    #[tokio::test]
    async fn test_record_observation() {
        let store = Arc::new(HybridStore::open_temp().expect("Failed to create store"));
        let pattern_store = GenericPatternAggregateStore::new(store);

        let agg = pattern_store
            .record_observation(
                "code://rust/*/tls/cert_verification",
                "enabled",
                "abc123",
                "false",
                "project_hash_1",
                1706832000,
            )
            .await
            .expect("record");

        assert_eq!(agg.subject, "code://rust/*/tls/cert_verification");
        assert_eq!(agg.predicate, "enabled");
        assert_eq!(agg.project_count, 1);
        assert_eq!(agg.observation_count, 1);
    }

    #[tokio::test]
    async fn test_project_deduplication() {
        let store = Arc::new(HybridStore::open_temp().expect("Failed to create store"));
        let pattern_store = GenericPatternAggregateStore::new(store);

        // Same project reports twice
        pattern_store
            .record_observation(
                "code://rust/*/tls/cert",
                "enabled",
                "abc123",
                "false",
                "project_1",
                1000,
            )
            .await
            .expect("record");

        let agg = pattern_store
            .record_observation(
                "code://rust/*/tls/cert",
                "enabled",
                "abc123",
                "false",
                "project_1", // Same project
                2000,
            )
            .await
            .expect("record");

        // Project count should still be 1, but observation count should be 2
        assert_eq!(agg.project_count, 1);
        assert_eq!(agg.observation_count, 2);
    }

    #[tokio::test]
    async fn test_multiple_projects() {
        let store = Arc::new(HybridStore::open_temp().expect("Failed to create store"));
        let pattern_store = GenericPatternAggregateStore::new(store);

        // Different projects report
        pattern_store
            .record_observation(
                "code://rust/*/jwt/audience",
                "required",
                "def456",
                "true",
                "project_1",
                1000,
            )
            .await
            .expect("record");

        let agg = pattern_store
            .record_observation(
                "code://rust/*/jwt/audience",
                "required",
                "def456",
                "true",
                "project_2", // Different project
                2000,
            )
            .await
            .expect("record");

        assert_eq!(agg.project_count, 2);
        assert_eq!(agg.observation_count, 2);
    }

    #[tokio::test]
    async fn test_get_popular_patterns() {
        let store = Arc::new(HybridStore::open_temp().expect("Failed to create store"));
        let pattern_store = GenericPatternAggregateStore::new(store);

        // Create a pattern with 3 projects
        for i in 0..3 {
            pattern_store
                .record_observation(
                    "code://rust/*/tls/cert",
                    "enabled",
                    "abc123",
                    "false",
                    &format!("project_{}", i),
                    1000 + i as u64,
                )
                .await
                .expect("record");
        }

        // Create a pattern with only 1 project
        pattern_store
            .record_observation(
                "code://rust/*/other/setting",
                "value",
                "xyz789",
                "42",
                "project_0",
                1000,
            )
            .await
            .expect("record");

        // Should only return the pattern with 3 projects
        let patterns = pattern_store.get_popular_patterns(3, 10).await.expect("get");
        assert_eq!(patterns.len(), 1);
        assert_eq!(patterns[0].subject, "code://rust/*/tls/cert");
        assert_eq!(patterns[0].project_count, 3);
    }
}
