//! Key-file armor for the pluggable second factor. The file holds the raw
//! 32-byte secret (base64) behind a version header -- it is the "something
//! you have", not an encrypted artifact (the passphrase is the other factor).
//!
//! The format is intentionally minimal and human-inspectable:
//!
//! ```text
//! relicario-keyfile-v1
//! <base64-standard of 32 bytes>
//! ```
//!
//! Two lines, both terminated with `\n`. There is no encryption here: the file
//! is as sensitive as the raw secret itself. Store it accordingly (encrypted
//! drive, hardware token, offline backup). The passphrase remains the second
//! factor; together they feed Argon2id to derive the master key.

use base64::{engine::general_purpose::STANDARD, Engine};
use zeroize::Zeroizing;

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

const HEADER: &str = "relicario-keyfile-v1";

/// Encode a 32-byte secret into the `relicario-keyfile-v1` armor format.
///
/// Output is: `"relicario-keyfile-v1\n<base64>\n"` as UTF-8 bytes.
/// The returned bytes are suitable for writing directly to a `.relkey` file.
pub fn keyfile_encode(secret: &[u8; 32]) -> Vec<u8> {
    format!("{HEADER}\n{}\n", STANDARD.encode(secret)).into_bytes()
}

/// Decode a `relicario-keyfile-v1` armored file back to the raw 32-byte secret.
///
/// Returns [`RelicarioError::Format`] if the header is missing or wrong, if the
/// body is not valid base64, or if the decoded length is not exactly 32 bytes.
pub fn keyfile_decode(bytes: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
    let text = std::str::from_utf8(bytes)
        .map_err(|_| RelicarioError::Format("key file is not UTF-8".into()))?;
    let mut lines = text.lines();
    if lines.next() != Some(HEADER) {
        return Err(RelicarioError::Format("bad key-file header".into()));
    }
    let b64 = match lines.next() {
        Some(line) => line.trim(),
        None => return Err(RelicarioError::Format("key-file missing body line".into())),
    };
    let decoded: Zeroizing<Vec<u8>> = Zeroizing::new(
        STANDARD
            .decode(b64)
            .map_err(|_| RelicarioError::Format("key-file body not base64".into()))?,
    );
    if decoded.len() != 32 {
        return Err(RelicarioError::Format("key-file secret must be 32 bytes".into()));
    }
    let mut arr = Zeroizing::new([0u8; 32]);
    arr.copy_from_slice(&decoded);
    Ok(arr)
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn round_trip() {
        let secret = [9u8; 32];
        let armored = keyfile_encode(&secret);
        assert!(std::str::from_utf8(&armored).unwrap().starts_with("relicario-keyfile-v1\n"));
        let back = keyfile_decode(&armored).unwrap();
        assert_eq!(*back, secret);
    }
    #[test]
    fn rejects_bad_header() {
        assert!(keyfile_decode(b"not-a-keyfile\nAAAA\n").is_err());
    }
    #[test]
    fn rejects_wrong_length() {
        assert!(keyfile_decode(b"relicario-keyfile-v1\nAAAA\n").is_err()); // decodes to <32 bytes
    }
    #[test]
    fn rejects_non_utf8() {
        assert!(keyfile_decode(b"\xff\xfe\nAAAA\n").is_err());
    }
    #[test]
    fn rejects_missing_body() {
        assert!(keyfile_decode(b"relicario-keyfile-v1\n").is_err());
    }
}
