use std::path::PathBuf;

use clap::Parser;

use binstalk_downloader::download::{Download, PkgFmt};
use detect_targets::{TARGET, get_desired_targets};
use miette::{IntoDiagnostic, Result, miette};
use tracing::{debug, info};

use crate::download::{DownloadSource, client, fetch_latest_version};

use super::Context;

#[cfg(unix)]
fn check_exe_writable() -> Result<()> {
	let exe_path = std::env::current_exe().into_diagnostic()?;
	let exe_dir = exe_path
		.parent()
		.ok_or_else(|| miette!("current exe is not in a directory"))?;

	// Try to check if we can actually write to the directory
	let test_file = exe_dir.join(".bestool_write_test");
	if std::fs::write(&test_file, b"test").is_err() {
		return Err(miette!(
			"Cannot write to executable directory: {}\n\
			Please run with sudo: sudo bestool self-update",
			exe_dir.display()
		));
	}
	let _ = std::fs::remove_file(test_file);

	Ok(())
}

#[cfg(unix)]
pub(crate) fn is_package_manager_install() -> bool {
	std::path::Path::new("/usr/share/doc/bestool/copyright").exists()
}

#[cfg(not(unix))]
pub(crate) fn is_package_manager_install() -> bool {
	false
}

#[cfg(unix)]
fn check_package_manager_install(force: bool) -> Result<()> {
	if is_package_manager_install() && !force {
		return Err(miette!(
			"bestool appears to be installed via a package manager.\n\
			Please use your package manager to update bestool (e.g., 'apt update && apt upgrade bestool').\n\
			If you want to override this and self-update anyway, use: bestool self-update --force"
		));
	}

	Ok(())
}

/// Update this bestool.
///
/// Alias: self
#[derive(Debug, Clone, Parser)]
pub struct SelfUpdateArgs {
	/// Version to update to.
	#[arg(long, default_value = "latest")]
	pub version: String,

	/// Target to download.
	///
	/// Usually the auto-detected default is fine, in rare cases you may need to override it.
	#[arg(long)]
	pub target: Option<String>,

	/// Temporary directory to download to.
	///
	/// Defaults to the system temp directory.
	#[arg(long)]
	pub temp_dir: Option<PathBuf>,

	/// Add to the PATH (only on Windows).
	#[cfg(windows)]
	#[arg(short = 'P', long)]
	pub add_to_path: bool,

	/// Force reinstall, even if already on the latest version or installed via package manager.
	#[arg(long)]
	pub force: bool,
}

pub async fn run(args: SelfUpdateArgs, _ctx: Context) -> Result<()> {
	#[cfg(unix)]
	{
		check_exe_writable()?;
		check_package_manager_install(args.force)?;
	}

	let SelfUpdateArgs {
		version,
		target,
		temp_dir,
		force,
		#[cfg(windows)]
		add_to_path,
	} = args;

	if version == "latest" && !force {
		match fetch_latest_version().await {
			Ok(latest) => {
				let current = env!("CARGO_PKG_VERSION");
				if latest == current {
					info!(
						version = current,
						"Already on the latest version. Use --force to reinstall."
					);
					return Ok(());
				}
				info!(current = current, latest = %latest, "Update available, proceeding");
			}
			Err(err) => {
				debug!("Failed to check latest version, continuing with download: {err}");
			}
		}
	}

	let client = client().await?;

	let detected_targets = get_desired_targets(target.map(|t| vec![t]));
	let detected_targets = detected_targets.get().await;

	let dir = temp_dir.unwrap_or_else(std::env::temp_dir);
	let filename = format!(
		"bestool{ext}",
		ext = if cfg!(windows) { ".exe" } else { "" }
	);
	let dest = dir.join(&filename);
	let _ = tokio::fs::remove_file(&dest).await;

	let host = DownloadSource::Tools.host();
	let target = detected_targets
		.first()
		.cloned()
		.unwrap_or_else(|| TARGET.into());
	let bin_path = format!("/bestool/{version}/{target}/{filename}");
	let bin_url = host.join(&bin_path).into_diagnostic()?;
	let tzst_url = host
		.join(&format!("{bin_path}.tar.zst"))
		.into_diagnostic()?;

	let tzst_available = client
		.remote_gettable(tzst_url.clone())
		.await
		.unwrap_or(false);

	if tzst_available {
		info!(url = %tzst_url, "downloading compressed");
		Download::new(client, tzst_url)
			.and_extract(PkgFmt::Tzstd, &dir)
			.await
			.into_diagnostic()?;
	} else {
		info!(url = %bin_url, "downloading");
		Download::new(client, bin_url)
			.and_extract(PkgFmt::Bin, &dest)
			.await
			.into_diagnostic()?;
	}

	#[cfg(windows)]
	if add_to_path && let Err(err) = add_self_to_path() {
		tracing::error!("{err:?}");
	}

	info!(?dest, "downloaded, self-upgrading");
	upgrade::run_upgrade(&dest, true, vec!["--version"])
		.map_err(|err| miette!("upgrade: {err:?}"))?;
	
	#[cfg(windows)]
	if is_alertd_service_running().await {
		if let Err(err) = schedule_service_restart() {
			tracing::warn!("failed to schedule service restart: {err:?}");
		}
	}
	
	Ok(())
}

