use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use super::database::{AppImageRecord, Database};

#[derive(Debug)]
pub enum IntegrationError {
    IoError(std::io::Error),
    NoAppName,
}

impl std::fmt::Display for IntegrationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::IoError(e) => write!(f, "I/O error: {}", e),
            Self::NoAppName => write!(f, "Cannot integrate: no application name"),
        }
    }
}

impl From<std::io::Error> for IntegrationError {
    fn from(e: std::io::Error) -> Self {
        Self::IoError(e)
    }
}

/// Escape a string for use inside a double-quoted Exec argument in a .desktop file.
/// Per the Desktop Entry spec, `\`, `"`, `` ` ``, and `$` must be escaped with `\`.
fn escape_exec_arg(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '\\' | '"' | '`' | '$' => {
                out.push('\\');
                out.push(c);
            }
            _ => out.push(c),
        }
    }
    out
}

pub struct IntegrationResult {
    pub desktop_file_path: PathBuf,
    pub icon_install_path: Option<PathBuf>,
}

pub fn applications_dir() -> PathBuf {
    crate::config::data_dir_fallback()
        .join("applications")
}

fn icons_dir() -> PathBuf {
    crate::config::data_dir_fallback()
        .join("icons/hicolor")
}

/// Generate a sanitized app ID.
pub fn make_app_id(app_name: &str) -> String {
    let id: String = app_name
        .chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '-' || c == '_' {
                c.to_ascii_lowercase()
            } else {
                '-'
            }
        })
        .collect();
    id.trim_matches('-').to_string()
}

/// Integrate an AppImage: create .desktop file and install icon.
pub fn integrate(record: &AppImageRecord) -> Result<IntegrationResult, IntegrationError> {
    let app_name = record
        .app_name
        .as_deref()
        .or(Some(&record.filename))
        .ok_or(IntegrationError::NoAppName)?;

    let app_id = make_app_id(app_name);
    let desktop_filename = format!("driftwood-{}.desktop", app_id);

    let apps_dir = applications_dir();
    fs::create_dir_all(&apps_dir)?;

    let desktop_path = apps_dir.join(&desktop_filename);

    // Build the .desktop file content
    let categories = record
        .categories
        .as_deref()
        .unwrap_or("");
    let comment = record
        .description
        .as_deref()
        .unwrap_or("");
    let version = record
        .app_version
        .as_deref()
        .unwrap_or("");
    let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string();

    let icon_id = format!("driftwood-{}", app_id);

    let mime_types = record.mime_types.as_deref().unwrap_or("");
    let wm_class = record.startup_wm_class.as_deref().unwrap_or("");

    let mut desktop_content = format!("\
[Desktop Entry]
Type=Application
Name={name}
Exec=\"{exec}\" %U
Icon={icon}
Categories={categories}
Comment={comment}
Terminal=false
X-AppImage-Path={path}
X-AppImage-Version={version}
X-AppImage-Managed-By=Driftwood
X-AppImage-Integrated-Date={date}
",
        name = app_name,
        exec = escape_exec_arg(&record.path),
        icon = icon_id,
        categories = categories,
        comment = comment,
        path = record.path,
        version = version,
        date = now,
    );

    if !mime_types.is_empty() {
        desktop_content.push_str(&format!("MimeType={}\n", mime_types));
    }
    if !wm_class.is_empty() {
        desktop_content.push_str(&format!("StartupWMClass={}\n", wm_class));
    }

    fs::write(&desktop_path, &desktop_content)?;

    // Install icon if we have a cached one
    let icon_install_path = if let Some(ref cached_icon) = record.icon_path {
        let cached = Path::new(cached_icon);
        if cached.exists() {
            install_icon(cached, &icon_id)?
        } else {
            None
        }
    } else {
        None
    };

    // Update desktop database (best effort)
    update_desktop_database();

    Ok(IntegrationResult {
        desktop_file_path: desktop_path,
        icon_install_path,
    })
}

