use aes_gcm::aead::{Aead, KeyInit, OsRng, Payload};
use aes_gcm::{Aes256Gcm, Nonce as AesNonce};
use rand::RngCore;
use serde_json::{Value, json};
use std::io::Write;
use zeroize::Zeroizing;

use crate::config::Config;
use crate::errors::{MCSError, Result};

// ML-KEM (FIPS 203) via the pure-Rust RustCrypto `ml-kem` crate. Keys and
// ciphertexts use the standard FIPS 203 encodings, so they remain byte-compatible
// with files produced by earlier (pqcrypto-based) releases of this server.
use ml_kem::kem::{Decapsulate, Encapsulate, Kem};
use ml_kem::{
    Ciphertext as MlKemCiphertext, DecapsulationKey, EncapsulationKey, ExpandedDecapsulationKey,
    Key, KeyExport, MlKem768, MlKem1024,
};
// Legacy expanded decapsulation-key encoding (full FIPS 203 `dk`, 2400/3168 bytes).
// Deprecated upstream in favour of 64-byte seeds, but we keep it for wire-format
// compatibility with secret keys generated by previous versions of this server.
#[allow(deprecated)]
use ml_kem::ExpandedKeyEncoding as _;

// ── Constants ────────────────────────────────────────────

const MAGIC: &[u8] = b"MCPE";
const VERSION: u8 = 1;
const NONCE_SIZE: usize = 12;
const SYMMETRIC_KEY_SIZE: usize = 32;
const HEADER_BASE: usize = 8;

const ALGO_AES256GCM: u8 = 1;
const ALGO_CHACHA20: u8 = 2;
// IDs 3 and 4 were RSA-2048/4096-OAEP, removed in the 2026 post-quantum cleanup
// (deprecated by CNSA 2.0 / NIST IR 8547). The IDs are left unused to avoid
// reassigning them to a different algorithm in any older on-disk headers.
const ALGO_MLKEM768: u8 = 5;
const ALGO_MLKEM1024: u8 = 6;

// ── Tool: encrypt_file ───────────────────────────────────

