//! Nushell plugin for KMS operations.
//!
//! This plugin provides Key Management System commands supporting multiple backends:
//! - RustyVault (primary - local Vault-compatible)
//! - Age (local file-based encryption)
//! - Cosmian (privacy-preserving)
//! - AWS KMS (production fallback)
//! - HashiCorp Vault
//!
//! Commands:
//! - `kms encrypt` - Encrypt data with selected backend
//! - `kms decrypt` - Decrypt data
//! - `kms generate-key` - Generate encryption keys
//! - `kms status` - Show backend status
//! - `kms list-backends` - List supported backends

use nu_plugin::{
    serve_plugin, EngineInterface, EvaluatedCall, MsgPackSerializer, Plugin, PluginCommand,
    SimplePluginCommand,
};
use nu_protocol::{record, Category, Example, LabeledError, Signature, SyntaxShape, Type, Value};

pub mod error;
mod helpers;

#[cfg(test)]
mod tests;

/// Nushell plugin for KMS operations.
#[derive(Debug)]
pub struct KmsPlugin;

impl Plugin for KmsPlugin {
    fn version(&self) -> String {
        env!("CARGO_PKG_VERSION").into()
    }

    fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
        vec![
            Box::new(KmsEncrypt),
            Box::new(KmsDecrypt),
            Box::new(KmsGenerateKey),
            Box::new(KmsStatus),
            Box::new(KmsListBackends),
        ]
    }
}

// =============================================================================
// Encrypt Command
// =============================================================================

/// Encrypt command - Encrypt data with selected backend.
#[derive(Debug)]
pub struct KmsEncrypt;

impl SimplePluginCommand for KmsEncrypt {
    type Plugin = KmsPlugin;

    fn name(&self) -> &str {
        "kms encrypt"
    }

    fn signature(&self) -> Signature {
        Signature::build(PluginCommand::name(self))
            .input_output_type(Type::String, Type::String)
            .required("data", SyntaxShape::String, "Data to encrypt")
            .named(
                "backend",
                SyntaxShape::String,
                "Backend: rustyvault, age, cosmian, aws, vault",
                Some('b'),
            )
            .named("key", SyntaxShape::String, "Key ID or recipient", Some('k'))
            .category(Category::Custom("provisioning".into()))
    }

    fn description(&self) -> &str {
        "Encrypt data using KMS backend"
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![
            Example {
                example: "kms encrypt \"secret data\" --backend rustyvault",
                description: "Encrypt with RustyVault",
                result: None,
            },
            Example {
                example: "kms encrypt \"data\" --backend age --key age1...",
                description: "Encrypt with Age recipient",
                result: None,
            },
            Example {
                example: "kms encrypt \"data\" --backend aws --key alias/my-key",
                description: "Encrypt with AWS KMS",
                result: None,
            },
        ]
    }

