//! Specialized storage for high-velocity vote ingestion.
//!
//! The Ballot Box pattern separates votes from assertions to enable
//! thousands of agents to vote simultaneously without lock contention.
//!
//! # Storage Layout
//!
//! | Key Pattern | Value | Purpose |
//! |-------------|-------|---------|
//! | `{subject}\x00V:{assertion_hex}:{vote_hex}` | Serialized Vote | Individual votes |
//! | `{subject}\x00VC:{assertion_hex}` | u64 (LE) | Vote count cache |
//! | `{subject}\x00VW:{assertion_hex}` | f32 (LE) | Aggregate weight cache |
//!
//! # Design Philosophy
//!
//! This module follows the "Deep Module" principle from Philosophy of Software Design:
//! - Simple interface hiding complex vote aggregation
//! - O(1) vote counts via counter cache
//! - Pre-calculated aggregate weights for Materialized Views
//!
//! All operations are append-only and content-addressed.

mod store_impl;

use async_trait::async_trait;
use stemedb_core::types::{Hash, Vote};

use crate::error::Result;

pub use store_impl::GenericVoteStore;

/// Specialized storage trait for high-velocity vote operations.
///
/// This trait provides vote-specific operations on top of a generic KVStore,
/// enabling efficient vote ingestion and aggregation for the Ballot Box pattern.
///
/// All methods require a `subject` parameter to co-locate vote data with the
/// assertion's subject for range sharding.
///
/// # Example
///
/// ```ignore
/// let vote_store = GenericVoteStore::new(kv_store);
/// let vote_hash = vote_store.put_vote(&vote, "Tesla").await?;
/// let votes = vote_store.get_votes_for_assertion(&assertion_hash, "Tesla").await?;
/// ```
#[async_trait]
pub trait VoteStore: Send + Sync {
    /// Store a vote and return its content-addressed hash.
    ///
    /// This operation:
    /// 1. Serializes the vote using rkyv
    /// 2. Computes BLAKE3 hash for content addressing
    /// 3. Stores at `{subject}\x00V:{assertion_hex}:{vote_hex}`
    /// 4. Updates vote count and aggregate weight caches
    ///
    /// # Returns
    /// The BLAKE3 hash of the serialized vote (content address).
    async fn put_vote(&self, vote: &Vote, subject: &str) -> Result<Hash>;

    /// Get a specific vote by its hash.
    ///
    /// # Arguments
    /// * `assertion_hash` - The assertion this vote is for
    /// * `vote_hash` - The content-addressed hash of the vote
    /// * `subject` - The subject the assertion belongs to
    ///
    /// # Returns
    /// The vote if found, None otherwise.
    async fn get_vote(
        &self,
        assertion_hash: &Hash,
        vote_hash: &Hash,
        subject: &str,
    ) -> Result<Option<Vote>>;

    /// Get all votes for a specific assertion.
    ///
    /// Scans all keys with prefix `{subject}\x00V:{assertion_hex}:` and deserializes.
    ///
    /// # Performance
    /// O(n) where n is the number of votes. For high-cardinality assertions,
    /// consider using `get_vote_count` and `get_aggregate_weight` instead.
    ///
    /// # Returns
    /// Vector of votes, empty if no votes exist.
    async fn get_votes_for_assertion(
        &self,
        assertion_hash: &Hash,
        subject: &str,
    ) -> Result<Vec<Vote>>;

    /// Get the number of votes for an assertion.
    ///
    /// Uses cached counter at `{subject}\x00VC:{assertion_hex}` for O(1) performance.
    ///
    /// # Returns
    /// Vote count, 0 if no votes exist.
    async fn get_vote_count(&self, assertion_hash: &Hash, subject: &str) -> Result<u64>;

    /// Get the aggregate weight (sum of all vote weights) for an assertion.
    ///
    /// Uses cached value at `{subject}\x00VW:{assertion_hex}` for O(1) performance.
    /// The weight is the sum of all `vote.weight` values.
    ///
    /// # Returns
    /// Aggregate weight, 0.0 if no votes exist.
    async fn get_aggregate_weight(&self, assertion_hash: &Hash, subject: &str) -> Result<f32>;