/// Install an icon to the hicolor icon theme directory.
fn install_icon(source: &Path, icon_id: &str) -> Result<Option<PathBuf>, IntegrationError> {
    let ext = source
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("png");

    let (subdir, filename) = if ext == "svg" {
        ("scalable/apps", format!("{}.svg", icon_id))
    } else {
        ("256x256/apps", format!("{}.png", icon_id))
    };

    let dest_dir = icons_dir().join(subdir);
    fs::create_dir_all(&dest_dir)?;
    let dest = dest_dir.join(&filename);
    fs::copy(source, &dest)?;

    Ok(Some(dest))
}

/// Remove integration for an AppImage.
pub fn remove_integration(record: &AppImageRecord) -> Result<(), IntegrationError> {
    let app_name = record
        .app_name
        .as_deref()
        .or(Some(&record.filename))
        .ok_or(IntegrationError::NoAppName)?;

    let app_id = make_app_id(app_name);

    // Remove .desktop file
    if let Some(ref desktop_file) = record.desktop_file {
        let path = Path::new(desktop_file);
        if path.exists() {
            fs::remove_file(path)?;
        }
    } else {
        // Try the conventional path
        let desktop_path = applications_dir().join(format!("driftwood-{}.desktop", app_id));
        if desktop_path.exists() {
            fs::remove_file(&desktop_path)?;
        }
    }

    // Remove icon files
    let icon_id = format!("driftwood-{}", app_id);
    remove_icon_files(&icon_id);

    update_desktop_database();

    Ok(())
}

fn remove_icon_files(icon_id: &str) {
    let base = icons_dir();
    let candidates = [
        base.join(format!("256x256/apps/{}.png", icon_id)),
        base.join(format!("scalable/apps/{}.svg", icon_id)),
        base.join(format!("128x128/apps/{}.png", icon_id)),
        base.join(format!("48x48/apps/{}.png", icon_id)),
    ];
    for path in &candidates {
        if path.exists() {
            fs::remove_file(path).ok();
        }
    }
}

/// Integrate and track all created files in the system_modifications table.
pub fn integrate_tracked(record: &AppImageRecord, db: &Database) -> Result<IntegrationResult, IntegrationError> {
    let result = integrate(record)?;

    // Register desktop file
    db.register_modification(
        record.id,
        "desktop_file",
        &result.desktop_file_path.to_string_lossy(),
        None,
    ).ok();

    // Register icon file
    if let Some(ref icon_path) = result.icon_install_path {
        db.register_modification(
            record.id,
            "icon",
            &icon_path.to_string_lossy(),
            None,
        ).ok();
    }

    Ok(result)
}

pub fn autostart_dir() -> PathBuf {
    dirs::config_dir()
        .unwrap_or_else(|| PathBuf::from("/tmp"))
        .join("autostart")
}

