use std::process::Command;
use std::fs;

pub struct WifiNetwork
{
	pub ssid:      String,
	pub signal:    u8,
	pub connected: bool,
}

pub struct BluetoothDevice
{
	pub name:      String,
	pub connected: bool,
}

pub fn wifi_enabled() -> bool
{
	Command::new( "nmcli" ).args( ["radio", "wifi"] ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| s.trim() == "enabled" )
		.unwrap_or( false )
}

pub fn set_wifi_enabled( on: bool )
{
	let state = if on { "on" } else { "off" };
	let _ = Command::new( "nmcli" ).args( ["radio", "wifi", state] ).output();
}

pub fn wifi_networks() -> Vec<WifiNetwork>
{
	let output = Command::new( "nmcli" )
		.args( ["-t", "-f", "ACTIVE,SIGNAL,SSID", "dev", "wifi", "list"] )
		.output().ok();

	let text = output
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.unwrap_or_default();

	text.lines().filter_map( |line|
	{
		let parts: Vec<&str> = line.splitn( 3, ':' ).collect();
		if parts.len() < 3 { return None; }
		let ssid = parts[2].to_string();
		if ssid.is_empty() { return None; }
		Some( WifiNetwork
		{
			connected: parts[0] == "yes",
			signal:    parts[1].parse().unwrap_or( 0 ),
			ssid,
		})
	}).collect()
}

pub fn bluetooth_enabled() -> bool
{
	Command::new( "bluetoothctl" ).arg( "show" ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| s.contains( "Powered: yes" ) )
		.unwrap_or( false )
}

pub fn set_bluetooth_enabled( on: bool )
{
	let state = if on { "on" } else { "off" };
	let _ = Command::new( "bluetoothctl" ).args( ["power", state] ).output();
}

