//! Persona credential policy builder.

use std::collections::HashMap;
use std::marker::PhantomData;

use serde::{Deserialize, Serialize};

use super::{Complete, CredentialConstraint, Incomplete, PolicyBuilder};
use crate::error::LastIDError;
use crate::types::{CredentialPolicyRequest, EndpointUrl};

/// Fields that can be requested in a persona credential.
///
/// These field names match the actual `LastID.Persona` schema in the IDP.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum PersonaField {
    /// User's display name (public-facing name)
    DisplayName,
    /// User's email address
    Email,
    /// User's phone number
    Phone,
    /// User's company/organization
    Company,
    /// User's job title
    JobTitle,
    /// User's avatar/profile picture URL
    AvatarUrl,
    /// User's website URL
    Website,
    /// User's location
    Location,
    /// Profile visibility setting
    Visibility,
}

impl PersonaField {
    /// Get the field name as a string for the API.
    #[must_use]
    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::DisplayName => "display_name",
            Self::Email => "email",
            Self::Phone => "phone",
            Self::Company => "company",
            Self::JobTitle => "job_title",
            Self::AvatarUrl => "avatar_url",
            Self::Website => "website",
            Self::Location => "location",
            Self::Visibility => "visibility",
        }
    }
}

/// Policy builder for persona credentials (user identity information).
///
/// # Type-State Pattern
///
/// - `PersonaPolicy<Incomplete>` - Initial state, callback not set
/// - `PersonaPolicy<Complete>` - Ready to build
///
/// # Required vs Optional Fields
///
/// - **Required fields** (`with_fields`): Must be present in the credential
/// - **Optional fields** (`with_optional_fields`): User can choose to disclose
///
/// # Example
///
/// ```rust,no_run
/// use lastid_sdk::{PersonaField, PersonaPolicy, PolicyBuilder};
///
/// let policy = PersonaPolicy::new()
///     .with_fields(&[PersonaField::DisplayName])  // Required
///     .with_optional_fields(&[PersonaField::Email, PersonaField::Phone])  // Optional
///     .with_callback("https://example.com/callback");
///
/// let request = policy.to_request().unwrap();
/// ```
#[cfg(feature = "persona-policy")]
pub struct PersonaPolicy<State = Incomplete> {
    callback_url: Option<String>,
    email_domain: Option<String>,
    issuer_did: Option<String>,
    subject_did: Option<String>,
    fields: Vec<PersonaField>,
    optional_fields: Vec<PersonaField>,
    _marker: PhantomData<State>,
}

