//! File encryption and decryption with streaming support
//! 
//! This module provides efficient file encryption that doesn't load the entire
//! file into memory, making it suitable for encrypting multi-GB files.

use crate::crypto::{derive_key_from_password, NONCE_SIZE, SALT_SIZE, TAG_SIZE};
use crate::error::{CryptoError, Result};
use chacha20poly1305::{
    aead::{Aead, KeyInit, OsRng},
    XChaCha20Poly1305, XNonce,
};
use rand::RngCore;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};
use std::path::Path;

/// Chunk size for streaming encryption (1 MB)
/// Balances memory usage and performance
const CHUNK_SIZE: usize = 1024 * 1024;

/// Encrypt a file using XChaCha20-Poly1305 with password-based key derivation
/// 
/// This function encrypts files of any size using a chunked streaming approach.
/// Each chunk is encrypted independently with its own nonce, allowing for efficient
/// memory usage even with multi-GB files.
/// 
/// # Output Format
/// 
/// ```text
/// [salt (16 bytes)]
/// [master_nonce (24 bytes)]
/// [file_size (8 bytes, little-endian)]
/// [chunk_1: nonce (24) + encrypted_data + tag (16)]
/// [chunk_2: nonce (24) + encrypted_data + tag (16)]
/// ...
/// ```
/// 
/// # Arguments
/// 
/// * `input_path` - Path to the file to encrypt
/// * `output_path` - Path where encrypted file will be written
/// * `password` - Password for key derivation
/// 
/// # Security
/// 
/// - Each file uses a unique random salt
/// - Each chunk uses a unique nonce (derived from master nonce + counter)
/// - Authenticated encryption prevents tampering
/// 
/// # Example
/// 
/// ```rust,no_run
/// use irene_cipher::encrypt_file;
/// 
/// encrypt_file("document.pdf", "document.pdf.enc", "my_password").unwrap();
/// ```
pub fn encrypt_file<P: AsRef<Path>>(
    input_path: P,
    output_path: P,
    password: &str,
) -> Result<()> {
    let input_path = input_path.as_ref();
    let output_path = output_path.as_ref();

    // Open input file
    let input_file = File::open(input_path)?;
    let mut reader = BufReader::new(input_file);
    let file_size = input_path.metadata()?.len();

    // Open output file
    let output_file = File::create(output_path)?;
    let mut writer = BufWriter::new(output_file);

    // Generate random salt for key derivation
    let mut salt = [0u8; SALT_SIZE];
    OsRng.fill_bytes(&mut salt);

    // Derive key from password
    let key_material = derive_key_from_password(password, &salt)?;
    let cipher = XChaCha20Poly1305::new(key_material.as_bytes().into());

    // Generate master nonce (will be incremented for each chunk)
    let mut master_nonce = [0u8; NONCE_SIZE];
    OsRng.fill_bytes(&mut master_nonce);

    // Write header: salt + master_nonce + file_size
    writer.write_all(&salt)?;
    writer.write_all(&master_nonce)?;
    writer.write_all(&file_size.to_le_bytes())?;

    // Encrypt file in chunks
    let mut buffer = vec![0u8; CHUNK_SIZE];
    let mut chunk_counter: u64 = 0;

    loop {
        let bytes_read = reader.read(&mut buffer)?;
        if bytes_read == 0 {
            break; // EOF
        }

        let chunk = &buffer[..bytes_read];

        // Create unique nonce for this chunk (master_nonce + counter)
        let chunk_nonce = create_chunk_nonce(&master_nonce, chunk_counter);

        // Encrypt chunk
        let encrypted_chunk = cipher
            .encrypt(&chunk_nonce, chunk)
            .map_err(|e| CryptoError::EncryptionError(e.to_string()))?;

        // Write: chunk_nonce + encrypted_chunk (includes tag)
        writer.write_all(&chunk_nonce)?;
        writer.write_all(&encrypted_chunk)?;

        chunk_counter += 1;
    }

    writer.flush()?;
    Ok(())
}