pub fn bluetooth_devices() -> Vec<BluetoothDevice>
{
	let output = Command::new( "bluetoothctl" ).arg( "devices" ).output().ok();
	let text = output
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.unwrap_or_default();

	let connected_out = Command::new( "bluetoothctl" )
		.args( ["devices", "Connected"] ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.unwrap_or_default();

	text.lines().filter_map( |line|
	{
		let rest = line.strip_prefix( "Device " )?;
		let ( addr, name ) = rest.split_once( ' ' )?;
		Some( BluetoothDevice
		{
			name:      name.to_string(),
			connected: connected_out.contains( addr ),
		})
	}).collect()
}

pub fn brightness() -> f32
{
	let max = read_backlight( "max_brightness" ).unwrap_or( 100.0 );
	let cur = read_backlight( "brightness" ).unwrap_or( 50.0 );
	if max > 0.0 { cur / max } else { 0.5 }
}

pub fn set_brightness( val: f32 )
{
	let pct = ( val * 100.0 ).round() as u32;
	let _ = Command::new( "brightnessctl" )
		.args( ["set", &format!( "{}%", pct )] ).output();
}

fn read_backlight( file: &str ) -> Option<f32>
{
	let base = "/sys/class/backlight";
	let entries = fs::read_dir( base ).ok()?;
	for entry in entries.flatten()
	{
		let path = entry.path().join( file );
		if let Ok( val ) = fs::read_to_string( &path )
		{
			if let Ok( n ) = val.trim().parse::<f32>() { return Some( n ); }
		}
	}
	None
}

/// Volume (0.0–1.0) and mute state in a single `wpctl get-volume` call.
/// `wpctl` prints both on one line (e.g. `Volume: 0.45 [MUTED]`), so the
/// previous split into `volume()` + `muted()` spawned the identical
/// subprocess twice per refresh for data already in one output.
pub fn audio_state() -> ( f32, bool )
{
	Command::new( "wpctl" ).args( ["get-volume", "@DEFAULT_AUDIO_SINK@"] ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| parse_wpctl_volume( &s ) )
		.unwrap_or( ( 0.5, false ) )
}

/// Parse one line of `wpctl get-volume` output into `(volume, muted)`.
///
/// `wpctl` prints e.g. `Volume: 0.65` or `Volume: 0.00 [MUTED]`. The volume is
/// the second whitespace-separated token; a `[MUTED]` marker anywhere on the
/// line means the sink is muted. Falls back to `0.5` if the token is missing or
/// unparseable, matching the previous inline behaviour.
fn parse_wpctl_volume( s: &str ) -> ( f32, bool )
{
	let vol = s.split_whitespace().nth( 1 )
		.and_then( |v| v.parse::<f32>().ok() )
		.unwrap_or( 0.5 );
	( vol, s.contains( "[MUTED]" ) )
}

pub fn set_volume( val: f32 )
{
	let _ = Command::new( "wpctl" )
		.args( ["set-volume", "@DEFAULT_AUDIO_SINK@", &format!( "{:.2}", val )] ).output();
}

pub fn set_muted( m: bool )
{
	let val = if m { "1" } else { "0" };
	let _ = Command::new( "wpctl" ).args( ["set-mute", "@DEFAULT_AUDIO_SINK@", val] ).output();
}

pub fn battery_percent() -> Option<u8>
{
	let base = "/sys/class/power_supply";
	for entry in fs::read_dir( base ).ok()?.flatten()
	{
		let typ = fs::read_to_string( entry.path().join( "type" ) ).unwrap_or_default();
		if typ.trim() != "Battery" { continue; }
		let cap = fs::read_to_string( entry.path().join( "capacity" ) ).ok()?;
		return cap.trim().parse().ok();
	}
	None
}

pub fn battery_charging() -> bool
{
	let base = "/sys/class/power_supply";
	for entry in fs::read_dir( base ).ok().into_iter().flatten().flatten()
	{
		let typ = fs::read_to_string( entry.path().join( "type" ) ).unwrap_or_default();
		if typ.trim() != "Battery" { continue; }
		let status = fs::read_to_string( entry.path().join( "status" ) ).unwrap_or_default();
		return status.trim() == "Charging";
	}
	false
}

pub fn battery_present() -> bool
{
	let base = "/sys/class/power_supply";
	if let Ok( entries ) = fs::read_dir( base )
	{
		for entry in entries.flatten()
		{
			let typ = fs::read_to_string( entry.path().join( "type" ) ).unwrap_or_default();
			if typ.trim() == "Battery" { return true; }
		}
	}
	false
}

/// Read either `charge_<field>` (mAh-class batteries) or `energy_<field>`
/// (mWh-class batteries) — drivers expose only one of the pair, but the
/// caller usually doesn't care which because the ratio is the same.
fn read_battery_metric( field: &str ) -> Option<u64>
{
	let base = "/sys/class/power_supply";
	for entry in fs::read_dir( base ).ok()?.flatten()
	{
		let typ = fs::read_to_string( entry.path().join( "type" ) ).unwrap_or_default();
		if typ.trim() != "Battery" { continue; }
		for name in [ format!( "charge_{field}" ), format!( "energy_{field}" ) ]
		{
			if let Ok( s ) = fs::read_to_string( entry.path().join( &name ) )
			{
				if let Ok( n ) = s.trim().parse::<u64>() { return Some( n ); }
			}
		}
	}
	None
}

pub fn battery_full_design() -> Option<u64> { read_battery_metric( "full_design" ) }
pub fn battery_full()        -> Option<u64> { read_battery_metric( "full" ) }

pub fn battery_cycle_count() -> Option<u32>
{
	let base = "/sys/class/power_supply";
	for entry in fs::read_dir( base ).ok()?.flatten()
	{
		let typ = fs::read_to_string( entry.path().join( "type" ) ).unwrap_or_default();
		if typ.trim() != "Battery" { continue; }
		if let Ok( s ) = fs::read_to_string( entry.path().join( "cycle_count" ) )
		{
			if let Ok( n ) = s.trim().parse::<u32>() { return Some( n ); }
		}
	}
	None
}

/// Seconds until empty (discharging) or until full (charging). `None`
/// when the battery is idle or the driver doesn't expose the value.
pub fn battery_time_remaining_secs() -> Option<i64>
{
	let dev = Command::new( "upower" ).arg( "-e" ).output().ok()?;
	let devs = String::from_utf8( dev.stdout ).ok()?;
	for path in devs.lines()
	{
		if !path.contains( "battery" ) { continue; }
		let info = Command::new( "upower" ).args( ["-i", path] ).output().ok()?;
		let text = String::from_utf8( info.stdout ).ok()?;
		// `time to empty: 2.3 hours` or `time to full: 35.4 minutes`
		for line in text.lines()
		{
			let line = line.trim();
			let ( sign, rest ) =
				if      let Some( r ) = line.strip_prefix( "time to empty:" ) { (  1, r ) }
				else if let Some( r ) = line.strip_prefix( "time to full:"  ) { ( -1, r ) }
				else { continue };
			let parts: Vec<&str> = rest.trim().splitn( 2, ' ' ).collect();
			if parts.len() < 2 { continue; }
			let n: f64 = parts[0].parse().ok()?;
			let secs = match parts[1].trim_end_matches( 's' )
			{
				"second" | "seconds" => n,
				"minute" | "minutes" => n * 60.0,
				"hour"   | "hours"   => n * 3600.0,
				"day"    | "days"    => n * 86400.0,
				_ => continue,
			};
			return Some( ( secs as i64 ) * sign );
		}
	}
	None
}

pub fn ac_online() -> bool
{
	let base = "/sys/class/power_supply";
	if let Ok( entries ) = fs::read_dir( base )
	{
		for entry in entries.flatten()
		{
			let typ = fs::read_to_string( entry.path().join( "type" ) ).unwrap_or_default();
			if typ.trim() != "Mains" { continue; }
			if let Ok( s ) = fs::read_to_string( entry.path().join( "online" ) )
			{
				return s.trim() == "1";
			}
		}
	}
	false
}

// ─── Power profile (power-profiles-daemon) ──────────────────────────

pub fn power_profile() -> String
{
	Command::new( "powerprofilesctl" ).arg( "get" ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| s.trim().to_string() )
		.unwrap_or_else( || "balanced".into() )
}

pub fn set_power_profile( p: &str )
{
	let _ = Command::new( "powerprofilesctl" ).args( ["set", p] ).output();
}

// ─── Night Light (gsettings; falls back to wlsunset toggle) ─────────

const NL_SCHEMA: &str = "org.gnome.settings-daemon.plugins.color";

pub fn night_light_enabled() -> bool
{
	gsettings_bool( NL_SCHEMA, "night-light-enabled" ).unwrap_or( false )
}

pub fn set_night_light_enabled( on: bool )
{
	let _ = Command::new( "gsettings" )
		.args( [ "set", NL_SCHEMA, "night-light-enabled", if on { "true" } else { "false" } ] )
		.output();
}

pub fn night_light_temp() -> u32
{
	gsettings_uint( NL_SCHEMA, "night-light-temperature" ).unwrap_or( 4700 )
}

pub fn set_night_light_temp( t: u32 )
{
	let _ = Command::new( "gsettings" )
		.args( [ "set", NL_SCHEMA, "night-light-temperature", &t.to_string() ] )
		.output();
}

// ─── UI scale (gsettings text-scaling-factor) ───────────────────────

pub fn ui_scale() -> f32
{
	Command::new( "gsettings" )
		.args( [ "get", "org.gnome.desktop.interface", "text-scaling-factor" ] )
		.output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.and_then( |s| s.trim().parse().ok() )
		.unwrap_or( 1.0 )
}

pub fn set_ui_scale( s: f32 )
{
	let _ = Command::new( "gsettings" )
		.args( [ "set", "org.gnome.desktop.interface", "text-scaling-factor", &format!( "{s:.2}" ) ] )
		.output();
}

// ─── Sleep timeout (AC, seconds; 0 = never) ─────────────────────────

const PWR_SCHEMA: &str = "org.gnome.settings-daemon.plugins.power";

pub fn sleep_timeout_secs() -> u32
{
	gsettings_uint( PWR_SCHEMA, "sleep-inactive-ac-timeout" ).unwrap_or( 0 )
}

pub fn set_sleep_timeout_secs( t: u32 )
{
	let _ = Command::new( "gsettings" )
		.args( [ "set", PWR_SCHEMA, "sleep-inactive-ac-timeout", &t.to_string() ] )
		.output();
}

// ─── Rotation (forge IPC) ───────────────────────────────────────────

#[ derive( Clone, Copy, PartialEq ) ]
pub enum Rotation { Portrait, Landscape, InversePortrait, InverseLandscape }

impl Rotation
{
	fn as_arg( self ) -> &'static str
	{
		match self
		{
			Self::Portrait         => "portrait",
			Self::Landscape        => "landscape",
			Self::InversePortrait  => "inverse-portrait",
			Self::InverseLandscape => "inverse-landscape",
		}
	}
}

pub fn lock_rotation( r: Rotation )
{
	let _ = Command::new( "forge" )
		.args( [ "msg", "orientation", "--lock", r.as_arg() ] )
		.output();
}

pub fn unlock_rotation()
{
	let _ = Command::new( "forge" )
		.args( [ "msg", "orientation", "--unlock" ] )
		.output();
}

// ─── gsettings helpers ──────────────────────────────────────────────

fn gsettings_bool( schema: &str, key: &str ) -> Option<bool>
{
	let out = Command::new( "gsettings" ).args( ["get", schema, key] ).output().ok()?;
	let s = String::from_utf8( out.stdout ).ok()?;
	Some( s.trim() == "true" )
}

fn gsettings_uint( schema: &str, key: &str ) -> Option<u32>
{
	let out = Command::new( "gsettings" ).args( ["get", schema, key] ).output().ok()?;
	let s = String::from_utf8( out.stdout ).ok()?;
	// `gsettings get` returns "uint32 4700" or "uint32 N". Trim the type prefix.
	let s = s.trim();
	let n = s.strip_prefix( "uint32 " ).unwrap_or( s );
	n.trim().parse().ok()
}

pub fn hostname() -> String
{
	fs::read_to_string( "/etc/hostname" )
		.map( |s| s.trim().to_string() )
		.unwrap_or_else( |_| "eydos".into() )
}

pub fn kernel_version() -> String
{
	fs::read_to_string( "/proc/sys/kernel/osrelease" )
		.map( |s| s.trim().to_string() )
		.unwrap_or_default()
}

pub fn cpu_info() -> ( String, usize )
{
	let content = fs::read_to_string( "/proc/cpuinfo" ).ok().unwrap_or_default();
	let model = content.lines().find_map( |l|
		l.strip_prefix( "model name" )
			.map( |r| r.trim_start_matches( ": " ).trim().to_string() )
	).unwrap_or_else( || "Unknown CPU".into() );
	let cores = content.lines().filter( |l| l.starts_with( "processor" ) ).count().max( 1 );
	( model, cores )
}

#[allow(dead_code)] pub fn cpu_model() -> String { cpu_info().0 }
#[allow(dead_code)] pub fn cpu_cores() -> usize  { cpu_info().1 }

pub fn memory_info() -> ( u64, u64 )
{
	let content = fs::read_to_string( "/proc/meminfo" ).ok().unwrap_or_default();
	let total = meminfo_field( &content, "MemTotal:" ).unwrap_or( 0 );
	let avail = meminfo_field( &content, "MemAvailable:" ).unwrap_or( 0 );
	( total, total.saturating_sub( avail ) )
}

#[allow(dead_code)] pub fn total_memory() -> u64 { memory_info().0 }
pub fn used_memory()  -> u64 { memory_info().1 }

fn meminfo_field( s: &str, prefix: &str ) -> Option<u64>
{
	s.lines().find_map( |l|
		l.strip_prefix( prefix )?
			.trim().split_whitespace().next()?
			.parse::<u64>().ok()
			.map( |kb| kb * 1024 )
	)
}

pub fn uptime_secs() -> u64
{
	fs::read_to_string( "/proc/uptime" ).ok()
		.and_then( |s| s.split_whitespace().next()?.parse::<f64>().ok() )
		.map( |f| f as u64 ).unwrap_or( 0 )
}

pub fn primary_ip() -> Option<String>
{
	let out = Command::new( "ip" ).args( ["-4", "route", "get", "1.1.1.1"] ).output().ok()?;
	let s   = String::from_utf8( out.stdout ).ok()?;
	let mut words = s.split_whitespace();
	while let Some( w ) = words.next()
	{
		if w == "src" { return words.next().map( |s| s.to_string() ); }
	}
	None
}

pub fn username() -> String
{
	std::env::var( "USER" ).unwrap_or_else( |_| "user".into() )
}

// ─── Notifications / lock screen / location / DND ───────────────────

const NOTIF_SCHEMA: &str = "org.gnome.desktop.notifications";

pub fn lockscreen_show_notifications() -> bool
{
	gsettings_bool( NOTIF_SCHEMA, "show-in-lock-screen" ).unwrap_or( true )
}

pub fn set_lockscreen_show_notifications( on: bool )
{
	let _ = Command::new( "gsettings" )
		.args( [ "set", NOTIF_SCHEMA, "show-in-lock-screen", if on { "true" } else { "false" } ] )
		.output();
}

pub fn dnd_enabled() -> bool
{
	!gsettings_bool( NOTIF_SCHEMA, "show-banners" ).unwrap_or( true )
}

pub fn set_dnd_enabled( on: bool )
{
	let _ = Command::new( "gsettings" )
		.args( [ "set", NOTIF_SCHEMA, "show-banners", if on { "false" } else { "true" } ] )
		.output();
}

const LOCATION_SCHEMA: &str = "org.gnome.system.location";

pub fn location_enabled() -> bool
{
	gsettings_bool( LOCATION_SCHEMA, "enabled" ).unwrap_or( false )
}

pub fn set_location_enabled( on: bool )
{
	let _ = Command::new( "gsettings" )
		.args( [ "set", LOCATION_SCHEMA, "enabled", if on { "true" } else { "false" } ] )
		.output();
}

// ─── Installed applications (.desktop scan, shared with the launcher) ─

#[ derive( Clone ) ]
pub struct AppEntry
{
	pub name: String,
	pub exec: String,
}

pub fn installed_apps() -> Vec<AppEntry>
{
	let mut apps: Vec<AppEntry> = Vec::new();
	let home = std::env::var( "HOME" ).unwrap_or_default();
	let dirs = [
		std::path::PathBuf::from( "/usr/share/applications" ),
		std::path::PathBuf::from( "/usr/local/share/applications" ),
		std::path::PathBuf::from( format!( "{home}/.local/share/applications" ) ),
	];
	for dir in dirs
	{
		let Ok( entries ) = fs::read_dir( &dir ) else { continue };
		for e in entries.flatten()
		{
			let path = e.path();
			if path.extension().map( |x| x == "desktop" ).unwrap_or( false )
			{
				if let Some( a ) = parse_desktop( &path ) { apps.push( a ); }
			}
		}
	}
	apps.sort_by( |a, b| a.name.to_lowercase().cmp( &b.name.to_lowercase() ) );
	apps.dedup_by( |a, b| a.name == b.name );
	apps
}

fn parse_desktop( path: &std::path::Path ) -> Option<AppEntry>
{
	let content = fs::read_to_string( path ).ok()?;
	let mut name: Option<String> = None;
	let mut exec: Option<String> = None;
	let mut no_display = false;
	let mut in_section = false;
	for line in content.lines()
	{
		let l = line.trim();
		if l.starts_with( '[' ) { in_section = l == "[Desktop Entry]"; continue; }
		if !in_section { continue; }
		if let Some( ( k, v ) ) = l.split_once( '=' )
		{
			match k.trim()
			{
				"Name"      => if name.is_none() { name = Some( v.trim().to_string() ); }
				"Exec"      => if exec.is_none() {
					exec = Some( v.split_whitespace().filter( |s| !s.starts_with( '%' ) )
						.collect::<Vec<_>>().join( " " ) );
				}
				"NoDisplay" => no_display = v.trim().eq_ignore_ascii_case( "true" ),
				_ => {}
			}
		}
	}
	if no_display { return None; }
	Some( AppEntry { name: name?, exec: exec? } )
}

pub fn launch_app( exec: &str )
{
	let parts: Vec<&str> = exec.split_whitespace().collect();
	if parts.is_empty() { return; }
	let _ = Command::new( parts[0] ).args( &parts[1..] ).spawn();
}

// ─── Updater / firewall / hotspot stub helpers ──────────────────────

pub fn updates_available() -> Option<u32>
{
	// dnf path (Fedora / Bazzite). `dnf check-update` exits 100 when
	// updates are available, 0 when none, anything else on error.
	if let Ok( out ) = Command::new( "dnf" ).arg( "check-update" ).output()
	{
		let code = out.status.code().unwrap_or( -1 );
		if code == 0 { return Some( 0 ); }
		if code == 100
		{
			let text = String::from_utf8_lossy( &out.stdout );
			let n = text.lines().filter( |l| !l.is_empty() && !l.starts_with( "Last metadata" ) && l.chars().next().map( |c| !c.is_whitespace() ).unwrap_or( false ) ).count();
			return Some( n as u32 );
		}
	}
	if let Ok( out ) = Command::new( "apt" ).args( ["list", "--upgradable"] ).output()
	{
		let text = String::from_utf8_lossy( &out.stdout );
		let n = text.lines().filter( |l| l.contains( "/" ) ).count();
		return Some( n as u32 );
	}
	None
}

pub fn firewall_active() -> bool
{
	Command::new( "firewall-cmd" ).arg( "--state" ).output().ok()
		.map( |o| o.status.success() ).unwrap_or( false ) ||
	Command::new( "ufw" ).arg( "status" ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| s.contains( "Status: active" ) ).unwrap_or( false )
}

pub fn selinux_state() -> Option<String>
{
	Command::new( "getenforce" ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| s.trim().to_string() )
}

pub fn hotspot_active() -> bool
{
	Command::new( "nmcli" ).args( ["-t", "-f", "NAME,TYPE", "connection", "show", "--active"] ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| s.lines().any( |l| l.starts_with( "Hotspot" ) ) )
		.unwrap_or( false )
}

pub fn airplane_mode() -> bool
{
	Command::new( "rfkill" ).args( ["list", "all"] ).output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s| s.lines().filter( |l| l.contains( "Soft blocked: yes" ) ).count() >= 2 )
		.unwrap_or( false )
}

