//! Client builder with type-state pattern.

use std::marker::PhantomData;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;

use super::LastIDClient;
use crate::config::SDKConfig;
use crate::crypto::DPoPKeyPair;
use crate::error::LastIDError;

/// Type-state marker: Configuration not yet provided
pub struct NoConfig;

/// Type-state marker: Configuration provided
pub struct HasConfig;

/// Builder for `LastIDClient` with type-state pattern.
///
/// # Type-State Pattern
///
/// The builder enforces that configuration must be provided before building:
/// - `ClientBuilder<NoConfig>` - Initial state, no config
/// - `ClientBuilder<HasConfig>` - Config provided, can build
///
/// # Example
///
/// ```rust,no_run
/// use lastid_sdk::ClientBuilder;
///
/// # async fn example() -> Result<(), lastid_sdk::LastIDError> {
/// // Auto-discover configuration
/// let client = ClientBuilder::new().with_auto_config()?.build()?;
///
/// // Or use explicit configuration
/// use lastid_sdk::SDKConfig;
///
/// let config = SDKConfig::builder()
///     .idp_endpoint("https://human.lastid.co")
///     .build()?;
///
/// let client = ClientBuilder::new().with_config(config).build()?;
/// # Ok(())
/// # }
/// ```
///
/// # Serverless Keypair Persistence
///
/// For serverless deployments, see
/// [`with_keypair`](ClientBuilder::with_keypair) to restore a persisted `DPoP`
/// keypair across invocations.
pub struct ClientBuilder<State> {
    config: Option<SDKConfig>,
    keypair: Option<DPoPKeyPair>,
    _marker: PhantomData<State>,
}

impl Default for ClientBuilder<NoConfig> {
    fn default() -> Self {
        Self::new()
    }
}

impl ClientBuilder<NoConfig> {
    /// Create a new client builder.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            config: None,
            keypair: None,
            _marker: PhantomData,
        }
    }

    /// Auto-discover configuration from filesystem and environment.
    ///
    /// Searches for TOML files in standard locations:
    /// 1. `./lastid.toml` (current directory)
    /// 2. `~/.config/lastid/config.toml` (user config)
    /// 3. `/etc/lastid/config.toml` (system config, Unix only)
    ///
    /// Environment variables override file-based configuration.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError::Config` if no configuration is found.
    #[must_use = "this method consumes the builder and returns a configured builder that should be used"]
    pub fn with_auto_config(self) -> Result<ClientBuilder<HasConfig>, LastIDError> {
        let config = SDKConfig::discover()?;
        Ok(ClientBuilder {
            config: Some(config),
            keypair: self.keypair,
            _marker: PhantomData,
        })
    }

    /// Use an explicit configuration.
    #[must_use]
    pub fn with_config(self, config: SDKConfig) -> ClientBuilder<HasConfig> {
        ClientBuilder {
            config: Some(config),
            keypair: self.keypair,
            _marker: PhantomData,
        }
    }

    /// Load configuration from a TOML file.
    ///
    /// # Note
    ///
    /// This method is only available on native platforms (not WASM).
    /// In WASM, use [`with_config`](Self::with_config) or store config in
    /// localStorage.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError::Config` if the file cannot be read or parsed.
    #[cfg(not(target_arch = "wasm32"))]
    #[must_use = "this method consumes the builder and returns a configured builder that should be used"]
    pub fn with_toml_file(self, path: &Path) -> Result<ClientBuilder<HasConfig>, LastIDError> {
        let mut config = SDKConfig::from_file(path)?;
        config.apply_env_overrides();
        Ok(ClientBuilder {
            config: Some(config),
            keypair: self.keypair,
            _marker: PhantomData,
        })
    }
}

