use std::fs;
use std::io::Read;
use std::path::Path;

use serde::Deserialize;

/// Types of update information embedded in AppImages.
#[derive(Debug, Clone, PartialEq)]
pub enum UpdateType {
    /// Direct zsync URL
    Zsync { url: String },
    /// GitHub Releases with zsync pattern
    GhReleasesZsync {
        owner: String,
        repo: String,
        release_tag: String,
        filename_pattern: String,
    },
    /// GitLab Releases with zsync pattern
    GlReleasesZsync {
        host: String,
        owner: String,
        repo: String,
        release_tag: String,
        filename_pattern: String,
    },
    /// OCS (AppImageHub) direct
    Ocs { url: String },
    /// Bintray (deprecated)
    Bintray { raw: String },
}

impl UpdateType {
    pub fn type_label(&self) -> &'static str {
        match self {
            Self::Zsync { .. } => "zsync",
            Self::GhReleasesZsync { .. } => "gh-releases-zsync",
            Self::GlReleasesZsync { .. } => "gl-releases-zsync",
            Self::Ocs { .. } => "ocs",
            Self::Bintray { .. } => "bintray",
        }
    }

    pub fn type_label_display(&self) -> &'static str {
        match self {
            Self::Zsync { .. } => "zsync (direct)",
            Self::GhReleasesZsync { .. } => "GitHub Releases",
            Self::GlReleasesZsync { .. } => "GitLab Releases",
            Self::Ocs { .. } => "AppImageHub (OCS)",
            Self::Bintray { .. } => "Bintray (deprecated)",
        }
    }
}

/// Result of checking for updates.
#[derive(Debug, Clone)]
pub struct UpdateCheckResult {
    pub update_available: bool,
    pub latest_version: Option<String>,
    pub download_url: Option<String>,
    pub release_notes: Option<String>,
    pub file_size: Option<u64>,
}

/// Detect the source URL for an AppImage from its update_info string.
pub fn detect_source_url(update_info: Option<&str>) -> Option<String> {
    let info = update_info?;
    let parts: Vec<&str> = info.split('|').collect();
    if info.starts_with("gh-releases-zsync|") && parts.len() >= 3 {
        return Some(format!("https://github.com/{}/{}", parts[1], parts[2]));
    }
    if info.starts_with("gl-releases-zsync|") && parts.len() >= 4 {
        return Some(format!("https://{}/{}/{}", parts[1], parts[2], parts[3]));
    }
    if info.starts_with("zsync|") {
        if let Some(url_part) = parts.get(1) {
            // Extract the base URL (up to the hostname)
            if let Some(idx) = url_part.find("://") {
                if let Some(slash) = url_part[idx + 3..].find('/') {
                    return Some(url_part[..idx + 3 + slash].to_string());
                }
            }
        }
    }
    None
}

/// Parse the raw update info string from an AppImage's ELF section.
pub fn parse_update_info(raw: &str) -> Option<UpdateType> {
    let raw = raw.trim().trim_matches('\0');
    if raw.is_empty() {
        return None;
    }

    let parts: Vec<&str> = raw.split('|').collect();
    if parts.is_empty() {
        return None;
    }

    match parts[0] {
        "zsync" if parts.len() >= 2 => {
            Some(UpdateType::Zsync {
                url: parts[1].to_string(),
            })
        }
        "gh-releases-zsync" if parts.len() >= 5 => {
            Some(UpdateType::GhReleasesZsync {
                owner: parts[1].to_string(),
                repo: parts[2].to_string(),
                release_tag: parts[3].to_string(),
                filename_pattern: parts[4].to_string(),
            })
        }
        "gl-releases-zsync" if parts.len() >= 6 => {
            Some(UpdateType::GlReleasesZsync {
                host: parts[1].to_string(),
                owner: parts[2].to_string(),
                repo: parts[3].to_string(),
                release_tag: parts[4].to_string(),
                filename_pattern: parts[5].to_string(),
            })
        }
        "ocs-v1-appimagehub-direct" if parts.len() >= 2 => {
            Some(UpdateType::Ocs {
                url: parts[1].to_string(),
            })
        }
        "bintray-zsync" => {
            Some(UpdateType::Bintray {
                raw: raw.to_string(),
            })
        }
        _ => {
            log::warn!("Unknown update info format: {}", raw);
            None
        }
    }
}