pub async fn encrypt_file(args: Option<&Value>, config: &Config) -> Result<Value> {
    let path = get_str_arg(args, "path")?;
    let algorithm = get_opt_str(args, "algorithm").unwrap_or_else(|| "aes-256-gcm".to_string());
    let output = get_opt_str(args, "output");
    let generate = get_opt_bool(args, "generateKey").unwrap_or(false);

    let key_opt = get_opt_str(args, "key");
    let key_file_opt = get_opt_str(args, "keyFile");
    let pub_key_opt = get_opt_str(args, "publicKey");

    let valid_path = config.sandbox().resolve_path(&path)?;
    let metadata = config.sandbox().metadata(&path).await?;
    if !metadata.is_file() {
        return Err(MCSError::InvalidParams(format!("Not a file: {path}")));
    }
    if metadata.len() > config.max_file_size {
        return Err(MCSError::FilesystemError(format!(
            "File size {size} exceeds max {max}",
            size = metadata.len(),
            max = config.max_file_size
        )));
    }

    let output_path = resolve_crypto_output(&valid_path, output.as_deref(), config)?;
    if output_path == valid_path {
        return Err(MCSError::InvalidParams(
            "Output must differ from source".into(),
        ));
    }

    let algo_id = algorithm_to_id(&algorithm)?;
    let file_data = config.sandbox().read(&path).await?;

    let dst = output_path.clone();

    // Validate keyFile path through sandbox before spawn_blocking
    let resolved_key_bytes = if let Some(ref kf) = key_file_opt {
        let validated_kf = config.sandbox().resolve_path(kf)?;
        let content = std::fs::read_to_string(&validated_kf)
            .map_err(|e| MCSError::FilesystemError(format!("Cannot read key file: {e}")))?;
        let trimmed = content.trim().to_string();
        let bytes = hex::decode(&trimmed)
            .map_err(|e| MCSError::InvalidParams(format!("Invalid key hex in file: {e}")))?;
        if bytes.len() != SYMMETRIC_KEY_SIZE {
            return Err(MCSError::InvalidParams(format!(
                "Key must be {SYMMETRIC_KEY_SIZE} hex bytes"
            )));
        }
        Some(bytes)
    } else {
        None
    };

    let result =
        tokio::task::spawn_blocking(move || -> std::result::Result<EncryptResult, String> {
            match algo_id {
                ALGO_AES256GCM | ALGO_CHACHA20 => {
                    let key = match (key_opt, resolved_key_bytes, generate) {
                        (Some(k), _, _) => {
                            let bytes =
                                hex::decode(&k).map_err(|e| format!("Invalid key hex: {e}"))?;
                            if bytes.len() != SYMMETRIC_KEY_SIZE {
                                return Err(format!("Key must be {SYMMETRIC_KEY_SIZE} hex bytes"));
                            }
                            bytes
                        }
                        (_, Some(bytes), _) => bytes,
                        (None, None, true) => {
                            let mut key = vec![0u8; SYMMETRIC_KEY_SIZE];
                            OsRng.fill_bytes(&mut key);
                            key
                        }
                        (None, None, false) => {
                            return Err(
                                "No key provided. Use 'key', 'keyFile', or 'generateKey: true'"
                                    .into(),
                            );
                        }
                    };
                    let key_hex = Zeroizing::new(hex::encode(&*key));
                    let (header, ciphertext) = symmetric_encrypt(&file_data, &key_hex, algo_id)?;
                    write_encrypted(&dst, &header, &ciphertext)?;
                    let returned = if generate {
                        Some(hex::encode(&*key))
                    } else {
                        None
                    };
                    Ok(EncryptResult { key: returned })
                }
                ALGO_MLKEM768 | ALGO_MLKEM1024 => {
                    let pub_hex = pub_key_opt
                        .as_ref()
                        .ok_or_else(|| "Missing required: 'publicKey'".to_string())?;
                    let pub_bytes =
                        hex::decode(pub_hex).map_err(|e| format!("Invalid public key hex: {e}"))?;
                    let (header, ciphertext, _sym_key) =
                        mlkem_encrypt(&file_data, &pub_bytes, algo_id)?;
                    write_encrypted(&dst, &header, &ciphertext)?;
                    Ok(EncryptResult { key: None })
                }
                _ => Err("Unsupported algorithm".to_string()),
            }
        })
        .await
        .map_err(|e| MCSError::FilesystemError(format!("Encrypt task failed: {e}")))?
        .map_err(MCSError::FilesystemError)?;

    let mut resp = json!({
        "success": true,
        "source": valid_path.to_string_lossy(),
        "output": output_path.to_string_lossy(),
        "algorithm": algorithm,
    });
    if let Some(k) = result.key {
        resp["key"] = json!(k);
    }
    Ok(resp)
}

// ── Tool: decrypt_file ───────────────────────────────────

