//! Employment 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};

/// Employment status types.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub enum EmploymentStatus {
    /// Currently employed
    Current,
    /// Previously employed
    Former,
    /// Either current or former
    Any,
}

impl EmploymentStatus {
    /// Get the status as a string for the API.
    #[must_use]
    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::Current => "current",
            Self::Former => "former",
            Self::Any => "any",
        }
    }
}

/// Fields that can be requested in an employment credential.
///
/// These field names match the actual `LastID.Employment` schema in the IDP.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum EmploymentField {
    /// Company/employer name
    Company,
    /// Job title
    Title,
    /// Department
    Department,
    /// Employment start date
    StartDate,
    /// Employment end date (for former employment)
    EndDate,
    /// Whether this is current employment
    IsCurrent,
    /// Job responsibilities
    Responsibilities,
}

impl EmploymentField {
    /// Get the field name as a string for the API.
    #[must_use]
    pub const fn as_str(&self) -> &'static str {
        match self {
            Self::Company => "company",
            Self::Title => "title",
            Self::Department => "department",
            Self::StartDate => "start_date",
            Self::EndDate => "end_date",
            Self::IsCurrent => "is_current",
            Self::Responsibilities => "responsibilities",
        }
    }
}

/// Policy builder for employment credentials.
///
/// # Type-State Pattern
///
/// - `EmploymentPolicy<Incomplete>` - Initial state
/// - `EmploymentPolicy<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::{
///     EmploymentField, EmploymentPolicy, EmploymentStatus, PolicyBuilder,
/// };
///
/// let policy = EmploymentPolicy::new()
///     .with_employer_domain("example.com")
///     .with_status(EmploymentStatus::Current)
///     .with_fields(&[EmploymentField::Company, EmploymentField::Title])
///     .with_optional_fields(&[EmploymentField::Department])
///     .with_callback("https://example.com/callback");
///
/// let request = policy.to_request().unwrap();
/// ```
#[cfg(feature = "employment-policy")]
pub struct EmploymentPolicy<State = Incomplete> {
    callback_url: Option<String>,
    employer_domain: Option<String>,
    issuer_did: Option<String>,
    employer_did: Option<String>,
    status: Option<EmploymentStatus>,
    fields: Vec<EmploymentField>,
    optional_fields: Vec<EmploymentField>,
    _marker: PhantomData<State>,
}

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

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

    /// Constrain to a specific employer domain (e.g., "example.com").
    ///
    /// This filters credentials to those with email addresses ending in the
    /// specified domain suffix.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::EmploymentPolicy;
    ///
    /// let policy = EmploymentPolicy::new()
    ///     .with_employer_domain("acme.com")
    ///     .with_callback("https://example.com/callback");
    /// ```
    #[must_use]
    pub fn with_employer_domain(mut self, domain: impl Into<String>) -> Self {
        self.employer_domain = Some(domain.into());
        self
    }

    /// Constrain to a specific issuer DID.
    ///
    /// Only credentials issued by this DID will be accepted. This is useful
    /// for enterprise scenarios where employment verification must come from
    /// a specific trusted issuer.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::EmploymentPolicy;
    ///
    /// // Only accept credentials issued by ACME Corp's issuer
    /// let policy = EmploymentPolicy::new()
    ///     .with_issuer_did("did:lastid:acme-corp-issuer")
    ///     .with_callback("https://example.com/callback");
    /// ```
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error at build time if the DID format is
    /// invalid.
    #[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 employer DID.
    ///
    /// Only employment credentials for this specific employer will be accepted.
    /// This is more precise than `with_employer_domain` when you know the
    /// exact employer DID.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::EmploymentPolicy;
    ///
    /// // Only accept employment credentials for ACME Corp
    /// let policy = EmploymentPolicy::new()
    ///     .with_employer_did("did:lastid:acme-corp")
    ///     .with_callback("https://example.com/callback");
    /// ```
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error at build time if the DID format is
    /// invalid.
    #[must_use]
    pub fn with_employer_did(mut self, did: impl Into<String>) -> Self {
        self.employer_did = Some(did.into());
        self
    }

    /// Set the required employment status.
    #[must_use]
    pub const fn with_status(mut self, status: EmploymentStatus) -> Self {
        self.status = Some(status);
        self
    }

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

    /// Add a single required field to request.
    #[must_use]
    pub fn with_field(mut self, field: EmploymentField) -> 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: &[EmploymentField]) -> 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: EmploymentField) -> Self {
        self.optional_fields.push(field);
        self
    }

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

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

    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 employer domain if present
        if let Some(ref domain) = self.employer_domain {
            CredentialConstraint::validate_domain(domain)?;
        }

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

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

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

        // Build constraints for employer domain, DIDs, and status
        let mut constraints_map = HashMap::new();

        if let Some(domain) = &self.employer_domain {
            constraints_map.insert("employerDomain".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.employer_did {
            constraints_map.insert("employer_did".to_string(), serde_json::json!(did));
        }

        if let Some(status) = self.status {
            constraints_map.insert(
                "employmentStatus".to_string(),
                serde_json::json!(status.as_str()),
            );
        }

        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 = "employment-policy")]