pub fn set_airplane_mode( on: bool )
{
	let cmd = if on { "block" } else { "unblock" };
	let _ = Command::new( "rfkill" ).args( [ cmd, "all" ] ).output();
}

pub fn os_version() -> String
{
	fs::read_to_string( "/etc/os-release" ).ok()
		.and_then( |content|
		{
			for line in content.lines()
			{
				if let Some( rest ) = line.strip_prefix( "PRETTY_NAME=" )
				{
					return Some( rest.trim_matches( '"' ).to_string() );
				}
			}
			None
		})
		.unwrap_or_else( || "Eydos OS".into() )
}

// ─── Cellular / SIM (ModemManager) ──────────────────────────────────

fn mmcli_keyvalue( modem: &str ) -> Option<String>
{
	Command::new( "mmcli" )
		.args( ["-m", modem, "--output-keyvalue"] )
		.output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
}

/// Single mmcli call returning `(imei, operator_name)`.
pub fn sim_info() -> ( Option<String>, Option<String> )
{
	let text = mmcli_keyvalue( "0" ).unwrap_or_default();
	let imei = text.lines().find_map( |l|
		l.strip_prefix( "modem.3gpp.imei" )
			.map( |r| r.trim_start_matches( " : " ).trim().to_string() )
	);
	let operator = text.lines().find_map( |l|
		l.strip_prefix( "modem.3gpp.operator-name" )
			.map( |r| r.trim_start_matches( " : " ).trim().to_string() )
	);
	( imei, operator )
}

