//! Policy configuration types loaded from YAML.
//!
//! The [`PolicyConfig`] is the top-level struct that defines all guardrail
//! rules. It can be loaded from a YAML string or file.

use serde::{Deserialize, Serialize};
use std::path::Path;

/// Top-level policy configuration.
///
/// Controls which guardrail rules are active and their thresholds.
/// Fields that are `None` or empty mean the corresponding rule category
/// is disabled.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PolicyConfig {
    /// Cost and performance control rules
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cost_control: Option<CostControlConfig>,

    /// Data protection and access control rules
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub data_protection: Option<DataProtectionConfig>,

    /// Query pattern restrictions
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub query_patterns: Option<QueryPatternsConfig>,

    /// Tag-based rules
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tag_rules: Vec<TagRuleConfig>,
}

/// Cost and performance control configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CostControlConfig {
    /// Maximum number of joins allowed
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_joins: Option<usize>,

    /// Maximum number of tables allowed
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_tables: Option<usize>,

    /// Whether to block SELECT * queries
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub block_select_star: Option<bool>,

    /// Whether to require a LIMIT clause
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub require_limit: Option<bool>,

    /// Maximum LIMIT value allowed
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_limit: Option<usize>,

    /// Whether to block CROSS JOIN queries
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub block_cross_join: Option<bool>,

    /// Maximum complexity score allowed
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_complexity_score: Option<usize>,
}

/// Data protection and access control configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataProtectionConfig {
    /// Columns that are blocked from being accessed
    #[serde(default)]
    pub blocked_columns: Vec<BlockedColumnConfig>,

    /// Tables that are blocked from being accessed
    #[serde(default)]
    pub blocked_tables: Vec<BlockedTableConfig>,

    /// Whether to require a WHERE clause on queries
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub require_where_clause: Option<bool>,
}

/// Configuration for a blocked column rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockedColumnConfig {
    /// Table containing the blocked columns
    pub table: String,

    /// Column names that are blocked
    pub columns: Vec<String>,

    /// Custom message to display when this rule is violated
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

/// Configuration for a blocked table rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockedTableConfig {
    /// Table name that is blocked
    pub table: String,

    /// Custom message to display when this rule is violated
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

/// Query pattern restriction configuration.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct QueryPatternsConfig {
    /// If set, only these statement types are allowed (e.g., ["SELECT"])
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub allowed_statements: Option<Vec<String>>,

    /// If set, these statement types are blocked (e.g., ["DROP", "DELETE"])
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub block_statements: Option<Vec<String>>,
}

/// Configuration for a tag-based rule.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TagRuleConfig {
    /// The tag to match (e.g., "pii", "deprecated")
    pub tag: String,

    /// Action to take when the tag matches
    pub action: TagAction,

    /// Custom message to display
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,

    /// Column patterns to exempt from this rule (e.g., ["users.id"])
    #[serde(default)]
    pub exceptions: Vec<String>,
}

/// Action to take when a tag rule matches.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TagAction {
    /// Block the query (violation with Error severity)
    Block,
    /// Warn about the query (warning only)
    Warn,
}

impl PolicyConfig {
    /// Load a policy configuration from a YAML string.
    pub fn from_yaml(yaml: &str) -> Result<Self, serde_yaml::Error> {
        serde_yaml::from_str(yaml)
    }

    /// Load a policy configuration from a YAML file.
    pub fn from_file(path: &Path) -> Result<Self, PolicyLoadError> {
        let content = std::fs::read_to_string(path)?;
        let config = serde_yaml::from_str(&content)?;
        Ok(config)
    }
}

/// Errors that can occur when loading a policy file.
#[derive(Debug, thiserror::Error)]
pub enum PolicyLoadError {
    /// Failed to read the file
    #[error("Failed to read policy file: {0}")]
    IoError(#[from] std::io::Error),

    /// Failed to parse the YAML content
    #[error("Failed to parse policy YAML: {0}")]
    YamlError(#[from] serde_yaml::Error),
}

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

    #[test]
    fn test_empty_policy() {
        let yaml = "{}";
        let config = PolicyConfig::from_yaml(yaml).unwrap();
        assert!(config.cost_control.is_none());
        assert!(config.data_protection.is_none());
        assert!(config.query_patterns.is_none());
        assert!(config.tag_rules.is_empty());
    }

    #[test]
    fn test_cost_control_policy() {
        let yaml = r#"
cost_control:
  max_joins: 3
  max_tables: 5
  block_select_star: true
  require_limit: true
  block_cross_join: true
  max_complexity_score: 8
"#;
        let config = PolicyConfig::from_yaml(yaml).unwrap();
        let cc = config.cost_control.unwrap();
        assert_eq!(cc.max_joins, Some(3));
        assert_eq!(cc.max_tables, Some(5));
        assert_eq!(cc.block_select_star, Some(true));
        assert_eq!(cc.require_limit, Some(true));
        assert_eq!(cc.block_cross_join, Some(true));
        assert_eq!(cc.max_complexity_score, Some(8));
    }

    #[test]
    fn test_data_protection_policy() {
        let yaml = r#"
data_protection:
  blocked_columns:
    - table: users
      columns: [ssn, password_hash]
      message: "PII columns are restricted"
  blocked_tables:
    - table: audit_log
      message: "Audit log is read-restricted"
  require_where_clause: true
"#;
        let config = PolicyConfig::from_yaml(yaml).unwrap();
        let dp = config.data_protection.unwrap();
        assert_eq!(dp.blocked_columns.len(), 1);
        assert_eq!(dp.blocked_columns[0].columns, vec!["ssn", "password_hash"]);
        assert_eq!(dp.blocked_tables.len(), 1);
        assert_eq!(dp.require_where_clause, Some(true));
    }

    #[test]
    fn test_tag_rules_policy() {
        let yaml = r#"
tag_rules:
  - tag: pii
    action: block
    message: "PII data access requires approval"
    exceptions: ["users.id"]
  - tag: deprecated
    action: warn
    message: "This column is deprecated"
"#;
        let config = PolicyConfig::from_yaml(yaml).unwrap();
        assert_eq!(config.tag_rules.len(), 2);
        assert_eq!(config.tag_rules[0].tag, "pii");
        assert_eq!(config.tag_rules[0].action, TagAction::Block);
        assert_eq!(config.tag_rules[0].exceptions, vec!["users.id"]);
        assert_eq!(config.tag_rules[1].action, TagAction::Warn);
    }

    #[test]
    fn test_query_patterns_policy() {
        let yaml = r#"
query_patterns:
  allowed_statements:
    - SELECT
  block_statements:
    - DROP
    - DELETE
"#;
        let config = PolicyConfig::from_yaml(yaml).unwrap();
        let qp = config.query_patterns.unwrap();
        assert_eq!(qp.allowed_statements, Some(vec!["SELECT".to_string()]));
        assert_eq!(
            qp.block_statements,
            Some(vec!["DROP".to_string(), "DELETE".to_string()])
        );
    }
}
