//! Base credential policy builder.

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

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

/// Policy builder for base credentials (user DID only).
///
/// # Type-State Pattern
///
/// This builder uses the type-state pattern to ensure compile-time correctness:
/// - `BaseCredentialPolicy<Incomplete>` - Initial state, callback not set
/// - `BaseCredentialPolicy<Complete>` - Ready to build
///
/// # Example
///
/// ```rust,no_run
/// use lastid_sdk::policies::BaseCredentialPolicy;
/// use lastid_sdk::PolicyBuilder;
///
/// let policy = BaseCredentialPolicy::new()
///     .with_callback("https://example.com/callback");
///
/// // This compiles because we're in Complete state
/// let request = policy.to_request().unwrap();
/// ```
///
/// # `OpenID4VP` Parameters
///
/// The builder supports standard `OpenID4VP` parameters for CSRF protection
/// and cryptographic binding:
///
/// ```rust,no_run
/// use lastid_sdk::policies::BaseCredentialPolicy;
///
/// let policy = BaseCredentialPolicy::new()
///     .with_state("csrf-token")       // CSRF protection
///     .with_nonce("binding-nonce")    // Key binding
///     .with_callback("https://example.com/callback");
/// ```
///
/// # Compile-Time Safety
///
/// The following code will NOT compile because `to_request()` is only
/// available on `BaseCredentialPolicy<Complete>`:
///
/// ```compile_fail
/// use lastid_sdk::policies::BaseCredentialPolicy;
///
/// let policy = BaseCredentialPolicy::new();
/// let request = policy.to_request(); // ERROR: method not found
/// ```
pub struct BaseCredentialPolicy<State = Incomplete> {
    callback_url: Option<String>,
    state: Option<String>,
    nonce: Option<String>,
    subject_did: Option<String>,
    issuer_did: Option<String>,
    claim_constraints: HashMap<String, serde_json::Value>,
    _marker: PhantomData<State>,
}

impl Default for BaseCredentialPolicy<Incomplete> {
    fn default() -> Self {
        Self::new()
    }
}

impl BaseCredentialPolicy<Incomplete> {
    /// Create a new base credential policy builder.
    #[must_use]
    pub fn new() -> Self {
        Self {
            callback_url: None,
            state: None,
            nonce: None,
            subject_did: None,
            issuer_did: None,
            claim_constraints: HashMap::new(),
            _marker: PhantomData,
        }
    }

    /// Set the `OpenID4VP` state parameter for CSRF protection.
    ///
    /// The state value is returned unchanged in the authorization response,
    /// allowing the client to verify the response matches the request.
    #[must_use]
    pub fn with_state(mut self, state: impl Into<String>) -> Self {
        self.state = Some(state.into());
        self
    }

