use std::fs::{self, create_dir_all};
use std::path::{Path, PathBuf};
use std::process::Command;
use tauri::Emitter;

use crate::browser::BrowserType;
use crate::download::DownloadProgress;

pub struct Extractor;

impl Extractor {
  pub fn new() -> Self {
    Self
  }

  pub async fn extract_browser(
    &self,
    app_handle: &tauri::AppHandle,
    browser_type: BrowserType,
    version: &str,
    archive_path: &Path,
    dest_dir: &Path,
  ) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
    // Emit extraction start progress
    let progress = DownloadProgress {
      browser: browser_type.as_str().to_string(),
      version: version.to_string(),
      downloaded_bytes: 0,
      total_bytes: None,
      percentage: 0.0,
      speed_bytes_per_sec: 0.0,
      eta_seconds: None,
      stage: "extracting".to_string(),
    };
    let _ = app_handle.emit("download-progress", &progress);

    let extension = archive_path
      .extension()
      .and_then(|ext| ext.to_str())
      .unwrap_or("");

    match extension {
      "dmg" => self.extract_dmg(archive_path, dest_dir).await,
      "zip" => self.extract_zip(archive_path, dest_dir).await,
      _ => Err(format!("Unsupported archive format: {extension}").into()),
    }
  }

  pub async fn extract_dmg(
    &self,
    dmg_path: &Path,
    dest_dir: &Path,
  ) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
    // Create a temporary mount point
    let mount_point = std::env::temp_dir().join(format!(
      "donut_mount_{}",
      std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs()
    ));
    create_dir_all(&mount_point)?;

    // Mount the DMG
    let output = Command::new("hdiutil")
      .args([
        "attach",
        "-nobrowse",
        "-mountpoint",
        mount_point.to_str().unwrap(),
        dmg_path.to_str().unwrap(),
      ])
      .output()?;

    if !output.status.success() {
      return Err(
        format!(
          "Failed to mount DMG: {}",
          String::from_utf8_lossy(&output.stderr)
        )
        .into(),
      );
    }

    // Find the .app directory in the mount point
    let app_entry = fs::read_dir(&mount_point)?
      .filter_map(Result::ok)
      .find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
      .ok_or("No .app found in DMG")?;

    // Copy the .app to the destination
    let app_path = dest_dir.join(app_entry.file_name());

    let output = Command::new("cp")
      .args([
        "-R",
        app_entry.path().to_str().unwrap(),
        app_path.to_str().unwrap(),
      ])
      .output()?;

    if !output.status.success() {
      return Err(
        format!(
          "Failed to copy app: {}",
          String::from_utf8_lossy(&output.stderr)
        )
        .into(),
      );
    }

    // Remove quarantine attributes
    let _ = Command::new("xattr")
      .args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()])
      .output();

    let _ = Command::new("xattr")
      .args(["-cr", app_path.to_str().unwrap()])
      .output();

    // Try to unmount the DMG with retries
    let mut retry_count = 0;
    let max_retries = 3;
    let mut unmounted = false;

    while retry_count < max_retries && !unmounted {
      // Wait a bit before trying to unmount
      tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;

      let output = Command::new("hdiutil")
        .args(["detach", mount_point.to_str().unwrap()])
        .output()?;

      if output.status.success() {
        unmounted = true;
      } else if retry_count == max_retries - 1 {
        // Force unmount on last retry
        let _ = Command::new("hdiutil")
          .args(["detach", "-force", mount_point.to_str().unwrap()])
          .output();
        unmounted = true; // Consider it unmounted even if force fails
      }
      retry_count += 1;
    }

    // Clean up mount point directory
    let _ = fs::remove_dir_all(&mount_point);

    Ok(app_path)
  }

  pub async fn extract_zip(
    &self,
    zip_path: &Path,
    dest_dir: &Path,
  ) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
    // Use unzip command to extract
    let output = Command::new("unzip")
      .args([
        "-q", // quiet
        zip_path.to_str().unwrap(),
        "-d",
        dest_dir.to_str().unwrap(),
      ])
      .output()?;

    if !output.status.success() {
      return Err(
        format!(
          "Failed to extract zip: {}",
          String::from_utf8_lossy(&output.stderr)
        )
        .into(),
      );
    }

    // Find the extracted .app directory or Chromium.app specifically
    let mut app_path: Option<PathBuf> = None;

    // First, try to find any .app file in the destination directory
    if let Ok(entries) = fs::read_dir(dest_dir) {
      for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().is_some_and(|ext| ext == "app") {
          app_path = Some(path);
          break;
        }
        // For Chromium, check subdirectories (chrome-mac folder)
        if path.is_dir() {
          if let Ok(sub_entries) = fs::read_dir(&path) {
            for sub_entry in sub_entries.flatten() {
              let sub_path = sub_entry.path();
              if sub_path.extension().is_some_and(|ext| ext == "app") {
                // Move the app to the root destination directory
                let target_path = dest_dir.join(sub_path.file_name().unwrap());
                fs::rename(&sub_path, &target_path)?;
                app_path = Some(target_path);

                // Clean up the now-empty subdirectory
                let _ = fs::remove_dir_all(&path);
                break;
              }
            }
            if app_path.is_some() {
              break;
            }
          }
        }
      }
    }

    let app_path = app_path.ok_or("No .app found after extraction")?;

    // Remove quarantine attributes
    let _ = Command::new("xattr")
      .args(["-dr", "com.apple.quarantine", app_path.to_str().unwrap()])
      .output();

    let _ = Command::new("xattr")
      .args(["-cr", app_path.to_str().unwrap()])
      .output();

    Ok(app_path)
  }
}

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

  #[test]
  fn test_extractor_creation() {
    let _extractor = Extractor::new();
    // Just verify we can create an extractor instance
  }

  #[test]
  fn test_unsupported_archive_format() {
    let _extractor = Extractor::new();
    let temp_dir = TempDir::new().unwrap();
    let fake_archive = temp_dir.path().join("test.rar");
    File::create(&fake_archive).unwrap();

    // Create a mock app handle (this won't work in real tests without Tauri runtime)
    // For now, we'll just test the logic without the actual extraction

    // Test that unsupported formats return an error
    let extension = fake_archive
      .extension()
      .and_then(|ext| ext.to_str())
      .unwrap_or("");

    assert_eq!(extension, "rar");
    // We know this would fail with "Unsupported archive format: rar"
  }

  #[test]
  fn test_dmg_path_validation() {
    let temp_dir = TempDir::new().unwrap();
    let dmg_path = temp_dir.path().join("test.dmg");

    // Test that we can identify DMG files correctly
    let extension = dmg_path
      .extension()
      .and_then(|ext| ext.to_str())
      .unwrap_or("");

    assert_eq!(extension, "dmg");
  }

  #[test]
  fn test_zip_path_validation() {
    let temp_dir = TempDir::new().unwrap();
    let zip_path = temp_dir.path().join("test.zip");

    // Test that we can identify ZIP files correctly
    let extension = zip_path
      .extension()
      .and_then(|ext| ext.to_str())
      .unwrap_or("");

    assert_eq!(extension, "zip");
  }

  #[test]
  fn test_mount_point_generation() {
    // Test that mount point generation creates unique paths
    let mount_point1 = std::env::temp_dir().join(format!(
      "donut_mount_{}",
      std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs()
    ));

    std::thread::sleep(std::time::Duration::from_millis(10));

    let mount_point2 = std::env::temp_dir().join(format!(
      "donut_mount_{}",
      std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs()
    ));

    // They should be different (or at least have the potential to be)
    assert!(mount_point1.to_string_lossy().contains("donut_mount_"));
    assert!(mount_point2.to_string_lossy().contains("donut_mount_"));
  }

  #[test]
  fn test_app_path_detection() {
    let temp_dir = TempDir::new().unwrap();

    // Create a fake .app directory
    let app_dir = temp_dir.path().join("TestApp.app");
    std::fs::create_dir_all(&app_dir).unwrap();

    // Test finding .app directories
    let entries: Vec<_> = fs::read_dir(temp_dir.path())
      .unwrap()
      .filter_map(Result::ok)
      .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
      .collect();

    assert_eq!(entries.len(), 1);
    assert_eq!(entries[0].file_name(), "TestApp.app");
  }

  #[test]
  fn test_nested_app_detection() {
    let temp_dir = TempDir::new().unwrap();

    // Create a nested structure like Chromium
    let chrome_dir = temp_dir.path().join("chrome-mac");
    std::fs::create_dir_all(&chrome_dir).unwrap();

    let app_dir = chrome_dir.join("Chromium.app");
    std::fs::create_dir_all(&app_dir).unwrap();

    // Test finding nested .app directories
    let mut found_app = false;

    if let Ok(entries) = fs::read_dir(temp_dir.path()) {
      for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
          if let Ok(sub_entries) = fs::read_dir(&path) {
            for sub_entry in sub_entries.flatten() {
              let sub_path = sub_entry.path();
              if sub_path.extension().is_some_and(|ext| ext == "app") {
                found_app = true;
                break;
              }
            }
          }
        }
      }
    }

    assert!(found_app);
  }
}