/// Read the .upd_info section from an AppImage's ELF binary.
/// The update info is stored as a null-terminated string in an ELF section
/// named ".upd_info" or ".updinfo". It can also be found at a fixed offset
/// in the AppImage runtime (bytes 0x414..0x614 in the ELF header area).
pub fn read_update_info(path: &Path) -> Option<String> {
    // Only read the first 1MB - update info is always in the ELF header area,
    // never deep in the squashfs payload. Avoids loading 1.5GB+ files into memory.
    let mut file = fs::File::open(path).ok()?;
    let file_len = file.metadata().ok()?.len() as usize;
    let read_len = file_len.min(1024 * 1024);
    let mut data = vec![0u8; read_len];
    use std::io::Read;
    file.read_exact(&mut data).ok()?;

    // Method 1: Try to read from fixed offset range in AppImage Type 2 runtime.
    // The update info is typically at offset 0xC48 (3144) in the ELF, but the
    // exact offset varies. The standard range is after the ELF headers.
    // The runtime stores it as a null-terminated ASCII string.
    // Try the known range where AppImageKit stores update info.
    if let Some(info) = extract_update_info_fixed_offset(&data) {
        return Some(info);
    }

    // Method 2: Parse ELF section headers to find .upd_info section.
    if let Some(info) = extract_update_info_elf_section(&data) {
        return Some(info);
    }

    // Method 3: Run the AppImage with --appimage-updateinformation
    if let Some(info) = extract_update_info_runtime(path) {
        return Some(info);
    }

    None
}

/// Extract update info from fixed offset range in Type 2 AppImage runtime.
/// The runtime stores update info as a null-terminated string starting
/// at a well-known offset region.
fn extract_update_info_fixed_offset(data: &[u8]) -> Option<String> {
    // AppImage Type 2 runtime stores update info in a fixed region.
    // The region is typically 512 bytes starting at various offsets depending
    // on the runtime version. Common offsets: 0xA48, 0xB48, 0xC48, 0xD48.
    // We scan a broader range and look for the characteristic pipe-delimited format.
    let scan_start = 0x800;
    let scan_end = std::cmp::min(data.len(), 0x2000);

    if scan_end <= scan_start {
        return None;
    }

    let region = &data[scan_start..scan_end];

    // Look for known update info prefixes in this region
    for prefix in &[
        b"zsync|" as &[u8],
        b"gh-releases-zsync|",
        b"gl-releases-zsync|",
        b"ocs-v1-appimagehub-direct|",
        b"bintray-zsync|",
    ] {
        if let Some(pos) = find_bytes(region, prefix) {
            let start = pos;
            let rest = &region[start..];
            // Read until null terminator or non-printable char
            let end = rest
                .iter()
                .position(|&b| b == 0 || b < 0x20 || b > 0x7E)
                .unwrap_or(rest.len());
            if end > prefix.len() {
                let s = String::from_utf8_lossy(&rest[..end]).to_string();
                if !s.is_empty() {
                    return Some(s);
                }
            }
        }
    }

    None
}

/// Find a byte sequence needle in haystack.
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
    haystack
        .windows(needle.len())
        .position(|w| w == needle)
}

/// Extract update info by parsing ELF section headers.
fn extract_update_info_elf_section(data: &[u8]) -> Option<String> {
    if data.len() < 64 {
        return None;
    }

    // Verify ELF magic
    if &data[0..4] != b"\x7FELF" {
        return None;
    }

    let is_64bit = data[4] == 2;

    if is_64bit {
        parse_elf64_sections(data)
    } else {
        parse_elf32_sections(data)
    }
}

fn parse_elf64_sections(data: &[u8]) -> Option<String> {
    if data.len() < 64 {
        return None;
    }

    let shoff = u64::from_le_bytes(data[40..48].try_into().ok()?) as usize;
    let shentsize = u16::from_le_bytes(data[58..60].try_into().ok()?) as usize;
    let shnum = u16::from_le_bytes(data[60..62].try_into().ok()?) as usize;
    let shstrndx = u16::from_le_bytes(data[62..64].try_into().ok()?) as usize;

    if shoff == 0 || shnum == 0 || shentsize < 64 {
        return None;
    }

    // Get section header string table
    let strtab_offset = shoff + shstrndx * shentsize;
    if strtab_offset + shentsize > data.len() {
        return None;
    }
    let strtab_sh_offset =
        u64::from_le_bytes(data[strtab_offset + 24..strtab_offset + 32].try_into().ok()?) as usize;
    let strtab_sh_size =
        u64::from_le_bytes(data[strtab_offset + 32..strtab_offset + 40].try_into().ok()?) as usize;

    if strtab_sh_offset + strtab_sh_size > data.len() {
        return None;
    }

    let strtab = &data[strtab_sh_offset..strtab_sh_offset + strtab_sh_size];

    // Search for .upd_info or .updinfo section
    for i in 0..shnum {
        let offset = shoff + i * shentsize;
        if offset + shentsize > data.len() {
            break;
        }

        let name_idx =
            u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
        if name_idx >= strtab.len() {
            continue;
        }

        let name_end = strtab[name_idx..]
            .iter()
            .position(|&b| b == 0)
            .unwrap_or(0);
        let name = std::str::from_utf8(&strtab[name_idx..name_idx + name_end]).ok()?;

        if name == ".upd_info" || name == ".updinfo" {
            let sec_offset =
                u64::from_le_bytes(data[offset + 24..offset + 32].try_into().ok()?) as usize;
            let sec_size =
                u64::from_le_bytes(data[offset + 32..offset + 40].try_into().ok()?) as usize;

            if sec_offset + sec_size <= data.len() && sec_size > 0 {
                let section_data = &data[sec_offset..sec_offset + sec_size];
                // Trim null bytes and whitespace
                let end = section_data
                    .iter()
                    .position(|&b| b == 0)
                    .unwrap_or(section_data.len());
                let s = String::from_utf8_lossy(&section_data[..end])
                    .trim()
                    .to_string();
                if !s.is_empty() {
                    return Some(s);
                }
            }
        }
    }

    None
}

