use crate::fakers::Command;
use crate::fakers::CommandRunner;
use crate::models::DistroboxTask;
use crate::models::RootStore;
use anyhow::{Context, anyhow};
use gtk::glib;
use gtk::prelude::*;
use std::path::PathBuf;

pub const DISTROBOX_VERSION: &str = "1.8.2.4";
// SHA256 of the tar.gz file from github
pub const DISTROBOX_SHA256: &str =
    "83eab6ba893dce56f0523a70ebfcf2a78f7785637a6895255777ef1b72d4eb89";

pub fn get_bundled_distrobox_path() -> PathBuf {
    let user_data_dir = glib::user_data_dir();
    user_data_dir
        .join("distroshelf")
        .join(format!("distrobox-{}", DISTROBOX_VERSION))
        .join("distrobox")
}

pub fn get_bundled_distrobox_dir() -> PathBuf {
    let user_data_dir = glib::user_data_dir();
    user_data_dir.join("distroshelf")
}

/// Resolves the best available bundled distrobox path.
/// Returns the current version's path if it exists, otherwise finds the most recent older version.
/// Returns None if no bundled version is found at all.
pub fn resolve_bundled_distrobox_path() -> Option<PathBuf> {
    let current = get_bundled_distrobox_path();
    if current.exists() {
        return Some(current);
    }
    find_latest_bundled_version()
}

/// Returns true if the user has a bundled distrobox but it's not the latest (DISTROBOX_VERSION).
/// This means an update is available to download.
pub fn is_bundled_update_available() -> bool {
    let current = get_bundled_distrobox_path();
    if current.exists() {
        return false; // Already have the latest
    }
    // An update is available if there's an older version on disk but not the current one
    find_latest_bundled_version().is_some()
}

/// Scans the bundled distrobox directory for version subdirectories and returns the path
/// to the distrobox binary in the most recent version found.
fn find_latest_bundled_version() -> Option<PathBuf> {
    let parent = get_bundled_distrobox_dir();
    let entries = std::fs::read_dir(&parent).ok()?;

    let mut versions: Vec<(Vec<u32>, PathBuf)> = entries
        .filter_map(|entry| {
            let entry = entry.ok()?;
            let name = entry.file_name();
            let name_str = name.to_str()?;
            let version_str = name_str.strip_prefix("distrobox-")?;
            let binary_path = entry.path().join("distrobox");
            if !binary_path.exists() {
                return None;
            }
            let parts: Option<Vec<u32>> = version_str.split('.').map(|p| p.parse().ok()).collect();
            Some((parts?, binary_path))
        })
        .collect();

    versions.sort_by(|a, b| a.0.cmp(&b.0));
    versions.last().map(|(_, path)| path.clone())
}

/// Removes all bundled distrobox version directories except the current one (DISTROBOX_VERSION).
pub fn cleanup_old_bundled_versions() {
    let parent = get_bundled_distrobox_dir();
    let current_dir_name = format!("distrobox-{}", DISTROBOX_VERSION);

    let entries = match std::fs::read_dir(&parent) {
        Ok(e) => e,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let name = entry.file_name();
        let name_str = match name.to_str() {
            Some(s) => s.to_string(),
            None => continue,
        };
        if name_str.starts_with("distrobox-") && name_str != current_dir_name {
            if entry.path().is_dir() {
                if let Err(e) = std::fs::remove_dir_all(entry.path()) {
                    tracing::warn!("Failed to remove old bundled version {:?}: {}", entry.path(), e);
                } else {
                    tracing::info!("Removed old bundled version: {}", name_str);
                }
            }
        }
    }
}

fn log(task: &DistroboxTask, msg: &str) {
    task.append_output(msg);
    task.append_output("\n");
}

