//! Device identity: ed25519 keypairs in OpenSSH format, signing and verification.

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use serde::{Deserialize, Serialize};
use ssh_key::{LineEnding, PrivateKey, PublicKey};
use zeroize::Zeroizing;

use crate::error::{RelicarioError, Result};

/// A registered device entry in devices.json.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeviceEntry {
    pub name: String,
    /// OpenSSH public key format: "ssh-ed25519 AAAA..."
    pub public_key: String,
    pub added_at: i64,
    pub added_by: String,
}

/// A revoked device entry in revoked.json.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RevokedEntry {
    pub name: String,
    pub public_key: String,
    pub revoked_at: i64,
    pub revoked_by: String,
}

/// Generate a new ed25519 keypair, returning (private_openssh, public_openssh).
pub fn generate_keypair() -> Result<(Zeroizing<String>, String)> {
    use ssh_key::private::{Ed25519Keypair, Ed25519PrivateKey, KeypairData};
    use ssh_key::public::Ed25519PublicKey;

    let signing_key = SigningKey::generate(&mut rand::rngs::OsRng);
    let verifying_key = signing_key.verifying_key();

    // Build ssh-key types from raw bytes
    let ed_private = Ed25519PrivateKey::from_bytes(signing_key.as_bytes());
    let ed_public = Ed25519PublicKey(*verifying_key.as_bytes());
    let keypair = Ed25519Keypair { public: ed_public, private: ed_private };
    let keypair_data = KeypairData::Ed25519(keypair);

    let ssh_private = PrivateKey::new(keypair_data, "")
        .map_err(|e| RelicarioError::DeviceKey(format!("private key create: {e}")))?;
    let ssh_public = ssh_private.public_key();

    let private_pem = ssh_private
        .to_openssh(LineEnding::LF)
        .map_err(|e| RelicarioError::DeviceKey(format!("private key encode: {e}")))?;
    let public_line = ssh_public
        .to_openssh()
        .map_err(|e| RelicarioError::DeviceKey(format!("public key encode: {e}")))?;

    Ok((Zeroizing::new(private_pem.to_string()), public_line))
}

/// Sign data with an OpenSSH private key, returning base64 signature.
pub fn sign(private_key_openssh: &str, data: &[u8]) -> Result<String> {
    use base64::Engine;

    let private = PrivateKey::from_openssh(private_key_openssh)
        .map_err(|e| RelicarioError::DeviceKey(format!("parse private key: {e}")))?;

    let key_data = private
        .key_data()
        .ed25519()
        .ok_or_else(|| RelicarioError::DeviceKey("not an ed25519 key".into()))?;

    let secret_slice: &[u8] = key_data.private.as_ref();
    let secret_bytes: [u8; 32] = secret_slice
        .try_into()
        .map_err(|_| RelicarioError::DeviceKey("invalid key length".into()))?;

    let signing_key = SigningKey::from_bytes(&secret_bytes);
    let signature = signing_key.sign(data);
    Ok(base64::engine::general_purpose::STANDARD.encode(signature.to_bytes()))
}

/// Verify a signature against an OpenSSH public key.
pub fn verify(public_key_openssh: &str, data: &[u8], signature_b64: &str) -> Result<bool> {
    use base64::Engine;

    let public = PublicKey::from_openssh(public_key_openssh)
        .map_err(|e| RelicarioError::DeviceKey(format!("parse public key: {e}")))?;

    let key_data = public
        .key_data()
        .ed25519()
        .ok_or_else(|| RelicarioError::DeviceKey("not an ed25519 key".into()))?;

    let pub_slice: &[u8] = key_data.as_ref();
    let pub_bytes: [u8; 32] = pub_slice
        .try_into()
        .map_err(|_| RelicarioError::DeviceKey("invalid key length".into()))?;

    let verifying_key = VerifyingKey::from_bytes(&pub_bytes)
        .map_err(|e| RelicarioError::DeviceKey(format!("invalid public key: {e}")))?;

    let sig_bytes = base64::engine::general_purpose::STANDARD
        .decode(signature_b64)
        .map_err(|e| RelicarioError::DeviceKey(format!("decode signature: {e}")))?;

    let signature = Signature::from_slice(&sig_bytes)
        .map_err(|e| RelicarioError::DeviceKey(format!("parse signature: {e}")))?;

    Ok(verifying_key.verify(data, &signature).is_ok())
}

/// Compute the OpenSSH SHA-256 fingerprint of a public key.
/// Output format matches `ssh-keygen -lf` and `git verify-commit --raw`:
/// `SHA256:<43-char base64 without padding>`.
pub fn fingerprint(public_key_openssh: &str) -> Result<String> {
    use ssh_key::HashAlg;
    let public = PublicKey::from_openssh(public_key_openssh)
        .map_err(|e| RelicarioError::DeviceKey(format!("parse public key: {e}")))?;
    Ok(public.fingerprint(HashAlg::Sha256).to_string())
}

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

    #[test]
    fn generate_and_sign_verify_roundtrip() {
        let (private, public) = generate_keypair().unwrap();
        let data = b"hello world";
        let sig = sign(&private, data).unwrap();
        assert!(verify(&public, data, &sig).unwrap());
    }

    #[test]
    fn verify_rejects_wrong_data() {
        let (private, public) = generate_keypair().unwrap();
        let sig = sign(&private, b"hello").unwrap();
        assert!(!verify(&public, b"world", &sig).unwrap());
    }

    #[test]
    fn verify_rejects_wrong_key() {
        let (private, _) = generate_keypair().unwrap();
        let (_, other_public) = generate_keypair().unwrap();
        let sig = sign(&private, b"hello").unwrap();
        assert!(!verify(&other_public, b"hello", &sig).unwrap());
    }

    #[test]
    fn fingerprint_matches_ssh_keygen_format() {
        let (_, public) = generate_keypair().unwrap();
        let fp = fingerprint(&public).unwrap();
        assert!(fp.starts_with("SHA256:"), "fingerprint should start with SHA256: prefix, got {fp}");
        let body = fp.strip_prefix("SHA256:").unwrap();
        assert_eq!(body.len(), 43, "SHA-256 fingerprint body is 43 base64 chars (no padding)");
        assert!(body.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/'));
    }

    #[test]
    fn fingerprint_is_deterministic() {
        let (_, public) = generate_keypair().unwrap();
        assert_eq!(fingerprint(&public).unwrap(), fingerprint(&public).unwrap());
    }

    #[test]
    fn fingerprint_differs_per_key() {
        let (_, p1) = generate_keypair().unwrap();
        let (_, p2) = generate_keypair().unwrap();
        assert_ne!(fingerprint(&p1).unwrap(), fingerprint(&p2).unwrap());
    }
}