fn parse_elf32_sections(data: &[u8]) -> Option<String> {
    if data.len() < 52 {
        return None;
    }

    let shoff = u32::from_le_bytes(data[32..36].try_into().ok()?) as usize;
    let shentsize = u16::from_le_bytes(data[46..48].try_into().ok()?) as usize;
    let shnum = u16::from_le_bytes(data[48..50].try_into().ok()?) as usize;
    let shstrndx = u16::from_le_bytes(data[50..52].try_into().ok()?) as usize;

    if shoff == 0 || shnum == 0 || shentsize < 40 {
        return None;
    }

    let strtab_offset = shoff + shstrndx * shentsize;
    if strtab_offset + shentsize > data.len() {
        return None;
    }
    let strtab_sh_offset =
        u32::from_le_bytes(data[strtab_offset + 16..strtab_offset + 20].try_into().ok()?) as usize;
    let strtab_sh_size =
        u32::from_le_bytes(data[strtab_offset + 20..strtab_offset + 24].try_into().ok()?) as usize;

    if strtab_sh_offset + strtab_sh_size > data.len() {
        return None;
    }

    let strtab = &data[strtab_sh_offset..strtab_sh_offset + strtab_sh_size];

    for i in 0..shnum {
        let offset = shoff + i * shentsize;
        if offset + shentsize > data.len() {
            break;
        }

        let name_idx =
            u32::from_le_bytes(data[offset..offset + 4].try_into().ok()?) as usize;
        if name_idx >= strtab.len() {
            continue;
        }

        let name_end = strtab[name_idx..]
            .iter()
            .position(|&b| b == 0)
            .unwrap_or(0);
        let name = std::str::from_utf8(&strtab[name_idx..name_idx + name_end]).ok()?;

        if name == ".upd_info" || name == ".updinfo" {
            let sec_offset =
                u32::from_le_bytes(data[offset + 16..offset + 20].try_into().ok()?) as usize;
            let sec_size =
                u32::from_le_bytes(data[offset + 20..offset + 24].try_into().ok()?) as usize;

            if sec_offset + sec_size <= data.len() && sec_size > 0 {
                let section_data = &data[sec_offset..sec_offset + sec_size];
                let end = section_data
                    .iter()
                    .position(|&b| b == 0)
                    .unwrap_or(section_data.len());
                let s = String::from_utf8_lossy(&section_data[..end])
                    .trim()
                    .to_string();
                if !s.is_empty() {
                    return Some(s);
                }
            }
        }
    }

    None
}

/// Fallback: run the AppImage with --appimage-updateinformation flag.
/// Uses a 5-second timeout to avoid hanging on apps with custom AppRun scripts.
fn extract_update_info_runtime(path: &Path) -> Option<String> {
    let mut child = std::process::Command::new(path)
        .arg("--appimage-updateinformation")
        .env("APPIMAGE_EXTRACT_AND_RUN", "1")
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::null())
        .spawn()
        .ok()?;

    let timeout = std::time::Duration::from_secs(5);
    let start = std::time::Instant::now();
    loop {
        match child.try_wait() {
            Ok(Some(status)) => {
                if status.success() {
                    let mut output = String::new();
                    if let Some(mut stdout) = child.stdout.take() {
                        use std::io::Read;
                        stdout.read_to_string(&mut output).ok()?;
                    }
                    let info = output.trim().to_string();
                    if !info.is_empty() && info.contains('|') {
                        return Some(info);
                    }
                }
                return None;
            }
            Ok(None) => {
                if start.elapsed() >= timeout {
                    let _ = child.kill();
                    let _ = child.wait();
                    log::warn!("Timed out reading update info from {}", path.display());
                    return None;
                }
                std::thread::sleep(std::time::Duration::from_millis(50));
            }
            Err(_) => return None,
        }
    }
}

