use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use super::database::Database;

/// Information about an AppImage's runtime binary.
#[derive(Debug, Clone)]
pub struct RuntimeInfo {
    pub runtime_size: u64,
    pub payload_offset: u64,
    pub runtime_type: RuntimeType,
    pub runtime_version: Option<String>,
}

/// The type of AppImage runtime.
#[derive(Debug, Clone, PartialEq)]
pub enum RuntimeType {
    OldFuse2,
    NewMulti,
    Static,
    Unknown,
}

impl RuntimeType {
    pub fn as_str(&self) -> &str {
        match self {
            Self::OldFuse2 => "old-fuse2",
            Self::NewMulti => "new-multi",
            Self::Static => "static",
            Self::Unknown => "unknown",
        }
    }

    pub fn label(&self) -> &str {
        match self {
            Self::OldFuse2 => "Legacy FUSE 2 only",
            Self::NewMulti => "Multi-runtime (FUSE 2/3 + static)",
            Self::Static => "Static (no FUSE needed)",
            Self::Unknown => "Unknown runtime",
        }
    }
}

/// Result of a runtime replacement operation.
#[derive(Debug)]
pub struct RepackageResult {
    pub original_path: PathBuf,
    pub backup_path: PathBuf,
    pub old_runtime_type: RuntimeType,
    pub new_runtime_type: String,
    pub old_size: u64,
    pub new_size: u64,
    pub success: bool,
}

/// Detect the runtime type and payload offset of an AppImage.
/// Type 2 AppImages store the SquashFS offset in the ELF section header.
pub fn detect_runtime(appimage_path: &Path) -> Result<RuntimeInfo, RepackageError> {
    let mut file = fs::File::open(appimage_path)
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    // Read ELF header to find section headers
    let mut header = [0u8; 64];
    file.read_exact(&mut header)
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    // Verify ELF magic
    if &header[0..4] != b"\x7fELF" {
        return Err(RepackageError::NotAppImage("Not an ELF file".to_string()));
    }

    // Find the SquashFS payload by searching for the magic bytes
    let payload_offset = find_squashfs_offset(appimage_path)?;

    let runtime_size = payload_offset;

    // Classify the runtime type based on size and content
    let runtime_type = classify_runtime(appimage_path, runtime_size)?;

    Ok(RuntimeInfo {
        runtime_size,
        payload_offset,
        runtime_type,
        runtime_version: None,
    })
}

/// Find the offset where the SquashFS payload starts.
/// SquashFS magic is 'hsqs' (0x73717368) at the start of the payload.
fn find_squashfs_offset(appimage_path: &Path) -> Result<u64, RepackageError> {
    let mut file = fs::File::open(appimage_path)
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    let file_size = file.metadata()
        .map(|m| m.len())
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    // SquashFS magic: 'hsqs' = [0x68, 0x73, 0x71, 0x73]
    let magic = b"hsqs";

    // Search in chunks starting from reasonable offsets (runtime is typically 100-300KB)
    let mut buf = [0u8; 65536];
    let search_start = 4096u64; // Skip the ELF header
    let search_end = std::cmp::min(file_size, 1_048_576); // Don't search beyond 1MB

    let mut offset = search_start;
    use std::io::Seek;
    file.seek(std::io::SeekFrom::Start(offset))
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    while offset < search_end {
        let n = file.read(&mut buf)
            .map_err(|e| RepackageError::Io(e.to_string()))?;
        if n == 0 { break; }

        // Search for magic in this chunk
        for i in 0..n.saturating_sub(3) {
            if &buf[i..i + 4] == magic {
                return Ok(offset + i as u64);
            }
        }

        offset += n as u64 - 3; // Overlap by 3 to catch magic spanning chunks
        file.seek(std::io::SeekFrom::Start(offset))
            .map_err(|e| RepackageError::Io(e.to_string()))?;
    }

    Err(RepackageError::NotAppImage("SquashFS payload not found".to_string()))
}

