use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;

const MANIFEST_URLS: [&str; 4] = [
    "https://coder.vigthoria.io/releases/workbench-manifest.json",
    "https://extension.vigthoria.io/downloads/workbench-manifest.json",
    "https://landing.vigthoria.io/downloads/workbench-manifest.json",
    "https://market.vigthoria.io/downloads/workbench-manifest.json",
];
const CHANNEL: &str = "stable";
const MAX_DOWNLOAD_BYTES: u64 = 500 * 1024 * 1024;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkbenchUpdateInfo {
    pub current_version: String,
    pub latest_version: String,
    pub update_available: bool,
    pub notes: String,
    pub artifact_kind: Option<String>,
    pub download_url: Option<String>,
    pub sha256: Option<String>,
}

pub fn current_version() -> String {
    env!("CARGO_PKG_VERSION").to_string()
}

fn parse_version(version: &str) -> Vec<u64> {
    version
        .trim()
        .trim_start_matches('v')
        .split('.')
        .map(|part| {
            part.chars()
                .take_while(|c| c.is_ascii_digit())
                .collect::<String>()
        })
        .map(|digits| digits.parse::<u64>().unwrap_or(0))
        .collect()
}

fn is_newer(latest: &str, current: &str) -> bool {
    let latest_parts = parse_version(latest);
    let current_parts = parse_version(current);
    let len = latest_parts.len().max(current_parts.len());
    for i in 0..len {
        let l = latest_parts.get(i).copied().unwrap_or(0);
        let c = current_parts.get(i).copied().unwrap_or(0);
        if l != c {
            return l > c;
        }
    }
    false
}

fn platform_artifact_for(
    platform: &str,
    platforms: &Value,
) -> (Option<String>, Option<String>, Option<String>) {
    let pick = |key: &str| -> Option<(String, String)> {
        let entry = platforms.get(key)?;
        let url = entry.get("url")?.as_str()?.to_string();
        let sha = entry.get("sha256")?.as_str()?.to_string();
        Some((url, sha))
    };

    if platform == "windows" {
        if let Some((url, sha)) = pick("windows-setup") {
            return (Some("windows-setup".into()), Some(url), Some(sha));
        }
        if let Some((url, sha)) = pick("windows-portable") {
            return (Some("windows-portable".into()), Some(url), Some(sha));
        }
    } else if platform == "macos" {
        if let Some((url, sha)) = pick("macos-dmg") {
            return (Some("macos-dmg".into()), Some(url), Some(sha));
        }
    } else if let Some((url, sha)) = pick("linux-deb") {
        return (Some("linux-deb".into()), Some(url), Some(sha));
    }
    (None, None, None)
}

fn platform_artifact(platforms: &Value) -> (Option<String>, Option<String>, Option<String>) {
    let platform = if cfg!(windows) {
        "windows"
    } else if cfg!(target_os = "macos") {
        "macos"
    } else {
        "linux"
    };
    platform_artifact_for(platform, platforms)
}

pub fn check_for_update() -> Result<WorkbenchUpdateInfo, String> {
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(10))
        .build();
    let mut manifest = None;
    let mut errors = Vec::new();
    for url in MANIFEST_URLS {
        match agent.get(url).set("Accept", "application/json").call() {
            Ok(response) => match response.into_json::<Value>() {
                Ok(value) => {
                    manifest = Some(value);
                    break;
                }
                Err(error) => errors.push(format!("{url}: invalid JSON ({error})")),
            },
            Err(error) => errors.push(format!("{url}: {error}")),
        }
    }
    let manifest = manifest.ok_or_else(|| {
        format!("All Workbench update manifests failed: {}", errors.join("; "))
    })?;

    let channel = manifest
        .get("channels")
        .and_then(|c| c.get(CHANNEL))
        .ok_or_else(|| "Update manifest missing stable channel".to_string())?;

    let latest_version = channel
        .get("version")
        .and_then(|v| v.as_str())
        .ok_or_else(|| "Update manifest missing version".to_string())?
        .to_string();
    let notes = channel
        .get("notes")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();

    let empty = serde_json::json!({});
    let platforms = channel.get("platforms").unwrap_or(&empty);
    let (artifact_kind, download_url, sha256) = platform_artifact(platforms);

    let current = current_version();
    let update_available = is_newer(&latest_version, &current);

    Ok(WorkbenchUpdateInfo {
        current_version: current,
        latest_version,
        update_available,
        notes,
        artifact_kind,
        download_url,
        sha256,
    })
}

