//! Hybrid SSH Credential Management
//!
//! Manages three levels of SSH credentials with automatic fallback:
//! 1. Permanent keys from Control-Center (via Vault)
//! 2. Temporal keys generated locally (via SshKeyManager)
//! 3. Static provisioning key (filesystem-based fallback)

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;

use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};

use super::{SshPoolError, SshPoolErrorKind, SshPoolResult};
use crate::ssh::{SshKeyManager, SshKeyRequest, SshKeyType, TemporalSshKey};

/// SSH credentials for establishing connections
#[derive(Debug, Clone)]
pub struct SshCredentials {
    /// Credential identifier (unique per acquisition)
    pub id: String,
    /// Target username
    pub username: String,
    /// Target host
    pub host: String,
    /// Private key material (PEM format for key-based, OTP string for OTP)
    pub private_key_or_password: String,
    /// Public key (if available, OpenSSH format)
    pub public_key: Option<String>,
    /// Credential type
    pub credential_type: SshCredentialType,
    /// Fingerprint (SHA256 of public key)
    pub fingerprint: Option<String>,
    /// Expiration time (None for static keys)
    pub expires_at: Option<DateTime<Utc>>,
    /// Source of credential (for logging/debugging)
    pub source: String,
}

/// Type of SSH credential
#[derive(Debug, Clone, PartialEq)]
pub enum SshCredentialType {
    /// Permanent key from Control-Center Vault
    PermanentKey { vault_path: String },
    /// Temporal key generated locally
    TemporalKey {
        key_id: String,
        expires_at: DateTime<Utc>,
    },
    /// Static provisioning key from filesystem
    StaticKey { key_path: PathBuf },
}

/// Cached permanent SSH key with TTL
#[derive(Debug, Clone)]
struct CachedSshKey {
    /// Key material (PEM format)
    private_key: String,
    /// Public key (OpenSSH format)
    public_key: String,
    /// Key fingerprint
    fingerprint: String,
    /// When this cache entry was created
    cached_at: DateTime<Utc>,
    /// When this cache entry expires (based on Control-Center token expiry)
    expires_at: DateTime<Utc>,
}

impl CachedSshKey {
    /// Check if cached key is still valid
    fn is_valid(&self) -> bool {
        Utc::now() < self.expires_at
    }
}

/// Control-Center API client for permanent key retrieval
#[derive(Clone)]
struct ControlCenterClient {
    base_url: String,
    http_client: reqwest::Client,
}

impl ControlCenterClient {
    /// Create new Control-Center client
    fn new(base_url: String) -> Self {
        Self {
            base_url,
            http_client: reqwest::Client::new(),
        }
    }