    /// Set the `OpenID4VP` nonce for cryptographic binding.
    ///
    /// The nonce is used in key binding proofs to ensure freshness and
    /// prevent replay attacks.
    #[must_use]
    pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
        self.nonce = Some(nonce.into());
        self
    }

    /// Constrain to a specific subject DID.
    ///
    /// Only credentials for this specific subject will be accepted. This is
    /// useful for re-authentication scenarios where you need to verify the
    /// same user who previously authenticated.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::policies::BaseCredentialPolicy;
    ///
    /// // Re-authenticate a known user
    /// let policy = BaseCredentialPolicy::new()
    ///     .with_subject_did("did:lastid:user123")
    ///     .with_callback("https://example.com/callback");
    /// ```
    ///
    /// # Errors
    ///
    /// Returns `PolicyValidation` error at build time if the DID format is
    /// invalid.
    #[must_use]
    pub fn with_subject_did(mut self, did: impl Into<String>) -> Self {
        self.subject_did = Some(did.into());
        self
    }

    /// Constrain to a specific issuer DID.
    ///
    /// Only credentials issued by this DID will be accepted. This is useful
    /// when you need to accept credentials only from a specific trusted issuer.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::policies::BaseCredentialPolicy;
    ///
    /// // Only accept credentials from the LastID root issuer
    /// let policy = BaseCredentialPolicy::new()
    ///     .with_issuer_did("did:lastid:root-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
    }

    /// Add a custom claim constraint.
    ///
    /// This allows setting arbitrary constraints on credential claims that will
    /// be validated by the IDP. Use this for advanced filtering scenarios not
    /// covered by the specific constraint methods.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use lastid_sdk::policies::BaseCredentialPolicy;
    ///
    /// let policy = BaseCredentialPolicy::new()
    ///     .with_claim_constraint("trust_level", serde_json::json!(3))
    ///     .with_callback("https://example.com/callback");
    /// ```
    #[must_use]
    pub fn with_claim_constraint(
        mut self,
        claim: impl Into<String>,
        value: serde_json::Value,
    ) -> Self {
        self.claim_constraints.insert(claim.into(), value);
        self
    }

    /// Set the callback URL and transition to Complete state.
    ///
    /// The callback URL is where the user will be redirected after completing
    /// the credential request.
    #[must_use]
    pub fn with_callback(self, url: impl Into<String>) -> BaseCredentialPolicy<Complete> {
        BaseCredentialPolicy {
            callback_url: Some(url.into()),
            state: self.state,
            nonce: self.nonce,
            subject_did: self.subject_did,
            issuer_did: self.issuer_did,
            claim_constraints: self.claim_constraints,
            _marker: PhantomData,
        }
    }
}

impl BaseCredentialPolicy<Complete> {
    /// Set the `OpenID4VP` state parameter for CSRF protection (on Complete
    /// state).
    ///
    /// The state value is returned unchanged in the authorization response,
    /// allowing the client to verify the response matches the request.
    #[must_use]
    pub fn with_state(mut self, state: impl Into<String>) -> Self {
        self.state = Some(state.into());
        self
    }

    /// Set the `OpenID4VP` nonce for cryptographic binding (on Complete state).
    ///
    /// The nonce is used in key binding proofs to ensure freshness and
    /// prevent replay attacks.
    #[must_use]
    pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
        self.nonce = Some(nonce.into());
        self
    }

    /// Constrain to a specific subject DID (on Complete state).
    #[must_use]
    pub fn with_subject_did(mut self, did: impl Into<String>) -> Self {
        self.subject_did = Some(did.into());
        self
    }

    /// Constrain to a specific issuer DID (on Complete state).
    #[must_use]
    pub fn with_issuer_did(mut self, did: impl Into<String>) -> Self {
        self.issuer_did = Some(did.into());
        self
    }

    /// Add a custom claim constraint (on Complete state).
    #[must_use]
    pub fn with_claim_constraint(
        mut self,
        claim: impl Into<String>,
        value: serde_json::Value,
    ) -> Self {
        self.claim_constraints.insert(claim.into(), value);
        self
    }
}

impl PolicyBuilder for BaseCredentialPolicy<Complete> {
    fn credential_type(&self) -> &'static str {
        "LastID.Base"
    }

    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 subject DID if present
        if let Some(ref did) = self.subject_did {
            CredentialConstraint::validate_did(did)?;
        }

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

        // Base credential is a root credential, so with_chain creates single-element
        // chain
        let mut request = CredentialPolicyRequest::with_chain(self.credential_type());
        request.callback_url = self.callback_url;
        request.state = self.state;
        request.nonce = self.nonce;

        // Build constraints from subject DID, issuer DID, and claim constraints
        let mut constraints_map = self.claim_constraints;

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

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

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

        Ok(request)
    }
}