/// Decrypt a file encrypted with `encrypt_file`
/// 
/// Reads and decrypts the file in chunks, allowing efficient handling of
/// large files without loading them entirely into memory.
/// 
/// # Arguments
/// 
/// * `input_path` - Path to the encrypted file
/// * `output_path` - Path where decrypted file will be written
/// * `password` - Password for key derivation
/// 
/// # Errors
/// 
/// Returns `CryptoError::AuthenticationFailed` if:
/// - The password is incorrect
/// - Any chunk has been tampered with
/// - The file format is invalid
/// 
/// # Example
/// 
/// ```rust,no_run
/// use irene_cipher::decrypt_file;
/// 
/// decrypt_file("document.pdf.enc", "document.pdf", "my_password").unwrap();
/// ```
pub fn decrypt_file<P: AsRef<Path>>(
    input_path: P,
    output_path: P,
    password: &str,
) -> Result<()> {
    let input_path = input_path.as_ref();
    let output_path = output_path.as_ref();

    // Open input file
    let input_file = File::open(input_path)?;
    let mut reader = BufReader::new(input_file);

    // Open output file
    let output_file = File::create(output_path)?;
    let mut writer = BufWriter::new(output_file);

    // Read header: salt + master_nonce + file_size
    let mut salt = [0u8; SALT_SIZE];
    reader.read_exact(&mut salt)?;

    let mut master_nonce = [0u8; NONCE_SIZE];
    reader.read_exact(&mut master_nonce)?;

    let mut file_size_bytes = [0u8; 8];
    reader.read_exact(&mut file_size_bytes)?;
    let total_file_size = u64::from_le_bytes(file_size_bytes);

    // Derive key from password
    let key_material = derive_key_from_password(password, &salt)?;
    let cipher = XChaCha20Poly1305::new(key_material.as_bytes().into());

    // Decrypt file in chunks
    let mut chunk_counter: u64 = 0;
    let mut bytes_written: u64 = 0;

    loop {
        // Read chunk nonce
        let mut chunk_nonce = [0u8; NONCE_SIZE];
        match reader.read_exact(&mut chunk_nonce) {
            Ok(_) => {}
            Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
                // End of file
                break;
            }
            Err(e) => return Err(e.into()),
        }

        // Calculate expected chunk size
        let remaining = total_file_size - bytes_written;
        let expected_plaintext_size = std::cmp::min(remaining, CHUNK_SIZE as u64) as usize;
        let expected_ciphertext_size = expected_plaintext_size + TAG_SIZE;

        // Read encrypted chunk
        let mut encrypted_chunk = vec![0u8; expected_ciphertext_size];
        reader.read_exact(&mut encrypted_chunk)?;

        // Verify chunk nonce matches expected
        let expected_nonce = create_chunk_nonce(&master_nonce, chunk_counter);
        if chunk_nonce != expected_nonce.as_slice() {
            return Err(CryptoError::InvalidFormat(
                "Chunk nonce mismatch".to_string(),
            ));
        }

        // Decrypt chunk
        let nonce = XNonce::from_slice(&chunk_nonce);
        let plaintext = cipher
            .decrypt(nonce, encrypted_chunk.as_slice())
            .map_err(|_| CryptoError::AuthenticationFailed)?;

        // Write decrypted chunk
        writer.write_all(&plaintext)?;
        bytes_written += plaintext.len() as u64;
        chunk_counter += 1;
    }

    // Verify we wrote the expected amount of data
    if bytes_written != total_file_size {
        return Err(CryptoError::InvalidFormat(format!(
            "File size mismatch: expected {}, got {}",
            total_file_size, bytes_written
        )));
    }

    writer.flush()?;
    Ok(())
}