pub async fn decrypt_file(args: Option<&Value>, config: &Config) -> Result<Value> {
    let path = get_str_arg(args, "path")?;

    let key_opt = get_opt_str(args, "key");
    let key_file_opt = get_opt_str(args, "keyFile");
    let priv_key_opt = get_opt_str(args, "privateKey");

    let valid_path = config.sandbox().resolve_path(&path)?;
    let file_data = config.sandbox().read(&path).await?;

    let (algo_id, enc_key, nonce, body) = parse_encrypted(&file_data)
        .map_err(|e| MCSError::InvalidParams(format!("Invalid encrypted file: {e}")))?;
    let body = body.to_vec();
    let algorithm = id_to_algorithm(algo_id)?;
    let output = get_opt_str(args, "output");
    let output_path = resolve_decrypt_output(&valid_path, output.as_deref(), config)?;

    if output_path == valid_path {
        return Err(MCSError::InvalidParams(
            "Output must differ from source".into(),
        ));
    }

    // Validate keyFile path through sandbox before spawn_blocking
    let resolved_key_bytes = if let Some(ref kf) = key_file_opt {
        let validated_kf = config.sandbox().resolve_path(kf)?;
        let content = std::fs::read_to_string(&validated_kf)
            .map_err(|e| MCSError::FilesystemError(format!("Cannot read key file: {e}")))?;
        let trimmed = content.trim().to_string();
        let bytes = hex::decode(&trimmed)
            .map_err(|e| MCSError::InvalidParams(format!("Invalid key hex in file: {e}")))?;
        if bytes.len() != SYMMETRIC_KEY_SIZE {
            return Err(MCSError::InvalidParams(format!(
                "Key must be {SYMMETRIC_KEY_SIZE} hex bytes"
            )));
        }
        Some(bytes)
    } else {
        None
    };

    let result = tokio::task::spawn_blocking(move || -> std::result::Result<Vec<u8>, String> {
        match algo_id {
            ALGO_AES256GCM | ALGO_CHACHA20 => {
                let key = match (key_opt, resolved_key_bytes) {
                    (Some(k), _) => {
                        let bytes = hex::decode(&k).map_err(|e| format!("Invalid key hex: {e}"))?;
                        if bytes.len() != SYMMETRIC_KEY_SIZE {
                            return Err(format!("Key must be {SYMMETRIC_KEY_SIZE} hex bytes"));
                        }
                        bytes
                    }
                    (_, Some(bytes)) => bytes,
                    (None, None) => return Err("No key provided. Use 'key' or 'keyFile'".into()),
                };
                let key_hex = hex::encode(&key);
                symmetric_decrypt(&body, &key_hex, &nonce, algo_id)
            }
            ALGO_MLKEM768 | ALGO_MLKEM1024 => {
                let priv_hex = priv_key_opt
                    .as_ref()
                    .ok_or_else(|| "Missing required: 'privateKey'".to_string())?;
                let priv_bytes =
                    hex::decode(priv_hex).map_err(|e| format!("Invalid private key hex: {e}"))?;
                mlkem_decrypt(&body, &enc_key, &nonce, &priv_bytes, algo_id)
            }
            _ => Err("Unsupported algorithm".into()),
        }
    })
    .await
    .map_err(|e| MCSError::FilesystemError(format!("Decrypt task failed: {e}")))?
    .map_err(MCSError::FilesystemError)?;

    let output_str = output_path.to_string_lossy().to_string();
    config.sandbox().write(&output_str, &result).await?;

    Ok(json!({
        "success": true,
        "source": valid_path.to_string_lossy(),
        "output": output_path.to_string_lossy(),
        "algorithm": algorithm,
        "decryptedSize": result.len(),
    }))
}

// ── Tool: generate_key ───────────────────────────────────

pub async fn generate_key(args: Option<&Value>, config: &Config) -> Result<Value> {
    let algorithm = get_opt_str(args, "algorithm").unwrap_or_else(|| "aes-256".to_string());
    let output = get_opt_str(args, "output");

    // Validate output path through sandbox and use canonical path
    let validated_output = if let Some(ref out) = output {
        let canonical = config.sandbox().resolve_destination_path(out)?;
        Some(canonical)
    } else {
        None
    };

    let alg_clone = algorithm.clone();
    let out_clone = validated_output.clone();

    let result =
        tokio::task::spawn_blocking(move || -> std::result::Result<KeygenResult, String> {
            match alg_clone.as_str() {
                "aes-256" => {
                    let mut key = vec![0u8; SYMMETRIC_KEY_SIZE];
                    OsRng.fill_bytes(&mut key);
                    let key_hex = hex::encode(&key);
                    if let Some(ref out) = out_clone {
                        std::fs::write(out, &key_hex)
                            .map_err(|e| format!("Cannot write key file: {e}"))?;
                    }
                    Ok(KeygenResult {
                        key: Some(key_hex),
                        public_key: None,
                        private_key: None,
                    })
                }
                "kyber-768" | "kyber-1024" | "mlkem-768" | "mlkem-1024" => {
                    #[allow(deprecated)]
                    let (pk_hex, sk_hex) = if alg_clone == "kyber-768" || alg_clone == "mlkem-768" {
                        let (dk, ek) = MlKem768::generate_keypair();
                        (
                            hex::encode(ek.to_bytes()),
                            hex::encode(dk.to_expanded_bytes()),
                        )
                    } else {
                        let (dk, ek) = MlKem1024::generate_keypair();
                        (
                            hex::encode(ek.to_bytes()),
                            hex::encode(dk.to_expanded_bytes()),
                        )
                    };
                    if let Some(ref out) = out_clone {
                        let pub_path = out.with_extension("pub");
                        std::fs::write(out, &sk_hex)
                            .map_err(|e| format!("Cannot write secret key: {e}"))?;
                        std::fs::write(&pub_path, &pk_hex)
                            .map_err(|e| format!("Cannot write public key: {e}"))?;
                    }
                    Ok(KeygenResult {
                        key: None,
                        public_key: Some(pk_hex),
                        private_key: Some(sk_hex),
                    })
                }
                _ => Err(format!("Unsupported key algorithm: {alg_clone}")),
            }
        })
        .await
        .map_err(|e| MCSError::FilesystemError(format!("Key generation failed: {e}")))?
        .map_err(MCSError::FilesystemError)?;

    let mut resp = json!({ "success": true, "algorithm": algorithm });
    if let Some(k) = result.key {
        resp["key"] = json!(k);
    }
    if let Some(pk) = result.public_key {
        resp["publicKey"] = json!(pk);
    }
    if let Some(sk) = result.private_key {
        resp["privateKey"] = json!(sk);
    }
    if let Some(ref out) = validated_output {
        if algorithm == "aes-256" {
            resp["keyFile"] = json!(out.to_string_lossy().to_string());
        } else {
            let pub_path = out.with_extension("pub");
            resp["privateKeyFile"] = json!(out.to_string_lossy().to_string());
            resp["publicKeyFile"] = json!(pub_path.to_string_lossy().to_string());
        }
    }
    Ok(resp)
}

