//! Vault SSH Secrets Engine Integration
//!
//! Supports both OTP (One-Time Password) and CA (Certificate Authority) modes.

use std::time::Duration as StdDuration;

use anyhow::{bail, Context, Result};
use chrono::Utc;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tracing::{debug, info, warn};
use uuid::Uuid;

use super::{SshKeyRequest, SshKeyType, TemporalSshKey};

/// Vault SSH secrets engine client
pub struct VaultSshEngine {
    /// HTTP client
    client: Client,
    /// Vault address
    vault_addr: String,
    /// Vault token
    vault_token: String,
    /// SSH mount point
    mount_point: String,
    /// SSH mode (ca or otp)
    mode: String,
}

/// Vault OTP credential response
#[derive(Debug, Deserialize)]
struct VaultOtpResponse {
    data: VaultOtpData,
    lease_id: String,
    lease_duration: u64,
}

#[derive(Debug, Deserialize)]
struct VaultOtpData {
    key: String,
    key_type: String,
    port: u16,
    username: String,
    ip: String,
}

/// Vault certificate signing response
#[derive(Debug, Deserialize)]
struct VaultCertResponse {
    data: VaultCertData,
    lease_id: String,
    lease_duration: u64,
}

#[derive(Debug, Deserialize)]
struct VaultCertData {
    signed_key: String,
    serial_number: String,
}

/// Certificate signing request
#[derive(Debug, Serialize)]
struct SignCertRequest {
    public_key: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    ttl: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    valid_principals: Option<String>,
}

/// OTP generation request
#[derive(Debug, Serialize)]
struct GenerateOtpRequest {
    ip: String,
    username: String,
}

impl VaultSshEngine {
    /// Create new Vault SSH engine client
    pub async fn new(
        vault_addr: &str,
        vault_token: &str,
        mount_point: &str,
        mode: &str,
    ) -> Result<Self> {
        let client = Client::builder()
            .timeout(StdDuration::from_secs(30))
            .build()
            .context("Failed to create HTTP client")?;

        let vault_addr = vault_addr.trim_end_matches('/').to_string();

        Ok(Self {
            client,
            vault_addr,
            vault_token: vault_token.to_string(),
            mount_point: mount_point.to_string(),
            mode: mode.to_string(),
        })
    }

    /// Generate OTP (One-Time Password)
    pub async fn generate_otp(&self, request: &SshKeyRequest) -> Result<TemporalSshKey> {
        if self.mode != "otp" {
            bail!("Vault SSH engine not in OTP mode");
        }

        info!(
            "Generating OTP for {}@{}",
            request.user, request.target_server
        );

        // Parse IP from target_server or use allowed_ip
        let ip = request
            .allowed_ip
            .clone()
            .unwrap_or_else(|| request.target_server.clone());

        let otp_request = GenerateOtpRequest {
            ip: ip.clone(),
            username: request.user.clone(),
        };

        let url = format!(
            "{}/v1/{}/creds/otp_key_role",
            self.vault_addr, self.mount_point
        );

        debug!("Vault OTP request URL: {}", url);

        let response = self
            .client
            .post(&url)
            .header("X-Vault-Token", &self.vault_token)
            .json(&otp_request)
            .send()
            .await
            .context("Failed to send OTP request to Vault")?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            bail!("Vault OTP request failed ({}): {}", status, body);
        }

        let vault_response: VaultOtpResponse = response
            .json()
            .await
            .context("Failed to parse Vault OTP response")?;

        let created_at = Utc::now();
        let ttl = chrono::Duration::seconds(vault_response.lease_duration as i64);
        let expires_at = created_at + ttl;