// -- GitHub/GitLab API types for JSON deserialization --

#[derive(Deserialize)]
struct GhRelease {
    tag_name: String,
    name: Option<String>,
    body: Option<String>,
    assets: Vec<GhAsset>,
}

#[derive(Deserialize)]
struct GhAsset {
    name: String,
    browser_download_url: String,
    size: u64,
}

#[derive(Deserialize)]
struct GlRelease {
    tag_name: String,
    name: Option<String>,
    description: Option<String>,
    assets: Option<GlReleaseAssets>,
}

#[derive(Deserialize)]
struct GlReleaseAssets {
    links: Vec<GlAssetLink>,
}

#[derive(Deserialize)]
struct GlAssetLink {
    name: String,
    direct_asset_url: Option<String>,
    url: String,
}

/// Check for updates based on parsed update info.
/// Returns None if the check fails (network error, API error, etc.)
pub fn check_for_update(
    update_type: &UpdateType,
    current_version: Option<&str>,
) -> Option<UpdateCheckResult> {
    match update_type {
        UpdateType::GhReleasesZsync {
            owner,
            repo,
            release_tag,
            filename_pattern,
        } => check_github_release(owner, repo, release_tag, filename_pattern, current_version),

        UpdateType::GlReleasesZsync {
            host,
            owner,
            repo,
            release_tag,
            filename_pattern,
        } => check_gitlab_release(host, owner, repo, release_tag, filename_pattern, current_version),

        UpdateType::Zsync { url } => check_zsync_url(url, current_version),

        UpdateType::Ocs { url } => {
            log::info!("OCS update check not yet implemented for: {}", url);
            None
        }

        UpdateType::Bintray { .. } => {
            log::info!("Bintray is defunct - no update check possible");
            None
        }
    }
}

/// Check GitHub Releases API for the latest release.
fn check_github_release(
    owner: &str,
    repo: &str,
    release_tag: &str,
    filename_pattern: &str,
    current_version: Option<&str>,
) -> Option<UpdateCheckResult> {
    let api_url = if release_tag == "latest" {
        format!(
            "https://api.github.com/repos/{}/{}/releases/latest",
            owner, repo
        )
    } else {
        format!(
            "https://api.github.com/repos/{}/{}/releases/tags/{}",
            owner, repo, release_tag
        )
    };

    log::info!("Checking GitHub release: {}", api_url);

    let mut response = ureq::get(&api_url)
        .header("Accept", "application/vnd.github.v3+json")
        .header("User-Agent", "Driftwood-AppImage-Manager/0.1")
        .call()
        .ok()?;

    let release: GhRelease = response.body_mut().read_json().ok()?;

    log::info!(
        "GitHub release: tag={}, name={:?}",
        release.tag_name,
        release.name.as_deref().unwrap_or("(none)"),
    );

    let latest_version = clean_version(&release.tag_name);

    // Find matching asset using glob-like pattern
    let matching_asset = find_matching_asset_gh(
        &release.assets,
        filename_pattern,
    );

    let update_available = if let Some(current) = current_version {
        version_is_newer(&latest_version, current)
    } else {
        // No current version to compare - assume update might be available
        true
    };

    Some(UpdateCheckResult {
        update_available,
        latest_version: Some(latest_version),
        download_url: matching_asset.as_ref().map(|a| a.browser_download_url.clone()),
        release_notes: release.body,
        file_size: matching_asset.as_ref().map(|a| a.size),
    })
}

/// Check GitLab Releases API for the latest release.
fn check_gitlab_release(
    host: &str,
    owner: &str,
    repo: &str,
    release_tag: &str,
    filename_pattern: &str,
    current_version: Option<&str>,
) -> Option<UpdateCheckResult> {
    let project_path = format!("{}/{}", owner, repo);
    let encoded_path = project_path.replace('/', "%2F");

    let api_url = if release_tag == "latest" {
        format!(
            "https://{}/api/v4/projects/{}/releases/permalink/latest",
            host, encoded_path
        )
    } else {
        format!(
            "https://{}/api/v4/projects/{}/releases/{}",
            host, encoded_path, release_tag
        )
    };

    log::info!("Checking GitLab release: {}", api_url);

    let mut response = ureq::get(&api_url)
        .header("User-Agent", "Driftwood-AppImage-Manager/0.1")
        .call()
        .ok()?;

    let release: GlRelease = response.body_mut().read_json().ok()?;

    log::info!(
        "GitLab release: tag={}, name={:?}",
        release.tag_name,
        release.name.as_deref().unwrap_or("(none)"),
    );

    let latest_version = clean_version(&release.tag_name);

    let download_url = release.assets.and_then(|assets| {
        find_matching_link_gl(&assets.links, filename_pattern)
            .map(|link| link.direct_asset_url.clone().unwrap_or_else(|| link.url.clone()))
    });

    let update_available = if let Some(current) = current_version {
        version_is_newer(&latest_version, current)
    } else {
        true
    };

    Some(UpdateCheckResult {
        update_available,
        latest_version: Some(latest_version),
        download_url,
        release_notes: release.description,
        file_size: None,
    })
}