/// Create a unique nonce for a chunk by combining master nonce with counter
/// 
/// This uses a simple XOR-based mixing approach where the counter is XORed
/// with the last 8 bytes of the master nonce. This ensures each chunk has
/// a unique nonce while being deterministic for verification.
fn create_chunk_nonce(master_nonce: &[u8; NONCE_SIZE], counter: u64) -> XNonce {
    let mut chunk_nonce = *master_nonce;
    let counter_bytes = counter.to_le_bytes();

    // XOR counter into the last 8 bytes of the nonce
    for (i, &byte) in counter_bytes.iter().enumerate() {
        chunk_nonce[NONCE_SIZE - 8 + i] ^= byte;
    }

    *XNonce::from_slice(&chunk_nonce)
}

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

    #[test]
    fn test_encrypt_decrypt_small_file() {
        let temp_dir = TempDir::new().unwrap();
        let input_path = temp_dir.path().join("input.txt");
        let encrypted_path = temp_dir.path().join("encrypted.bin");
        let decrypted_path = temp_dir.path().join("decrypted.txt");

        // Create test file
        let content = b"Hello, World! This is a test file.";
        std::fs::write(&input_path, content).unwrap();

        // Encrypt
        encrypt_file(&input_path, &encrypted_path, "password").unwrap();
        assert!(encrypted_path.exists());

        // Verify encrypted file is different
        let encrypted_content = std::fs::read(&encrypted_path).unwrap();
        assert_ne!(encrypted_content.as_slice(), content);

        // Decrypt
        decrypt_file(&encrypted_path, &decrypted_path, "password").unwrap();

        // Verify decrypted content matches original
        let decrypted_content = std::fs::read(&decrypted_path).unwrap();
        assert_eq!(decrypted_content.as_slice(), content);
    }

    #[test]
    fn test_encrypt_decrypt_large_file() {
        let temp_dir = TempDir::new().unwrap();
        let input_path = temp_dir.path().join("large.bin");
        let encrypted_path = temp_dir.path().join("large.enc");
        let decrypted_path = temp_dir.path().join("large.dec");

        // Create 5 MB test file
        let content = vec![42u8; 5 * 1024 * 1024];
        std::fs::write(&input_path, &content).unwrap();

        // Encrypt
        encrypt_file(&input_path, &encrypted_path, "test_password").unwrap();

        // Decrypt
        decrypt_file(&encrypted_path, &decrypted_path, "test_password").unwrap();

        // Verify
        let decrypted_content = std::fs::read(&decrypted_path).unwrap();
        assert_eq!(decrypted_content, content);
    }

    #[test]
    fn test_wrong_password_fails() {
        let temp_dir = TempDir::new().unwrap();
        let input_path = temp_dir.path().join("input.txt");
        let encrypted_path = temp_dir.path().join("encrypted.bin");
        let decrypted_path = temp_dir.path().join("decrypted.txt");

        std::fs::write(&input_path, b"secret data").unwrap();
        encrypt_file(&input_path, &encrypted_path, "correct_password").unwrap();

        let result = decrypt_file(&encrypted_path, &decrypted_path, "wrong_password");
        assert!(matches!(result, Err(CryptoError::AuthenticationFailed)));
    }

    #[test]
    fn test_tampered_file_fails() {
        let temp_dir = TempDir::new().unwrap();
        let input_path = temp_dir.path().join("input.txt");
        let encrypted_path = temp_dir.path().join("encrypted.bin");
        let decrypted_path = temp_dir.path().join("decrypted.txt");

        std::fs::write(&input_path, b"secret data").unwrap();
        encrypt_file(&input_path, &encrypted_path, "password").unwrap();

        // Tamper with encrypted file
        let mut encrypted_content = std::fs::read(&encrypted_path).unwrap();
        let len = encrypted_content.len();
        encrypted_content[len - 10] ^= 0xFF;
        std::fs::write(&encrypted_path, encrypted_content).unwrap();

        let result = decrypt_file(&encrypted_path, &decrypted_path, "password");
        assert!(matches!(result, Err(CryptoError::AuthenticationFailed)));
    }

    #[test]
    fn test_empty_file() {
        let temp_dir = TempDir::new().unwrap();
        let input_path = temp_dir.path().join("empty.txt");
        let encrypted_path = temp_dir.path().join("empty.enc");
        let decrypted_path = temp_dir.path().join("empty.dec");

        std::fs::write(&input_path, b"").unwrap();
        encrypt_file(&input_path, &encrypted_path, "password").unwrap();
        decrypt_file(&encrypted_path, &decrypted_path, "password").unwrap();

        let decrypted = std::fs::read(&decrypted_path).unwrap();
        assert_eq!(decrypted.len(), 0);
    }

    #[test]
    fn test_chunk_nonce_uniqueness() {
        let master = [42u8; NONCE_SIZE];
        let nonce0 = create_chunk_nonce(&master, 0);
        let nonce1 = create_chunk_nonce(&master, 1);
        let nonce2 = create_chunk_nonce(&master, 2);

        assert_ne!(nonce0.as_slice(), nonce1.as_slice());
        assert_ne!(nonce1.as_slice(), nonce2.as_slice());
        assert_ne!(nonce0.as_slice(), nonce2.as_slice());
    }
}
