use futures_util::StreamExt;
use serde::{Deserialize, Serialize};
use std::io::Write;
use tauri::{AppHandle, Emitter};

const GITEA_API: &str =
    "https://gitea.whitlocktech.com/api/v1/repos/whitlocktech/Artificers-Scrollwork/releases";

const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");

// ── Gitea API types ───────────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
struct GiteaRelease {
    tag_name: String,
    body: String,
    assets: Vec<GiteaAsset>,
    #[serde(default)]
    prerelease: bool,
    #[serde(default)]
    draft: bool,
}

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

// ── Public types returned to the frontend ────────────────────────────────────

#[derive(Debug, Serialize)]
pub struct UpdateInfo {
    pub available: bool,
    pub latest_version: String,
    pub current_version: String,
    pub release_notes: String,
    pub download_url: String,
    pub asset_name: String,
    /// File size in bytes (0 if unknown)
    pub asset_size: u64,
}

// ── Version helpers ───────────────────────────────────────────────────────────

fn parse_semver(v: &str) -> (u32, u32, u32) {
    let v = v.trim_start_matches('v');
    let mut parts = v.split('.').filter_map(|p| p.parse::<u32>().ok());
    (
        parts.next().unwrap_or(0),
        parts.next().unwrap_or(0),
        parts.next().unwrap_or(0),
    )
}

fn is_newer(tag: &str, current: &str) -> bool {
    parse_semver(tag) > parse_semver(current)
}

// ── Commands ─────────────────────────────────────────────────────────────────

/// Check the Gitea releases API for a newer version.
/// Returns `UpdateInfo` with `available: true` when a newer MSI release exists.
#[tauri::command]
pub async fn check_for_update() -> Result<UpdateInfo, String> {
    let client = reqwest::Client::builder()
        .user_agent(format!(
            "ArtificersScrollwork/{CURRENT_VERSION} (updater)"
        ))
        .timeout(std::time::Duration::from_secs(15))
        .build()
        .map_err(|e| e.to_string())?;

    let releases: Vec<GiteaRelease> = client
        .get(format!("{GITEA_API}?limit=10&page=1"))
        .send()
        .await
        .map_err(|e| format!("Network error: {e}"))?
        .json()
        .await
        .map_err(|e| format!("Failed to parse release data: {e}"))?;

    // Pick the latest non-draft, non-prerelease that ships an MSI
    let release = releases
        .iter()
        .find(|r| !r.draft && !r.prerelease && r.assets.iter().any(|a| a.name.ends_with(".msi")));

    let Some(release) = release else {
        return Ok(UpdateInfo {
            available: false,
            latest_version: CURRENT_VERSION.to_string(),
            current_version: CURRENT_VERSION.to_string(),
            release_notes: String::new(),
            download_url: String::new(),
            asset_name: String::new(),
            asset_size: 0,
        });
    };

    let msi = release
        .assets
        .iter()
        .find(|a| a.name.ends_with(".msi"))
        .unwrap(); // safe: filtered above

    Ok(UpdateInfo {
        available: is_newer(&release.tag_name, CURRENT_VERSION),
        latest_version: release.tag_name.trim_start_matches('v').to_string(),
        current_version: CURRENT_VERSION.to_string(),
        release_notes: release.body.clone(),
        download_url: msi.browser_download_url.clone(),
        asset_name: msi.name.clone(),
        asset_size: msi.size,
    })
}

/// Download the MSI to the system temp directory, emitting `update-progress`
/// events as chunks arrive, then launch the installer via msiexec.
///
/// The frontend should listen for `update-progress` events:
///   `{ downloaded: number, total: number }`
///
/// After this returns Ok(()), the installer will be running and the user
/// can follow the wizard. The app does NOT auto-close; the installer handles it.
#[tauri::command]
pub async fn download_and_install_update(
    app: AppHandle,
    download_url: String,
    asset_name: String,
) -> Result<(), String> {
    let client = reqwest::Client::builder()
        .user_agent(format!(
            "ArtificersScrollwork/{CURRENT_VERSION} (updater)"
        ))
        .timeout(std::time::Duration::from_secs(300))
        .build()
        .map_err(|e| e.to_string())?;

    let response = client
        .get(&download_url)
        .send()
        .await
        .map_err(|e| format!("Download request failed: {e}"))?;

    if !response.status().is_success() {
        return Err(format!(
            "Download failed with status {}",
            response.status()
        ));
    }

    let total = response.content_length().unwrap_or(0);

    // Write to temp dir
    let installer_path = std::env::temp_dir().join(&asset_name);
    let mut file =
        std::fs::File::create(&installer_path).map_err(|e| format!("Cannot create temp file: {e}"))?;

    let mut downloaded = 0u64;
    let mut stream = response.bytes_stream();

    while let Some(chunk) = stream.next().await {
        let chunk = chunk.map_err(|e| format!("Download interrupted: {e}"))?;
        file.write_all(&chunk)
            .map_err(|e| format!("Write error: {e}"))?;
        downloaded += chunk.len() as u64;
        let _ = app.emit(
            "update-progress",
            serde_json::json!({ "downloaded": downloaded, "total": total }),
        );
    }

    drop(file); // ensure file handle is closed before launching

    // Launch installer — Windows opens .msi files with msiexec automatically
    #[cfg(target_os = "windows")]
    {
        std::process::Command::new("msiexec")
            .args(["/i", &installer_path.to_string_lossy().to_string()])
            .spawn()
            .map_err(|e| format!("Failed to launch installer: {e}"))?;
    }

    #[cfg(not(target_os = "windows"))]
    {
        // Fallback for non-Windows (future proofing)
        opener::open(&installer_path).map_err(|e| format!("Failed to open installer: {e}"))?;
    }

    Ok(())
}