/// Classify the runtime type based on its binary content.
fn classify_runtime(appimage_path: &Path, runtime_size: u64) -> Result<RuntimeType, RepackageError> {
    let mut file = fs::File::open(appimage_path)
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    let read_size = std::cmp::min(runtime_size, 65536) as usize;
    let mut buf = vec![0u8; read_size];
    file.read_exact(&mut buf)
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    let content = String::from_utf8_lossy(&buf);

    // Check for known strings in the runtime binary
    if content.contains("libfuse3") || content.contains("fuse3") {
        Ok(RuntimeType::NewMulti)
    } else if content.contains("static-runtime") || content.contains("no-fuse") {
        Ok(RuntimeType::Static)
    } else if content.contains("libfuse") || content.contains("fuse2") {
        Ok(RuntimeType::OldFuse2)
    } else if runtime_size < 4096 {
        // Suspiciously small runtime - probably not a valid AppImage runtime
        Ok(RuntimeType::Unknown)
    } else {
        // Default: older runtimes are typically fuse2-only
        Ok(RuntimeType::OldFuse2)
    }
}

/// Replace the runtime of an AppImage with a new one.
/// Creates a backup of the original file before modifying.
pub fn replace_runtime(
    appimage_path: &Path,
    new_runtime_path: &Path,
    keep_backup: bool,
) -> Result<RepackageResult, RepackageError> {
    if !appimage_path.exists() {
        return Err(RepackageError::NotAppImage("File not found".to_string()));
    }
    if !new_runtime_path.exists() {
        return Err(RepackageError::Io("New runtime file not found".to_string()));
    }

    let info = detect_runtime(appimage_path)?;
    let old_size = fs::metadata(appimage_path)
        .map(|m| m.len())
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    // Create backup
    let backup_path = appimage_path.with_extension("bak");
    fs::copy(appimage_path, &backup_path)
        .map_err(|e| RepackageError::Io(format!("Backup failed: {}", e)))?;

    // Read new runtime
    let new_runtime = fs::read(new_runtime_path)
        .map_err(|e| RepackageError::Io(format!("Failed to read new runtime: {}", e)))?;

    // Read the SquashFS payload from the original file
    let mut original = fs::File::open(appimage_path)
        .map_err(|e| RepackageError::Io(e.to_string()))?;
    use std::io::Seek;
    original.seek(std::io::SeekFrom::Start(info.payload_offset))
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    let mut payload = Vec::new();
    original.read_to_end(&mut payload)
        .map_err(|e| RepackageError::Io(e.to_string()))?;
    drop(original);

    // Write new AppImage: new_runtime + payload
    let mut output = fs::File::create(appimage_path)
        .map_err(|e| RepackageError::Io(e.to_string()))?;
    output.write_all(&new_runtime)
        .map_err(|e| RepackageError::Io(e.to_string()))?;
    output.write_all(&payload)
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    // Set executable permission
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let perms = fs::Permissions::from_mode(0o755);
        fs::set_permissions(appimage_path, perms).ok();
    }

    let new_size = fs::metadata(appimage_path)
        .map(|m| m.len())
        .unwrap_or(0);

    // Verify the new file is a valid AppImage
    let success = verify_appimage(appimage_path);

    if !success {
        // Rollback from backup
        log::error!("Verification failed, rolling back from backup");
        fs::copy(&backup_path, appimage_path).ok();
        if !keep_backup {
            fs::remove_file(&backup_path).ok();
        }
        return Err(RepackageError::VerificationFailed);
    }

    if !keep_backup {
        fs::remove_file(&backup_path).ok();
    }

    Ok(RepackageResult {
        original_path: appimage_path.to_path_buf(),
        backup_path,
        old_runtime_type: info.runtime_type,
        new_runtime_type: "new".to_string(),
        old_size,
        new_size,
        success: true,
    })
}

/// Batch-replace runtimes for all AppImages in the database that use the old runtime.
pub fn batch_replace_runtimes(
    db: &Database,
    new_runtime_path: &Path,
    dry_run: bool,
) -> Vec<RepackageResult> {
    let records = db.get_all_appimages().unwrap_or_default();
    let mut results = Vec::new();

    for record in &records {
        let path = Path::new(&record.path);
        if !path.exists() {
            continue;
        }

        let info = match detect_runtime(path) {
            Ok(i) => i,
            Err(e) => {
                log::warn!("Skipping {}: {}", record.filename, e);
                continue;
            }
        };

        // Only repackage old fuse2 runtimes
        if info.runtime_type != RuntimeType::OldFuse2 {
            continue;
        }

        if dry_run {
            results.push(RepackageResult {
                original_path: path.to_path_buf(),
                backup_path: path.with_extension("bak"),
                old_runtime_type: info.runtime_type,
                new_runtime_type: "new".to_string(),
                old_size: fs::metadata(path).map(|m| m.len()).unwrap_or(0),
                new_size: 0,
                success: true,
            });
            continue;
        }

        match replace_runtime(path, new_runtime_path, true) {
            Ok(result) => {
                // Record in database
                db.record_runtime_update(
                    record.id,
                    Some(info.runtime_type.as_str()),
                    Some("new"),
                    result.backup_path.to_str(),
                    true,
                ).ok();
                results.push(result);
            }
            Err(e) => {
                log::error!("Failed to repackage {}: {}", record.filename, e);
                db.record_runtime_update(
                    record.id,
                    Some(info.runtime_type.as_str()),
                    Some("new"),
                    None,
                    false,
                ).ok();
            }
        }
    }

    results
}

