// ArchiveProcessor Trait - Universal Interface for All Compression Formats

use anyhow::Result;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

/// Archive Format Type Enumeration
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArchiveFormat {
    // Core formats (always enabled)
    Zip,
    Tar,
    Gzip,
    Zstd,
    Bzip2,
    Lz4,
    TarGzip,
    TarBzip2,
    TarZstd,
    
    // Optional formats (controversial)
    Rar,    // ⚠️ Legal risk (RARLAB patent)
    Xz,     // ⚠️ External dependency (liblzma)
    SevenZ, // ⚠️ Unstable library (sevenz-rust 0.21.0)
    
    Unknown,
}

impl std::fmt::Display for ArchiveFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ArchiveFormat::Zip => write!(f, "ZIP"),
            ArchiveFormat::Tar => write!(f, "TAR"),
            ArchiveFormat::Gzip => write!(f, "GZIP"),
            ArchiveFormat::Zstd => write!(f, "ZSTD"),
            ArchiveFormat::Bzip2 => write!(f, "BZIP2"),
            ArchiveFormat::Lz4 => write!(f, "LZ4"),
            ArchiveFormat::TarGzip => write!(f, "TAR.GZ"),
            ArchiveFormat::TarBzip2 => write!(f, "TAR.BZ2"),
            ArchiveFormat::TarZstd => write!(f, "TAR.ZST"),
            ArchiveFormat::Rar => write!(f, "RAR"),
            ArchiveFormat::Xz => write!(f, "XZ"),
            ArchiveFormat::SevenZ => write!(f, "7Z"),
            ArchiveFormat::Unknown => write!(f, "Unknown"),
        }
    }
}

/// Archive Processor Trait - Universal Interface
/// All compression format processors must implement this trait
pub trait ArchiveProcessor: Send + Sync {
    /// Format type supported by this processor
    fn format(&self) -> ArchiveFormat;
    
    /// Open archive file and read metadata
    fn open(&mut self, path: &Path) -> Result<ArchiveMetadata>;
    
    /// List all file entries in archive
    fn list_entries(&self) -> Result<Vec<ArchiveEntry>>;
    
    /// Extract single file (on-demand decompression)
    fn extract_file(&self, entry_path: &Path, output: &mut Vec<u8>) -> Result<u64>;
    
    /// Extract all files to directory (batch extraction)
    fn extract_all(&self, output_dir: &Path) -> Result<ExtractResult>;
    
    /// Check if this processor can handle the format
    fn can_process(format: ArchiveFormat) -> bool where Self: Sized;
    
    /// Create new processor instance
    fn new() -> Self where Self: Sized;
}

/// Archive File Metadata
#[derive(Debug, Clone)]
pub struct ArchiveMetadata {
    pub format: ArchiveFormat,
    pub total_files: u64,
    pub total_size: u64,
    pub compressed_size: u64,
    pub compression_ratio: f64,
    pub is_encrypted: bool,
    pub is_multi_volume: bool,
    pub created_time: Option<SystemTime>,
}

impl ArchiveMetadata {
    /// Calculate compression ratio
    pub fn compression_ratio(&self) -> f64 {
        if self.compressed_size == 0 {
            0.0
        } else {
            self.total_size as f64 / self.compressed_size as f64
        }
    }
}

/// Archive Entry Information
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
    pub path: PathBuf,
    pub size: u64,
    pub compressed_size: u64,
    pub is_dir: bool,
    pub is_file: bool,
    pub is_encrypted: bool,
    pub modified: SystemTime,
    pub permissions: Option<u32>,
}

/// Extract Result Statistics
#[derive(Debug)]
pub struct ExtractResult {
    pub total_files: u64,
    pub total_bytes: u64,
    pub failed_files: Vec<PathBuf>,
    pub warnings: Vec<String>,
}

impl ExtractResult {
    pub fn new() -> Self {
        Self {
            total_files: 0,
            total_bytes: 0,
            failed_files: Vec::new(),
            warnings: Vec::new(),
        }
    }
    
    pub fn success_rate(&self) -> f64 {
        if self.total_files == 0 {
            100.0
        } else {
            let success_count = self.total_files - self.failed_files.len() as u64;
            (success_count as f64 / self.total_files as f64) * 100.0
        }
    }
}