/// Check a direct zsync URL via HEAD request to see if the file has changed.
fn check_zsync_url(url: &str, _current_version: Option<&str>) -> Option<UpdateCheckResult> {
    // For zsync URLs, we can do a HEAD request to check if the file exists
    // and has been modified. The actual version comparison would need the zsync
    // control file, which is more complex.
    log::info!("Checking zsync URL: {}", url);

    let response = ureq::head(url)
        .header("User-Agent", "Driftwood-AppImage-Manager/0.1")
        .call()
        .ok()?;

    let status = response.status();
    if status == 200 {
        // The zsync file exists - an update might be available.
        // Without downloading and parsing the zsync file, we can't tell the version.
        // Mark as "check succeeded but version unknown".
        Some(UpdateCheckResult {
            update_available: false, // Can't determine without full zsync comparison
            latest_version: None,
            download_url: Some(url.replace(".zsync", "")),
            release_notes: None,
            file_size: None,
        })
    } else {
        None
    }
}

/// Find a matching GitHub asset using a glob-like pattern.
/// Patterns like "*x86_64.AppImage.zsync" or "App-*-x86_64.AppImage"
fn find_matching_asset_gh<'a>(
    assets: &'a [GhAsset],
    pattern: &str,
) -> Option<&'a GhAsset> {
    // Try to match the AppImage file (not the .zsync file)
    // If the pattern ends with .zsync, also look for the AppImage itself
    let appimage_pattern = pattern.replace(".zsync", "");

    // First try to find the AppImage binary
    if let Some(asset) = match_asset_name(assets, &appimage_pattern) {
        return Some(asset);
    }

    // Fall back to the original pattern (might be zsync)
    match_asset_name(assets, pattern)
}

fn match_asset_name<'a>(assets: &'a [GhAsset], pattern: &str) -> Option<&'a GhAsset> {
    for asset in assets {
        if glob_match(pattern, &asset.name) {
            return Some(asset);
        }
    }
    None
}

/// Find a matching GitLab asset link using a glob-like pattern.
fn find_matching_link_gl<'a>(
    links: &'a [GlAssetLink],
    pattern: &str,
) -> Option<&'a GlAssetLink> {
    let appimage_pattern = pattern.replace(".zsync", "");

    for link in links {
        if glob_match(&appimage_pattern, &link.name) {
            return Some(link);
        }
    }
    for link in links {
        if glob_match(pattern, &link.name) {
            return Some(link);
        }
    }
    None
}

/// Simple glob matching supporting only '*' as wildcard.
fn glob_match(pattern: &str, text: &str) -> bool {
    let parts: Vec<&str> = pattern.split('*').collect();

    if parts.len() == 1 {
        // No wildcards - exact match
        return pattern == text;
    }

    let mut pos = 0;

    // First part must match at the beginning (unless pattern starts with *)
    if !parts[0].is_empty() {
        if !text.starts_with(parts[0]) {
            return false;
        }
        pos = parts[0].len();
    }

    // Last part must match at the end (unless pattern ends with *)
    let last = parts[parts.len() - 1];
    let end_limit = if !last.is_empty() {
        if !text.ends_with(last) {
            return false;
        }
        text.len() - last.len()
    } else {
        text.len()
    };

    // Middle parts must appear in order within the allowed range
    for part in &parts[1..parts.len() - 1] {
        if part.is_empty() {
            continue;
        }
        if pos >= end_limit {
            return false;
        }
        if let Some(found) = text[pos..end_limit].find(part) {
            pos += found + part.len();
        } else {
            return false;
        }
    }

    true
}

/// Clean a version string - strip leading 'v' or 'V' prefix.
pub(crate) fn clean_version(version: &str) -> String {
    let v = version.trim();
    v.strip_prefix('v')
        .or_else(|| v.strip_prefix('V'))
        .unwrap_or(v)
        .to_string()
}