/// Download the latest AppImage runtime binary.
pub fn download_latest_runtime() -> Result<PathBuf, RepackageError> {
    let url = "https://github.com/AppImage/type2-runtime/releases/latest/download/runtime-x86_64";

    let dest = dirs::cache_dir()
        .unwrap_or_else(|| PathBuf::from("/tmp"))
        .join("driftwood")
        .join("runtime-x86_64");

    fs::create_dir_all(dest.parent().unwrap()).ok();

    let response = ureq::get(url)
        .call()
        .map_err(|e| RepackageError::Network(e.to_string()))?;

    let mut file = fs::File::create(&dest)
        .map_err(|e| RepackageError::Io(e.to_string()))?;

    let mut reader = response.into_body().into_reader();
    let mut buf = [0u8; 65536];
    loop {
        let n = reader.read(&mut buf)
            .map_err(|e| RepackageError::Network(e.to_string()))?;
        if n == 0 { break; }
        file.write_all(&buf[..n])
            .map_err(|e| RepackageError::Io(e.to_string()))?;
    }

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&dest, fs::Permissions::from_mode(0o755)).ok();
    }

    Ok(dest)
}

/// Basic verification that a file is still a valid AppImage.
fn verify_appimage(path: &Path) -> bool {
    // Check ELF magic
    let mut file = match fs::File::open(path) {
        Ok(f) => f,
        Err(_) => return false,
    };

    let mut magic = [0u8; 4];
    if file.read_exact(&mut magic).is_err() {
        return false;
    }
    if &magic != b"\x7fELF" {
        return false;
    }

    // Check that SquashFS payload exists
    find_squashfs_offset(path).is_ok()
}

// --- Error types ---

#[derive(Debug)]
pub enum RepackageError {
    NotAppImage(String),
    Io(String),
    Network(String),
    VerificationFailed,
}

impl std::fmt::Display for RepackageError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NotAppImage(e) => write!(f, "Not a valid AppImage: {}", e),
            Self::Io(e) => write!(f, "I/O error: {}", e),
            Self::Network(e) => write!(f, "Network error: {}", e),
            Self::VerificationFailed => write!(f, "Verification failed after repackaging"),
        }
    }
}

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

    #[test]
    fn test_runtime_type_as_str() {
        assert_eq!(RuntimeType::OldFuse2.as_str(), "old-fuse2");
        assert_eq!(RuntimeType::NewMulti.as_str(), "new-multi");
        assert_eq!(RuntimeType::Static.as_str(), "static");
        assert_eq!(RuntimeType::Unknown.as_str(), "unknown");
    }

    #[test]
    fn test_runtime_type_label() {
        assert!(RuntimeType::OldFuse2.label().contains("Legacy"));
        assert!(RuntimeType::NewMulti.label().contains("Multi"));
        assert!(RuntimeType::Static.label().contains("no FUSE"));
    }

    #[test]
    fn test_repackage_error_display() {
        let err = RepackageError::NotAppImage("bad magic".to_string());
        assert!(format!("{}", err).contains("bad magic"));
        let err = RepackageError::VerificationFailed;
        assert!(format!("{}", err).contains("Verification failed"));
    }

    #[test]
    fn test_detect_runtime_nonexistent() {
        let result = detect_runtime(Path::new("/nonexistent.AppImage"));
        assert!(result.is_err());
    }

    #[test]
    fn test_detect_runtime_not_elf() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("not-an-elf");
        fs::write(&path, "This is not an ELF file").unwrap();
        let result = detect_runtime(&path);
        assert!(result.is_err());
    }

    #[test]
    fn test_verify_appimage_nonexistent() {
        assert!(!verify_appimage(Path::new("/nonexistent")));
    }

    #[test]
    fn test_verify_appimage_not_elf() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("not-elf");
        fs::write(&path, "hello").unwrap();
        assert!(!verify_appimage(&path));
    }
}