pub fn enable_autostart(db: &Database, record: &AppImageRecord) -> Result<PathBuf, String> {
    let dir = autostart_dir();
    fs::create_dir_all(&dir).map_err(|e| format!("Failed to create autostart dir: {}", e))?;

    let app_id = record.app_name.as_deref()
        .map(|n| make_app_id(n))
        .unwrap_or_else(|| format!("appimage-{}", record.id));
    let desktop_filename = format!("driftwood-{}.desktop", app_id);
    let desktop_path = dir.join(&desktop_filename);

    let app_name = record.app_name.as_deref().unwrap_or(&record.filename);
    let icon = record.icon_path.as_deref().unwrap_or("application-x-executable");

    let content = format!("\
[Desktop Entry]
Type=Application
Name={}
Exec=\"{}\" %U
Icon={}
X-GNOME-Autostart-enabled=true
X-Driftwood-AppImage-ID={}
", app_name, escape_exec_arg(&record.path), icon, record.id);

    fs::write(&desktop_path, &content)
        .map_err(|e| format!("Failed to write autostart file: {}", e))?;

    db.register_modification(record.id, "autostart", &desktop_path.to_string_lossy(), None)
        .map_err(|e| format!("Failed to register modification: {}", e))?;

    db.set_autostart(record.id, true).ok();

    Ok(desktop_path)
}

pub fn disable_autostart(db: &Database, record_id: i64) -> Result<(), String> {
    let mods = db.get_modifications(record_id).unwrap_or_default();
    for m in &mods {
        if m.mod_type == "autostart" {
            let path = Path::new(&m.file_path);
            if path.exists() {
                fs::remove_file(path).ok();
            }
            db.remove_modification(m.id).ok();
        }
    }
    db.set_autostart(record_id, false).ok();
    Ok(())
}

/// Undo all tracked system modifications for an AppImage.
pub fn undo_all_modifications(db: &Database, appimage_id: i64) -> Result<(), String> {
    let mods = db.get_modifications(appimage_id)
        .map_err(|e| format!("Failed to get modifications: {}", e))?;

    for m in &mods {
        match m.mod_type.as_str() {
            "desktop_file" | "autostart" | "icon" => {
                let path = Path::new(&m.file_path);
                if path.exists() {
                    if let Err(e) = fs::remove_file(path) {
                        log::warn!("Failed to remove {}: {}", m.file_path, e);
                    }
                }
            }
            "mime_default" => {
                if let Some(ref prev) = m.previous_value {
                    let _ = Command::new("xdg-mime")
                        .args(["default", prev, &m.file_path])
                        .status();
                }
            }
            "default_app" => {
                if let Some(ref prev) = m.previous_value {
                    match m.file_path.as_str() {
                        "web-browser" => {
                            let _ = Command::new("xdg-settings")
                                .args(["set", "default-web-browser", prev])
                                .status();
                        }
                        "email-client" => {
                            let _ = Command::new("xdg-settings")
                                .args(["set", "default-url-scheme-handler", "mailto", prev])
                                .status();
                        }
                        "file-manager" => {
                            let _ = Command::new("xdg-mime")
                                .args(["default", prev, "inode/directory"])
                                .status();
                        }
                        _ => {}
                    }
                }
            }
            "system_desktop" | "system_icon" | "system_binary" => {
                let _ = Command::new("pkexec")
                    .args(["rm", "-f", &m.file_path])
                    .status();
            }
            _ => {
                log::warn!("Unknown modification type: {}", m.mod_type);
            }
        }
        db.remove_modification(m.id).ok();
    }

    // Refresh desktop database and icon cache
    update_desktop_database();

    Ok(())
}

/// Set an AppImage as the default handler for a MIME type.
/// Stores the previous default in system_modifications for reversal.
pub fn set_mime_default(
    db: &Database,
    appimage_id: i64,
    app_id: &str,
    mime_type: &str,
) -> Result<(), String> {
    let desktop_filename = format!("driftwood-{}.desktop", app_id);

    // Query current default
    let prev = Command::new("xdg-mime")
        .args(["query", "default", mime_type])
        .output()
        .ok()
        .and_then(|o| {
            if o.status.success() {
                let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
                if s.is_empty() { None } else { Some(s) }
            } else {
                None
            }
        });

    // Set new default
    let status = Command::new("xdg-mime")
        .args(["default", &desktop_filename, mime_type])
        .status()
        .map_err(|e| format!("xdg-mime failed: {}", e))?;

    if !status.success() {
        return Err("xdg-mime returned non-zero".to_string());
    }

    // Track the modification
    db.register_modification(
        appimage_id,
        "mime_default",
        mime_type,
        prev.as_deref(),
    ).ok();

    Ok(())
}

/// Unset this AppImage as the default handler for a MIME type.
/// Restores the previous default if one was recorded, then removes the tracking record.
pub fn unset_mime_default(
    db: &Database,
    appimage_id: i64,
    mime_type: &str,
) -> Result<(), String> {
    let mods = db.get_modifications(appimage_id).unwrap_or_default();
    for m in &mods {
        if m.mod_type == "mime_default" && m.file_path == mime_type {
            if let Some(ref prev) = m.previous_value {
                Command::new("xdg-mime")
                    .args(["default", prev, mime_type])
                    .status()
                    .map_err(|e| format!("xdg-mime failed: {}", e))?;
            }
            db.remove_modification(m.id).ok();
            return Ok(());
        }
    }
    Err("No tracked default to unset".to_string())
}

/// A system-level default application type that an AppImage can serve as.
#[derive(Debug, Clone, PartialEq)]
pub enum DefaultAppType {
    WebBrowser,
    EmailClient,
    FileManager,
}

impl DefaultAppType {
    pub fn label(&self) -> &'static str {
        match self {
            Self::WebBrowser => "Web browser",
            Self::EmailClient => "Email client",
            Self::FileManager => "File manager",
        }
    }
}