    /// Authenticate with Control-Center and get JWT token
    async fn authenticate(&self, token: &str) -> SshPoolResult<String> {
        let url = format!("{}/api/v1/auth/login", self.base_url);

        let response = self
            .http_client
            .post(&url)
            .bearer_auth(token)
            .send()
            .await
            .map_err(|e| SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "control_center_auth".to_string(),
                },
                context: format!("Failed to authenticate with Control-Center: {}", e),
                source: Some(Box::new(e)),
                backtrace: std::backtrace::Backtrace::capture(),
            })?;

        if !response.status().is_success() {
            return Err(SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "control_center_auth".to_string(),
                },
                context: format!(
                    "Control-Center authentication failed: {}",
                    response.status()
                ),
                source: None,
                backtrace: std::backtrace::Backtrace::capture(),
            });
        }

        let auth_response: ControlCenterAuthResponse =
            response.json().await.map_err(|e| SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "control_center_parsing".to_string(),
                },
                context: format!("Failed to parse Control-Center response: {}", e),
                source: Some(Box::new(e)),
                backtrace: std::backtrace::Backtrace::capture(),
            })?;

        Ok(auth_response.access_token)
    }

    /// Retrieve permanent SSH key from Control-Center Vault
    async fn get_ssh_key(
        &self,
        jwt_token: &str,
        vault_path: &str,
    ) -> SshPoolResult<(String, String, String)> {
        let url = format!("{}/api/v1/secrets/vault{}", self.base_url, vault_path);

        let response = self
            .http_client
            .get(&url)
            .bearer_auth(jwt_token)
            .send()
            .await
            .map_err(|e| SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "vault_key".to_string(),
                },
                context: format!("Failed to retrieve key from Control-Center Vault: {}", e),
                source: Some(Box::new(e)),
                backtrace: std::backtrace::Backtrace::capture(),
            })?;

        if !response.status().is_success() {
            return Err(SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "vault_key".to_string(),
                },
                context: format!(
                    "Failed to retrieve key from Vault at {}: {}",
                    vault_path,
                    response.status()
                ),
                source: None,
                backtrace: std::backtrace::Backtrace::capture(),
            });
        }

        let vault_response: VaultKeyResponse = response.json().await.map_err(|e| SshPoolError {
            kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                credential_type: "vault_parsing".to_string(),
            },
            context: format!("Failed to parse Vault response: {}", e),
            source: Some(Box::new(e)),
            backtrace: std::backtrace::Backtrace::capture(),
        })?;

        Ok((
            vault_response.private_key,
            vault_response.public_key,
            vault_response.fingerprint,
        ))
    }
}

/// Control-Center authentication response
#[derive(Debug, Deserialize)]
struct ControlCenterAuthResponse {
    access_token: String,
    #[allow(dead_code)]
    expires_in: u64,
}

/// Vault key response from Control-Center
#[derive(Debug, Deserialize)]
struct VaultKeyResponse {
    private_key: String,
    public_key: String,
    fingerprint: String,
}

/// Configuration for credential management
#[derive(Debug, Clone)]
pub struct CredentialConfig {
    /// Control-Center base URL
    pub control_center_url: String,
    /// Control-Center authentication token
    pub control_center_token: String,
    /// Whether to use Control-Center permanent keys
    pub use_permanent_keys: bool,
    /// Whether to use temporal keys generated locally
    pub use_temporal_keys: bool,
    /// Default TTL for temporal keys (seconds)
    pub temporal_key_ttl_seconds: u64,
    /// Path to static provisioning key
    pub provisioning_key_path: Option<PathBuf>,
    /// Cache refresh interval for permanent keys (seconds)
    pub refresh_interval_seconds: u64,
    /// Whether to cache permanent keys in memory
    pub cache_permanent_keys: bool,
}

impl Default for CredentialConfig {
    fn default() -> Self {
        Self {
            control_center_url: "http://localhost:9080".to_string(),
            control_center_token: String::new(),
            use_permanent_keys: true,
            use_temporal_keys: true,
            temporal_key_ttl_seconds: 3600, // 1 hour
            provisioning_key_path: None,
            refresh_interval_seconds: 1800, // 30 minutes
            cache_permanent_keys: true,
        }
    }
}

/// Hybrid SSH credential manager
pub struct CredentialManager {
    config: CredentialConfig,
    control_center_client: Arc<ControlCenterClient>,
    ssh_key_manager: Arc<SshKeyManager>,
    /// Cache for permanent keys: vault_path -> CachedSshKey
    permanent_key_cache: Arc<RwLock<HashMap<String, CachedSshKey>>>,
    /// Current Control-Center JWT token
    jwt_token: Arc<RwLock<Option<String>>>,
    /// Last token refresh time
    last_token_refresh: Arc<RwLock<DateTime<Utc>>>,
}