// ── Encryption / Decryption Helpers ──────────────────────

#[derive(Default)]
struct EncryptResult {
    key: Option<String>,
}

struct KeygenResult {
    key: Option<String>,
    public_key: Option<String>,
    private_key: Option<String>,
}

fn symmetric_encrypt(
    plaintext: &[u8],
    key_hex: &str,
    algo_id: u8,
) -> std::result::Result<(Vec<u8>, Vec<u8>), String> {
    let key_bytes = hex::decode(key_hex).map_err(|e| format!("Invalid key hex: {e}"))?;
    if key_bytes.len() != SYMMETRIC_KEY_SIZE {
        return Err(format!("Key must be {SYMMETRIC_KEY_SIZE} bytes"));
    }

    let mut nonce = [0u8; NONCE_SIZE];
    OsRng.fill_bytes(&mut nonce);

    let header = build_header(algo_id, &[]);
    let aad = &header[..header.len() - 2];

    let ciphertext = if algo_id == ALGO_CHACHA20 {
        use chacha20poly1305::aead::Aead as _;
        let key = chacha20poly1305::Key::from_slice(&key_bytes);
        let cipher = chacha20poly1305::ChaCha20Poly1305::new(key);
        let nonce_arr = chacha20poly1305::Nonce::from_slice(&nonce);
        cipher
            .encrypt(
                nonce_arr,
                Payload {
                    msg: plaintext,
                    aad,
                },
            )
            .map_err(|e| format!("ChaCha20 encryption failed: {e}"))?
    } else {
        let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&key_bytes);
        let cipher = Aes256Gcm::new(key);
        cipher
            .encrypt(
                AesNonce::from_slice(&nonce),
                Payload {
                    msg: plaintext,
                    aad,
                },
            )
            .map_err(|e| format!("AES-256-GCM encryption failed: {e}"))?
    };

    Ok(([header, nonce.to_vec()].concat(), ciphertext))
}

fn symmetric_decrypt(
    ciphertext: &[u8],
    key_hex: &str,
    nonce: &[u8],
    algo_id: u8,
) -> std::result::Result<Vec<u8>, String> {
    let key_bytes = hex::decode(key_hex).map_err(|e| format!("Invalid key hex: {e}"))?;
    if key_bytes.len() != SYMMETRIC_KEY_SIZE {
        return Err(format!("Key must be {SYMMETRIC_KEY_SIZE} bytes"));
    }

    let header = build_header(algo_id, &[]);
    let aad = &header[..header.len() - 2];

    let plaintext = if algo_id == ALGO_CHACHA20 {
        use chacha20poly1305::aead::Aead as _;
        let key = chacha20poly1305::Key::from_slice(&key_bytes);
        let cipher = chacha20poly1305::ChaCha20Poly1305::new(key);
        let nonce_arr = chacha20poly1305::Nonce::from_slice(nonce);
        cipher
            .decrypt(
                nonce_arr,
                Payload {
                    msg: ciphertext,
                    aad,
                },
            )
            .map_err(|e| format!("ChaCha20 decryption failed (wrong key or data): {e}"))?
    } else {
        let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&key_bytes);
        let cipher = Aes256Gcm::new(key);
        cipher
            .decrypt(
                AesNonce::from_slice(nonce),
                Payload {
                    msg: ciphertext,
                    aad,
                },
            )
            .map_err(|e| format!("AES-256-GCM decryption failed (wrong key or data): {e}"))?
    };

    Ok(plaintext)
}

