//! Main `LastID` client implementation.

// WASM is single-threaded, so futures don't need to be Send
#![cfg_attr(target_arch = "wasm32", allow(clippy::future_not_send))]

#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;

use chrono::{TimeZone, Utc};
#[cfg(feature = "tracing")]
use tracing::{debug, info, instrument, warn};

use crate::auth::{TokenManager, TokenManagerConfig, TokenManagerTrait};
use crate::config::SDKConfig;
use crate::crypto::DPoPKeyPair;
use crate::error::LastIDError;
use crate::http::{HttpIdpClient, IdpClient};
use crate::policies::PolicyBuilder;
use crate::shared::SharedPtr;
use crate::trust_registry::{IssuerInfo, TrustRegistry, TrustRegistryClient};
use crate::types::{CredentialRequestResponse, RequestId, RequestStatus, VerifiedCredential};
use crate::verification::VerifiablePresentation;

/// Main SDK client for `LastID` IDP integration.
///
/// Handles credential requests, polling, verification, and trust registry
/// validation. Requires a caller-provided tokio runtime (native) or runs on
/// wasm-bindgen-futures (wasm).
///
/// # Example
///
/// ```rust,no_run
/// use lastid_sdk::policies::BaseCredentialPolicy;
/// use lastid_sdk::ClientBuilder;
///
/// # async fn example() -> Result<(), lastid_sdk::LastIDError> {
/// let client = ClientBuilder::new().with_auto_config()?.build()?;
///
/// let policy = BaseCredentialPolicy::new()
///     .with_callback("https://example.com/callback");
///
/// let response = client.request_credential(policy).await?;
/// println!("Request ID: {}", response.request_id);
/// # Ok(())
/// # }
/// ```
pub struct LastIDClient {
    config: SDKConfig,
    idp_client: SharedPtr<dyn IdpClient>,
    trust_registry: SharedPtr<dyn TrustRegistry>,
    token_manager: SharedPtr<TokenManager>,
}

impl LastIDClient {
    /// Create a new client from configuration.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError` if:
    /// - HTTP client creation fails
    /// - Trust registry client creation fails
    /// - Token manager creation fails
    pub fn new(config: SDKConfig) -> Result<Self, LastIDError> {
        Self::with_optional_keypair(config, None)
    }

    /// Create a new client with an optional pre-generated `DPoP` keypair.
    ///
    /// This is useful for serverless deployments where the keypair needs to
    /// persist across invocations. If `keypair` is `None`, a new keypair is
    /// generated.
    ///
    /// # Errors
    ///
    /// Returns `LastIDError` if:
    /// - HTTP client creation fails
    /// - Trust registry client creation fails
    /// - Token manager creation fails
    pub fn with_optional_keypair(
        config: SDKConfig,
        keypair: Option<DPoPKeyPair>,
    ) -> Result<Self, LastIDError> {
        // Create HTTP client for IDP with full network configuration
        // This includes proxy settings, timeouts, connection pooling, and correlation
        // IDs
        let idp_client = HttpIdpClient::with_network_config(
            config.idp_endpoint.clone(),
            &config.network,
            config.retry.clone(),
        )
        .map_err(|e| LastIDError::config(format!("Failed to create IDP client: {e}")))?;

        // Create trust registry client
        let trust_registry_endpoint = config.effective_trust_registry_endpoint();
        let trust_registry =
            TrustRegistryClient::new(trust_registry_endpoint, &config.cache, config.retry.clone())
                .map_err(|e| {
                    LastIDError::config(format!("Failed to create trust registry client: {e}"))
                })?;

        // Create token manager for OAuth authentication
        let token_manager_config = TokenManagerConfig {
            client_id: config.client_id.clone(),
            refresh_buffer_seconds: 30,
        };
        let token_manager = TokenManager::with_optional_keypair(
            &config.idp_endpoint,
            token_manager_config,
            keypair,
        )
        .map_err(|e| LastIDError::config(format!("Failed to create token manager: {e}")))?;

        Ok(Self {
            config,
            idp_client: SharedPtr::new(idp_client),
            trust_registry: SharedPtr::new(trust_registry),
            token_manager: SharedPtr::new(token_manager),
        })
    }

