//! Compliance rule configuration and hot-reload support
//!
//! Provides database-backed compliance rule loading with `PostgreSQL` NOTIFY/LISTEN
//! for hot-reload capabilities. Integrates with the `ComplianceValidator` in the
//! risk crate to enable dynamic rule configuration without service restarts.

use serde::{Deserialize, Serialize};

#[cfg(feature = "postgres")]
use crate::error::ConfigResult;
#[cfg(feature = "postgres")]
use std::collections::HashMap;
#[cfg(feature = "postgres")]
use std::sync::Arc;
#[cfg(feature = "postgres")]
use std::time::{Duration, Instant};
#[cfg(feature = "postgres")]
use tokio::sync::RwLock;
#[cfg(feature = "postgres")]
use tracing::{error, info};

#[cfg(feature = "postgres")]
use sqlx::postgres::{PgListener, PgPool};

/// Compliance rule loader with `PostgreSQL` integration and hot-reload support
///
/// Loads compliance rules from the `PostgreSQL` database and automatically
/// reloads them when changes are detected via `PostgreSQL` NOTIFY/LISTEN.
#[cfg(feature = "postgres")]
pub struct PostgresComplianceRuleLoader {
    /// Database connection pool
    pool: PgPool,
    /// `PostgreSQL` listener for rule change notifications
    listener: Arc<RwLock<Option<PgListener>>>,
    /// Cached compliance rules by rule_id
    rules_cache: Arc<RwLock<HashMap<String, ComplianceRuleConfig>>>,
    /// Timestamp of last full cache refresh
    cache_loaded_at: Arc<RwLock<Option<Instant>>>,
    /// Cache timeout duration
    cache_timeout: Duration,
}

/// Compliance rule configuration structure
///
/// Represents a compliance rule loaded from the database.
///
/// This structure is designed to be compatible with both the database
/// schema and the `ComplianceRule` type in the risk crate.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "postgres", derive(sqlx::FromRow))]
pub struct ComplianceRuleConfig {
    /// Unique rule identifier
    pub rule_id: String,
    /// Human-readable rule name
    pub name: String,
    /// Detailed description
    pub description: String,
    /// Rule type (`POSITION_LIMIT`, `MARKET_ABUSE`, etc.)
    pub rule_type: String,
    /// Whether the rule is active
    pub active: bool,
    /// Rule version for audit trail
    pub version: i32,
    /// Severity level (Info, Low, Medium, High, Critical)
    pub severity: String,
    /// Priority for evaluation (0-100)
    pub priority: i32,
    /// Flexible rule parameters as JSON
    #[cfg_attr(feature = "postgres", sqlx(json))]
    pub parameters: serde_json::Value,
    /// Regulatory framework
    pub regulatory_framework: Option<String>,
    /// Regulatory reference
    pub regulatory_reference: Option<String>,
}

#[cfg(feature = "postgres")]
impl PostgresComplianceRuleLoader {
    /// Creates a new compliance rule loader with PostgreSQL integration
    ///
    /// # Arguments
    ///
    /// * `database_url` - PostgreSQL connection URL
    ///
    /// # Returns
    ///
    /// Result containing the initialized loader or an error
    ///
    /// # Errors
    /// Returns error if the operation fails
    pub async fn new(database_url: &str) -> ConfigResult<Self> {
        let pool = PgPool::connect(database_url).await?;

        Ok(Self {
            pool,
            listener: Arc::new(RwLock::new(None)),
            rules_cache: Arc::new(RwLock::new(HashMap::new())),
            cache_loaded_at: Arc::new(RwLock::new(None)),
            cache_timeout: Duration::from_secs(300), // 5 minutes
        })
    }

    /// Creates a loader with an existing connection pool
    ///
    /// # Arguments
    ///
    /// * `pool` - Existing PostgreSQL connection pool
    ///
    /// # Returns
    ///
    /// Configured compliance rule loader
    pub fn with_pool(pool: PgPool) -> Self {
        Self {
            pool,
            listener: Arc::new(RwLock::new(None)),
            rules_cache: Arc::new(RwLock::new(HashMap::new())),
            cache_loaded_at: Arc::new(RwLock::new(None)),
            cache_timeout: Duration::from_secs(300),
        }
    }