impl Clone for BaseCredentialPolicy<Complete> {
    fn clone(&self) -> Self {
        Self {
            callback_url: self.callback_url.clone(),
            state: self.state.clone(),
            nonce: self.nonce.clone(),
            subject_did: self.subject_did.clone(),
            issuer_did: self.issuer_did.clone(),
            claim_constraints: self.claim_constraints.clone(),
            _marker: PhantomData,
        }
    }
}

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

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

        let request = policy.to_request().unwrap();
        assert_eq!(request.credential_type, "LastID.Base");
        assert_eq!(request.callback_url, Some("https://example.com".into()));
        assert!(request.constraints.is_none());
    }

    #[test]
    fn test_invalid_callback_url() {
        let policy = BaseCredentialPolicy::new().with_callback("not-a-url");

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

    #[test]
    fn test_http_localhost_callback_allowed() {
        // SEC-001: HTTP is allowed for localhost (development)
        let policy = BaseCredentialPolicy::new().with_callback("http://localhost:3000/callback");

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

    #[test]
    fn test_http_remote_callback_rejected() {
        // SEC-001: HTTP to remote hosts should be rejected
        let policy = BaseCredentialPolicy::new().with_callback("http://example.com/callback");

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

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

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

    #[test]
    fn test_policy_is_clone() {
        let policy = BaseCredentialPolicy::new().with_callback("https://example.com");
        let cloned = policy.clone();

        let request = cloned.to_request().unwrap();
        assert_eq!(request.callback_url, Some("https://example.com".into()));
    }

    #[test]
    fn test_policy_with_state_and_nonce() {
        let policy = BaseCredentialPolicy::new()
            .with_state("csrf-token-123")
            .with_nonce("binding-nonce-456")
            .with_callback("https://example.com");

        let request = policy.to_request().unwrap();
        assert_eq!(request.state, Some("csrf-token-123".into()));
        assert_eq!(request.nonce, Some("binding-nonce-456".into()));
    }

    #[test]
    fn test_policy_state_nonce_after_callback() {
        // State and nonce can also be set after with_callback
        let policy = BaseCredentialPolicy::new()
            .with_callback("https://example.com")
            .with_state("csrf-token")
            .with_nonce("nonce");

        let request = policy.to_request().unwrap();
        assert_eq!(request.state, Some("csrf-token".into()));
        assert_eq!(request.nonce, Some("nonce".into()));
    }

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

        let request = policy.to_request().unwrap();
        assert!(request.state.is_none());
        assert!(request.nonce.is_none());
    }

    #[test]
    fn test_with_subject_did() {
        let policy = BaseCredentialPolicy::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_with_issuer_did() {
        let policy = BaseCredentialPolicy::new()
            .with_issuer_did("did:lastid:issuer456")
            .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:issuer456"))
        );
    }

    #[test]
    fn test_with_claim_constraint() {
        let policy = BaseCredentialPolicy::new()
            .with_claim_constraint("trust_level", serde_json::json!(3))
            .with_callback("https://example.com");

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

    #[test]
    fn test_invalid_subject_did() {
        let policy = BaseCredentialPolicy::new()
            .with_subject_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_issuer_did() {
        let policy = BaseCredentialPolicy::new()
            .with_issuer_did("also-not-a-did")
            .with_callback("https://example.com");

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

    #[test]
    fn test_combined_constraints() {
        let policy = BaseCredentialPolicy::new()
            .with_subject_did("did:lastid:user123")
            .with_issuer_did("did:lastid:issuer456")
            .with_claim_constraint("custom_field", serde_json::json!("value"))
            .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"))
        );
        assert_eq!(
            constraints.get("iss"),
            Some(&serde_json::json!("did:lastid:issuer456"))
        );
        assert_eq!(
            constraints.get("custom_field"),
            Some(&serde_json::json!("value"))
        );
    }

    #[test]
    fn test_constraints_after_callback() {
        // Constraints can also be set after with_callback
        let policy = BaseCredentialPolicy::new()
            .with_callback("https://example.com")
            .with_subject_did("did:lastid:user789");

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