//! Global predicate index for querying assertions by predicate alone.
//!
//! This index enables queries like "find all assertions with predicate 'acknowledged'"
//! without knowing the subject. Used by Aphoria for policy export to find all
//! acknowledgment assertions.
//!
//! # Storage Layout
//!
//! | Key Pattern | Value | Purpose |
//! |-------------|-------|---------|
//! | `\x00PRED:{predicate}:{hash_hex}` | empty | Presence marker in predicate index |
//!
//! Unlike the subject index which stores Vec<Hash>, the predicate index uses
//! individual keys for each hash. This trades storage space for simpler
//! concurrent append semantics (no read-modify-write).

use crate::error::Result;
use crate::key_codec;
use crate::traits::KVStore;
use async_trait::async_trait;
use stemedb_core::types::Hash;
use tracing::{debug, instrument};

/// Specialized storage trait for global predicate index operations.
///
/// This trait provides predicate-based queries across all subjects,
/// enabling use cases like "find all acknowledged conflicts" for policy export.
///
/// # Example
///
/// ```ignore
/// let predicate_store = GenericPredicateIndexStore::new(kv_store);
///
/// // Index an assertion by predicate
/// predicate_store.add_to_predicate_index("acknowledged", &hash).await?;
///
/// // Find all assertions with this predicate
/// let hashes = predicate_store.get_by_predicate("acknowledged").await?;
/// ```
#[async_trait]
pub trait PredicateIndexStore: Send + Sync {
    /// Add an assertion hash to the global predicate index.
    ///
    /// Creates a key `\x00PRED:{predicate}:{hash_hex}` with empty value.
    /// Idempotent: re-adding the same hash is a no-op.
    ///
    /// # Arguments
    /// * `predicate` - The assertion predicate
    /// * `assertion_hash` - The content-addressed hash of the assertion
    async fn add_to_predicate_index(&self, predicate: &str, assertion_hash: &Hash) -> Result<()>;

    /// Get all assertion hashes for a given predicate.
    ///
    /// Scans the `\x00PRED:{predicate}:` prefix and extracts hashes from keys.
    ///
    /// # Returns
    /// Vector of assertion hashes, empty if no assertions exist for this predicate.
    async fn get_by_predicate(&self, predicate: &str) -> Result<Vec<Hash>>;

    /// Check if any assertions exist for a given predicate.
    async fn has_predicate(&self, predicate: &str) -> Result<bool>;
}

/// PredicateIndexStore implementation backed by a generic KVStore.
///
/// Uses individual keys per (predicate, hash) pair for lock-free appends.
pub struct GenericPredicateIndexStore<S> {
    store: S,
}

impl<S: KVStore> GenericPredicateIndexStore<S> {
    /// Create a new PredicateIndexStore backed by the given KVStore.
    pub fn new(store: S) -> Self {
        Self { store }
    }
}

#[async_trait]
impl<S: KVStore + 'static> PredicateIndexStore for GenericPredicateIndexStore<S> {
    #[instrument(skip(self, assertion_hash), fields(predicate = %predicate, hash = %hex::encode(assertion_hash)))]
    async fn add_to_predicate_index(&self, predicate: &str, assertion_hash: &Hash) -> Result<()> {
        let hash_hex = hex::encode(assertion_hash);
        let key = key_codec::predicate_index_key(predicate, &hash_hex);

        // Store empty value - presence of key is sufficient
        self.store.put(&key, &[]).await?;

        debug!(predicate, hash = %hash_hex, "Added to predicate index");
        Ok(())
    }

    #[instrument(skip(self), fields(predicate = %predicate))]
    async fn get_by_predicate(&self, predicate: &str) -> Result<Vec<Hash>> {
        let prefix = key_codec::predicate_scan_prefix(predicate);
        let entries = self.store.scan_prefix(&prefix).await?;

        let mut hashes = Vec::with_capacity(entries.len());
        for (key, _value) in entries {
            if let Some(hash_hex) = key_codec::extract_predicate_index_hash(&key) {
                if let Ok(hash_bytes) = hex::decode(&hash_hex) {
                    if hash_bytes.len() == 32 {
                        let mut hash: Hash = [0u8; 32];
                        hash.copy_from_slice(&hash_bytes);
                        hashes.push(hash);
                    }
                }
            }
        }

        debug!(predicate, count = hashes.len(), "Retrieved by predicate");
        Ok(hashes)
    }

    #[instrument(skip(self), fields(predicate = %predicate))]
    async fn has_predicate(&self, predicate: &str) -> Result<bool> {
        let prefix = key_codec::predicate_scan_prefix(predicate);
        let entries = self.store.scan_prefix(&prefix).await?;
        Ok(!entries.is_empty())
    }
}

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

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

        let predicate = "acknowledged";
        let hash1 = [1u8; 32];
        let hash2 = [2u8; 32];

        // Add two assertions with same predicate
        predicate_store.add_to_predicate_index(predicate, &hash1).await.expect("add 1");
        predicate_store.add_to_predicate_index(predicate, &hash2).await.expect("add 2");

        // Should retrieve both
        let hashes = predicate_store.get_by_predicate(predicate).await.expect("get");
        assert_eq!(hashes.len(), 2);
        assert!(hashes.contains(&hash1));
        assert!(hashes.contains(&hash2));
    }

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

        let hash1 = [1u8; 32];
        let hash2 = [2u8; 32];

        predicate_store.add_to_predicate_index("acknowledged", &hash1).await.expect("add");
        predicate_store.add_to_predicate_index("enabled", &hash2).await.expect("add");

        // Each predicate should only see its own hashes
        let ack_hashes = predicate_store.get_by_predicate("acknowledged").await.expect("get");
        assert_eq!(ack_hashes.len(), 1);
        assert_eq!(ack_hashes[0], hash1);

        let enabled_hashes = predicate_store.get_by_predicate("enabled").await.expect("get");
        assert_eq!(enabled_hashes.len(), 1);
        assert_eq!(enabled_hashes[0], hash2);
    }

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

        let predicate = "acknowledged";
        let hash = [1u8; 32];

        // Add same hash multiple times
        predicate_store.add_to_predicate_index(predicate, &hash).await.expect("add 1");
        predicate_store.add_to_predicate_index(predicate, &hash).await.expect("add 2");
        predicate_store.add_to_predicate_index(predicate, &hash).await.expect("add 3");

        // Should only appear once
        let hashes = predicate_store.get_by_predicate(predicate).await.expect("get");
        assert_eq!(hashes.len(), 1);
        assert_eq!(hashes[0], hash);
    }

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

        let hashes = predicate_store.get_by_predicate("nonexistent").await.expect("get");
        assert!(hashes.is_empty(), "Should return empty vec, not error");
    }

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

        let predicate = "acknowledged";
        let hash = [1u8; 32];

        // Initially false
        assert!(!predicate_store.has_predicate(predicate).await.expect("has"));

        // Add assertion
        predicate_store.add_to_predicate_index(predicate, &hash).await.expect("add");

        // Now true
        assert!(predicate_store.has_predicate(predicate).await.expect("has"));
    }
}
