use std::process::Command;

use super::cmd_stdout;

pub struct BluetoothDevice
{
	pub addr:      String,
	pub name:      String,
	/// freedesktop icon name derived by BlueZ from the Class of Device
	/// ("input-mouse", "audio-headset", "phone", …); empty when unknown.
	pub icon:      String,
	pub paired:    bool,
	pub connected: bool,
}

pub fn bluetooth_enabled() -> bool
{
	cmd_stdout( "bluetoothctl", &["show"] ).contains( "Powered: yes" )
}

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 list = | filter: Option<&str> | -> String
	{
		match filter
		{
			Some( f ) => cmd_stdout( "bluetoothctl", &["devices", f] ),
			None      => cmd_stdout( "bluetoothctl", &["devices"] ),
		}
	};
	// bluetoothctl also prints asynchronous event lines ("[NEW] Device …",
	// "[CHG] Device … RSSI: …") while attached — frequent during discovery
	// bursts. Only the result lines start with "Device ", so membership is
	// parsed strictly from those; a raw substring search over the output
	// would take any event mentioning a MAC as list membership.
	let addr_set = | text: &str | -> std::collections::HashSet<String>
	{
		text.lines()
			.filter_map( |l| l.strip_prefix( "Device " ) )
			.filter_map( |r| r.split_once( ' ' ).map( |( a, _ )| a.to_string() ) )
			.collect()
	};
	let text      = list( None );
	let paired    = addr_set( &list( Some( "Paired" ) ) );
	let connected = addr_set( &list( Some( "Connected" ) ) );

	let device_icon = | addr: &str | -> String
	{
		cmd_stdout( "bluetoothctl", &["info", addr] ).lines()
			.find_map( |l| l.trim().strip_prefix( "Icon: " ).map( str::to_string ) )
			.unwrap_or_default()
	};

	text.lines().filter_map( |line|
	{
		let rest = line.strip_prefix( "Device " )?;
		let ( addr, name ) = rest.split_once( ' ' )?;
		// Nameless BLE beacons show up with the address (dash-separated)
		// as their name — noise on every scan, skip them like phones do.
		// Paired devices are always kept: hiding one over late name
		// resolution would make a known device undiscoverable.
		if name == addr.replace( ':', "-" ) && !paired.contains( addr ) { return None; }
		Some( BluetoothDevice
		{
			addr:      addr.to_string(),
			name:      name.to_string(),
			icon:      device_icon( addr ),
			paired:    paired.contains( addr ),
			connected: connected.contains( addr ),
		})
	}).collect()
}

/// Run a discovery window: `scan on` fills the adapter's device cache
/// for `secs` seconds (bluetoothctl exits when the timeout lapses), so
/// a `bluetooth_devices()` call afterwards includes what was found.
pub fn bluetooth_discover( secs: u32 )
{
	let _ = Command::new( "bluetoothctl" )
		.args( ["--timeout", &secs.to_string(), "scan", "on"] ).output();
}

#[ derive( Clone, Copy, PartialEq, Eq ) ]
pub enum BtConnectError
{
	PairingFailed,
	ConnectFailed,
}

// An unpaired device is paired and trusted first. Pairing runs inside a
// live bluetoothctl session driven over stdin: one-shot `bluetoothctl
// pair` cannot register its agent ("Failed to register agent object")
// and every just-works handshake dies with AuthenticationFailed, while
// a session registers it automatically. Discovery stays on during the
// attempt because BLE peripherals rotate their random address when
// re-entering pairing mode, and the handshake needs the identity that
// is advertising right now.
pub fn bluetooth_connect( addr: &str, pair_first: bool ) -> Result<(), BtConnectError>
{
	if pair_first
	{
		use std::io::Write;
		use std::process::Stdio;
		let child = Command::new( "bluetoothctl" )
			.stdin( Stdio::piped() )
			.stdout( Stdio::piped() )
			.stderr( Stdio::null() )
			.spawn();
		let Ok( mut child ) = child else { return Err( BtConnectError::ConnectFailed ) };
		if let Some( mut stdin ) = child.stdin.take()
		{
			let _ = stdin.write_all( format!( "scan on\npair {addr}\n" ).as_bytes() );
			let _ = stdin.flush();
			std::thread::sleep( std::time::Duration::from_secs( 12 ) );
			let _ = stdin.write_all( b"scan off\nquit\n" );
		}
		let out = child.wait_with_output().ok()
			.map( |o| String::from_utf8_lossy( &o.stdout ).to_string() )
			.unwrap_or_default();
		if !( out.contains( "Pairing successful" ) || out.contains( "Bonded: yes" ) )
		{
			return Err( BtConnectError::PairingFailed );
		}
		let _ = Command::new( "bluetoothctl" ).args( ["trust", addr] ).output();
	}
	// `connect` works one-shot (no agent involved). It can abort
	// transiently right after a fresh bond (le-connection-abort-by-local)
	// while the peripheral settles, so retry once before giving up.
	for attempt in 0..2
	{
		let connected = Command::new( "bluetoothctl" ).args( ["connect", addr] ).output().ok()
			.map( |o| o.status.success()
				&& String::from_utf8_lossy( &o.stdout ).contains( "successful" ) )
			.unwrap_or( false );
		if connected { return Ok( () ); }
		if attempt == 0 { std::thread::sleep( std::time::Duration::from_secs( 2 ) ); }
	}
	Err( BtConnectError::ConnectFailed )
}

pub fn bluetooth_disconnect( addr: &str )
{
	let _ = Command::new( "bluetoothctl" ).args( ["disconnect", addr] ).output();
}

pub fn bluetooth_forget( addr: &str )
{
	let _ = Command::new( "bluetoothctl" ).args( ["remove", addr] ).output();
}
