//! Economic throttling via Token Bucket quotas (The Meter).
//!
//! This module implements per-agent per-hour quota tracking to prevent
//! runaway agents from exhausting system resources.
//!
//! # Storage Layout
//!
//! | Key Pattern | Value | Purpose |
//! |-------------|-------|---------|
//! | `QT:{agent_id}:{hour}` | Serialized QuotaRecord | Per-agent per-hour usage |
//! | `QL:{agent_id}` | u64 (LE) | Per-agent quota limit override |
//!
//! # Cost Model
//!
//! | Operation | Base Cost | Notes |
//! |-----------|-----------|-------|
//! | Assert | 10 tokens | Writing truth is expensive |
//! | Vote | 1 token | Voting is cheap |
//! | Query | 5 tokens | + 1 per Lens applied |
//! | Payload | +1/KB | Prevents spamming massive blobs |
//!
//! # Design Philosophy
//!
//! Token Bucket algorithm allows bursts while enforcing long-term limits.
//! Hourly windows force cool-down periods if an agent goes haywire.

mod model;
mod store_impl;

pub use model::*;
pub use store_impl::*;

use crate::error::Result;
use async_trait::async_trait;

/// Key prefix for quota records.
#[allow(dead_code)] // Documented for reference; actual key construction uses format!()
const QUOTA_PREFIX: &[u8] = b"QT:";

/// Default quota limit per agent per hour (10,000 tokens).
pub const DEFAULT_QUOTA_LIMIT: u64 = 10_000;

/// Specialized storage trait for quota tracking operations.
///
/// This trait provides economic throttling operations on top of a generic KVStore,
/// enabling per-agent per-hour quota enforcement.
#[async_trait]
pub trait QuotaStore: Send + Sync {
    /// Check if an operation is allowed and record the cost if so.
    ///
    /// This is the main entry point for quota enforcement. It:
    /// 1. Calculates the cost for the operation
    /// 2. Checks if the agent has sufficient quota
    /// 3. Records the cost if allowed
    ///
    /// # Arguments
    /// * `agent_id` - The agent's Ed25519 public key
    /// * `operation` - Type of operation being performed
    /// * `payload_bytes` - Size of the request payload
    /// * `timestamp` - Current Unix timestamp
    ///
    /// # Returns
    /// `QuotaCheckResult` indicating whether the operation is allowed
    async fn check_and_record(
        &self,
        agent_id: &[u8; 32],
        operation: OperationType,
        payload_bytes: usize,
        timestamp: u64,
    ) -> Result<QuotaCheckResult>;

    /// Get the current quota status for an agent without recording any cost.
    ///
    /// # Arguments
    /// * `agent_id` - The agent's Ed25519 public key
    /// * `timestamp` - Current Unix timestamp (to determine window)
    ///
    /// # Returns
    /// Current quota status including remaining tokens
    async fn get_quota_status(
        &self,
        agent_id: &[u8; 32],
        timestamp: u64,
    ) -> Result<QuotaCheckResult>;

    /// Set a custom quota limit for an agent.
    ///
    /// # Arguments
    /// * `agent_id` - The agent's Ed25519 public key
    /// * `limit` - New quota limit (tokens per hour)
    async fn set_quota_limit(&self, agent_id: &[u8; 32], limit: u64) -> Result<()>;

    /// Get the quota limit for an agent.
    ///
    /// Returns the custom limit if set, otherwise the default.
    ///
    /// # Arguments
    /// * `agent_id` - The agent's Ed25519 public key
    async fn get_quota_limit(&self, agent_id: &[u8; 32]) -> Result<u64>;