    /// Check if any votes exist for an assertion.
    ///
    /// More efficient than `get_vote_count() > 0` as it can short-circuit.
    async fn has_votes(&self, assertion_hash: &Hash, subject: &str) -> Result<bool>;
}

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

    const TEST_SUBJECT: &str = "TestSubject";

    fn create_test_vote(assertion_hash: Hash, weight: f32) -> Vote {
        Vote {
            assertion_hash,
            agent_id: [1u8; 32],
            weight,
            signature: [2u8; 64],
            timestamp: 1000,
            source_url: None,
            observed_context: None,
        }
    }

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

        let assertion_hash = [0u8; 32];
        let vote = create_test_vote(assertion_hash, 0.8);

        // Put vote
        let vote_hash = vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("Failed to put vote");

        // Get vote back
        let retrieved = vote_store
            .get_vote(&assertion_hash, &vote_hash, TEST_SUBJECT)
            .await
            .expect("Failed to get vote");

        assert!(retrieved.is_some());
        let retrieved_vote = retrieved.expect("Vote should exist");
        assert_eq!(retrieved_vote.weight, 0.8);
        assert_eq!(retrieved_vote.assertion_hash, assertion_hash);
    }

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

        let assertion_hash = [1u8; 32];
        let other_assertion = [2u8; 32];

        // Add multiple votes for the same assertion
        let vote1 = create_test_vote(assertion_hash, 0.5);
        let vote2 = Vote {
            agent_id: [3u8; 32], // Different agent
            ..create_test_vote(assertion_hash, 0.7)
        };
        let vote_other = create_test_vote(other_assertion, 0.9);

        vote_store.put_vote(&vote1, TEST_SUBJECT).await.expect("put");
        vote_store.put_vote(&vote2, TEST_SUBJECT).await.expect("put");
        vote_store.put_vote(&vote_other, TEST_SUBJECT).await.expect("put");

        // Get votes for assertion
        let votes =
            vote_store.get_votes_for_assertion(&assertion_hash, TEST_SUBJECT).await.expect("get");

        assert_eq!(votes.len(), 2);

        // Get votes for other assertion
        let other_votes =
            vote_store.get_votes_for_assertion(&other_assertion, TEST_SUBJECT).await.expect("get");

        assert_eq!(other_votes.len(), 1);
    }

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

        let assertion_hash = [3u8; 32];

        // Initially zero
        let count = vote_store.get_vote_count(&assertion_hash, TEST_SUBJECT).await.expect("count");
        assert_eq!(count, 0);

        // Add votes and check count increments
        for i in 0..5 {
            let vote = Vote { agent_id: [i; 32], ..create_test_vote(assertion_hash, 0.5) };
            vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("put");

            let count =
                vote_store.get_vote_count(&assertion_hash, TEST_SUBJECT).await.expect("count");
            assert_eq!(count, (i as u64) + 1);
        }
    }

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

        let assertion_hash = [4u8; 32];

        // Initially zero
        let weight =
            vote_store.get_aggregate_weight(&assertion_hash, TEST_SUBJECT).await.expect("weight");
        assert!((weight - 0.0).abs() < f32::EPSILON);

        // Add votes with known weights
        let weights = [0.3, 0.5, 0.7, 0.2];
        let mut expected_total = 0.0_f32;

        for (i, &w) in weights.iter().enumerate() {
            let vote =
                Vote { agent_id: [i as u8; 32], weight: w, ..create_test_vote(assertion_hash, w) };
            vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("put");
            expected_total += w;

            let actual = vote_store
                .get_aggregate_weight(&assertion_hash, TEST_SUBJECT)
                .await
                .expect("weight");

            // Float comparison with tolerance
            assert!(
                (actual - expected_total).abs() < 0.001,
                "Expected {}, got {}",
                expected_total,
                actual
            );
        }
    }

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

        let assertion_hash = [5u8; 32];

        // No votes initially
        assert!(!vote_store.has_votes(&assertion_hash, TEST_SUBJECT).await.expect("has"));

        // Add a vote
        let vote = create_test_vote(assertion_hash, 0.5);
        vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("put");

        // Now has votes
        assert!(vote_store.has_votes(&assertion_hash, TEST_SUBJECT).await.expect("has"));
    }

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

        let nonexistent = [99u8; 32];
        let votes =
            vote_store.get_votes_for_assertion(&nonexistent, TEST_SUBJECT).await.expect("get");

        assert!(votes.is_empty(), "Should return empty vec, not error");
    }

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

        let assertion_hash = [6u8; 32];
        let vote = create_test_vote(assertion_hash, 0.5);

        // Same vote should produce same hash
        let hash1 = vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("put");
        let hash2 = vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("put");

        assert_eq!(hash1, hash2, "Same vote should produce same hash");

        // Count should still increment (idempotent storage but not idempotent counting)
        // This is by design - duplicate vote detection is a higher-level concern
        let count = vote_store.get_vote_count(&assertion_hash, TEST_SUBJECT).await.expect("count");
        assert_eq!(count, 2);
    }

    #[tokio::test]
    async fn test_high_velocity_simulation() {
        // Simulate many agents voting on the same assertion
        let store = Arc::new(HybridStore::open_temp().expect("Failed to create store"));
        let vote_store = Arc::new(GenericVoteStore::new(store));

        let assertion_hash = [7u8; 32];
        let num_votes = 100;

        // Simulate concurrent voting (sequential for simplicity in test)
        for i in 0..num_votes {
            let vote = Vote {
                assertion_hash,
                agent_id: {
                    let mut id = [0u8; 32];
                    id[0] = (i % 256) as u8;
                    id[1] = (i / 256) as u8;
                    id
                },
                weight: 0.5 + (i as f32 * 0.001),
                signature: [i as u8; 64],
                timestamp: 1000 + i,
                source_url: None,
                observed_context: None,
            };
            vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("put");
        }

        // Verify counts
        let count = vote_store.get_vote_count(&assertion_hash, TEST_SUBJECT).await.expect("count");
        assert_eq!(count, num_votes);

        // Verify we can retrieve all votes
        let votes =
            vote_store.get_votes_for_assertion(&assertion_hash, TEST_SUBJECT).await.expect("get");
        assert_eq!(votes.len(), num_votes as usize);
    }

    /// Test concurrent vote ingestion for race condition safety.
    ///
    /// Spawns multiple tasks voting on the same assertion concurrently.
    /// Without atomic operations, this would lose updates due to
    /// read-modify-write races. With atomic `fetch_and_add_u64` and
    /// `compare_and_swap_f32`, all votes should be counted correctly.
    #[tokio::test]
    async fn test_concurrent_vote_ingestion() {
        use tokio::task::JoinSet;

        let store = Arc::new(HybridStore::open_temp().expect("Failed to create store"));
        let vote_store = Arc::new(GenericVoteStore::new(store));

        let assertion_hash = [8u8; 32];
        let num_concurrent_tasks = 10;
        let votes_per_task = 10;
        let vote_weight = 0.5_f32;

        let mut join_set = JoinSet::new();

        for task_id in 0..num_concurrent_tasks {
            let vs = vote_store.clone();
            join_set.spawn(async move {
                for vote_idx in 0..votes_per_task {
                    let vote = Vote {
                        assertion_hash,
                        agent_id: {
                            let mut id = [0u8; 32];
                            id[0] = task_id as u8;
                            id[1] = vote_idx as u8;
                            id
                        },
                        weight: vote_weight,
                        signature: [task_id as u8; 64],
                        timestamp: 1000 + task_id as u64 * 100 + vote_idx as u64,
                        source_url: None,
                        observed_context: None,
                    };
                    vs.put_vote(&vote, TEST_SUBJECT).await.expect("concurrent put should succeed");
                }
            });
        }

        // Wait for all tasks to complete
        while let Some(result) = join_set.join_next().await {
            result.expect("task should not panic");
        }

        // Verify final vote count is exactly num_concurrent_tasks * votes_per_task
        let expected_count = (num_concurrent_tasks * votes_per_task) as u64;
        let actual_count =
            vote_store.get_vote_count(&assertion_hash, TEST_SUBJECT).await.expect("count");
        assert_eq!(
            actual_count, expected_count,
            "Vote count should be {} (got {}). Race condition detected!",
            expected_count, actual_count
        );

        // Verify aggregate weight is approximately correct
        // (some float imprecision is expected with concurrent additions)
        let expected_weight = (num_concurrent_tasks * votes_per_task) as f32 * vote_weight;
        let actual_weight =
            vote_store.get_aggregate_weight(&assertion_hash, TEST_SUBJECT).await.expect("weight");
        let tolerance = 0.01 * expected_weight; // 1% tolerance for float accumulation
        assert!(
            (actual_weight - expected_weight).abs() < tolerance,
            "Aggregate weight should be ~{} (got {}). Race condition detected!",
            expected_weight,
            actual_weight
        );
    }

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

        let assertion_hash = [10u8; 32];
        let vote = Vote {
            assertion_hash,
            agent_id: [1u8; 32],
            weight: 0.9,
            signature: [2u8; 64],
            timestamp: 1000,
            source_url: Some("https://example.com/article".to_string()),
            observed_context: Some(b"The study found that...".to_vec()),
        };

        // Put vote with provenance
        let vote_hash = vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("Failed to put vote");

        // Get vote back and verify provenance fields
        let retrieved = vote_store
            .get_vote(&assertion_hash, &vote_hash, TEST_SUBJECT)
            .await
            .expect("Failed to get vote");

        assert!(retrieved.is_some());
        let retrieved_vote = retrieved.expect("Vote should exist");
        assert_eq!(retrieved_vote.source_url, Some("https://example.com/article".to_string()));
        assert_eq!(retrieved_vote.observed_context, Some(b"The study found that...".to_vec()));
        assert_eq!(retrieved_vote.weight, 0.9);
    }

    #[tokio::test]
    async fn test_vote_backward_compatibility() {
        // Test that votes without provenance fields (None) work correctly
        let store = Arc::new(HybridStore::open_temp().expect("Failed to create store"));
        let vote_store = GenericVoteStore::new(store);

        let assertion_hash = [11u8; 32];
        let vote = create_test_vote(assertion_hash, 0.7);

        // Verify test helper creates None fields (backward compat)
        assert_eq!(vote.source_url, None);
        assert_eq!(vote.observed_context, None);

        // Put and retrieve
        let vote_hash = vote_store.put_vote(&vote, TEST_SUBJECT).await.expect("Failed to put vote");
        let retrieved = vote_store
            .get_vote(&assertion_hash, &vote_hash, TEST_SUBJECT)
            .await
            .expect("Failed to get vote");

        assert!(retrieved.is_some());
        let retrieved_vote = retrieved.expect("Vote should exist");
        assert_eq!(retrieved_vote.source_url, None);
        assert_eq!(retrieved_vote.observed_context, None);
        assert_eq!(retrieved_vote.weight, 0.7);
    }

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

        let assertion_hash = [12u8; 32];
        let vote = create_test_vote(assertion_hash, 0.5);

        // Store vote under "Tesla"
        vote_store.put_vote(&vote, "Tesla").await.expect("put");

        // Should NOT be visible under "Apple"
        let count = vote_store.get_vote_count(&assertion_hash, "Apple").await.expect("count");
        assert_eq!(count, 0, "Votes should be isolated by subject");

        // Should be visible under "Tesla"
        let count = vote_store.get_vote_count(&assertion_hash, "Tesla").await.expect("count");
        assert_eq!(count, 1);
    }
}