    fn run(
        &self,
        _plugin: &KmsPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        _input: &Value,
    ) -> Result<Value, LabeledError> {
        let data: String = call.req(0)?;
        let backend_name: Option<String> = call.get_flag("backend")?;
        let key: Option<String> = call.get_flag("key")?;

        // Create tokio runtime for async operations
        let runtime = tokio::runtime::Runtime::new()
            .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?;

        // Detect or use specified backend
        let backend = runtime.block_on(async {
            if let Some(name) = backend_name {
                match name.as_str() {
                    "rustyvault" => {
                        let addr = std::env::var("RUSTYVAULT_ADDR")
                            .unwrap_or_else(|_| "http://localhost:8200".to_string());
                        let token = std::env::var("RUSTYVAULT_TOKEN")
                            .map_err(|_| LabeledError::new("RUSTYVAULT_TOKEN not set"))?;
                        helpers::Backend::new_rustyvault(&addr, &token)
                            .await
                            .map_err(|e| LabeledError::new(e))
                    }
                    "age" => {
                        let recipient = key
                            .clone()
                            .ok_or_else(|| LabeledError::new("Age requires --key recipient"))?;
                        helpers::Backend::new_age(&recipient, None)
                            .map_err(|e| LabeledError::new(e))
                    }
                    "aws" => {
                        let key_id = key
                            .clone()
                            .ok_or_else(|| LabeledError::new("AWS KMS requires --key key-id"))?;
                        Ok(helpers::Backend::new_aws_kms(&key_id))
                    }
                    "vault" => {
                        let addr = std::env::var("VAULT_ADDR")
                            .unwrap_or_else(|_| "http://localhost:8200".to_string());
                        let token = std::env::var("VAULT_TOKEN")
                            .map_err(|_| LabeledError::new("VAULT_TOKEN not set"))?;
                        Ok(helpers::Backend::new_vault(&addr, &token))
                    }
                    backend @ _ => {
                        let url = std::env::var("KMS_HTTP_URL")
                            .unwrap_or_else(|_| "http://localhost:8081".to_string());
                        Ok(helpers::Backend::new_http_fallback(backend, &url))
                    }
                }
            } else {
                Ok(helpers::detect_backend().await)
            }
        })?;

        // Encrypt based on backend
        let encrypted = match backend {
            helpers::Backend::RustyVault { ref client } => {
                let key_name = key.unwrap_or_else(|| "provisioning-main".to_string());
                helpers::encrypt_rustyvault(client, &key_name, data.as_bytes())
                    .map_err(|e| LabeledError::new(e))?
            }
            helpers::Backend::Age { ref recipient, .. } => {
                helpers::encrypt_age(data.as_bytes(), recipient)
                    .map_err(|e| LabeledError::new(e))?
            }
            helpers::Backend::AwsKms { ref key_id } => {
                runtime.block_on(async {
                    helpers::encrypt_aws_kms(key_id, data.as_bytes())
                        .await
                        .map_err(|e| LabeledError::new(e))
                })?
            }
            helpers::Backend::Vault { ref addr, ref token } => {
                let key_name = key.unwrap_or_else(|| "provisioning-main".to_string());
                runtime.block_on(async {
                    helpers::encrypt_vault(addr, token, &key_name, data.as_bytes())
                        .await
                        .map_err(|e| LabeledError::new(e))
                })?
            }
            helpers::Backend::HttpFallback {
                ref backend_name,
                ref url,
            } => runtime.block_on(async {
                helpers::encrypt_http(url, backend_name, data.as_bytes())
                    .await
                    .map_err(|e| LabeledError::new(e))
            })?,
        };

        Ok(Value::string(encrypted, call.head))
    }
}

// =============================================================================
// Decrypt Command
// =============================================================================

/// Decrypt command - Decrypt data using KMS backend.
#[derive(Debug)]
pub struct KmsDecrypt;

impl SimplePluginCommand for KmsDecrypt {
    type Plugin = KmsPlugin;

    fn name(&self) -> &str {
        "kms decrypt"
    }

    fn signature(&self) -> Signature {
        Signature::build(PluginCommand::name(self))
            .input_output_type(Type::String, Type::String)
            .required("encrypted", SyntaxShape::String, "Encrypted data")
            .named(
                "backend",
                SyntaxShape::String,
                "Backend: rustyvault, age, cosmian, aws, vault",
                Some('b'),
            )
            .named(
                "key",
                SyntaxShape::String,
                "Key ID or private key path",
                Some('k'),
            )
            .category(Category::Custom("provisioning".into()))
    }

    fn description(&self) -> &str {
        "Decrypt data using KMS backend"
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![
            Example {
                example: "kms decrypt $encrypted --backend rustyvault",
                description: "Decrypt with RustyVault",
                result: None,
            },
            Example {
                example: "kms decrypt $encrypted --backend age --key ~/.age/key.txt",
                description: "Decrypt with Age identity file",
                result: None,
            },
        ]
    }