pub fn download_distrobox(root_store: &RootStore) -> DistroboxTask {
    let root_store_weak = root_store.downgrade();
    // We should be able to actually use a CommandRunner runs in the flatpak sandbox, because it has all the tools we need.
    // Also, the data folder is writable there and should be mapped to the host.
    let command_runner = CommandRunner::new_real();

    DistroboxTask::new("system", "Downloading Distrobox", move |task| async move {
        let download_dir = get_bundled_distrobox_dir();
        let tarball_path = download_dir.join("distrobox.tar.gz");
        let url = format!(
            "https://github.com/89luca89/distrobox/archive/refs/tags/{}.tar.gz",
            DISTROBOX_VERSION
        );

        // Ensure directory exists
        std::fs::create_dir_all(&download_dir).context("Failed to create download directory")?;

        log(
            &task,
            &format!("Using download directory: {:?}", download_dir),
        );

        // 1. Download
        log(&task, &format!("Downloading {}...", url));
        let mut curl_cmd = Command::new("curl");
        curl_cmd.arg("-L");
        curl_cmd.arg("-o");
        curl_cmd.arg(tarball_path.to_str().unwrap());
        curl_cmd.arg(&url);
        curl_cmd.stdout = crate::fakers::FdMode::Pipe;
        curl_cmd.stderr = crate::fakers::FdMode::Pipe;

        let child = command_runner
            .spawn(curl_cmd)
            .context("Failed to run curl")?;

        task.handle_child_output(child).await?;

        // 2. Verify SHA256
        log(&task, "Verifying checksum...");
        let mut sha_cmd = Command::new("sha256sum");
        sha_cmd.arg(tarball_path.to_str().unwrap());
        sha_cmd.stdout = crate::fakers::FdMode::Pipe;
        sha_cmd.stderr = crate::fakers::FdMode::Pipe;

        let output = command_runner.output(sha_cmd).await?;
        if !output.status.success() {
            return Err(anyhow!("sha256sum failed"));
        }

        let stdout = String::from_utf8_lossy(&output.stdout);
        let calculated_hash = stdout.split_whitespace().next().unwrap_or("");

        if calculated_hash != DISTROBOX_SHA256 {
            return Err(anyhow!(
                "Checksum mismatch. Expected {}, got {}",
                DISTROBOX_SHA256,
                calculated_hash
            ));
        }
        log(&task, "Checksum verified.");

        // 3. Extract
        log(&task, "Extracting...");
        let mut tar_cmd = Command::new("tar");
        tar_cmd.arg("xzf");
        tar_cmd.arg(tarball_path.to_str().unwrap());
        tar_cmd.arg("-C");
        tar_cmd.arg(download_dir.to_str().unwrap());
        tar_cmd.stdout = crate::fakers::FdMode::Pipe;
        tar_cmd.stderr = crate::fakers::FdMode::Pipe;

        let child = command_runner.spawn(tar_cmd).context("Failed to run tar")?;

        task.handle_child_output(child).await?;

        // 3b. Clean up tarball
        log(&task, "Removing tarball...");
        std::fs::remove_file(&tarball_path).context("Failed to remove tarball")?;

        // 4. Make executable (it should be already, but just in case)
        let binary_path = get_bundled_distrobox_path();
        log(
            &task,
            &format!("Setting executable permissions on {:?}...", binary_path),
        );

        let mut chmod_cmd = Command::new("chmod");
        chmod_cmd.arg("+x");
        chmod_cmd.arg(binary_path.to_str().unwrap());
        chmod_cmd.stdout = crate::fakers::FdMode::Pipe;
        chmod_cmd.stderr = crate::fakers::FdMode::Pipe;

        let output = command_runner.output(chmod_cmd).await?;
        if !output.status.success() {
            return Err(anyhow!("chmod failed"));
        }

        log(&task, "Distrobox installed successfully.");

        // Clean up old bundled versions
        log(&task, "Cleaning up old bundled versions...");
        cleanup_old_bundled_versions();

        if let Some(root_store) = root_store_weak.upgrade() {
            root_store.distrobox_version().refetch();
            root_store.update_bundled_update_available();
            root_store.set_current_dialog(crate::models::DialogType::None);
        }

        Ok(())
    })
}