/// Detect which system-default roles an AppImage can fill based on its categories.
pub fn detect_default_capabilities(categories: &str) -> Vec<DefaultAppType> {
    let mut caps = Vec::new();
    if categories.contains("WebBrowser") {
        caps.push(DefaultAppType::WebBrowser);
    }
    if categories.contains("Email") {
        caps.push(DefaultAppType::EmailClient);
    }
    if categories.contains("FileManager") {
        caps.push(DefaultAppType::FileManager);
    }
    caps
}

/// Set an AppImage as a system-default application for the given role.
/// Stores the previous default in system_modifications for reversal.
pub fn set_default_app(
    db: &Database,
    appimage_id: i64,
    app_id: &str,
    cap: &DefaultAppType,
) -> Result<(), String> {
    let desktop_filename = format!("driftwood-{}.desktop", app_id);

    match cap {
        DefaultAppType::WebBrowser => {
            let prev = Command::new("xdg-settings")
                .args(["get", "default-web-browser"])
                .output()
                .ok()
                .and_then(|o| {
                    if o.status.success() {
                        let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
                        if s.is_empty() { None } else { Some(s) }
                    } else {
                        None
                    }
                });

            let status = Command::new("xdg-settings")
                .args(["set", "default-web-browser", &desktop_filename])
                .status()
                .map_err(|e| format!("xdg-settings failed: {}", e))?;
            if !status.success() {
                return Err("xdg-settings returned non-zero".to_string());
            }
            db.register_modification(appimage_id, "default_app", "web-browser", prev.as_deref()).ok();
        }
        DefaultAppType::EmailClient => {
            let prev = Command::new("xdg-settings")
                .args(["get", "default-url-scheme-handler", "mailto"])
                .output()
                .ok()
                .and_then(|o| {
                    if o.status.success() {
                        let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
                        if s.is_empty() { None } else { Some(s) }
                    } else {
                        None
                    }
                });

            let status = Command::new("xdg-settings")
                .args(["set", "default-url-scheme-handler", "mailto", &desktop_filename])
                .status()
                .map_err(|e| format!("xdg-settings failed: {}", e))?;
            if !status.success() {
                return Err("xdg-settings returned non-zero".to_string());
            }
            db.register_modification(appimage_id, "default_app", "email-client", prev.as_deref()).ok();
        }
        DefaultAppType::FileManager => {
            let prev = Command::new("xdg-mime")
                .args(["query", "default", "inode/directory"])
                .output()
                .ok()
                .and_then(|o| {
                    if o.status.success() {
                        let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
                        if s.is_empty() { None } else { Some(s) }
                    } else {
                        None
                    }
                });

            let status = Command::new("xdg-mime")
                .args(["default", &desktop_filename, "inode/directory"])
                .status()
                .map_err(|e| format!("xdg-mime failed: {}", e))?;
            if !status.success() {
                return Err("xdg-mime returned non-zero".to_string());
            }
            db.register_modification(appimage_id, "default_app", "file-manager", prev.as_deref()).ok();
        }
    }

    Ok(())
}