    fn run(
        &self,
        _plugin: &KmsPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        _input: &Value,
    ) -> Result<Value, LabeledError> {
        let encrypted: String = call.req(0)?;
        let backend_name: Option<String> = call.get_flag("backend")?;
        let key: Option<String> = call.get_flag("key")?;

        // Create tokio runtime for async operations
        let runtime = tokio::runtime::Runtime::new()
            .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?;

        // Detect or use specified backend
        let backend = runtime.block_on(async {
            if let Some(name) = backend_name {
                match name.as_str() {
                    "rustyvault" => {
                        let addr = std::env::var("RUSTYVAULT_ADDR")
                            .unwrap_or_else(|_| "http://localhost:8200".to_string());
                        let token = std::env::var("RUSTYVAULT_TOKEN")
                            .map_err(|_| LabeledError::new("RUSTYVAULT_TOKEN not set"))?;
                        helpers::Backend::new_rustyvault(&addr, &token)
                            .await
                            .map_err(|e| LabeledError::new(e))
                    }
                    "age" => {
                        let identity = key
                            .clone()
                            .ok_or_else(|| LabeledError::new("Age requires --key identity_path"))?;
                        helpers::Backend::new_age("", Some(identity))
                            .map_err(|e| LabeledError::new(e))
                    }
                    "aws" => {
                        let key_id = key.clone();
                        Ok(helpers::Backend::new_aws_kms(&key_id.unwrap_or_default()))
                    }
                    "vault" => {
                        let addr = std::env::var("VAULT_ADDR")
                            .unwrap_or_else(|_| "http://localhost:8200".to_string());
                        let token = std::env::var("VAULT_TOKEN")
                            .map_err(|_| LabeledError::new("VAULT_TOKEN not set"))?;
                        Ok(helpers::Backend::new_vault(&addr, &token))
                    }
                    backend @ _ => {
                        let url = std::env::var("KMS_HTTP_URL")
                            .unwrap_or_else(|_| "http://localhost:8081".to_string());
                        Ok(helpers::Backend::new_http_fallback(backend, &url))
                    }
                }
            } else {
                Ok(helpers::detect_backend().await)
            }
        })?;

        // Decrypt based on backend
        let decrypted = match backend {
            helpers::Backend::RustyVault { ref client } => {
                let key_name = key.unwrap_or_else(|| "provisioning-main".to_string());
                helpers::decrypt_rustyvault(client, &key_name, &encrypted)
                    .map_err(|e| LabeledError::new(e))?
            }
            helpers::Backend::Age { ref identity, .. } => {
                let identity_path = identity.as_ref().ok_or_else(|| {
                    LabeledError::new("Age requires identity path for decryption")
                })?;
                helpers::decrypt_age(&encrypted, identity_path).map_err(|e| LabeledError::new(e))?
            }
            helpers::Backend::AwsKms { ref key_id } => {
                runtime.block_on(async {
                    helpers::decrypt_aws_kms(key_id, &encrypted)
                        .await
                        .map_err(|e| LabeledError::new(e))
                })?
            }
            helpers::Backend::Vault { ref addr, ref token } => {
                let key_name = key.unwrap_or_else(|| "provisioning-main".to_string());
                runtime.block_on(async {
                    helpers::decrypt_vault(addr, token, &key_name, &encrypted)
                        .await
                        .map_err(|e| LabeledError::new(e))
                })?
            }
            helpers::Backend::HttpFallback {
                ref backend_name,
                ref url,
            } => runtime.block_on(async {
                helpers::decrypt_http(url, backend_name, &encrypted)
                    .await
                    .map_err(|e| LabeledError::new(e))
            })?,
        };

        // Convert bytes to string
        let plaintext = String::from_utf8(decrypted)
            .map_err(|e| LabeledError::new(format!("Failed to convert to UTF-8: {}", e)))?;

        Ok(Value::string(plaintext, call.head))
    }
}

// =============================================================================
// Generate Key Command
// =============================================================================

/// Generate data key command.
#[derive(Debug)]
pub struct KmsGenerateKey;

impl SimplePluginCommand for KmsGenerateKey {
    type Plugin = KmsPlugin;

    fn name(&self) -> &str {
        "kms generate-key"
    }

    fn signature(&self) -> Signature {
        Signature::build(PluginCommand::name(self))
            .input_output_type(Type::Nothing, Type::Record([].into()))
            .named(
                "spec",
                SyntaxShape::String,
                "Key spec: AES128, AES256",
                Some('s'),
            )
            .named("backend", SyntaxShape::String, "Backend", Some('b'))
            .category(Category::Custom("provisioning".into()))
    }