// ─── Hotspot toggle ─────────────────────────────────────────────────

pub fn set_hotspot_enabled( on: bool )
{
	// Try to bring up / down the first hotspot-type connection.
	let action = if on { "up" } else { "down" };
	let out = Command::new( "nmcli" )
		.args( ["-t", "-f", "TYPE,NAME", "con", "show"] )
		.output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.unwrap_or_default();
	for line in out.lines()
	{
		if line.to_lowercase().contains( "802-11-wireless" ) || line.contains( "hotspot" )
		{
			if let Some( name ) = line.splitn( 2, ':' ).nth( 1 )
			{
				let _ = Command::new( "nmcli" ).args( ["con", action, name.trim()] ).output();
				return;
			}
		}
	}
}

// ─── VPN ────────────────────────────────────────────────────────────

pub fn vpn_connections() -> Vec<String>
{
	Command::new( "nmcli" )
		.args( ["-t", "-f", "TYPE,NAME", "con", "show"] )
		.output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |text|
			text.lines()
				.filter_map( |l|
				{
					let mut parts = l.splitn( 2, ':' );
					let typ  = parts.next()?;
					let name = parts.next()?;
					if typ.contains( "vpn" ) { Some( name.trim().to_string() ) } else { None }
				})
				.collect()
		)
		.unwrap_or_default()
}