        Ok(TemporalSshKey {
            id: Uuid::new_v4().to_string(),
            key_type: SshKeyType::Otp,
            public_key: vault_response.data.key.clone(),
            private_key: None,
            fingerprint: format!("OTP:{}", vault_response.data.key_type),
            user: vault_response.data.username,
            target_server: vault_response.data.ip,
            created_at,
            expires_at,
            ttl,
            used: false,
            deployed: false,
            vault_lease_id: Some(vault_response.lease_id),
        })
    }

    /// Sign SSH public key with Vault CA
    pub async fn sign_certificate(&self, request: &SshKeyRequest) -> Result<TemporalSshKey> {
        if self.mode != "ca" {
            bail!("Vault SSH engine not in CA mode");
        }

        info!(
            "Signing SSH certificate for {}@{}",
            request.user, request.target_server
        );

        // For CA mode, we need a public key to sign
        // Generate one if not provided (in production, user would provide their key)
        let key_generator = super::KeyGenerator::new();
        let temp_key = key_generator.generate_keypair(request).await?;

        let sign_request = SignCertRequest {
            public_key: temp_key.public_key.clone(),
            ttl: Some(format!("{}s", request.ttl.num_seconds())),
            valid_principals: request
                .principal
                .clone()
                .or_else(|| Some(request.user.clone())),
        };

        let url = format!("{}/v1/{}/sign/default", self.vault_addr, self.mount_point);

        debug!("Vault certificate signing request URL: {}", url);

        let response = self
            .client
            .post(&url)
            .header("X-Vault-Token", &self.vault_token)
            .json(&sign_request)
            .send()
            .await
            .context("Failed to send certificate signing request to Vault")?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            bail!("Vault certificate signing failed ({}): {}", status, body);
        }

        let vault_response: VaultCertResponse = response
            .json()
            .await
            .context("Failed to parse Vault certificate response")?;

        let created_at = Utc::now();
        let ttl = chrono::Duration::seconds(vault_response.lease_duration as i64);
        let expires_at = created_at + ttl;

        Ok(TemporalSshKey {
            id: Uuid::new_v4().to_string(),
            key_type: SshKeyType::Certificate,
            public_key: vault_response.data.signed_key,
            private_key: temp_key.private_key,
            fingerprint: format!(
                "CERT:{}:{}",
                vault_response.data.serial_number, temp_key.fingerprint
            ),
            user: request.user.clone(),
            target_server: request.target_server.clone(),
            created_at,
            expires_at,
            ttl,
            used: false,
            deployed: false,
            vault_lease_id: Some(vault_response.lease_id),
        })
    }

    /// Revoke Vault lease
    pub async fn revoke_lease(&self, lease_id: &str) -> Result<()> {
        info!("Revoking Vault lease: {}", lease_id);

        #[derive(Serialize)]
        struct RevokeRequest {
            lease_id: String,
        }

        let url = format!("{}/v1/sys/leases/revoke", self.vault_addr);

        let response = self
            .client
            .post(&url)
            .header("X-Vault-Token", &self.vault_token)
            .json(&RevokeRequest {
                lease_id: lease_id.to_string(),
            })
            .send()
            .await
            .context("Failed to send revoke request to Vault")?;

        if !response.status().is_success() {
            let status = response.status();
            let body = response.text().await.unwrap_or_default();
            warn!("Vault lease revocation failed ({}): {}", status, body);
            // Don't fail - lease might already be expired
        }

        Ok(())
    }

    /// Check Vault health
    pub async fn health_check(&self) -> Result<bool> {
        let url = format!("{}/v1/sys/health", self.vault_addr);

        let response = self
            .client
            .get(&url)
            .send()
            .await
            .context("Failed to check Vault health")?;

        Ok(response.status().is_success())
    }

    /// Get SSH engine configuration
    pub async fn get_config(&self) -> Result<VaultSshConfig> {
        let url = format!("{}/v1/{}/config/ca", self.vault_addr, self.mount_point);

        let response = self
            .client
            .get(&url)
            .header("X-Vault-Token", &self.vault_token)
            .send()
            .await
            .context("Failed to get SSH engine config")?;

        if !response.status().is_success() {
            bail!("Failed to get SSH engine configuration");
        }

        #[derive(Deserialize)]
        struct ConfigResponse {
            data: VaultSshConfig,
        }

        let config_response: ConfigResponse = response
            .json()
            .await
            .context("Failed to parse config response")?;

        Ok(config_response.data)
    }
}

/// Vault SSH configuration
#[derive(Debug, Deserialize, Serialize)]
pub struct VaultSshConfig {
    pub public_key: Option<String>,
}

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

    #[tokio::test]
    async fn test_vault_ssh_engine_new() {
        let engine = VaultSshEngine::new("http://localhost:8200", "test-token", "ssh", "ca")
            .await
            .unwrap();

        assert_eq!(engine.vault_addr, "http://localhost:8200");
        assert_eq!(engine.mount_point, "ssh");
        assert_eq!(engine.mode, "ca");
    }

    #[test]
    fn test_sign_cert_request_serialization() {
        let request = SignCertRequest {
            public_key: "ssh-ed25519 AAAA".to_string(),
            ttl: Some("3600s".to_string()),
            valid_principals: Some("root".to_string()),
        };

        let json = serde_json::to_string(&request).unwrap();
        assert!(json.contains("public_key"));
        assert!(json.contains("ttl"));
        assert!(json.contains("valid_principals"));
    }

    #[test]
    fn test_generate_otp_request_serialization() {
        let request = GenerateOtpRequest {
            ip: "192.168.1.100".to_string(),
            username: "root".to_string(),
        };

        let json = serde_json::to_string(&request).unwrap();
        assert!(json.contains("192.168.1.100"));
        assert!(json.contains("root"));
    }
}