    fn description(&self) -> &str {
        "Generate data encryption key"
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![
            Example {
                example: "kms generate-key --spec AES256",
                description: "Generate AES-256 data key",
                result: None,
            },
            Example {
                example: "kms generate-key --backend age",
                description: "Generate Age key pair",
                result: None,
            },
        ]
    }

    fn run(
        &self,
        _plugin: &KmsPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        _input: &Value,
    ) -> Result<Value, LabeledError> {
        let key_spec: Option<String> = call.get_flag("spec")?;
        let backend_name: Option<String> = call.get_flag("backend")?;
        let key_spec = key_spec.unwrap_or_else(|| "AES256".to_string());

        // Create tokio runtime for async operations
        let runtime = tokio::runtime::Runtime::new()
            .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?;

        // Detect or use specified backend
        let backend = runtime.block_on(async {
            if let Some(name) = backend_name {
                match name.as_str() {
                    "rustyvault" => {
                        let addr = std::env::var("RUSTYVAULT_ADDR")
                            .unwrap_or_else(|_| "http://localhost:8200".to_string());
                        let token = std::env::var("RUSTYVAULT_TOKEN")
                            .map_err(|_| LabeledError::new("RUSTYVAULT_TOKEN not set"))?;
                        helpers::Backend::new_rustyvault(&addr, &token)
                            .await
                            .map_err(|e| LabeledError::new(e))
                    }
                    "age" => Ok(helpers::Backend::new_age("age1placeholder", None)
                        .map_err(|e| LabeledError::new(e))?),
                    "aws" => Ok(helpers::Backend::new_aws_kms("default")),
                    "vault" => {
                        let addr = std::env::var("VAULT_ADDR")
                            .unwrap_or_else(|_| "http://localhost:8200".to_string());
                        let token = std::env::var("VAULT_TOKEN")
                            .map_err(|_| LabeledError::new("VAULT_TOKEN not set"))?;
                        Ok(helpers::Backend::new_vault(&addr, &token))
                    }
                    backend @ _ => {
                        let url = std::env::var("KMS_HTTP_URL")
                            .unwrap_or_else(|_| "http://localhost:8081".to_string());
                        Ok(helpers::Backend::new_http_fallback(backend, &url))
                    }
                }
            } else {
                Ok(helpers::detect_backend().await)
            }
        })?;

        // Generate key based on backend
        let (plaintext, ciphertext) = match backend {
            helpers::Backend::RustyVault { ref client } => {
                let key_name = "provisioning-main";
                helpers::generate_data_key_rustyvault(client, key_name, &key_spec)
                    .map_err(|e| LabeledError::new(e))?
            }
            helpers::Backend::Age { .. } => {
                // Age generates key pairs, not data keys
                helpers::generate_age_key()
                    .map(|(secret, public)| (secret, public))
                    .map_err(|e| LabeledError::new(e))?
            }
            helpers::Backend::AwsKms { ref key_id } => {
                runtime.block_on(async {
                    helpers::generate_data_key_aws(key_id, &key_spec)
                        .await
                        .map_err(|e| LabeledError::new(e))
                })?
            }
            helpers::Backend::Vault { ref addr, ref token } => {
                runtime.block_on(async {
                    helpers::generate_data_key_vault(addr, token, "provisioning-main", &key_spec)
                        .await
                        .map_err(|e| LabeledError::new(e))
                })?
            }
            helpers::Backend::HttpFallback {
                ref backend_name,
                ref url,
            } => runtime.block_on(async {
                helpers::generate_data_key_http(url, backend_name, &key_spec)
                    .await
                    .map_err(|e| LabeledError::new(e))
            })?,
        };

        Ok(Value::record(
            record! {
                "plaintext" => Value::string(plaintext, call.head),
                "ciphertext" => Value::string(ciphertext, call.head),
            },
            call.head,
        ))
    }
}

// =============================================================================
// Status Command
// =============================================================================

/// KMS status command.
#[derive(Debug)]
pub struct KmsStatus;

impl SimplePluginCommand for KmsStatus {
    type Plugin = KmsPlugin;

    fn name(&self) -> &str {
        "kms status"
    }