// ─── Fingerprint daemon ──────────────────────────────────────────────

pub fn fprintd_active() -> bool
{
	Command::new( "systemctl" )
		.args( ["is-active", "--quiet", "fprintd"] )
		.status()
		.map( |s| s.success() )
		.unwrap_or( false )
}

pub fn fprintd_enrolled() -> bool
{
	let user = username();
	Command::new( "fprintd-list" )
		.arg( &user )
		.output().ok()
		.and_then( |o| String::from_utf8( o.stdout ).ok() )
		.map( |s|
		{
			let s = s.trim();
			!s.is_empty() && !s.contains( "no fingerprints" )
		})
		.unwrap_or( false )
}

// ─── NFC ─────────────────────────────────────────────────────────────

pub fn nfc_available() -> bool
{
	fs::read_dir( "/sys/class/nfc" )
		.map( |mut d| d.next().is_some() )
		.unwrap_or( false )
}

// ─── USB mode ────────────────────────────────────────────────────────

pub fn usb_mode() -> String
{
	// Try configfs gadget first (Android-style devices).
	if let Ok( entries ) = fs::read_dir( "/sys/kernel/config/usb_gadget" )
	{
		for entry in entries.flatten()
		{
			let cfg_path = entry.path()
				.join( "configs/c.1/strings/0x409/configuration" );
			if let Ok( s ) = fs::read_to_string( &cfg_path )
			{
				let s = s.trim().to_string();
				if !s.is_empty() { return s; }
			}
		}
	}
	// Fall back to UDC state (if UDC exists the port is active).
	if let Ok( entries ) = fs::read_dir( "/sys/class/udc" )
	{
		if entries.flatten().next().is_some()
		{
			return "USB active".to_string();
		}
	}
	"Charging only".to_string()
}