#[cfg(windows)]
fn add_self_to_path() -> Result<()> {
	let self_path = std::env::current_exe().into_diagnostic()?;
	let self_dir = self_path
		.parent()
		.ok_or_else(|| miette!("current exe is not in a dir?"))?;
	let self_dir = self_dir
		.to_str()
		.ok_or_else(|| miette!("current exe path is not utf-8"))?;

	windows_env::prepend("PATH", self_dir).into_diagnostic()?;

	Ok(())
}

#[cfg(windows)]
async fn is_alertd_service_running() -> bool {
	// Try to query the alertd HTTP API status endpoint
	match reqwest::get("http://127.0.0.1:8271/status").await {
		Ok(response) => response.status().is_success(),
		Err(_) => false,
	}
}

#[cfg(windows)]
fn schedule_service_restart() -> Result<()> {
	use std::process::Command;
	
	// Schedule a service restart using a PowerShell command that waits 60 seconds
	// then restarts the service
	let ps_command = "Start-Sleep -Seconds 60; Restart-Service -Name bestool-alertd -Force";
	
	let output = Command::new("powershell")
		.args(&[
			"-NoProfile",
			"-Command",
			&format!("Start-Process powershell -ArgumentList '-NoProfile', '-Command', '{}' -WindowStyle Hidden", ps_command),
		])
		.output()
		.into_diagnostic()?;
	
	if !output.status.success() {
		let stderr = String::from_utf8_lossy(&output.stderr);
		return Err(miette!("failed to schedule service restart: {}", stderr));
	}
	
	info!("scheduled service restart for 1 minute later");
	Ok(())
}

#[cfg(all(test, unix))]
mod tests {
	use std::fs;
	use tempfile::TempDir;

	#[test]
	fn test_check_exe_writable_with_writable_dir() {
		// This test checks that the function succeeds when the exe is in a writable directory
		// We can't easily test this in a hermetic way since check_exe_writable() uses current_exe(),
		// but we can verify the logic by checking that a writable temp directory works
		let temp_dir = TempDir::new().unwrap();
		let test_file = temp_dir.path().join(".bestool_write_test");

		// Should succeed
		assert!(fs::write(&test_file, b"test").is_ok());
		assert!(fs::remove_file(test_file).is_ok());
	}

	#[test]
	fn test_check_exe_writable_with_readonly_dir() {
		// This test verifies that write attempts fail on read-only directories
		use std::os::unix::fs::PermissionsExt;

		let temp_dir = TempDir::new().unwrap();
		let temp_path = temp_dir.path();

		// Make directory read-only
		let mut perms = fs::metadata(temp_path).unwrap().permissions();
		perms.set_mode(0o555);
		fs::set_permissions(temp_path, perms).unwrap();

		let test_file = temp_path.join(".bestool_write_test");

		// Should fail
		assert!(fs::write(&test_file, b"test").is_err());

		// Restore permissions for cleanup
		let mut perms = fs::metadata(temp_path).unwrap().permissions();
		perms.set_mode(0o755);
		let _ = fs::set_permissions(temp_path, perms);
	}
}
