//! Credential constraint types for policy builders.
//!
//! This module defines the `CredentialConstraint` enum and validation helpers
//! used by policy builders to construct `presentation_definition` constraints.

use serde::{Deserialize, Serialize};

use crate::error::LastIDError;

/// Represents a constraint on a credential field.
///
/// Used internally by policy builders to construct `presentation_definition` constraints.
/// The IDP maps these constraints to DIF Presentation Exchange v2 filter types.
///
/// # Constraint Mapping
///
/// | Constraint Type | IDP Filter |
/// |-----------------|------------|
/// | `Equals` | `{ const: value }` |
/// | `OneOf` | `{ enum: values }` |
/// | `Minimum` | `{ minimum: value }` |
/// | `Maximum` | `{ maximum: value }` |
/// | `Pattern` | `{ pattern: regex }` |
/// | `EmailDomain` | `{ pattern: ".*@{domain}$" }` |
///
/// # Example
///
/// ```rust
/// use lastid_sdk::policies::CredentialConstraint;
///
/// // Exact value match
/// let issuer_constraint = CredentialConstraint::Equals {
///     field: "iss".to_string(),
///     value: serde_json::json!("did:lastid:issuer123"),
/// };
///
/// // Email domain filter
/// let domain_constraint = CredentialConstraint::EmailDomain {
///     field: "email".to_string(),
///     domain: "acme.com".to_string(),
/// };
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase", tag = "type")]
#[non_exhaustive]
pub enum CredentialConstraint {
    /// Exact value match (JSON-LD const).
    Equals {
        /// The field path to constrain (e.g., "iss", "sub", "email").
        field: String,
        /// The required value (will be serialized as JSON).
        value: serde_json::Value,
    },

    /// One of allowed values (JSON-LD enum).
    OneOf {
        /// The field path to constrain.
        field: String,
        /// The allowed values.
        values: Vec<serde_json::Value>,
    },

    /// Numeric minimum (JSON-LD minimum).
    Minimum {
        /// The field path to constrain.
        field: String,
        /// The minimum value (inclusive).
        value: i64,
    },

    /// Numeric maximum (JSON-LD maximum).
    Maximum {
        /// The field path to constrain.
        field: String,
        /// The maximum value (inclusive).
        value: i64,
    },

    /// Regex pattern match (JSON-LD pattern).
    Pattern {
        /// The field path to constrain.
        field: String,
        /// The regex pattern to match.
        pattern: String,
    },

    /// Domain suffix match for email fields.
    ///
    /// Translates to pattern: `.*@{domain}$`
    /// Matches "user@acme.com" and "user@sales.acme.com" for domain "acme.com".
    EmailDomain {
        /// The field path to constrain (typically "email").
        field: String,
        /// The domain to match (without @ symbol).
        domain: String,
    },
}

impl CredentialConstraint {
    /// Validate a DID format.
    ///
    /// DIDs must start with "did:" according to the W3C DID specification.
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error if the DID format is invalid.
    ///
    /// # Example
    ///
    /// ```rust
    /// use lastid_sdk::policies::CredentialConstraint;
    ///
    /// assert!(CredentialConstraint::validate_did("did:lastid:user123").is_ok());
    /// assert!(CredentialConstraint::validate_did("not-a-did").is_err());
    /// ```
    pub fn validate_did(did: &str) -> Result<(), LastIDError> {
        if did.is_empty() {
            return Err(LastIDError::policy_validation("DID cannot be empty"));
        }
        if !did.starts_with("did:") {
            return Err(LastIDError::policy_validation(format!(
                "Invalid DID format: '{did}' must start with 'did:'"
            )));
        }
        // Basic structure check: did:method:identifier
        let parts: Vec<&str> = did.splitn(3, ':').collect();
        if parts.len() < 3 || parts[1].is_empty() || parts[2].is_empty() {
            return Err(LastIDError::policy_validation(format!(
                "Invalid DID format: '{did}' must have format 'did:method:identifier'"
            )));
        }
        Ok(())
    }

    /// Validate a domain format.
    ///
    /// Domains must not contain `@` or `/` characters, and cannot be empty.
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error if the domain format is invalid.
    ///
    /// # Example
    ///
    /// ```rust
    /// use lastid_sdk::policies::CredentialConstraint;
    ///
    /// assert!(CredentialConstraint::validate_domain("acme.com").is_ok());
    /// assert!(CredentialConstraint::validate_domain("user@acme.com").is_err());
    /// assert!(CredentialConstraint::validate_domain("").is_err());
    /// ```
    pub fn validate_domain(domain: &str) -> Result<(), LastIDError> {
        if domain.is_empty() {
            return Err(LastIDError::policy_validation("Domain cannot be empty"));
        }
        if domain.contains('@') {
            return Err(LastIDError::policy_validation(format!(
                "Invalid domain format: '{domain}' must not contain '@' (use domain only, e.g., 'acme.com')"
            )));
        }
        if domain.contains('/') {
            return Err(LastIDError::policy_validation(format!(
                "Invalid domain format: '{domain}' must not contain '/'"
            )));
        }
        Ok(())
    }

    /// Validate a minimum age value.
    ///
    /// Age must be between 1 and 150 years (inclusive).
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error if the age is out of range.
    pub fn validate_minimum_age(years: u8) -> Result<(), LastIDError> {
        if years == 0 {
            return Err(LastIDError::policy_validation(
                "Minimum age must be at least 1 year",
            ));
        }
        if years > 150 {
            return Err(LastIDError::policy_validation(
                "Minimum age must be at most 150 years",
            ));
        }
        Ok(())
    }