#[allow(clippy::type_complexity)]
fn mlkem_encrypt(
    plaintext: &[u8],
    pub_key: &[u8],
    algo_id: u8,
) -> std::result::Result<(Vec<u8>, Vec<u8>, Vec<u8>), String> {
    let (shared_secret, ct) = if algo_id == ALGO_MLKEM768 {
        let key = Key::<EncapsulationKey<MlKem768>>::try_from(pub_key)
            .map_err(|_| "Invalid ML-KEM-768 public key length".to_string())?;
        let ek = EncapsulationKey::<MlKem768>::new(&key)
            .map_err(|_| "Invalid ML-KEM-768 public key".to_string())?;
        let (c, ss) = ek.encapsulate();
        (ss.to_vec(), c.to_vec())
    } else {
        let key = Key::<EncapsulationKey<MlKem1024>>::try_from(pub_key)
            .map_err(|_| "Invalid ML-KEM-1024 public key length".to_string())?;
        let ek = EncapsulationKey::<MlKem1024>::new(&key)
            .map_err(|_| "Invalid ML-KEM-1024 public key".to_string())?;
        let (c, ss) = ek.encapsulate();
        (ss.to_vec(), c.to_vec())
    };

    let mut nonce = [0u8; NONCE_SIZE];
    OsRng.fill_bytes(&mut nonce);

    let header = build_header(algo_id, &ct);
    let aad = &header[..header.len() - 2];

    let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&shared_secret);
    let cipher = Aes256Gcm::new(key);
    let ciphertext = cipher
        .encrypt(
            AesNonce::from_slice(&nonce),
            Payload {
                msg: plaintext,
                aad,
            },
        )
        .map_err(|e| format!("AES encryption failed: {e}"))?;

    Ok(([header, nonce.to_vec()].concat(), ciphertext, shared_secret))
}

fn mlkem_decrypt(
    ciphertext: &[u8],
    enc_key: &[u8],
    nonce: &[u8],
    priv_key: &[u8],
    algo_id: u8,
) -> std::result::Result<Vec<u8>, String> {
    #[allow(deprecated)]
    let shared_secret = if algo_id == ALGO_MLKEM768 {
        let sk_enc = ExpandedDecapsulationKey::<MlKem768>::try_from(priv_key)
            .map_err(|_| "Invalid ML-KEM-768 secret key length".to_string())?;
        let dk = DecapsulationKey::<MlKem768>::from_expanded_bytes(&sk_enc)
            .map_err(|_| "Invalid ML-KEM-768 secret key".to_string())?;
        let ct = MlKemCiphertext::<MlKem768>::try_from(enc_key)
            .map_err(|_| "Invalid ML-KEM ciphertext length".to_string())?;
        dk.decapsulate(&ct).to_vec()
    } else {
        let sk_enc = ExpandedDecapsulationKey::<MlKem1024>::try_from(priv_key)
            .map_err(|_| "Invalid ML-KEM-1024 secret key length".to_string())?;
        let dk = DecapsulationKey::<MlKem1024>::from_expanded_bytes(&sk_enc)
            .map_err(|_| "Invalid ML-KEM-1024 secret key".to_string())?;
        let ct = MlKemCiphertext::<MlKem1024>::try_from(enc_key)
            .map_err(|_| "Invalid ML-KEM ciphertext length".to_string())?;
        dk.decapsulate(&ct).to_vec()
    };

    let header = build_header(algo_id, enc_key);
    let aad = &header[..header.len() - 2];

    let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&shared_secret);
    let cipher = Aes256Gcm::new(key);
    let plaintext = cipher
        .decrypt(
            AesNonce::from_slice(nonce),
            Payload {
                msg: ciphertext,
                aad,
            },
        )
        .map_err(|e| format!("AES decryption failed: {e}"))?;

    Ok(plaintext)
}

// ── File Format Helpers ──────────────────────────────────

fn build_header(algo_id: u8, enc_key: &[u8]) -> Vec<u8> {
    let key_len = enc_key.len() as u16;
    let mut header = Vec::with_capacity(HEADER_BASE + enc_key.len());
    header.extend_from_slice(MAGIC);
    header.push(VERSION);
    header.push(algo_id);
    header.extend_from_slice(&key_len.to_be_bytes());
    header.extend_from_slice(enc_key);
    header
}