    /// Starts listening for rule change notifications
    ///
    /// Initiates PostgreSQL NOTIFY/LISTEN for hot-reload capabilities.
    ///
    /// When a rule is changed in the database, the cache will be automatically
    /// invalidated and reloaded.
    ///
    /// # Returns
    ///
    /// Result indicating success or error
    ///
    /// # Errors
    /// Returns error if the operation fails
    pub async fn start_listener(&self) -> ConfigResult<()> {
        let mut listener = PgListener::connect_with(&self.pool).await?;

        listener.listen("compliance_rules_changed").await?;

        *self.listener.write().await = Some(listener);

        info!("PostgreSQL NOTIFY/LISTEN started for compliance rule hot-reload");

        // Spawn background task to handle notifications
        let listener_clone = Arc::clone(&self.listener);
        let cache_clone = Arc::clone(&self.rules_cache);
        let pool_clone = self.pool.clone();

        tokio::spawn(async move {
            loop {
                let mut listener_guard = listener_clone.write().await;
                if let Some(listener) = listener_guard.as_mut() {
                    match listener.try_recv().await {
                        Ok(Some(notification)) => {
                            info!(
                                "Compliance rule change notification received: {}",
                                notification.payload()
                            );

                            // Parse notification payload to get rule_id
                            if let Ok(payload) =
                                serde_json::from_str::<serde_json::Value>(notification.payload())
                            {
                                if let Some(rule_id) =
                                    payload.get("rule_id").and_then(|v| v.as_str())
                                {
                                    // Invalidate cache for this rule
                                    cache_clone.write().await.remove(rule_id);
                                    info!("Invalidated cache for compliance rule: {}", rule_id);

                                    // Optionally reload the rule immediately
                                    if let Err(e) =
                                        Self::reload_rule_static(&pool_clone, &cache_clone, rule_id)
                                            .await
                                    {
                                        error!(
                                            "Failed to reload compliance rule {}: {}",
                                            rule_id, e
                                        );
                                    }
                                }
                            }
                        }
                        Ok(None) => {
                            // No notification available, continue
                            tokio::time::sleep(Duration::from_millis(100)).await;
                        }
                        Err(e) => {
                            error!("Error receiving compliance rule notification: {}", e);
                            tokio::time::sleep(Duration::from_secs(1)).await;
                        }
                    }
                }
                drop(listener_guard);
                tokio::time::sleep(Duration::from_millis(100)).await;
            }
        });

        Ok(())
    }

    /// Static helper for reloading a single rule (used in background task)
    async fn reload_rule_static(
        pool: &PgPool,
        cache: &Arc<RwLock<HashMap<String, ComplianceRuleConfig>>>,
        rule_id: &str,
    ) -> ConfigResult<()> {
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
                           severity, priority, parameters, regulatory_framework, regulatory_reference
                    FROM compliance_rules
                    WHERE rule_id = $1 AND active = true";

        let row = sqlx::query_as::<_, ComplianceRuleConfig>(query)
            .bind(rule_id)
            .fetch_optional(pool)
            .await?;

        if let Some(rule) = row {
            cache.write().await.insert(rule_id.to_owned(), rule);
            info!("Reloaded compliance rule: {}", rule_id);
        }

        Ok(())
    }

    /// Loads all active compliance rules from the database
    ///
    /// # Returns
    ///
    /// Vector of active compliance rules
    ///
    /// # Errors
    /// Returns error if the operation fails
    pub async fn load_all_active_rules(&self) -> ConfigResult<Vec<ComplianceRuleConfig>> {
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
                           severity, priority, parameters, regulatory_framework, regulatory_reference
                    FROM compliance_rules
                    WHERE active = true
                      AND effective_date <= NOW()
                      AND (expiry_date IS NULL OR expiry_date > NOW())
                    ORDER BY priority DESC, created_at ASC";

        let rules = sqlx::query_as::<_, ComplianceRuleConfig>(query)
            .fetch_all(&self.pool)
            .await?;

        // Update cache and refresh timestamp
        let mut cache = self.rules_cache.write().await;
        cache.clear();
        for rule in &rules {
            cache.insert(rule.rule_id.clone(), rule.clone());
        }
        drop(cache);
        *self.cache_loaded_at.write().await = Some(Instant::now());

        info!("Loaded {} active compliance rules", rules.len());

        Ok(rules)
    }

    /// Loads rules filtered by type
    ///
    /// # Arguments
    ///
    /// * `rule_type` - Rule type to filter by (e.g., "POSITION_LIMIT")
    ///
    /// # Returns
    ///
    /// Vector of rules matching the specified type
    ///
    /// # Errors
    /// Returns error if the operation fails
    pub async fn load_rules_by_type(
        &self,
        rule_type: &str,
    ) -> ConfigResult<Vec<ComplianceRuleConfig>> {
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
                           severity, priority, parameters, regulatory_framework, regulatory_reference
                    FROM compliance_rules
                    WHERE active = true
                      AND rule_type::text = $1
                      AND effective_date <= NOW()
                      AND (expiry_date IS NULL OR expiry_date > NOW())
                    ORDER BY priority DESC";

        let rules = sqlx::query_as::<_, ComplianceRuleConfig>(query)
            .bind(rule_type)
            .fetch_all(&self.pool)
            .await?;

        Ok(rules)
    }