    /// Validate a minimum level value.
    ///
    /// Level must be non-negative.
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error if the level is negative.
    pub fn validate_minimum_level(level: i64) -> Result<(), LastIDError> {
        if level < 0 {
            return Err(LastIDError::policy_validation(
                "Minimum level must be non-negative",
            ));
        }
        Ok(())
    }

    /// Get the constraint key for IDP serialization.
    ///
    /// Returns the key name used in the `constraints` `HashMap` sent to the IDP.
    #[must_use]
    pub fn constraint_key(&self) -> &str {
        match self {
            Self::Equals { field, .. }
            | Self::OneOf { field, .. }
            | Self::Minimum { field, .. }
            | Self::Maximum { field, .. }
            | Self::Pattern { field, .. }
            | Self::EmailDomain { field, .. } => field,
        }
    }

    /// Convert the constraint to a JSON value for IDP serialization.
    ///
    /// This produces the value to be stored in the `constraints` `HashMap`.
    #[must_use]
    pub fn to_constraint_value(&self) -> serde_json::Value {
        match self {
            Self::Equals { value, .. } => value.clone(),
            Self::OneOf { values, .. } => serde_json::json!(values),
            Self::Minimum { value, .. } => serde_json::json!({ "minimum": value }),
            Self::Maximum { value, .. } => serde_json::json!({ "maximum": value }),
            Self::Pattern { pattern, .. } => serde_json::json!(pattern),
            Self::EmailDomain { domain, .. } => serde_json::json!(domain),
        }
    }
}

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

    #[test]
    fn test_validate_did_valid() {
        assert!(CredentialConstraint::validate_did("did:lastid:user123").is_ok());
        assert!(CredentialConstraint::validate_did("did:web:example.com").is_ok());
        assert!(
            CredentialConstraint::validate_did(
                "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
            )
            .is_ok()
        );
    }

    #[test]
    fn test_validate_did_empty() {
        let result = CredentialConstraint::validate_did("");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("empty"));
    }

    #[test]
    fn test_validate_did_no_prefix() {
        let result = CredentialConstraint::validate_did("not-a-did");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("did:"));
    }

    #[test]
    fn test_validate_did_incomplete() {
        // Missing identifier
        let result = CredentialConstraint::validate_did("did:method:");
        assert!(result.is_err());

        // Missing method
        let result = CredentialConstraint::validate_did("did::identifier");
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_domain_valid() {
        assert!(CredentialConstraint::validate_domain("acme.com").is_ok());
        assert!(CredentialConstraint::validate_domain("sales.acme.com").is_ok());
        assert!(CredentialConstraint::validate_domain("example").is_ok());
    }

    #[test]
    fn test_validate_domain_empty() {
        let result = CredentialConstraint::validate_domain("");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("empty"));
    }

    #[test]
    fn test_validate_domain_with_at() {
        let result = CredentialConstraint::validate_domain("user@acme.com");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("@"));
    }

    #[test]
    fn test_validate_domain_with_slash() {
        let result = CredentialConstraint::validate_domain("acme.com/path");
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("/"));
    }

    #[test]
    fn test_validate_minimum_age_valid() {
        assert!(CredentialConstraint::validate_minimum_age(18).is_ok());
        assert!(CredentialConstraint::validate_minimum_age(1).is_ok());
        assert!(CredentialConstraint::validate_minimum_age(150).is_ok());
    }

    #[test]
    fn test_validate_minimum_age_zero() {
        let result = CredentialConstraint::validate_minimum_age(0);
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_minimum_level_valid() {
        assert!(CredentialConstraint::validate_minimum_level(0).is_ok());
        assert!(CredentialConstraint::validate_minimum_level(3).is_ok());
        assert!(CredentialConstraint::validate_minimum_level(100).is_ok());
    }

    #[test]
    fn test_validate_minimum_level_negative() {
        let result = CredentialConstraint::validate_minimum_level(-1);
        assert!(result.is_err());
    }

    #[test]
    fn test_constraint_key() {
        let constraint = CredentialConstraint::Equals {
            field: "iss".to_string(),
            value: serde_json::json!("did:lastid:issuer"),
        };
        assert_eq!(constraint.constraint_key(), "iss");

        let domain_constraint = CredentialConstraint::EmailDomain {
            field: "email".to_string(),
            domain: "acme.com".to_string(),
        };
        assert_eq!(domain_constraint.constraint_key(), "email");
    }

    #[test]
    fn test_to_constraint_value() {
        let equals = CredentialConstraint::Equals {
            field: "iss".to_string(),
            value: serde_json::json!("did:lastid:issuer"),
        };
        assert_eq!(
            equals.to_constraint_value(),
            serde_json::json!("did:lastid:issuer")
        );

        let minimum = CredentialConstraint::Minimum {
            field: "level".to_string(),
            value: 3,
        };
        assert_eq!(
            minimum.to_constraint_value(),
            serde_json::json!({ "minimum": 3 })
        );

        let domain = CredentialConstraint::EmailDomain {
            field: "email".to_string(),
            domain: "acme.com".to_string(),
        };
        assert_eq!(domain.to_constraint_value(), serde_json::json!("acme.com"));
    }

    #[test]
    fn test_constraint_serialization() {
        let constraint = CredentialConstraint::Equals {
            field: "iss".to_string(),
            value: serde_json::json!("did:lastid:issuer"),
        };

        let json = serde_json::to_string(&constraint).unwrap();
        assert!(json.contains("equals"));
        assert!(json.contains("iss"));

        let deserialized: CredentialConstraint = serde_json::from_str(&json).unwrap();
        assert_eq!(constraint, deserialized);
    }
}