/// Compare version strings to determine if `latest` is newer than `current`.
/// Supports semver-like versions: 1.2.3, 24.02.1, etc.
pub fn version_is_newer(latest: &str, current: &str) -> bool {
    let latest_clean = clean_version(latest);
    let current_clean = clean_version(current);

    if latest_clean == current_clean {
        return false;
    }

    let latest_parts = parse_version_parts(&latest_clean);
    let current_parts = parse_version_parts(&current_clean);

    // Compare each numeric part
    for (l, c) in latest_parts.iter().zip(current_parts.iter()) {
        match l.cmp(c) {
            std::cmp::Ordering::Greater => return true,
            std::cmp::Ordering::Less => return false,
            std::cmp::Ordering::Equal => continue,
        }
    }

    // If all compared parts are equal, only consider newer if extra parts are non-zero
    // (e.g., 1.2.1 > 1.2, but 1.2.0 == 1.2)
    if latest_parts.len() > current_parts.len() {
        return latest_parts[current_parts.len()..].iter().any(|&p| p > 0);
    }
    false
}

/// Parse a version string into numeric parts.
/// "1.2.3" -> [1, 2, 3], "24.02.1" -> [24, 2, 1]
fn parse_version_parts(version: &str) -> Vec<u64> {
    version
        .split(|c: char| c == '.' || c == '-' || c == '_')
        .filter_map(|part| {
            // Strip non-numeric suffixes (e.g., "3rc1" -> "3")
            let numeric: String = part.chars().take_while(|c| c.is_ascii_digit()).collect();
            numeric.parse::<u64>().ok()
        })
        .collect()
}

/// Check if AppImageUpdate tool is available on the system.
pub fn has_appimage_update_tool() -> bool {
    // Check that the binary exists and can be spawned (--help may return non-zero)
    std::process::Command::new("AppImageUpdate")
        .arg("--help")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .status()
        .is_ok()
}

/// Batch check: read update info from an AppImage and check for updates.
/// Returns (update_type_label, raw_info, check_result).
pub fn check_appimage_for_update(
    path: &Path,
    current_version: Option<&str>,
) -> (Option<String>, Option<String>, Option<UpdateCheckResult>) {
    let raw_info = read_update_info(path);

    let (type_label, result) = if let Some(ref info) = raw_info {
        if let Some(update_type) = parse_update_info(info) {
            let label = update_type.type_label().to_string();
            let result = check_for_update(&update_type, current_version);
            (Some(label), result)
        } else {
            (None, None)
        }
    } else {
        (None, None)
    };

    (type_label, raw_info, result)
}

// -- Download and Apply --

/// Progress callback type for update downloads.
/// Called with (bytes_downloaded, total_bytes_option).
pub type ProgressCallback = Box<dyn Fn(u64, Option<u64>) + Send>;

/// Error type for update operations.
#[derive(Debug)]
pub enum UpdateError {
    NoDownloadUrl,
    NetworkError(String),
    IoError(std::io::Error),
    InvalidAppImage,
    AppImageUpdateFailed(String),
}

impl std::fmt::Display for UpdateError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NoDownloadUrl => write!(f, "No download URL available"),
            Self::NetworkError(e) => write!(f, "Network error: {}", e),
            Self::IoError(e) => write!(f, "I/O error: {}", e),
            Self::InvalidAppImage => write!(f, "Downloaded file is not a valid AppImage"),
            Self::AppImageUpdateFailed(e) => write!(f, "AppImageUpdate failed: {}", e),
        }
    }
}

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

/// Result of a completed update.
#[derive(Debug)]
pub struct AppliedUpdate {
    pub new_path: std::path::PathBuf,
    pub old_path_backup: Option<std::path::PathBuf>,
    pub new_version: Option<String>,
}

/// Try to update an AppImage using AppImageUpdate tool (delta update via zsync).
/// This is the preferred method as it only downloads changed blocks.
pub fn update_with_appimage_update_tool(
    appimage_path: &Path,
) -> Result<AppliedUpdate, UpdateError> {
    log::info!(
        "Attempting delta update via AppImageUpdate for {}",
        appimage_path.display()
    );

    let output = std::process::Command::new("AppImageUpdate")
        .arg(appimage_path)
        .output()
        .map_err(|e| UpdateError::AppImageUpdateFailed(e.to_string()))?;

    if output.status.success() {
        // AppImageUpdate creates the new file alongside the old one with .zs-old suffix for backup
        let backup_path = appimage_path.with_extension("AppImage.zs-old");
        let old_backup = if backup_path.exists() {
            Some(backup_path)
        } else {
            None
        };

        Ok(AppliedUpdate {
            new_path: appimage_path.to_path_buf(),
            old_path_backup: old_backup,
            new_version: None, // Caller should re-inspect to get new version
        })
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        Err(UpdateError::AppImageUpdateFailed(stderr.to_string()))
    }
}