impl Clone for EmploymentPolicy<Complete> {
    fn clone(&self) -> Self {
        Self {
            callback_url: self.callback_url.clone(),
            employer_domain: self.employer_domain.clone(),
            issuer_did: self.issuer_did.clone(),
            employer_did: self.employer_did.clone(),
            status: self.status,
            fields: self.fields.clone(),
            optional_fields: self.optional_fields.clone(),
            _marker: PhantomData,
        }
    }
}

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

    #[test]
    fn test_employment_policy_build() {
        let policy = EmploymentPolicy::new()
            .with_employer_domain("example.com")
            .with_status(EmploymentStatus::Current)
            .with_fields(&[EmploymentField::Company, EmploymentField::Title])
            .with_optional_fields(&[EmploymentField::Department])
            .with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        assert_eq!(request.credential_type, "LastID.Employment");
        assert!(request.constraints.is_some());

        let constraints = request.constraints.unwrap();
        assert!(constraints.get("employerDomain").is_some());
        assert!(constraints.get("employmentStatus").is_some());

        // Fields are now in required_fields/optional_fields, not constraints
        assert_eq!(
            request.required_fields,
            Some(vec!["company".to_string(), "title".to_string()])
        );
        assert_eq!(
            request.optional_fields,
            Some(vec!["department".to_string()])
        );
    }

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

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

    #[test]
    fn test_invalid_employer_domain() {
        let policy = EmploymentPolicy::new()
            .with_employer_domain("user@example.com") // Invalid - contains @
            .with_callback("https://example.com");

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

    #[test]
    fn test_employment_status_as_str() {
        assert_eq!(EmploymentStatus::Current.as_str(), "current");
        assert_eq!(EmploymentStatus::Former.as_str(), "former");
        assert_eq!(EmploymentStatus::Any.as_str(), "any");
    }

    #[test]
    fn test_employment_field_as_str() {
        assert_eq!(EmploymentField::Company.as_str(), "company");
        assert_eq!(EmploymentField::Title.as_str(), "title");
        assert_eq!(EmploymentField::StartDate.as_str(), "start_date");
        assert_eq!(EmploymentField::EndDate.as_str(), "end_date");
        assert_eq!(EmploymentField::IsCurrent.as_str(), "is_current");
    }

    #[test]
    fn test_with_issuer_did() {
        let policy = EmploymentPolicy::new()
            .with_issuer_did("did:lastid:acme-corp")
            .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:acme-corp"))
        );
    }

    #[test]
    fn test_with_employer_did() {
        let policy = EmploymentPolicy::new()
            .with_employer_did("did:lastid:employer123")
            .with_callback("https://example.com");

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

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

        let result = policy.to_request();
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("did:"));
    }

    #[test]
    fn test_invalid_employer_did() {
        let policy = EmploymentPolicy::new()
            .with_employer_did("also-not-a-did")
            .with_callback("https://example.com");

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

    #[test]
    fn test_combined_enterprise_constraints() {
        let policy = EmploymentPolicy::new()
            .with_employer_domain("acme.com")
            .with_issuer_did("did:lastid:acme-issuer")
            .with_employer_did("did:lastid:acme-corp")
            .with_status(EmploymentStatus::Current)
            .with_callback("https://example.com");

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

        assert_eq!(
            constraints.get("employerDomain"),
            Some(&serde_json::json!("acme.com"))
        );
        assert_eq!(
            constraints.get("iss"),
            Some(&serde_json::json!("did:lastid:acme-issuer"))
        );
        assert_eq!(
            constraints.get("employer_did"),
            Some(&serde_json::json!("did:lastid:acme-corp"))
        );
        assert_eq!(
            constraints.get("employmentStatus"),
            Some(&serde_json::json!("current"))
        );
    }
}