    /// Gets a specific rule by ID (with caching)
    ///
    /// # Arguments
    ///
    /// * `rule_id` - Unique rule identifier
    ///
    /// # Returns
    ///
    /// Optional compliance rule configuration
    ///
    /// # Errors
    /// Returns error if the operation fails
    pub async fn get_rule(&self, rule_id: &str) -> ConfigResult<Option<ComplianceRuleConfig>> {
        // Check cache first, but only if it hasn't expired
        {
            let loaded_at = self.cache_loaded_at.read().await;
            if let Some(at) = *loaded_at {
                if at.elapsed() < self.cache_timeout {
                    let cache = self.rules_cache.read().await;
                    if let Some(rule) = cache.get(rule_id) {
                        return Ok(Some(rule.clone()));
                    }
                }
            }
        }

        // Load from database if not in cache
        let query = "SELECT rule_id, name, description, rule_type::text as rule_type, active, version,
                           severity, priority, parameters, regulatory_framework, regulatory_reference
                    FROM compliance_rules
                    WHERE rule_id = $1 AND active = true";

        let rule = sqlx::query_as::<_, ComplianceRuleConfig>(query)
            .bind(rule_id)
            .fetch_optional(&self.pool)
            .await?;

        // Update cache if found and mark refresh time
        if let Some(ref rule_data) = rule {
            self.rules_cache
                .write()
                .await
                .insert(rule_id.to_owned(), rule_data.clone());
            // If this is the first load, initialize the cache timestamp
            let mut loaded_at = self.cache_loaded_at.write().await;
            if loaded_at.is_none() {
                *loaded_at = Some(Instant::now());
            }
        }

        Ok(rule)
    }

    /// Records a compliance rule execution for audit trail
    ///
    /// # Arguments
    ///
    /// * `rule_id` - Rule that was executed
    /// * `result` - Execution result (PASS, WARN, FAIL, ERROR)
    ///
    /// * `violation_detected` - Whether a violation was detected
    /// * `order_id` - Optional order ID
    ///
    /// * `instrument_id` - Optional instrument ID
    ///
    /// # Returns
    ///
    /// Result indicating success or error
    ///
    /// # Errors
    /// Returns error if the operation fails
    pub async fn record_execution(
        &self,
        rule_id: &str,
        result: &str,
        violation_detected: bool,
        order_id: Option<&str>,
        instrument_id: Option<&str>,
    ) -> ConfigResult<()> {
        let query = "SELECT record_compliance_rule_execution($1, $2, $3, $4, $5, NULL, NULL, NULL)";

        sqlx::query(query)
            .bind(rule_id)
            .bind(result)
            .bind(violation_detected)
            .bind(order_id)
            .bind(instrument_id)
            .execute(&self.pool)
            .await?;

        Ok(())
    }

    /// Clears the rule cache (forces reload on next access)
    pub async fn clear_cache(&self) {
        self.rules_cache.write().await.clear();
        *self.cache_loaded_at.write().await = None;
        info!("Compliance rule cache cleared");
    }

    /// Gets the current cache size
    pub async fn cache_size(&self) -> usize {
        self.rules_cache.read().await.len()
    }
}

#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
    use super::*;

    #[test]
    fn test_compliance_rule_config_structure() {
        let rule = ComplianceRuleConfig {
            rule_id: "test_rule".to_owned(),
            name: "Test Rule".to_owned(),
            description: "Test description".to_owned(),
            rule_type: "POSITION_LIMIT".to_owned(),
            active: true,
            version: 1,
            severity: "High".to_owned(),
            priority: 80,
            parameters: serde_json::json!({"max_position": 1000000}),
            regulatory_framework: Some("Basel III".to_owned()),
            regulatory_reference: Some("Article 123".to_owned()),
        };

        assert_eq!(rule.rule_id, "test_rule");
        assert!(rule.active);
        assert_eq!(rule.priority, 80);
    }

    #[test]
    fn test_compliance_rule_config_serialization() {
        let rule = ComplianceRuleConfig {
            rule_id: "test_rule".to_owned(),
            name: "Test Rule".to_owned(),
            description: "Test description".to_owned(),
            rule_type: "MARKET_ABUSE".to_owned(),
            active: true,
            version: 1,
            severity: "Critical".to_owned(),
            priority: 95,
            parameters: serde_json::json!({"threshold": 1000000}),
            regulatory_framework: None,
            regulatory_reference: None,
        };

        let json = serde_json::to_string(&rule).expect("Failed to serialize");
        assert!(json.contains("test_rule"));
        assert!(json.contains("MARKET_ABUSE"));

        let deserialized: ComplianceRuleConfig =
            serde_json::from_str(&json).expect("Failed to deserialize");
        assert_eq!(deserialized.rule_id, rule.rule_id);
        assert_eq!(deserialized.severity, rule.severity);
    }
}