    /// Get the cost configuration.
    fn cost_config(&self) -> &CostConfig;
}

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

    fn test_agent() -> [u8; 32] {
        [1u8; 32]
    }

    #[test]
    fn test_cost_config_assert() {
        let config = CostConfig::default();

        // Small payload
        assert_eq!(config.cost_for_assert(100), 11); // 10 base + 1 for <1KB

        // Exactly 1KB
        assert_eq!(config.cost_for_assert(1024), 11); // 10 base + 1 for 1KB

        // 2KB
        assert_eq!(config.cost_for_assert(2048), 12); // 10 base + 2 for 2KB

        // 10KB
        assert_eq!(config.cost_for_assert(10240), 20); // 10 base + 10 for 10KB
    }

    #[test]
    fn test_cost_config_vote() {
        let config = CostConfig::default();

        // Small vote (typical)
        assert_eq!(config.cost_for_vote(100), 2); // 1 base + 1 for <1KB
    }

    #[test]
    fn test_cost_config_query() {
        let config = CostConfig::default();

        // Simple query, no lens
        assert_eq!(config.cost_for_query(0, 50), 6); // 5 base + 0 lens + 1 payload

        // Query with 1 lens
        assert_eq!(config.cost_for_query(1, 50), 7); // 5 base + 1 lens + 1 payload

        // Query with 3 lenses
        assert_eq!(config.cost_for_query(3, 50), 9); // 5 base + 3 lens + 1 payload
    }

    #[test]
    fn test_hour_window() {
        // 2024-01-15 09:30:00 UTC = 1705315800
        let timestamp = 1705315800;
        let window = GenericQuotaStore::<HybridStore>::hour_window(timestamp);

        // 1705315800 / 3600 = 473698.833... -> 473698 * 3600 = 1705312800
        assert_eq!(window, 1705312800);
        assert_eq!(window % 3600, 0);
    }

    #[test]
    fn test_reset_timestamp() {
        let timestamp = 1705315800; // :30 past the hour
        let reset = GenericQuotaStore::<HybridStore>::reset_timestamp(timestamp);

        // Window is 1705312800, next hour is +3600 = 1705316400
        assert_eq!(reset, 1705316400);
    }

    #[test]
    fn test_quota_record_basic() {
        let agent_id = test_agent();
        let mut record = QuotaRecord::new(agent_id, 1705314000);

        assert_eq!(record.tokens_used, 0);
        assert_eq!(record.operation_count, 0);
        assert_eq!(record.remaining(10000), 10000);
        assert!(!record.would_exceed(100, 10000));

        record.record(100, 1705314100);

        assert_eq!(record.tokens_used, 100);
        assert_eq!(record.operation_count, 1);
        assert_eq!(record.remaining(10000), 9900);
    }

    #[test]
    fn test_quota_record_would_exceed() {
        let agent_id = test_agent();
        let mut record = QuotaRecord::new(agent_id, 1705314000);

        record.tokens_used = 9900;

        assert!(!record.would_exceed(100, 10000)); // Exactly at limit
        assert!(record.would_exceed(101, 10000)); // Over limit
    }

    #[tokio::test]
    async fn test_check_and_record_basic() {
        let store = Arc::new(HybridStore::open_temp().expect("store"));
        let quota_store = GenericQuotaStore::new(store);

        let agent_id = test_agent();
        let timestamp = 1705315800;

        // First operation should succeed
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Assert, 100, timestamp)
            .await
            .expect("check");

        assert!(result.allowed);
        assert_eq!(result.cost, 11); // 10 base + 1 KB
        assert_eq!(result.remaining, 10000 - 11);
        assert_eq!(result.limit, 10000);
    }

    #[tokio::test]
    async fn test_quota_enforcement() {
        let store = Arc::new(HybridStore::open_temp().expect("store"));
        let quota_store = GenericQuotaStore::with_config(
            store,
            CostConfig::default(),
            100, // Low limit for testing
        );

        let agent_id = test_agent();
        let timestamp = 1705315800;

        // First operation (cost 11) should succeed
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Assert, 100, timestamp)
            .await
            .expect("check");
        assert!(result.allowed);

        // Consume most of the quota
        for _ in 0..8 {
            let result = quota_store
                .check_and_record(&agent_id, OperationType::Assert, 100, timestamp)
                .await
                .expect("check");
            assert!(result.allowed);
        }

        // Next operation should fail (9 * 11 = 99, need 11 more, only 1 left)
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Assert, 100, timestamp)
            .await
            .expect("check");
        assert!(!result.allowed);
        assert_eq!(result.remaining, 1); // 100 - 99 = 1
    }

    #[tokio::test]
    async fn test_quota_resets_each_hour() {
        let store = Arc::new(HybridStore::open_temp().expect("store"));
        let quota_store = GenericQuotaStore::with_config(store, CostConfig::default(), 50);

        let agent_id = test_agent();
        let hour1 = 1705314000; // 10:00:00
        let hour2 = 1705317600; // 11:00:00

        // Exhaust quota in hour 1
        for _ in 0..4 {
            let result = quota_store
                .check_and_record(&agent_id, OperationType::Assert, 100, hour1)
                .await
                .expect("check");
            assert!(result.allowed);
        }

        // Should be denied in hour 1
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Assert, 100, hour1)
            .await
            .expect("check");
        assert!(!result.allowed);

        // Should succeed in hour 2 (new window)
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Assert, 100, hour2)
            .await
            .expect("check");
        assert!(result.allowed);
        assert_eq!(result.remaining, 50 - 11);
    }

    #[tokio::test]
    async fn test_custom_quota_limit() {
        let store = Arc::new(HybridStore::open_temp().expect("store"));
        let quota_store = GenericQuotaStore::new(store);

        let agent_id = test_agent();

        // Default limit
        let limit = quota_store.get_quota_limit(&agent_id).await.expect("limit");
        assert_eq!(limit, DEFAULT_QUOTA_LIMIT);

        // Set custom limit
        quota_store.set_quota_limit(&agent_id, 50_000).await.expect("set");

        let limit = quota_store.get_quota_limit(&agent_id).await.expect("limit");
        assert_eq!(limit, 50_000);
    }

    #[tokio::test]
    async fn test_get_quota_status() {
        let store = Arc::new(HybridStore::open_temp().expect("store"));
        let quota_store = GenericQuotaStore::new(store);

        let agent_id = test_agent();
        let timestamp = 1705315800;

        // Check status before any operations
        let status = quota_store.get_quota_status(&agent_id, timestamp).await.expect("status");
        assert!(status.allowed);
        assert_eq!(status.remaining, 10000);
        assert_eq!(status.cost, 0);

        // Do an operation
        quota_store
            .check_and_record(&agent_id, OperationType::Vote, 50, timestamp)
            .await
            .expect("record");

        // Check status after operation
        let status = quota_store.get_quota_status(&agent_id, timestamp).await.expect("status");
        assert_eq!(status.remaining, 10000 - 2); // Vote cost = 2
    }

    #[tokio::test]
    async fn test_different_operation_types() {
        let store = Arc::new(HybridStore::open_temp().expect("store"));
        let quota_store = GenericQuotaStore::new(store);

        let agent_id = test_agent();
        let timestamp = 1705315800;

        // Assert
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Assert, 100, timestamp)
            .await
            .expect("assert");
        assert_eq!(result.cost, 11); // 10 + 1

        // Vote
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Vote, 100, timestamp)
            .await
            .expect("vote");
        assert_eq!(result.cost, 2); // 1 + 1

        // Query with lens
        let result = quota_store
            .check_and_record(&agent_id, OperationType::Query { lens_count: 2 }, 50, timestamp)
            .await
            .expect("query");
        assert_eq!(result.cost, 8); // 5 + 2 + 1
    }

    #[tokio::test]
    async fn test_serialization_roundtrip() {
        let record = QuotaRecord {
            agent_id: [42u8; 32],
            window_start: 1705314000,
            tokens_used: 500,
            operation_count: 25,
            last_operation: 1705315000,
        };

        let serialized =
            GenericQuotaStore::<HybridStore>::serialize_record(&record).expect("serialize");
        let deserialized =
            GenericQuotaStore::<HybridStore>::deserialize_record(&serialized).expect("deserialize");

        assert_eq!(record, deserialized);
    }
}
