//! Attachment refs (carried on Item) and summaries (carried in Manifest).
//!
//! Encryption helpers (`encrypt_attachment`, `decrypt_attachment`) are added
//! later in Task 22 once the crypto module is settled.

use serde::{Deserialize, Serialize};

use crate::ids::AttachmentId;

/// Reference to an attachment, carried on the Item record.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentRef {
    pub id: AttachmentId,
    pub filename: String,
    pub mime_type: String,
    /// Plaintext size in bytes.
    pub size: u64,
    /// Unix-seconds when this attachment was added.
    pub created: i64,
}

/// Compact summary of an attachment, carried in the Manifest so the popup
/// can show attachment indicators without decrypting the item file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentSummary {
    pub id: AttachmentId,
    pub filename: String,
    pub mime_type: String,
    pub size: u64,
}

impl From<&AttachmentRef> for AttachmentSummary {
    fn from(r: &AttachmentRef) -> Self {
        Self {
            id: r.id.clone(),
            filename: r.filename.clone(),
            mime_type: r.mime_type.clone(),
            size: r.size,
        }
    }
}

use zeroize::Zeroizing;

use crate::crypto::{decrypt, encrypt};
use crate::error::{RelicarioError, Result};

/// Encrypted attachment with the AID derived from plaintext content.
#[derive(Debug)]
pub struct EncryptedAttachment {
    pub id: AttachmentId,
    pub bytes: Vec<u8>,
}

/// Encrypt raw attachment bytes, deriving the [`AttachmentId`] from `sha256(plaintext)`.
///
/// Returns [`RelicarioError::AttachmentTooLarge`] immediately if `plaintext.len() > max_bytes`,
/// before any crypto work is done.
///
/// ## Call-site adaptation
///
/// `crypto::encrypt` accepts `&[u8; 32]`; we coerce `&Zeroizing<[u8; 32]>` via
/// `&**master_key` (double-deref: `Zeroizing<[u8;32]>` → `[u8;32]` → `&[u8;32]`).
pub fn encrypt_attachment(
    plaintext: &[u8],
    master_key: &Zeroizing<[u8; 32]>,
    max_bytes: u64,
) -> Result<EncryptedAttachment> {
    if plaintext.len() as u64 > max_bytes {
        return Err(RelicarioError::AttachmentTooLarge {
            size: plaintext.len() as u64,
            max: max_bytes,
        });
    }
    let id = AttachmentId::from_plaintext(plaintext);
    let bytes = encrypt(master_key, plaintext)?;
    Ok(EncryptedAttachment { id, bytes })
}

/// Decrypt a blob produced by [`encrypt_attachment`], returning the plaintext
/// wrapped in [`Zeroizing`] so it is wiped on drop.
///
/// ## Call-site adaptation
///
/// `crypto::decrypt` accepts `&[u8; 32]`; we coerce via `&**master_key`.
pub fn decrypt_attachment(
    encrypted: &[u8],
    master_key: &Zeroizing<[u8; 32]>,
) -> Result<Zeroizing<Vec<u8>>> {
    let plaintext = decrypt(master_key, encrypted)?;
    Ok(Zeroizing::new(plaintext))
}

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

    fn key() -> Zeroizing<[u8; 32]> {
        Zeroizing::new([0x42u8; 32])
    }

    #[test]
    fn attachment_round_trip() {
        let plaintext = b"the quick brown fox jumps over the lazy dog";
        let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
        let dec = decrypt_attachment(&enc.bytes, &key()).unwrap();
        assert_eq!(dec.as_slice(), plaintext);
    }

    #[test]
    fn attachment_id_matches_sha256() {
        let plaintext = b"hello world";
        let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
        assert_eq!(enc.id, AttachmentId::from_plaintext(plaintext));
    }

    #[test]
    fn oversize_attachment_rejected() {
        let plaintext = vec![0u8; 11_000_000];
        let err = encrypt_attachment(&plaintext, &key(), 10 * 1024 * 1024);
        assert!(matches!(err, Err(RelicarioError::AttachmentTooLarge { .. })));
    }

    #[test]
    fn wrong_key_fails_with_opaque_decrypt() {
        let plaintext = b"x";
        let enc = encrypt_attachment(plaintext, &key(), 1024).unwrap();
        let wrong = Zeroizing::new([0u8; 32]);
        let err = decrypt_attachment(&enc.bytes, &wrong);
        assert!(matches!(err, Err(RelicarioError::Decrypt)));
    }
}

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

    #[test]
    fn attachment_ref_round_trip() {
        let r = AttachmentRef {
            id: AttachmentId("0123456789abcdef".into()),
            filename: "doc.pdf".into(),
            mime_type: "application/pdf".into(),
            size: 12345,
            created: 1_700_000_000,
        };
        let json = serde_json::to_string(&r).unwrap();
        let parsed: AttachmentRef = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed.filename, "doc.pdf");
        assert_eq!(parsed.size, 12345);
    }

    #[test]
    fn attachment_summary_from_ref() {
        let r = AttachmentRef {
            id: AttachmentId("aabb".into()),
            filename: "x.txt".into(),
            mime_type: "text/plain".into(),
            size: 5,
            created: 0,
        };
        let s: AttachmentSummary = (&r).into();
        assert_eq!(s.filename, "x.txt");
        assert_eq!(s.id, r.id);
    }
}
