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

use super::cmd_stdout;

// ─── 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
{
	cmd_stdout( "gsettings", &[ "get", "org.gnome.desktop.interface", "text-scaling-factor" ] )
		.trim().parse().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();
}

// ─── Interface scale (forge's output scale) ─────────────────────────

/// Send one JSON line to forge's IPC socket and read its reply.
pub( super ) fn forge_request( payload: &str ) -> Option<String>
{
	use std::io::{ BufRead, BufReader, Write };
	use std::os::unix::net::UnixStream;

	let runtime = std::env::var( "XDG_RUNTIME_DIR" ).ok()?;
	let display = std::env::var( "WAYLAND_DISPLAY" ).ok()?;
	let mut stream = UnixStream::connect( format!( "{runtime}/forge-{display}.sock" ) ).ok()?;
	stream.write_all( payload.as_bytes() ).ok()?;
	stream.write_all( b"\n" ).ok()?;

	let mut line = String::new();
	BufReader::new( &stream ).read_line( &mut line ).ok()?;
	Some( line )
}

/// Detents the interface-scale slider stops on.
pub const INTERFACE_STEPS: [ f32; 7 ] = [ 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0 ];

/// Index of the detent closest to `scale`.
pub fn interface_step_index( scale: f32 ) -> usize
{
	let mut best = 0;
	for ( i, s ) in INTERFACE_STEPS.iter().enumerate()
	{
		if ( s - scale ).abs() < ( INTERFACE_STEPS[ best ] - scale ).abs()
		{
			best = i;
		}
	}
	best
}

/// Detent value for a slider position in `[0.0, 1.0]`.
pub fn interface_step_at( t: f32 ) -> f32
{
	let last = INTERFACE_STEPS.len() - 1;
	let idx  = ( t.clamp( 0.0, 1.0 ) * last as f32 ).round() as usize;
	INTERFACE_STEPS[ idx.min( last ) ]
}

/// `( pinned, automatic )` — the scale the user picked (`0.0` when forge is
/// deciding on its own) and the one the automatic path would choose right
/// now, which forge reports whether or not it is the one in force.
pub fn interface_scale() -> ( f32, f32 )
{
	let reply = forge_request( r#"{"action":"get_output_scale"}"# ).unwrap_or_default();
	// Take the numeric run that follows the key: the value is followed by a
	// `,` or the closing brace, and trimming would not stop at either.
	let field = | name: &str | -> Option<f32>
	{
		let rest = reply.split( &format!( "\"{name}\":" ) ).nth( 1 )?;
		let end  = rest
			.find( |c: char| !c.is_ascii_digit() && c != '.' )
			.unwrap_or( rest.len() );
		rest[ ..end ].parse().ok()
	};
	( field( "scale" ).unwrap_or( 0.0 ), field( "automatic" ).unwrap_or( 1.0 ) )
}

/// Pin the output scale. `0.0` clears the pick and returns to automatic.
pub fn set_interface_scale( s: f32 )
{
	let _ = forge_request( &format!( r#"{{"action":"set_output_scale","scale":{s:.2}}}"# ) );
}

// ─── 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 s = cmd_stdout( "gsettings", &["get", schema, key] );
	// `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()
}

// ─── 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();
}

// ─── Airplane mode (rfkill) ─────────────────────────────────────────

pub fn airplane_mode() -> bool
{
	cmd_stdout( "rfkill", &["list", "all"] ).lines()
		.filter( |l| l.contains( "Soft blocked: yes" ) ).count() >= 2
}

pub fn set_airplane_mode( on: bool )
{
	let cmd = if on { "block" } else { "unblock" };
	let _ = Command::new( "rfkill" ).args( [ cmd, "all" ] ).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();
}