#[allow(clippy::type_complexity)]
fn parse_encrypted(data: &[u8]) -> std::result::Result<(u8, Vec<u8>, Vec<u8>, &[u8]), String> {
    if data.len() < HEADER_BASE + NONCE_SIZE + 1 {
        return Err("File too small to be a valid encrypted file".into());
    }
    if &data[..4] != MAGIC {
        return Err("Invalid magic bytes — not an MCPE encrypted file".into());
    }
    if data[4] != VERSION {
        return Err(format!("Unsupported version: {}", data[4]));
    }

    let algo_id = data[5];
    let key_len = u16::from_be_bytes([data[6], data[7]]) as usize;
    let header_total = HEADER_BASE + key_len;

    if data.len() < header_total + NONCE_SIZE + 1 {
        return Err("File truncated".into());
    }

    let enc_key = data[HEADER_BASE..header_total].to_vec();
    let nonce = data[header_total..header_total + NONCE_SIZE].to_vec();
    let body = &data[header_total + NONCE_SIZE..];

    Ok((algo_id, enc_key, nonce, body))
}

fn write_encrypted(
    path: &std::path::Path,
    header: &[u8],
    ciphertext: &[u8],
) -> std::result::Result<(), String> {
    let mut file = std::fs::File::create(path).map_err(|e| format!("Cannot create output: {e}"))?;
    file.write_all(header)
        .map_err(|e| format!("Cannot write header: {e}"))?;
    file.write_all(ciphertext)
        .map_err(|e| format!("Cannot write ciphertext: {e}"))?;
    file.flush().map_err(|e| format!("Cannot flush: {e}"))?;
    Ok(())
}

// ── Algorithm Helpers ────────────────────────────────────

fn algorithm_to_id(name: &str) -> Result<u8> {
    Ok(match name {
        "aes-256-gcm" => ALGO_AES256GCM,
        "chacha20-poly1305" => ALGO_CHACHA20,
        "kyber-768" | "mlkem-768" => ALGO_MLKEM768,
        "kyber-1024" | "mlkem-1024" => ALGO_MLKEM1024,
        _ => {
            return Err(MCSError::InvalidParams(format!(
                "Unsupported algorithm: {name}"
            )));
        }
    })
}

fn id_to_algorithm(id: u8) -> Result<String> {
    Ok(match id {
        ALGO_AES256GCM => "aes-256-gcm".into(),
        ALGO_CHACHA20 => "chacha20-poly1305".into(),
        ALGO_MLKEM768 => "mlkem-768".into(),
        ALGO_MLKEM1024 => "mlkem-1024".into(),
        _ => {
            return Err(MCSError::InvalidParams(format!(
                "Unknown algorithm ID: {id}"
            )));
        }
    })
}

fn resolve_crypto_output(
    source: &std::path::Path,
    explicit: Option<&str>,
    config: &Config,
) -> Result<std::path::PathBuf> {
    if let Some(out) = explicit {
        config.sandbox().resolve_destination_path(out)
    } else {
        let mut result = source.to_path_buf();
        let name = result
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_default();
        result.set_file_name(format!("{name}.enc"));
        Ok(result)
    }
}

fn resolve_decrypt_output(
    source: &std::path::Path,
    explicit: Option<&str>,
    config: &Config,
) -> Result<std::path::PathBuf> {
    if let Some(out) = explicit {
        config.sandbox().resolve_destination_path(out)
    } else {
        let name = source
            .file_name()
            .map(|n| n.to_string_lossy().to_string())
            .unwrap_or_default();
        let stripped = name.strip_suffix(".enc").unwrap_or(&name);
        let mut result = source.to_path_buf();
        result.set_file_name(stripped);
        Ok(result)
    }
}

// ── Argument Helpers ─────────────────────────────────────

fn get_str_arg(args: Option<&Value>, name: &str) -> Result<String> {
    args.and_then(|a| a.get(name))
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
        .ok_or_else(|| MCSError::InvalidParams(format!("Missing required: '{name}'")))
}

fn get_opt_str(args: Option<&Value>, name: &str) -> Option<String> {
    args.and_then(|a| a.get(name))
        .and_then(|v| v.as_str())
        .map(|s| s.to_string())
}

fn get_opt_bool(args: Option<&Value>, name: &str) -> Option<bool> {
    args.and_then(|a| a.get(name)).and_then(|v| v.as_bool())
}