/// Security Validation - Zip Slip Protection
pub fn validate_extraction_path(entry_path: &Path, base_dir: &Path) -> Result<PathBuf> {
    use std::path::Component;
    
    // 1. Check path components
    for component in entry_path.components() {
        match component {
            // Prohibit parent directory reference (../)
            Component::ParentDir => {
                return Err(anyhow::anyhow!("Zip Slip detected: path contains '..'"));
            }
            // Prohibit root directory (/)
            Component::Prefix(_) | Component::RootDir => {
                return Err(anyhow::anyhow!("Zip Slip detected: absolute path"));
            }
            // Allow normal components
            Component::Normal(_) | Component::CurDir => {}
        }
    }
    
    // 2. Build full path
    let full_path = base_dir.join(entry_path);
    
    // 3. Canonicalize and validate (ensure within base_dir)
    let canonical_base = base_dir.canonicalize()
        .map_err(|e| anyhow::anyhow!("Cannot canonicalize base dir: {}", e))?;
    
    // Create parent directories first
    if let Some(parent) = full_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    
    // 4. Verify extraction path is within base_dir
    // Note: full_path may not exist yet, so we check parent directory
    if full_path.exists() {
        let canonical_full = full_path.canonicalize()
            .map_err(|e| anyhow::anyhow!("Cannot canonicalize full path: {}", e))?;
        
        if !canonical_full.starts_with(&canonical_base) {
            return Err(anyhow::anyhow!("Zip Slip detected: path escapes base directory"));
        }
    } else {
        // Check parent directory instead
        if let Some(parent) = full_path.parent() {
            let canonical_parent = parent.canonicalize()
                .map_err(|e| anyhow::anyhow!("Cannot canonicalize parent: {}", e))?;
            
            if !canonical_parent.starts_with(&canonical_base) {
                return Err(anyhow::anyhow!("Zip Slip detected: path escapes base directory"));
            }
        }
    }
    
    Ok(full_path)
}

/// Security Validation - Zip Bomb Protection
pub fn check_decompression_ratio(compressed_size: u64, decompressed_size: u64, max_ratio: u64) -> Result<()> {
    if compressed_size == 0 {
        return Ok(());  // Empty file, allow
    }
    
    let ratio = decompressed_size / compressed_size;
    
    if ratio > max_ratio {
        return Err(anyhow::anyhow!(
            "Zip Bomb detected: compression ratio {} exceeds limit {}",
            ratio,
            max_ratio
        ));
    }
    
    Ok(())
}

/// File size limit check
pub fn check_file_size_limit(file_size: u64, max_size: u64) -> Result<()> {
    if file_size > max_size {
        return Err(anyhow::anyhow!(
            "File size {} exceeds limit {} MB",
            file_size,
            max_size / 1024 / 1024
        ));
    }
    
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;
    
    #[test]
    fn test_zip_slip_protection() {
        let temp_dir = TempDir::new().unwrap();
        let base = temp_dir.path();
        
        // Safe path: should pass
        let safe_path = Path::new("safe/file.txt");
        assert!(validate_extraction_path(safe_path, base).is_ok());
        
        // Evil path: should be rejected
        let evil_path = Path::new("../../etc/passwd");
        assert!(validate_extraction_path(evil_path, base).is_err());
        
        // Absolute path: should be rejected
        let abs_path = Path::new("/etc/passwd");
        assert!(validate_extraction_path(abs_path, base).is_err());
    }
    
    #[test]
    fn test_zip_bomb_detection() {
        // Normal ratio: should pass
        assert!(check_decompression_ratio(1000, 5000, 1000).is_ok());
        
        // Zip Bomb ratio: should be rejected
        assert!(check_decompression_ratio(42_000, 5_000_000_000, 1000).is_err());
    }
    
    #[test]
    fn test_compression_ratio_calculation() {
        let metadata = ArchiveMetadata {
            format: ArchiveFormat::Zip,
            total_files: 10,
            total_size: 1000,
            compressed_size: 500,
            compression_ratio: 0.0,
            is_encrypted: false,
            is_multi_volume: false,
            created_time: None,
        };
        
        assert_eq!(metadata.compression_ratio(), 2.0);
    }
}