/// Install an AppImage system-wide via pkexec.
/// Copies the binary to /opt/driftwood-apps/, desktop file to
/// /usr/share/applications/, and icon to /usr/share/icons/hicolor/.
pub fn install_system_wide(
    record: &AppImageRecord,
    db: &Database,
) -> Result<(), String> {
    let app_name = record
        .app_name
        .as_deref()
        .or(Some(&record.filename))
        .ok_or("No app name")?;
    let app_id = make_app_id(app_name);

    let dest_dir = "/opt/driftwood-apps";
    let dest_binary = format!("{}/{}", dest_dir, record.filename);
    let desktop_filename = format!("driftwood-{}.desktop", app_id);
    let system_desktop = format!("/usr/share/applications/{}", desktop_filename);
    let icon_id = format!("driftwood-{}", app_id);

    // Create destination directory
    let status = Command::new("pkexec")
        .args(["mkdir", "-p", dest_dir])
        .status()
        .map_err(|e| format!("pkexec failed: {}", e))?;
    if !status.success() {
        return Err("Failed to create system directory".to_string());
    }

    // Copy AppImage binary
    let status = Command::new("pkexec")
        .args(["cp", &record.path, &dest_binary])
        .status()
        .map_err(|e| format!("pkexec cp failed: {}", e))?;
    if !status.success() {
        return Err("Failed to copy AppImage to system directory".to_string());
    }

    // Make executable
    let status = Command::new("pkexec")
        .args(["chmod", "+x", &dest_binary])
        .status()
        .map_err(|e| format!("pkexec chmod failed: {}", e))?;
    if !status.success() {
        return Err("Failed to set execute permission".to_string());
    }

    db.register_modification(record.id, "system_binary", &dest_binary, None).ok();

    // Generate system desktop file content
    let categories = record.categories.as_deref().unwrap_or("");
    let comment = record.description.as_deref().unwrap_or("");

    let mut desktop_content = format!("\
[Desktop Entry]
Type=Application
Name={name}
Exec=\"{exec}\" %U
Icon={icon}
Categories={categories}
Comment={comment}
Terminal=false
X-AppImage-Managed-By=Driftwood
",
        name = app_name,
        exec = escape_exec_arg(&dest_binary),
        icon = icon_id,
        categories = categories,
        comment = comment,
    );

    let mime_types = record.mime_types.as_deref().unwrap_or("");
    let wm_class = record.startup_wm_class.as_deref().unwrap_or("");
    if !mime_types.is_empty() {
        desktop_content.push_str(&format!("MimeType={}\n", mime_types));
    }
    if !wm_class.is_empty() {
        desktop_content.push_str(&format!("StartupWMClass={}\n", wm_class));
    }

    // Write desktop file via temp file + pkexec mv
    let tmp = std::env::temp_dir().join(&desktop_filename);
    fs::write(&tmp, &desktop_content)
        .map_err(|e| format!("Failed to write temp desktop file: {}", e))?;

    let status = Command::new("pkexec")
        .args(["mv", &tmp.to_string_lossy(), &system_desktop])
        .status()
        .map_err(|e| format!("pkexec mv failed: {}", e))?;
    if !status.success() {
        return Err("Failed to install system desktop file".to_string());
    }

    db.register_modification(record.id, "system_desktop", &system_desktop, None).ok();

    // Copy icon if available
    if let Some(ref cached_icon) = record.icon_path {
        let cached = Path::new(cached_icon);
        if cached.exists() {
            let ext = cached.extension().and_then(|e| e.to_str()).unwrap_or("png");
            let (subdir, icon_filename) = if ext == "svg" {
                ("scalable/apps", format!("{}.svg", icon_id))
            } else {
                ("256x256/apps", format!("{}.png", icon_id))
            };
            let system_icon_dir = format!("/usr/share/icons/hicolor/{}", subdir);
            let system_icon = format!("{}/{}", system_icon_dir, icon_filename);

            Command::new("pkexec")
                .args(["mkdir", "-p", &system_icon_dir])
                .status()
                .ok();

            let status = Command::new("pkexec")
                .args(["cp", &cached.to_string_lossy(), &system_icon])
                .status();
            if let Ok(s) = status {
                if s.success() {
                    db.register_modification(record.id, "system_icon", &system_icon, None).ok();
                }
            }
        }
    }

    db.set_system_wide(record.id, true).ok();

    Ok(())
}