impl CredentialManager {
    /// Create new credential manager
    pub async fn new(
        config: CredentialConfig,
        ssh_key_manager: Arc<SshKeyManager>,
    ) -> SshPoolResult<Self> {
        // Validate configuration
        if !config.use_permanent_keys && !config.use_temporal_keys {
            return Err(SshPoolError {
                kind: SshPoolErrorKind::ConfigurationError {
                    parameter: "credential_sources".to_string(),
                    reason: "At least one credential source (permanent or temporal) must be \
                             enabled"
                        .to_string(),
                },
                context: "Invalid credential configuration".to_string(),
                source: None,
                backtrace: std::backtrace::Backtrace::capture(),
            });
        }

        let control_center_client =
            Arc::new(ControlCenterClient::new(config.control_center_url.clone()));

        Ok(Self {
            config,
            control_center_client,
            ssh_key_manager,
            permanent_key_cache: Arc::new(RwLock::new(HashMap::new())),
            jwt_token: Arc::new(RwLock::new(None)),
            last_token_refresh: Arc::new(RwLock::new(Utc::now() - Duration::hours(1))),
        })
    }

    /// Get SSH credentials with fallback chain
    /// Priority: Permanent (Control-Center) → Temporal (Generated) → Static
    /// (Filesystem)
    pub async fn acquire_credentials(
        &self,
        host: &str,
        username: &str,
        preference: Option<&SshCredentialType>,
    ) -> SshPoolResult<SshCredentials> {
        // Try preferred type first
        if let Some(pref) = preference {
            match pref {
                SshCredentialType::PermanentKey { vault_path } => {
                    if let Ok(creds) = self.get_permanent_key(vault_path, username, host).await {
                        debug!(
                            "Acquired permanent key from Control-Center for {}@{}",
                            username, host
                        );
                        return Ok(creds);
                    }
                }
                SshCredentialType::TemporalKey { .. } => {
                    if let Ok(creds) = self.generate_temporal_key(host, username).await {
                        debug!(
                            "Generated temporal key for {}@{} from preferences",
                            username, host
                        );
                        return Ok(creds);
                    }
                }
                SshCredentialType::StaticKey { key_path: path_buf } => {
                    if let Ok(creds) = self.get_static_key(path_buf, username, host).await {
                        debug!(
                            "Acquired static key for {}@{} from preferences",
                            username, host
                        );
                        return Ok(creds);
                    }
                }
            }
        }

        // Fallback chain: permanent → temporal → static
        if self.config.use_permanent_keys && !self.config.control_center_token.is_empty() {
            if let Ok(creds) = self
                .get_permanent_key(&format!("/ssh/{}/{}", host, username), username, host)
                .await
            {
                debug!(
                    "Fallback 1: Acquired permanent key for {}@{}",
                    username, host
                );
                return Ok(creds);
            }
        }

        if self.config.use_temporal_keys {
            if let Ok(creds) = self.generate_temporal_key(host, username).await {
                debug!(
                    "Fallback 2: Generated temporal key for {}@{}",
                    username, host
                );
                return Ok(creds);
            }
        }

        // Last resort: static provisioning key
        if let Some(key_path) = &self.config.provisioning_key_path {
            if let Ok(creds) = self.get_static_key(key_path, username, host).await {
                info!(
                    "Fallback 3: Using static provisioning key for {}@{}",
                    username, host
                );
                return Ok(creds);
            }
        }

        Err(SshPoolError {
            kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                credential_type: "all_sources".to_string(),
            },
            context: format!(
                "Failed to acquire credentials for {}@{} from all sources (permanent, temporal, \
                 static)",
                username, host
            ),
            source: None,
            backtrace: std::backtrace::Backtrace::capture(),
        })
    }

    /// Get permanent key from Control-Center Vault
    async fn get_permanent_key(
        &self,
        vault_path: &str,
        username: &str,
        host: &str,
    ) -> SshPoolResult<SshCredentials> {
        // Check cache first
        if self.config.cache_permanent_keys {
            let cache = self.permanent_key_cache.read().await;
            if let Some(cached) = cache.get(vault_path) {
                if cached.is_valid() {
                    debug!("Using cached permanent key for {}", vault_path);
                    return Ok(SshCredentials {
                        id: format!("{}-{}", vault_path, Utc::now().timestamp()),
                        username: username.to_string(),
                        host: host.to_string(),
                        private_key_or_password: cached.private_key.clone(),
                        public_key: Some(cached.public_key.clone()),
                        credential_type: SshCredentialType::PermanentKey {
                            vault_path: vault_path.to_string(),
                        },
                        fingerprint: Some(cached.fingerprint.clone()),
                        expires_at: Some(cached.expires_at),
                        source: "control_center_cache".to_string(),
                    });
                }
            }
        }

        // Refresh JWT token if needed
        self.ensure_jwt_token().await?;

        let jwt = self.jwt_token.read().await;
        let token = jwt.as_ref().ok_or_else(|| SshPoolError {
            kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                credential_type: "jwt_missing".to_string(),
            },
            context: "JWT token not available".to_string(),
            source: None,
            backtrace: std::backtrace::Backtrace::capture(),
        })?;

        // Retrieve from Vault
        let (private_key, public_key, fingerprint) = self
            .control_center_client
            .get_ssh_key(token, vault_path)
            .await?;

        let expires_at =
            Utc::now() + Duration::seconds(self.config.refresh_interval_seconds as i64);

        // Cache the key
        if self.config.cache_permanent_keys {
            let cached = CachedSshKey {
                private_key: private_key.clone(),
                public_key: public_key.clone(),
                fingerprint: fingerprint.clone(),
                cached_at: Utc::now(),
                expires_at,
            };
            let mut cache = self.permanent_key_cache.write().await;
            cache.insert(vault_path.to_string(), cached);
        }

        Ok(SshCredentials {
            id: format!("{}-{}", vault_path, Utc::now().timestamp()),
            username: username.to_string(),
            host: host.to_string(),
            private_key_or_password: private_key,
            public_key: Some(public_key),
            credential_type: SshCredentialType::PermanentKey {
                vault_path: vault_path.to_string(),
            },
            fingerprint: Some(fingerprint),
            expires_at: Some(expires_at),
            source: "control_center".to_string(),
        })
    }

    /// Generate temporal key via SshKeyManager
    async fn generate_temporal_key(
        &self,
        host: &str,
        username: &str,
    ) -> SshPoolResult<SshCredentials> {
        let request = SshKeyRequest {
            key_type: SshKeyType::DynamicKeyPair,
            user: username.to_string(),
            target_server: host.to_string(),
            ttl: chrono::Duration::seconds(self.config.temporal_key_ttl_seconds as i64),
            allowed_ip: None,
            principal: None,
        };

        let temporal_key = self
            .ssh_key_manager
            .generate_key(request)
            .await
            .map_err(|e| SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "temporal_key_generation".to_string(),
                },
                context: format!("Failed to generate temporal key: {}", e),
                source: None,
                backtrace: std::backtrace::Backtrace::capture(),
            })?;

        let private_key = temporal_key
            .private_key
            .clone()
            .ok_or_else(|| SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "temporal_key_missing_private".to_string(),
                },
                context: "Temporal key missing private key material".to_string(),
                source: None,
                backtrace: std::backtrace::Backtrace::capture(),
            })?;

        Ok(SshCredentials {
            id: temporal_key.id.clone(),
            username: username.to_string(),
            host: host.to_string(),
            private_key_or_password: private_key,
            public_key: Some(temporal_key.public_key.clone()),
            credential_type: SshCredentialType::TemporalKey {
                key_id: temporal_key.id.clone(),
                expires_at: temporal_key.expires_at,
            },
            fingerprint: Some(temporal_key.fingerprint),
            expires_at: Some(temporal_key.expires_at),
            source: "local_generation".to_string(),
        })
    }

    /// Get static provisioning key from filesystem
    async fn get_static_key(
        &self,
        key_path: &PathBuf,
        username: &str,
        host: &str,
    ) -> SshPoolResult<SshCredentials> {
        let expanded_path = shellexpand::tilde(&key_path.to_string_lossy()).to_string();
        let path = PathBuf::from(expanded_path);

        let private_key = tokio::fs::read_to_string(&path)
            .await
            .map_err(|e| SshPoolError {
                kind: SshPoolErrorKind::CredentialAcquisitionFailed {
                    credential_type: "static_key_read".to_string(),
                },
                context: format!(
                    "Failed to read provisioning key from {}: {}",
                    path.display(),
                    e
                ),
                source: Some(Box::new(e)),
                backtrace: std::backtrace::Backtrace::capture(),
            })?;

        Ok(SshCredentials {
            id: format!("static-{}-{}", username, Utc::now().timestamp()),
            username: username.to_string(),
            host: host.to_string(),
            private_key_or_password: private_key,
            public_key: None,
            credential_type: SshCredentialType::StaticKey {
                key_path: key_path.clone(),
            },
            fingerprint: None,
            expires_at: None,
            source: "provisioning_key".to_string(),
        })
    }

    /// Ensure JWT token is fresh and available
    async fn ensure_jwt_token(&self) -> SshPoolResult<()> {
        let last_refresh = *self.last_token_refresh.read().await;
        let should_refresh = Utc::now() - last_refresh > Duration::minutes(25);

        if should_refresh {
            let token = self
                .control_center_client
                .authenticate(&self.config.control_center_token)
                .await?;

            let mut jwt = self.jwt_token.write().await;
            *jwt = Some(token);

            let mut refresh_time = self.last_token_refresh.write().await;
            *refresh_time = Utc::now();

            info!("JWT token refreshed from Control-Center");
        }

        Ok(())
    }

    /// Clear credential cache (for testing or manual refresh)
    pub async fn clear_cache(&self) {
        let mut cache = self.permanent_key_cache.write().await;
        cache.clear();
        debug!("Credential cache cleared");
    }

    /// Get cache statistics
    pub async fn cache_stats(&self) -> (usize, usize) {
        let cache = self.permanent_key_cache.read().await;
        let total = cache.len();
        let valid = cache.values().filter(|k| k.is_valid()).count();
        (total, valid)
    }

    /// Start background token refresh task
    pub async fn start_background_refresh(self: Arc<Self>) {
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(std::time::Duration::from_secs(
                self.config.refresh_interval_seconds,
            ));

            loop {
                interval.tick().await;

                if let Err(e) = self.ensure_jwt_token().await {
                    warn!("Failed to refresh JWT token: {}", e);
                }
            }
        });
    }
}

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

    #[test]
    fn test_credential_type_equality() {
        let type1 = SshCredentialType::StaticKey {
            key_path: PathBuf::from("/test"),
        };
        let type2 = SshCredentialType::StaticKey {
            key_path: PathBuf::from("/test"),
        };
        assert_eq!(type1, type2);
    }

    #[test]
    fn test_cached_key_validity() {
        let cached = CachedSshKey {
            private_key: "test".to_string(),
            public_key: "test".to_string(),
            fingerprint: "test".to_string(),
            cached_at: Utc::now(),
            expires_at: Utc::now() + Duration::hours(1),
        };
        assert!(cached.is_valid());

        let expired = CachedSshKey {
            private_key: "test".to_string(),
            public_key: "test".to_string(),
            fingerprint: "test".to_string(),
            cached_at: Utc::now() - Duration::hours(2),
            expires_at: Utc::now() - Duration::hours(1),
        };
        assert!(!expired.is_valid());
    }

    #[test]
    fn test_credential_config_defaults() {
        let config = CredentialConfig::default();
        assert!(config.use_permanent_keys);
        assert!(config.use_temporal_keys);
        assert_eq!(config.temporal_key_ttl_seconds, 3600);
        assert_eq!(config.refresh_interval_seconds, 1800);
    }
}