#[cfg(feature = "persona-policy")]
impl Default for PersonaPolicy<Incomplete> {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(feature = "persona-policy")]
impl PersonaPolicy<Incomplete> {
    /// Create a new persona credential policy builder.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            callback_url: None,
            email_domain: None,
            issuer_did: None,
            subject_did: None,
            fields: Vec::new(),
            optional_fields: Vec::new(),
            _marker: PhantomData,
        }
    }

    /// Add required fields to request in the persona credential.
    ///
    /// Required fields must be present in the credential (no `optional: true` flag).
    #[must_use]
    pub fn with_fields(mut self, fields: &[PersonaField]) -> Self {
        self.fields.extend(fields.iter().copied());
        self
    }

    /// Add a single required field to request.
    #[must_use]
    pub fn with_field(mut self, field: PersonaField) -> Self {
        self.fields.push(field);
        self
    }

    /// Add optional fields for selective disclosure.
    ///
    /// Optional fields have `optional: true` flag, allowing the user to choose
    /// whether to disclose them.
    #[must_use]
    pub fn with_optional_fields(mut self, fields: &[PersonaField]) -> Self {
        self.optional_fields.extend(fields.iter().copied());
        self
    }

    /// Add a single optional field for selective disclosure.
    #[must_use]
    pub fn with_optional_field(mut self, field: PersonaField) -> Self {
        self.optional_fields.push(field);
        self
    }

    /// Filter to personas with email addresses from a specific domain.
    ///
    /// This is useful for `SaaS` applications that want to verify users are
    /// from a specific organization based on their email domain.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::PersonaPolicy;
    ///
    /// // Only accept personas with @bigcorp.com email
    /// let policy = PersonaPolicy::new()
    ///     .with_email_domain("bigcorp.com")
    ///     .with_callback("https://example.com/callback");
    /// ```
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error at build time if the domain format is
    /// invalid.
    #[must_use]
    pub fn with_email_domain(mut self, domain: impl Into<String>) -> Self {
        self.email_domain = Some(domain.into());
        self
    }

    /// Constrain to a specific issuer DID.
    ///
    /// Only persona credentials issued by this DID will be accepted.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::PersonaPolicy;
    ///
    /// let policy = PersonaPolicy::new()
    ///     .with_issuer_did("did:lastid:trusted-issuer")
    ///     .with_callback("https://example.com/callback");
    /// ```
    #[must_use]
    pub fn with_issuer_did(mut self, did: impl Into<String>) -> Self {
        self.issuer_did = Some(did.into());
        self
    }

    /// Constrain to a specific subject DID.
    ///
    /// Only persona credentials for this specific subject will be accepted.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::PersonaPolicy;
    ///
    /// let policy = PersonaPolicy::new()
    ///     .with_subject_did("did:lastid:user123")
    ///     .with_callback("https://example.com/callback");
    /// ```
    #[must_use]
    pub fn with_subject_did(mut self, did: impl Into<String>) -> Self {
        self.subject_did = Some(did.into());
        self
    }

    /// Set the callback URL and transition to Complete state.
    #[must_use]
    pub fn with_callback(self, url: impl Into<String>) -> PersonaPolicy<Complete> {
        PersonaPolicy {
            callback_url: Some(url.into()),
            email_domain: self.email_domain,
            issuer_did: self.issuer_did,
            subject_did: self.subject_did,
            fields: self.fields,
            optional_fields: self.optional_fields,
            _marker: PhantomData,
        }
    }
}

#[cfg(feature = "persona-policy")]
impl PolicyBuilder for PersonaPolicy<Complete> {
    fn credential_type(&self) -> &'static str {
        "LastID.Persona"
    }

    fn to_request(self) -> Result<CredentialPolicyRequest, LastIDError> {
        // SEC-001: Validate callback URL with HTTPS enforcement
        // HTTP is only allowed for localhost (development)
        if let Some(ref url) = self.callback_url {
            EndpointUrl::new(url)?;
        }

        // Validate email domain if present
        if let Some(ref domain) = self.email_domain {
            CredentialConstraint::validate_domain(domain)?;
        }

        // Validate issuer DID if present
        if let Some(ref did) = self.issuer_did {
            CredentialConstraint::validate_did(did)?;
        }

        // Validate subject DID if present
        if let Some(ref did) = self.subject_did {
            CredentialConstraint::validate_did(did)?;
        }

        // Persona requires Base -> Persona chain (FR-007)
        let mut request = CredentialPolicyRequest::with_chain(self.credential_type());
        request.callback_url = self.callback_url;

        // Build constraints
        let mut constraints_map = HashMap::new();

        if let Some(domain) = &self.email_domain {
            constraints_map.insert("email_domain".to_string(), serde_json::json!(domain));
        }

        if let Some(did) = &self.issuer_did {
            constraints_map.insert("iss".to_string(), serde_json::json!(did));
        }

        if let Some(did) = &self.subject_did {
            constraints_map.insert("sub".to_string(), serde_json::json!(did));
        }

        if !constraints_map.is_empty() {
            request.constraints = Some(constraints_map);
        }

        // Set required fields (no optional flag in OpenID4VP)
        if !self.fields.is_empty() {
            request.required_fields =
                Some(self.fields.iter().map(|f| f.as_str().to_string()).collect());
        }

        // Set optional fields (optional: true in OpenID4VP)
        if !self.optional_fields.is_empty() {
            request.optional_fields = Some(
                self.optional_fields
                    .iter()
                    .map(|f| f.as_str().to_string())
                    .collect(),
            );
        }

        Ok(request)
    }
}