    fn signature(&self) -> Signature {
        Signature::build(PluginCommand::name(self))
            .input_output_type(Type::Nothing, Type::Record([].into()))
            .category(Category::Custom("provisioning".into()))
    }

    fn description(&self) -> &str {
        "Check KMS backend status and availability"
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![Example {
            example: "kms status",
            description: "Show current KMS backend status",
            result: None,
        }]
    }

    fn run(
        &self,
        _plugin: &KmsPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        _input: &Value,
    ) -> Result<Value, LabeledError> {
        // Create tokio runtime for async operations
        let runtime = tokio::runtime::Runtime::new()
            .map_err(|e| LabeledError::new(format!("Failed to create runtime: {}", e)))?;

        let backend = runtime.block_on(helpers::detect_backend());

        let (backend_type, available, config) = match backend {
            helpers::Backend::RustyVault { .. } => {
                let addr =
                    std::env::var("RUSTYVAULT_ADDR").unwrap_or_else(|_| "not set".to_string());
                ("rustyvault", true, format!("addr: {}", addr))
            }
            helpers::Backend::Age {
                ref recipient,
                ref identity,
            } => {
                let identity_status = identity.as_ref().map(|_| "set").unwrap_or("not set");
                (
                    "age",
                    true,
                    format!("recipient: {}, identity: {}", recipient, identity_status),
                )
            }
            helpers::Backend::AwsKms { ref key_id } => {
                ("aws", true, format!("key_id: {}", key_id))
            }
            helpers::Backend::Vault { ref addr, .. } => {
                ("vault", true, format!("addr: {}", addr))
            }
            helpers::Backend::HttpFallback {
                ref backend_name,
                ref url,
            } => (backend_name.as_str(), true, format!("url: {}", url)),
        };

        Ok(Value::record(
            record! {
                "backend" => Value::string(backend_type, call.head),
                "available" => Value::bool(available, call.head),
                "config" => Value::string(config, call.head),
            },
            call.head,
        ))
    }
}

// =============================================================================
// List Backends Command
// =============================================================================

/// List supported KMS backends command.
#[derive(Debug)]
pub struct KmsListBackends;

impl SimplePluginCommand for KmsListBackends {
    type Plugin = KmsPlugin;

    fn name(&self) -> &str {
        "kms list-backends"
    }

    fn signature(&self) -> Signature {
        Signature::build(PluginCommand::name(self))
            .input_output_type(Type::Nothing, Type::List(Box::new(Type::Record([].into()))))
            .category(Category::Custom("provisioning".into()))
    }

    fn description(&self) -> &str {
        "List all supported KMS backends and their availability"
    }

    fn examples(&self) -> Vec<Example<'_>> {
        vec![Example {
            example: "kms list-backends",
            description: "Show all supported backends",
            result: None,
        }]
    }

    fn run(
        &self,
        _plugin: &KmsPlugin,
        _engine: &EngineInterface,
        call: &EvaluatedCall,
        _input: &Value,
    ) -> Result<Value, LabeledError> {
        let backends = vec![
            ("rustyvault", "RustyVault Transit backend", "RUSTYVAULT_ADDR, RUSTYVAULT_TOKEN"),
            ("age", "Age file-based encryption", "AGE_RECIPIENT, AGE_IDENTITY"),
            ("aws", "AWS Key Management Service", "AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION"),
            ("vault", "HashiCorp Vault Transit", "VAULT_ADDR, VAULT_TOKEN"),
            ("cosmian", "Cosmian privacy-preserving encryption", "KMS_HTTP_URL"),
        ];

        let backend_values: Vec<Value> = backends
            .iter()
            .map(|(name, description, env_vars)| {
                let available = helpers::check_backend_available(name);
                Value::record(
                    record! {
                        "name" => Value::string(*name, call.head),
                        "description" => Value::string(*description, call.head),
                        "available" => Value::bool(available, call.head),
                        "env_vars" => Value::string(*env_vars, call.head),
                    },
                    call.head,
                )
            })
            .collect();

        Ok(Value::list(backend_values, call.head))
    }
}

/// Entry point for the plugin binary.
fn main() {
    serve_plugin(&KmsPlugin, MsgPackSerializer);
}