pub fn download_update(url: &str, expected_sha256: &str) -> Result<String, String> {
    if !release_url_allowed(url) {
        return Err("Refusing to download an update from an untrusted origin".into());
    }
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(180))
        .build();
    let response = agent
        .get(url)
        .call()
        .map_err(|e| format!("Download failed: {e}"))?;

    let mut bytes = Vec::new();
    response
        .into_reader()
        .take(MAX_DOWNLOAD_BYTES)
        .read_to_end(&mut bytes)
        .map_err(|e| format!("Failed to read update download: {e}"))?;

    if bytes.is_empty() {
        return Err("Downloaded update file is empty".into());
    }

    let mut hasher = Sha256::new();
    hasher.update(&bytes);
    let actual = format!("{:x}", hasher.finalize());
    let expected = expected_sha256.trim();
    if !actual.eq_ignore_ascii_case(expected) {
        return Err(format!(
            "Checksum mismatch for downloaded update (expected {expected}, got {actual})"
        ));
    }

    let file_name = url
        .rsplit('/')
        .next()
        .filter(|s| !s.is_empty())
        .unwrap_or("vigthoria-workbench-update")
        .to_string();
    let dest = std::env::temp_dir().join(file_name);
    std::fs::write(&dest, &bytes).map_err(|e| format!("Failed to save update file: {e}"))?;

    Ok(dest.to_string_lossy().into_owned())
}

fn release_url_allowed(url: &str) -> bool {
    let Some(rest) = url.trim().strip_prefix("https://") else {
        return false;
    };
    let authority = rest.split('/').next().unwrap_or_default();
    if authority.is_empty() || authority.contains('@') {
        return false;
    }
    let host = authority
        .split(':')
        .next()
        .unwrap_or_default()
        .trim_end_matches('.')
        .to_ascii_lowercase();
    matches!(
        host.as_str(),
        "coder.vigthoria.io"
            | "extension.vigthoria.io"
            | "landing.vigthoria.io"
            | "market.vigthoria.io"
    )
}

pub fn launch_installer_and_exit(path: &str) -> Result<(), String> {
    let path_buf = PathBuf::from(path);
    if !path_buf.exists() {
        return Err("Downloaded installer not found".into());
    }

    // Windows: launch the NSIS installer in passive mode (/P) with auto-
    // relaunch (/R). Without /P, the installer's own "is the app already
    // running?" check (CheckIfAppIsRunning in Tauri's bundled NSIS template)
    // shows a blocking MB_OKCANCEL dialog asking to close Workbench first --
    // since WE are the very process it's asking about (we just spawned the
    // installer and are about to exit), that dialog can easily be missed,
    // appear without focus, or get reflexively dismissed via Escape/Cancel,
    // which silently ABORTS the entire install and leaves the OLD binary in
    // place with no visible error -- exactly matching reports of "installed
    // the update but it's still the old version". Passive mode (/P) makes
    // the installer auto-kill the running instance with no prompt, show a
    // simple auto-closing progress window, and (/R) relaunch the new binary
    // automatically once installed.
    let spawn_result = if cfg!(windows) {
        Command::new(&path_buf).arg("/P").arg("/R").spawn()
    } else if cfg!(target_os = "macos") {
        Command::new("open").arg(&path_buf).spawn()
    } else {
        Command::new("xdg-open").arg(&path_buf).spawn()
    };

    spawn_result.map_err(|e| format!("Failed to launch installer: {e}"))?;

    // Give the child process a moment to start before we release file locks by exiting.
    std::thread::sleep(Duration::from_millis(300));
    std::process::exit(0);
}

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

    #[test]
    fn compares_numeric_versions_without_lexical_errors() {
        assert!(is_newer("1.10.0", "1.9.9"));
        assert!(is_newer("2.0.0", "1.99.99"));
        assert!(!is_newer("1.3.9", "1.3.9"));
        assert!(!is_newer("1.3.8", "1.3.9"));
    }

    #[test]
    fn chooses_complete_platform_entries_only() {
        let platforms = serde_json::json!({
            "windows-setup": {"url": "https://coder.vigthoria.io/setup.exe"},
            "windows-portable": {
                "url": "https://coder.vigthoria.io/portable.zip",
                "sha256": "abc"
            },
            "linux-deb": {
                "url": "https://coder.vigthoria.io/app.deb",
                "sha256": "def"
            }
        });
        let (_, url, sha) = platform_artifact(&platforms);
        assert!(url.is_some());
        assert!(sha.is_some());
    }

    #[test]
    fn chooses_native_macos_dmg() {
        let platforms = serde_json::json!({
            "macos-dmg": {
                "url": "https://coder.vigthoria.io/workbench.dmg",
                "sha256": "abc"
            }
        });
        let (kind, url, sha) = platform_artifact_for("macos", &platforms);
        assert_eq!(kind.as_deref(), Some("macos-dmg"));
        assert!(url.unwrap().ends_with(".dmg"));
        assert_eq!(sha.as_deref(), Some("abc"));
    }

    #[test]
    fn update_downloads_use_only_release_origins() {
        assert!(release_url_allowed(
            "https://extension.vigthoria.io/downloads/workbench.exe"
        ));
        assert!(release_url_allowed(
            "https://coder.vigthoria.io/releases/workbench.deb"
        ));
        assert!(!release_url_allowed("http://coder.vigthoria.io/update.exe"));
        assert!(!release_url_allowed(
            "https://coder.vigthoria.io.evil.example/update.exe"
        ));
    }
}