/// Remove a system-wide installation.
pub fn remove_system_wide(
    db: &Database,
    appimage_id: i64,
) -> Result<(), String> {
    let mods = db.get_modifications(appimage_id).unwrap_or_default();
    for m in &mods {
        match m.mod_type.as_str() {
            "system_binary" | "system_desktop" | "system_icon" => {
                let status = Command::new("pkexec")
                    .args(["rm", "-f", &m.file_path])
                    .status();
                if let Ok(s) = status {
                    if s.success() {
                        db.remove_modification(m.id).ok();
                    }
                }
            }
            _ => {}
        }
    }
    db.set_system_wide(appimage_id, false).ok();
    Ok(())
}

fn update_desktop_database() {
    let apps_dir = applications_dir();
    Command::new("update-desktop-database")
        .arg(&apps_dir)
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .ok();
}

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

    #[test]
    fn test_make_app_id() {
        assert_eq!(make_app_id("Firefox"), "firefox");
        assert_eq!(make_app_id("My Cool App"), "my-cool-app");
        assert_eq!(make_app_id("  Spaces  "), "spaces");
    }

    #[test]
    fn test_integrate_creates_desktop_file() {
        let _dir = tempfile::tempdir().unwrap();
        // Override the applications dir for testing by creating the record
        // with a specific path and testing the desktop content generation
        let record = AppImageRecord {
            id: 1,
            path: "/home/user/Apps/Firefox.AppImage".to_string(),
            filename: "Firefox.AppImage".to_string(),
            app_name: Some("Firefox".to_string()),
            app_version: Some("124.0".to_string()),
            appimage_type: Some(2),
            size_bytes: 100_000_000,
            sha256: None,
            icon_path: None,
            desktop_file: None,
            integrated: false,
            integrated_at: None,
            is_executable: true,
            desktop_entry_content: None,
            categories: Some("Network;WebBrowser".to_string()),
            description: Some("Web Browser".to_string()),
            developer: None,
            architecture: Some("x86_64".to_string()),
            first_seen: "2026-01-01".to_string(),
            last_scanned: "2026-01-01".to_string(),
            file_modified: None,
            fuse_status: None,
            wayland_status: None,
            update_info: None,
            update_type: None,
            latest_version: None,
            update_checked: None,
            update_url: None,
            notes: None,
            sandbox_mode: None,
            runtime_wayland_status: None,
            runtime_wayland_checked: None,
            analysis_status: None,
            launch_args: None,
            tags: None,
            pinned: false,
            avg_startup_ms: None,
            appstream_id: None,
            appstream_description: None,
            generic_name: None,
            license: None,
            homepage_url: None,
            bugtracker_url: None,
            donation_url: None,
            help_url: None,
            vcs_url: None,
            keywords: None,
            mime_types: None,
            content_rating: None,
            project_group: None,
            release_history: None,
            desktop_actions: None,
            has_signature: false,
            screenshot_urls: None,
            previous_version_path: None,
            source_url: None,
            autostart: false,
            startup_wm_class: None,
            verification_status: None,
            first_run_prompted: false,
            system_wide: false,
            is_portable: false,
            mount_point: None,
        };

        // We can't easily test the full integrate() without mocking dirs,
        // but we can verify make_app_id and the desktop content format
        let app_id = make_app_id(record.app_name.as_deref().unwrap());
        assert_eq!(app_id, "firefox");
        assert_eq!(format!("driftwood-{}.desktop", app_id), "driftwood-firefox.desktop");
    }
}