// ─── Connected Wi-Fi SSID ────────────────────────────────────────────

pub fn connected_wifi_ssid() -> Option<String>
{
	let out = Command::new( "nmcli" )
		.args( ["-t", "-f", "ACTIVE,SSID", "dev", "wifi", "list"] )
		.output().ok()?;
	let text = String::from_utf8( out.stdout ).ok()?;
	text.lines().find_map( |l|
		l.strip_prefix( "yes:" ).map( |s| s.trim().to_string() )
	)
}

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

	#[test]
	fn parses_unmuted_volume()
	{
		let ( vol, muted ) = parse_wpctl_volume( "Volume: 0.65" );
		assert!( ( vol - 0.65 ).abs() < 1e-6 );
		assert!( !muted );
	}

	#[test]
	fn parses_muted_volume()
	{
		let ( vol, muted ) = parse_wpctl_volume( "Volume: 0.00 [MUTED]" );
		assert!( ( vol - 0.0 ).abs() < 1e-6 );
		assert!( muted );
	}

	#[test]
	fn falls_back_when_token_missing()
	{
		// No numeric token after the label: fall back to 0.5, unmuted.
		let ( vol, muted ) = parse_wpctl_volume( "Volume:" );
		assert!( ( vol - 0.5 ).abs() < 1e-6 );
		assert!( !muted );
	}

	#[test]
	fn detects_mute_marker_regardless_of_position()
	{
		let ( _, muted ) = parse_wpctl_volume( "Volume: 0.42 [MUTED] (extra)" );
		assert!( muted );
	}
}