/// Download and apply a full update (no delta - downloads the entire AppImage).
pub fn download_and_apply_update(
    appimage_path: &Path,
    download_url: &str,
    keep_old: bool,
    progress: Option<ProgressCallback>,
) -> Result<AppliedUpdate, UpdateError> {
    log::info!("Downloading full update from {} for {}", download_url, appimage_path.display());

    // Download to a temp file in the same directory (for atomic rename)
    let parent = appimage_path.parent().unwrap_or(Path::new("."));
    let filename = appimage_path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("update");
    let temp_path = parent.join(format!(".{}.driftwood-update.tmp", filename));

    // Perform the download
    download_file(download_url, &temp_path, progress)?;

    // Verify it's a valid AppImage (check ELF magic + AppImage magic)
    if !verify_appimage(&temp_path) {
        fs::remove_file(&temp_path).ok();
        return Err(UpdateError::InvalidAppImage);
    }

    // Make it executable
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let perms = fs::Permissions::from_mode(0o755);
        fs::set_permissions(&temp_path, perms)?;
    }

    // Backup old file if requested
    let backup_path = if keep_old {
        let backup = appimage_path.with_extension("AppImage.old");
        fs::rename(appimage_path, &backup)?;
        Some(backup)
    } else {
        None
    };

    // Atomic rename temp -> target
    if let Err(e) = fs::rename(&temp_path, appimage_path) {
        // Clean up temp file and restore backup on failure
        fs::remove_file(&temp_path).ok();
        if let Some(ref backup) = backup_path {
            fs::rename(backup, appimage_path).ok();
        }
        return Err(UpdateError::IoError(e));
    }

    Ok(AppliedUpdate {
        new_path: appimage_path.to_path_buf(),
        old_path_backup: backup_path,
        new_version: None,
    })
}

/// Download a file from URL to a local path, reporting progress.
fn download_file(
    url: &str,
    dest: &Path,
    progress: Option<ProgressCallback>,
) -> Result<(), UpdateError> {
    let mut response = ureq::get(url)
        .header("User-Agent", "Driftwood-AppImage-Manager/0.1")
        .call()
        .map_err(|e| UpdateError::NetworkError(e.to_string()))?;

    let content_length: Option<u64> = response
        .headers()
        .get("content-length")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.parse().ok());

    let mut file = fs::File::create(dest)?;
    let mut downloaded: u64 = 0;
    let mut buf = [0u8; 65536]; // 64KB chunks

    let mut reader = response.body_mut().as_reader();
    loop {
        let n = reader.read(&mut buf)
            .map_err(UpdateError::IoError)?;
        if n == 0 {
            break;
        }
        std::io::Write::write_all(&mut file, &buf[..n])?;
        downloaded += n as u64;

        if let Some(ref cb) = progress {
            cb(downloaded, content_length);
        }
    }

    Ok(())
}

/// Verify that a file is a valid AppImage (has ELF header + AppImage magic bytes).
fn verify_appimage(path: &Path) -> bool {
    use std::io::Read;
    let mut file = match fs::File::open(path) {
        Ok(f) => f,
        Err(_) => return false,
    };
    let mut header = [0u8; 12];
    if file.read_exact(&mut header).is_err() {
        return false;
    }
    // Check ELF magic
    if &header[0..4] != b"\x7FELF" {
        return false;
    }
    // Check AppImage Type 2 magic at offset 8: AI\x02
    if header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x02 {
        return true;
    }
    // Check AppImage Type 1 magic at offset 8: AI\x01
    header[8] == 0x41 && header[9] == 0x49 && header[10] == 0x01
}