impl ClientBuilder<HasConfig> {
    /// Use a pre-generated or restored `DPoP` keypair.
    ///
    /// This is useful for serverless deployments where the keypair needs to be
    /// persisted across invocations. The keypair can be serialized to JSON and
    /// stored in a secrets manager, then deserialized and passed to this
    /// method.
    ///
    /// # Security
    ///
    /// The keypair contains the private key. Ensure it is stored encrypted at
    /// rest (e.g., AWS Secrets Manager, `HashiCorp` Vault, encrypted
    /// environment variables).
    ///
    /// # Example
    ///
    /// Using a generated keypair:
    ///
    /// ```rust,no_run
    /// use lastid_sdk::crypto::DPoPKeyPair;
    /// use lastid_sdk::{ClientBuilder, SDKConfig};
    ///
    /// # fn example() -> Result<(), lastid_sdk::LastIDError> {
    /// let keypair = DPoPKeyPair::generate()?;
    ///
    /// let config = SDKConfig::builder()
    ///     .idp_endpoint("https://human.lastid.co")
    ///     .client_id("my-app")
    ///     .build()?;
    ///
    /// let client = ClientBuilder::new()
    ///     .with_config(config)
    ///     .with_keypair(keypair)
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// Serializing and restoring a keypair (requires `keypair-serialization`
    /// feature):
    #[cfg_attr(feature = "keypair-serialization", doc = "```rust,no_run")]
    #[cfg_attr(not(feature = "keypair-serialization"), doc = "```rust,ignore")]
    /// use lastid_sdk::{ClientBuilder, SDKConfig};
    /// use lastid_sdk::crypto::DPoPKeyPair;
    ///
    /// # fn example() -> Result<(), lastid_sdk::LastIDError> {
    /// // Generate and serialize a keypair for initial deployment
    /// let keypair = DPoPKeyPair::generate()?;
    /// let serialized = serde_json::to_string(&keypair).unwrap();
    /// // Store `serialized` in your secrets manager...
    ///
    /// // Later, in a serverless function cold start:
    /// let restored: DPoPKeyPair = serde_json::from_str(&serialized).unwrap();
    ///
    /// let config = SDKConfig::builder()
    ///     .idp_endpoint("https://human.lastid.co")
    ///     .client_id("my-app")
    ///     .build()?;
    ///
    /// let client = ClientBuilder::new()
    ///     .with_config(config)
    ///     .with_keypair(restored)
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    #[must_use]
    pub fn with_keypair(mut self, keypair: DPoPKeyPair) -> Self {
        self.keypair = Some(keypair);
        self
    }

    /// Build the client.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError` if:
    /// - Configuration validation fails
    /// - HTTP client creation fails
    /// - `DPoP` key initialization fails
    #[must_use = "this method consumes the builder and returns a client that should be used"]
    pub fn build(self) -> Result<LastIDClient, LastIDError> {
        let config = self.config.expect("Config must be set in HasConfig state");
        config.validate()?;
        LastIDClient::with_optional_keypair(config, self.keypair)
    }
}

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

    #[test]
    fn test_builder_with_config() {
        let config = SDKConfig::builder()
            .idp_endpoint("https://test.lastid.co")
            .client_id("test-client")
            .build()
            .unwrap();

        // Build the client and verify configuration is correctly applied
        let client = ClientBuilder::new().with_config(config).build().unwrap();
        assert_eq!(client.config().idp_endpoint, "https://test.lastid.co");
        assert_eq!(client.config().client_id, "test-client");
    }

    #[test]
    fn test_builder_auto_config_handles_missing_config() {
        // When no config file exists and no env vars, should return an error.
        // Note: We don't clear env vars since remove_var is unsafe in Rust 2024
        // edition. Instead, we just verify the result handling is correct.
        let result = ClientBuilder::new().with_auto_config();

        // In CI/test environments without config files, this should fail.
        // We verify it either succeeds (config exists) or fails with Config error.
        match result {
            Ok(builder) => {
                // Config was found in environment - verify we can attempt to build
                // Build may fail due to validation, but the state transition worked
                let _build_result = builder.build();
            }
            Err(e) => {
                // No config found - this is expected in clean test environments
                assert!(matches!(e, LastIDError::Config(_)));
            }
        }
    }

    #[test]
    fn test_builder_with_keypair() {
        let config = SDKConfig::builder()
            .idp_endpoint("https://test.lastid.co")
            .client_id("test-client")
            .build()
            .unwrap();

        // Generate a keypair
        let keypair = DPoPKeyPair::generate().unwrap();
        let original_thumbprint = keypair.thumbprint().to_string();

        // Build client with the keypair
        let client = ClientBuilder::new()
            .with_config(config)
            .with_keypair(keypair)
            .build()
            .unwrap();

        // Verify the client was created with the correct configuration
        assert_eq!(client.config().idp_endpoint, "https://test.lastid.co");
        assert_eq!(client.config().client_id, "test-client");
        // Verify the keypair thumbprint is preserved (client uses our provided keypair)
        assert_eq!(client.dpop_thumbprint(), original_thumbprint);
    }

    #[cfg(feature = "keypair-serialization")]
    #[test]
    fn test_builder_with_serialized_keypair_roundtrip() {
        // This test simulates the serverless use case:
        // 1. Generate keypair on first invocation
        // 2. Serialize and "store" it
        // 3. On subsequent invocation, deserialize and use it

        // First invocation: generate and serialize
        let keypair = DPoPKeyPair::generate().unwrap();
        let thumbprint = keypair.thumbprint().to_string();
        let serialized = serde_json::to_string(&keypair).unwrap();

        // Subsequent invocation: deserialize and use
        let restored: DPoPKeyPair = serde_json::from_str(&serialized).unwrap();
        assert_eq!(restored.thumbprint(), thumbprint);

        let config = SDKConfig::builder()
            .idp_endpoint("https://test.lastid.co")
            .client_id("test-client")
            .build()
            .unwrap();

        // Should successfully build with restored keypair
        let result = ClientBuilder::new()
            .with_config(config)
            .with_keypair(restored)
            .build();

        assert!(result.is_ok());
    }
}