#[cfg(feature = "persona-policy")]
impl Clone for PersonaPolicy<Complete> {
    fn clone(&self) -> Self {
        Self {
            callback_url: self.callback_url.clone(),
            email_domain: self.email_domain.clone(),
            issuer_did: self.issuer_did.clone(),
            subject_did: self.subject_did.clone(),
            fields: self.fields.clone(),
            optional_fields: self.optional_fields.clone(),
            _marker: PhantomData,
        }
    }
}

#[cfg(all(test, feature = "persona-policy"))]
mod tests {
    use super::*;

    #[test]
    fn test_persona_policy_build() {
        let policy = PersonaPolicy::new()
            .with_fields(&[PersonaField::DisplayName])
            .with_optional_fields(&[PersonaField::Email, PersonaField::Phone])
            .with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        assert_eq!(request.credential_type, "LastID.Persona");
        assert_eq!(
            request.required_fields,
            Some(vec!["display_name".to_string()])
        );
        assert_eq!(
            request.optional_fields,
            Some(vec!["email".to_string(), "phone".to_string()])
        );
    }

    #[test]
    fn test_persona_policy_no_fields() {
        let policy = PersonaPolicy::new().with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        assert!(request.required_fields.is_none());
        assert!(request.optional_fields.is_none());
    }

    #[test]
    fn test_persona_field_as_str() {
        assert_eq!(PersonaField::DisplayName.as_str(), "display_name");
        assert_eq!(PersonaField::Email.as_str(), "email");
        assert_eq!(PersonaField::Phone.as_str(), "phone");
        assert_eq!(PersonaField::Company.as_str(), "company");
        assert_eq!(PersonaField::JobTitle.as_str(), "job_title");
    }

    #[test]
    fn test_with_email_domain() {
        let policy = PersonaPolicy::new()
            .with_email_domain("bigcorp.com")
            .with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        let constraints = request.constraints.unwrap();
        assert_eq!(
            constraints.get("email_domain"),
            Some(&serde_json::json!("bigcorp.com"))
        );
    }

    #[test]
    fn test_with_issuer_did() {
        let policy = PersonaPolicy::new()
            .with_issuer_did("did:lastid:trusted-issuer")
            .with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        let constraints = request.constraints.unwrap();
        assert_eq!(
            constraints.get("iss"),
            Some(&serde_json::json!("did:lastid:trusted-issuer"))
        );
    }

    #[test]
    fn test_with_subject_did() {
        let policy = PersonaPolicy::new()
            .with_subject_did("did:lastid:user123")
            .with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        let constraints = request.constraints.unwrap();
        assert_eq!(
            constraints.get("sub"),
            Some(&serde_json::json!("did:lastid:user123"))
        );
    }

    #[test]
    fn test_invalid_email_domain() {
        let policy = PersonaPolicy::new()
            .with_email_domain("user@bigcorp.com")
            .with_callback("https://example.com");

        let result = policy.to_request();
        assert!(result.is_err());
    }

    #[test]
    fn test_invalid_issuer_did() {
        let policy = PersonaPolicy::new()
            .with_issuer_did("not-a-did")
            .with_callback("https://example.com");

        let result = policy.to_request();
        assert!(result.is_err());
    }

    #[test]
    fn test_combined_persona_constraints() {
        let policy = PersonaPolicy::new()
            .with_email_domain("corp.com")
            .with_issuer_did("did:lastid:issuer")
            .with_subject_did("did:lastid:subject")
            .with_fields(&[PersonaField::DisplayName])
            .with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        let constraints = request.constraints.unwrap();

        assert_eq!(
            constraints.get("email_domain"),
            Some(&serde_json::json!("corp.com"))
        );
        assert_eq!(
            constraints.get("iss"),
            Some(&serde_json::json!("did:lastid:issuer"))
        );
        assert_eq!(
            constraints.get("sub"),
            Some(&serde_json::json!("did:lastid:subject"))
        );
        assert_eq!(
            request.required_fields,
            Some(vec!["display_name".to_string()])
        );
    }
}