    /// Create a builder for configuring the client.
    #[must_use]
    pub const fn builder() -> super::ClientBuilder<super::builder::NoConfig> {
        super::ClientBuilder::new()
    }

    /// Get the SDK configuration.
    #[must_use]
    pub const fn config(&self) -> &SDKConfig {
        &self.config
    }

    /// Get the `DPoP` key thumbprint for this client.
    ///
    /// The thumbprint is a base64url-encoded SHA-256 hash of the public key,
    /// used to bind access tokens to this client's `DPoP` keypair.
    /// This is useful for debugging and verifying key persistence in serverless
    /// deployments.
    #[must_use]
    pub fn dpop_thumbprint(&self) -> &str {
        use crate::auth::TokenManagerTrait;
        self.token_manager.thumbprint()
    }

    /// Request a credential presentation from the IDP.
    /// Returns `CredentialRequestResponse` with `request_uri` for QR code
    /// display.
    #[cfg_attr(
        feature = "tracing",
        instrument(skip(self, policy), fields(credential_type))
    )]
    pub async fn request_credential<P>(
        &self,
        policy: P,
    ) -> Result<CredentialRequestResponse, LastIDError>
    where
        P: PolicyBuilder,
    {
        #[cfg(feature = "tracing")]
        {
            tracing::Span::current().record("credential_type", policy.credential_type());
            info!(
                credential_type = policy.credential_type(),
                "Starting credential request"
            );
        }
        // Build policy request and inject client_id from SDK config
        let mut policy_request = policy.to_request()?;
        policy_request.client_id.clone_from(&self.config.client_id);

        // Get DPoP-bound access token from token manager
        #[cfg(feature = "tracing")]
        debug!("Acquiring OAuth token for authenticated request");

        let access_token = self.token_manager.get_token().await?;

        // Generate DPoP proof for the protected request with ath (access token hash)
        // claim
        let request_url = format!("{}/v1/verify/request", self.config.idp_endpoint);
        let dpop_proof =
            self.token_manager
                .create_dpop_proof("POST", &request_url, Some(&access_token))?;

        // Send request with DPoP-bound token and proof
        #[cfg(feature = "tracing")]
        debug!("Sending credential request to IDP with DPoP proof");

        let response = self
            .idp_client
            .request_credential(policy_request, &access_token, &dpop_proof)
            .await?;

        #[cfg(feature = "tracing")]
        info!(
            request_id = %response.request_id,
            request_uri = %response.request_uri,
            expires_in = response.expires_in,
            "Credential request created successfully"
        );

        Ok(response)
    }

    /// Poll for credential request status once. Cancellation-safe.
    #[cfg_attr(feature = "tracing", instrument(skip(self), fields(request_id = %request_id)))]
    pub async fn poll_request(&self, request_id: &RequestId) -> Result<RequestStatus, LastIDError> {
        #[cfg(feature = "tracing")]
        debug!("Polling request status");

        let status = self
            .idp_client
            .poll_status(request_id, &self.config.client_id)
            .await?;

        #[cfg(feature = "tracing")]
        debug!(status = ?status, "Received status");

        Ok(status)
    }

    /// Poll for credential completion with timeout and exponential backoff.
    /// Cancellation-safe: dropping the future stops polling cleanly.
    #[cfg(not(target_arch = "wasm32"))]
    #[cfg_attr(feature = "tracing", instrument(skip(self), fields(request_id = %request_id)))]
    #[allow(clippy::cast_possible_truncation)]
    pub async fn poll_for_completion(
        &self,
        request_id: &RequestId,
    ) -> Result<RequestStatus, LastIDError> {
        let start = std::time::Instant::now();
        let timeout = Duration::from_secs(self.config.polling.max_duration_seconds);
        let mut interval = Duration::from_millis(self.config.polling.initial_interval_ms);

        #[cfg(feature = "tracing")]
        info!(
            timeout_seconds = self.config.polling.max_duration_seconds,
            "Starting poll"
        );

        loop {
            if start.elapsed() >= timeout {
                #[cfg(feature = "tracing")]
                warn!(elapsed = ?start.elapsed(), "Polling timeout");
                return Err(LastIDError::Timeout(start.elapsed().as_secs()));
            }

            let status = self.poll_request(request_id).await?;
            if status.is_terminal() {
                #[cfg(feature = "tracing")]
                info!(status = ?status, "Terminal state reached");
                return Ok(status);
            }

            #[cfg(feature = "tracing")]
            debug!(interval_ms = interval.as_millis() as u64, "Waiting");

            tokio::time::sleep(interval).await;
            interval = Duration::from_millis(
                self.config
                    .polling
                    .next_interval(interval.as_millis() as u64),
            );
        }
    }

    /// Poll for credential completion with timeout (WASM version).
    #[cfg(target_arch = "wasm32")]
    #[cfg_attr(feature = "tracing", instrument(skip(self), fields(request_id = %request_id)))]
    #[allow(
        clippy::cast_precision_loss,
        clippy::cast_possible_truncation,
        clippy::cast_sign_loss
    )]
    pub async fn poll_for_completion(
        &self,
        request_id: &RequestId,
    ) -> Result<RequestStatus, LastIDError> {
        let start_ms = js_sys::Date::now();
        let timeout_ms = self.config.polling.max_duration_seconds as f64 * 1000.0;
        let mut interval_ms = self.config.polling.initial_interval_ms;

        loop {
            let elapsed_ms = js_sys::Date::now() - start_ms;
            if elapsed_ms >= timeout_ms {
                return Err(LastIDError::Timeout((elapsed_ms / 1000.0) as u64));
            }

            let status = self.poll_request(request_id).await?;
            if status.is_terminal() {
                return Ok(status);
            }

            wasm_bindgen_futures::JsFuture::from(js_sys::Promise::new(&mut |resolve, _| {
                web_sys::window()
                    .expect("no window")
                    .set_timeout_with_callback_and_timeout_and_arguments_0(
                        &resolve,
                        interval_ms as i32,
                    )
                    .expect("setTimeout failed");
            }))
            .await
            .expect("setTimeout promise failed");

            interval_ms = self.config.polling.next_interval(interval_ms);
        }
    }

    /// Subscribe via WebSocket for instant status updates.
    /// Falls back to HTTP polling if WebSocket fails.
    #[cfg(all(feature = "websocket", not(target_arch = "wasm32")))]
    #[cfg_attr(feature = "tracing", instrument(skip(self), fields(request_id = %request_id)))]
    pub async fn subscribe_for_completion(
        &self,
        request_id: &RequestId,
    ) -> Result<RequestStatus, LastIDError> {
        use crate::http::websocket::{StatusSubscription, create_transport};

        if !self.config.websocket.enabled {
            return self.poll_for_completion(request_id).await;
        }

        let Ok(transport) = create_transport(
            &self.config.idp_endpoint,
            request_id,
            &self.config.websocket,
        ) else {
            #[cfg(feature = "tracing")]
            warn!(request_id = %request_id, "WebSocket transport creation failed, falling back to HTTP polling");
            return self.poll_for_completion(request_id).await;
        };

        let mut sub =
            StatusSubscription::new(transport, self.config.websocket.clone(), request_id.clone());
        if sub.connect().await.is_err() {
            #[cfg(feature = "tracing")]
            warn!(request_id = %request_id, "WebSocket connection failed, falling back to HTTP polling");
            return self.poll_for_completion(request_id).await;
        }

        match sub.wait_for_completion().await {
            Ok(status) => {
                let _ = sub.disconnect().await;
                Ok(status)
            }
            Err(LastIDError::WebSocket(crate::error::WebSocketError::ReconnectionExhausted {
                ..
            })) => {
                #[cfg(feature = "tracing")]
                warn!(request_id = %request_id, "WebSocket reconnection exhausted, falling back to HTTP polling");
                self.poll_for_completion(request_id).await
            }
            Err(e) => Err(e),
        }
    }

    /// Subscribe via WebSocket (WASM version).
    #[cfg(all(feature = "websocket", target_arch = "wasm32"))]
    #[cfg_attr(feature = "tracing", instrument(skip(self), fields(request_id = %request_id)))]
    pub async fn subscribe_for_completion(
        &self,
        request_id: &RequestId,
    ) -> Result<RequestStatus, LastIDError> {
        use crate::http::websocket::{StatusSubscription, create_transport};

        if !self.config.websocket.enabled {
            return self.poll_for_completion(request_id).await;
        }

        let Ok(transport) = create_transport(
            &self.config.idp_endpoint,
            request_id,
            &self.config.websocket,
        ) else {
            #[cfg(feature = "tracing")]
            warn!(request_id = %request_id, "WebSocket transport creation failed, falling back to HTTP polling");
            return self.poll_for_completion(request_id).await;
        };

        let mut sub =
            StatusSubscription::new(transport, self.config.websocket.clone(), request_id.clone());
        if sub.connect().await.is_err() {
            #[cfg(feature = "tracing")]
            warn!(request_id = %request_id, "WebSocket connection failed, falling back to HTTP polling");
            return self.poll_for_completion(request_id).await;
        }

        match sub.wait_for_completion().await {
            Ok(status) => {
                let _ = sub.disconnect().await;
                Ok(status)
            }
            Err(LastIDError::WebSocket(crate::error::WebSocketError::ReconnectionExhausted {
                ..
            })) => {
                #[cfg(feature = "tracing")]
                warn!(request_id = %request_id, "WebSocket reconnection exhausted, falling back to HTTP polling");
                self.poll_for_completion(request_id).await
            }
            Err(e) => Err(e),
        }
    }

    /// Verify a credential presentation (SD-JWT).
    /// Validates issuer against trust registry and extracts claims.
    #[cfg_attr(feature = "tracing", instrument(skip(self, presentation)))]
    pub async fn verify_presentation(
        &self,
        presentation: &str,
    ) -> Result<VerifiedCredential, LastIDError> {
        #[cfg(feature = "tracing")]
        info!("Verifying presentation");
        // Parse SD-JWT
        let vp = VerifiablePresentation::parse(presentation)?;

        // Extract issuer DID
        let issuer_did = vp.issuer_did()?;

        // Validate issuer via trust registry
        #[cfg(feature = "tracing")]
        debug!(
            issuer_did = issuer_did,
            "Validating issuer against trust registry"
        );

        let issuer_info = self.trust_registry.validate_issuer(issuer_did).await?;

        #[cfg(feature = "tracing")]
        debug!(
            issuer_status = ?issuer_info.status,
            organization = %issuer_info.organization_name,
            "Issuer validated"
        );

        // Validate credential type against issuer's permitted types
        let credential_type = vp.credential_type()?;
        if !issuer_info.can_issue(credential_type) {
            #[cfg(feature = "tracing")]
            warn!(
                credential_type = credential_type,
                permitted_types = ?issuer_info.permitted_types,
                "Issuer is not authorized to issue this credential type"
            );
            return Err(LastIDError::invalid_credential(format!(
                "Issuer '{}' is not authorized to issue '{}' credentials. Permitted types: {:?}",
                issuer_did, credential_type, issuer_info.permitted_types
            )));
        }

        #[cfg(feature = "tracing")]
        debug!(
            credential_type = credential_type,
            "Credential type is permitted for this issuer"
        );

        // Verify JWT signature with issuer's public key
        let public_key_jwk = serde_json::to_value(&issuer_info.public_key).map_err(|e| {
            LastIDError::invalid_credential(format!("Invalid public key format: {e}"))
        })?;

        vp.verify_signature(&public_key_jwk)?;

        #[cfg(feature = "tracing")]
        info!("JWT signature verified successfully with issuer's public key");

        // Extract timestamps
        let issued_at = Utc
            .timestamp_opt(vp.issued_at()?, 0)
            .single()
            .ok_or_else(|| LastIDError::invalid_credential("Invalid issued_at timestamp"))?;
        let expires_at = Utc
            .timestamp_opt(vp.expires_at()?, 0)
            .single()
            .ok_or_else(|| LastIDError::invalid_credential("Invalid expires_at timestamp"))?;

        // SEC-005: Check expiration with clock skew tolerance
        let now = chrono::Utc::now();
        // Clock skew is typically small (30-300s), safe to convert
        let clock_skew_secs = i64::try_from(self.config.clock_skew_seconds).unwrap_or(i64::MAX);
        let clock_skew = chrono::TimeDelta::seconds(clock_skew_secs);

        // nbf (not-before) validation with clock skew
        if now + clock_skew < issued_at {
            #[cfg(feature = "tracing")]
            warn!(
                issued_at = %issued_at,
                now = %now,
                clock_skew_seconds = self.config.clock_skew_seconds,
                "Credential not yet valid (issued_at in the future)"
            );
            return Err(LastIDError::clock_skew_exceeded(
                self.config.clock_skew_seconds,
            ));
        }

        // exp (expiration) validation with clock skew
        if now - clock_skew >= expires_at {
            #[cfg(feature = "tracing")]
            warn!(
                expires_at = %expires_at,
                now = %now,
                clock_skew_seconds = self.config.clock_skew_seconds,
                "Credential has expired"
            );
            return Err(LastIDError::invalid_credential("Credential has expired"));
        }

        // Reconstruct claims
        let claims = vp.reconstruct_claims();

        #[cfg(feature = "tracing")]
        info!(
            subject_did = vp.subject_did().unwrap_or("unknown"),
            credential_type = vp.credential_type().unwrap_or("unknown"),
            "Presentation verified successfully"
        );

        Ok(VerifiedCredential {
            subject_did: vp.subject_did()?.to_string(),
            issuer_did: issuer_did.to_string(),
            credential_type: credential_type.to_string(),
            claims,
            issued_at,
            expires_at,
            issuer_status: issuer_info.status,
            signature_verified: true,
        })
    }

    /// Validate an issuer against the trust registry.
    ///
    /// # Errors
    ///
    /// * `LastIDError::TrustRegistry` - Issuer not found or invalid status
    pub async fn validate_issuer(&self, issuer_did: &str) -> Result<IssuerInfo, LastIDError> {
        let info = self.trust_registry.validate_issuer(issuer_did).await?;
        Ok(info)
    }
}

impl Clone for LastIDClient {
    fn clone(&self) -> Self {
        Self {
            config: self.config.clone(),
            idp_client: SharedPtr::clone(&self.idp_client),
            trust_registry: SharedPtr::clone(&self.trust_registry),
            token_manager: SharedPtr::clone(&self.token_manager),
        }
    }
}

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

    fn test_config() -> SDKConfig {
        SDKConfig::builder()
            .idp_endpoint("https://test.lastid.co")
            .client_id("test-client")
            .build()
            .unwrap()
    }

    #[test]
    fn test_client_creation() {
        let config = test_config();
        let client = LastIDClient::new(config);
        assert!(client.is_ok());
    }

    #[test]
    fn test_client_clone() {
        let config = test_config();
        let client = LastIDClient::new(config).unwrap();
        let cloned = client.clone();

        assert_eq!(client.config().idp_endpoint, cloned.config().idp_endpoint);
        assert_eq!(client.config().client_id, cloned.config().client_id);
    }

    #[test]
    fn test_clock_skew_config() {
        // Test that clock_skew_seconds is accessible in config
        let config = test_config();
        // Default is 60 seconds
        assert_eq!(config.clock_skew_seconds, 60);
    }
}