/// Perform an update using the best available method.
/// Tries AppImageUpdate (delta) first, falls back to full download.
pub fn perform_update(
    appimage_path: &Path,
    download_url: Option<&str>,
    keep_old: bool,
    progress: Option<ProgressCallback>,
) -> Result<AppliedUpdate, UpdateError> {
    // Try delta update via AppImageUpdate tool first
    if has_appimage_update_tool() {
        match update_with_appimage_update_tool(appimage_path) {
            Ok(result) => return Ok(result),
            Err(e) => {
                log::warn!("AppImageUpdate delta update failed, falling back to full download: {}", e);
            }
        }
    }

    // Fall back to full download
    let url = download_url.ok_or(UpdateError::NoDownloadUrl)?;
    download_and_apply_update(appimage_path, url, keep_old, progress)
}

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

    #[test]
    fn test_parse_update_info_github() {
        let info = "gh-releases-zsync|probonopd|Firefox|latest|Firefox-*x86_64.AppImage.zsync";
        let parsed = parse_update_info(info).unwrap();
        assert_eq!(parsed.type_label(), "gh-releases-zsync");
        match parsed {
            UpdateType::GhReleasesZsync {
                owner,
                repo,
                release_tag,
                filename_pattern,
            } => {
                assert_eq!(owner, "probonopd");
                assert_eq!(repo, "Firefox");
                assert_eq!(release_tag, "latest");
                assert_eq!(filename_pattern, "Firefox-*x86_64.AppImage.zsync");
            }
            _ => panic!("Expected GhReleasesZsync"),
        }
    }

    #[test]
    fn test_parse_update_info_zsync() {
        let info = "zsync|https://example.com/app-latest.AppImage.zsync";
        let parsed = parse_update_info(info).unwrap();
        match parsed {
            UpdateType::Zsync { url } => {
                assert_eq!(url, "https://example.com/app-latest.AppImage.zsync");
            }
            _ => panic!("Expected Zsync"),
        }
    }

    #[test]
    fn test_parse_update_info_gitlab() {
        let info = "gl-releases-zsync|gitlab.com|user|project|latest|App-*x86_64.AppImage.zsync";
        let parsed = parse_update_info(info).unwrap();
        match parsed {
            UpdateType::GlReleasesZsync {
                host,
                owner,
                repo,
                release_tag,
                filename_pattern,
            } => {
                assert_eq!(host, "gitlab.com");
                assert_eq!(owner, "user");
                assert_eq!(repo, "project");
                assert_eq!(release_tag, "latest");
                assert_eq!(filename_pattern, "App-*x86_64.AppImage.zsync");
            }
            _ => panic!("Expected GlReleasesZsync"),
        }
    }

    #[test]
    fn test_parse_update_info_ocs() {
        let info = "ocs-v1-appimagehub-direct|https://appimagehub.com/api/...";
        let parsed = parse_update_info(info).unwrap();
        assert!(matches!(parsed, UpdateType::Ocs { .. }));
    }

    #[test]
    fn test_parse_update_info_empty() {
        assert!(parse_update_info("").is_none());
        assert!(parse_update_info("\0\0\0").is_none());
    }

    #[test]
    fn test_version_is_newer() {
        assert!(version_is_newer("125.0", "124.0"));
        assert!(version_is_newer("1.2.4", "1.2.3"));
        assert!(version_is_newer("24.03.0", "24.02.1"));
        assert!(version_is_newer("2.0.0", "1.9.9"));
        assert!(!version_is_newer("1.0.0", "1.0.0"));
        assert!(!version_is_newer("1.0.0", "2.0.0"));
        assert!(!version_is_newer("v1.0", "1.0"));
    }

    #[test]
    fn test_version_is_newer_with_prefix() {
        assert!(version_is_newer("v2.0.0", "v1.9.0"));
        assert!(version_is_newer("v2.0.0", "1.9.0"));
        assert!(!version_is_newer("v1.0.0", "v1.0.0"));
    }

    #[test]
    fn test_clean_version() {
        assert_eq!(clean_version("v1.2.3"), "1.2.3");
        assert_eq!(clean_version("V2.0"), "2.0");
        assert_eq!(clean_version("1.0.0"), "1.0.0");
        assert_eq!(clean_version("  v3.1  "), "3.1");
    }

    #[test]
    fn test_glob_match() {
        assert!(glob_match("*.AppImage", "Firefox-124.0-x86_64.AppImage"));
        assert!(glob_match(
            "Firefox-*x86_64.AppImage",
            "Firefox-125.0-x86_64.AppImage"
        ));
        assert!(glob_match("*", "anything"));
        assert!(glob_match("exact", "exact"));
        assert!(!glob_match("exact", "different"));
        assert!(!glob_match("Firefox-*", "Chrome-1.0.AppImage"));
        assert!(glob_match(
            "App-*-x86_64.AppImage.zsync",
            "App-2.0-x86_64.AppImage.zsync"
        ));
    }

    #[test]
    fn test_parse_version_parts() {
        assert_eq!(parse_version_parts("1.2.3"), vec![1, 2, 3]);
        assert_eq!(parse_version_parts("24.02.1"), vec![24, 2, 1]);
        assert_eq!(parse_version_parts("1.0"), vec![1, 0]);
        assert_eq!(parse_version_parts("3.1rc1"), vec![3, 1]);
    }

    #[test]
    fn test_read_update_info_nonexistent() {
        let path = Path::new("/tmp/nonexistent_appimage_test.AppImage");
        assert!(read_update_info(path).is_none());
    }
}
