use std::io::{ BufRead, BufReader, Write };
use std::os::unix::net::{ UnixListener, UnixStream };
use std::thread;

use ltk::ChannelSender;

use crate::message::Message;

/// Bidirectional IPC: sends commands to forge, receives commands from external callers.
pub struct ForgeIpc
{
	forge_socket: String,
}

impl ForgeIpc
{
	pub fn new() -> Option<Self>
	{
		let runtime_dir = std::env::var( "XDG_RUNTIME_DIR" ).ok()?;
		let display = std::env::var( "WAYLAND_DISPLAY" ).ok()?;
		let forge_socket = format!( "{}/forge-{}.sock", runtime_dir, display );
		Some( Self { forge_socket } )
	}

	/// Start listening for incoming IPC commands on crustace's socket.
	/// Messages are sent directly into ltk's event loop via the channel sender.
	pub fn start_listener( sender: ChannelSender<Message> )
	{
		let runtime_dir = match std::env::var( "XDG_RUNTIME_DIR" )
		{
			Ok( d ) => d,
			Err( _ ) => return,
		};
		let display = match std::env::var( "WAYLAND_DISPLAY" )
		{
			Ok( d ) => d,
			Err( _ ) => return,
		};

		let listen_path = format!( "{}/crustace-{}.sock", runtime_dir, display );
		if std::path::Path::new( &listen_path ).exists()
		{
			let _ = std::fs::remove_file( &listen_path );
		}

		let listener = match UnixListener::bind( &listen_path )
		{
			Ok( l ) => l,
			Err( e ) =>
			{
				tracing::error!( "Failed to bind IPC socket {}: {}", listen_path, e );
				return;
			}
		};

		if let Err( e ) = thread::Builder::new()
			.name( "crustace-ipc".into() )
			.spawn( move || listen_loop( listener, sender ) )
		{
			tracing::error!( "Failed to spawn IPC listener thread: {}", e );
		}
	}

	/// Bring the first window matching `app_id` to the front. Used by
	/// the dock when the user clicks an icon whose app is already
	/// running so we focus the existing window instead of spawning a
	/// second instance.
	pub fn activate_app_id( &self, app_id: &str )
	{
		let escaped = app_id.replace( '\\', "\\\\" ).replace( '"', "\\\"" );
		let msg     = format!( r#"{{"action":"activate_app_id","app_id":"{}"}}"#, escaped );
		self.send( &msg );
	}

	pub fn set_dock_zone( &self, height_px: u32 )
	{
		let msg = format!( r#"{{"action":"set_dock_zone","height":{}}}"#, height_px );
		self.send( &msg );
	}

	/// Tell forge which palette variant it should render its own chrome
	/// (topbar clock/battery/icons + SSD title bars) against. Crustace is the
	/// authoritative source for the user's theme preference — it resolves the
	/// `--dark`/`--light` CLI flags, `config.shell.theme` and the Auto
	/// sunrise/sunset policy into a single `ThemeMode` that every process in
	/// the shell must agree on. Without this call forge would fall back to
	/// gsettings' `color-scheme`, which the user may never have set and which
	/// ignores the CLI flags entirely.
	pub fn set_theme( &self, mode: ltk::ThemeMode )
	{
		let label = match mode
		{
			ltk::ThemeMode::Dark  => "dark",
			ltk::ThemeMode::Light => "light",
		};
		let msg = format!( r#"{{"action":"set_theme","mode":"{}"}}"#, label );
		self.send( &msg );
	}

	pub fn close_app_id( &self, app_id: &str )
	{
		let escaped = app_id.replace( '\\', "\\\\" ).replace( '"', "\\\"" );
		let msg = format!( r#"{{"action":"close_app_id","app_id":"{}"}}"#, escaped );
		self.send( &msg );
	}

	fn send( &self, msg: &str )
	{
		if let Ok( mut stream ) = UnixStream::connect( &self.forge_socket )
		{
			if let Err( e ) = stream.write_all( msg.as_bytes() )
			{
				tracing::error!( "Failed to send IPC message to forge: {}", e );
			}
		}
	}
}

fn listen_loop( listener: UnixListener, sender: ChannelSender<Message> )
{
	for stream in listener.incoming()
	{
		match stream
		{
			Ok( stream ) => handle_client( stream, &sender ),
			Err( _ ) => break,
		}
	}
}

fn handle_client( stream: UnixStream, sender: &ChannelSender<Message> )
{
	let mut reader = BufReader::new( &stream );
	let mut line = String::new();
	if reader.read_line( &mut line ).is_err()
	{
		return;
	}

	let val: serde_json::Value = match serde_json::from_str( &line )
	{
		Ok( v ) => v,
		Err( _ ) => return,
	};

	let action = match val.get( "action" ).and_then( |a| a.as_str() )
	{
		Some( a ) => a,
		None => return,
	};

	let msg = match action
	{
		"toggle_launcher"                     => Message::ToggleLauncher,
		"launcher_search_open"                => Message::LauncherSearchOpen(
			val.get( "query" )
				.and_then( |q| q.as_str() )
				.unwrap_or_default()
				.to_string(),
		),
		"show_homescreen"                     => Message::ShowHomescreen,
		// Forge entering its own Overview/app-switcher: close any open
		// crustace overlays so they don't sit on top of the thumbnails.
		"hide_homescreen"                     => Message::ShowHomescreen,
		"toggle_overview"                     => Message::ToggleOverview,
		"start_launcher_wipe"                 => Message::StartLauncherWipe,
		"launcher_wiping"                     => Message::LauncherWiping(
			val.get( "progress" )
				.and_then( |p| p.as_f64() )
				.unwrap_or( 0.0 ) as f32,
		),
		"launcher_wipe_done"                  => Message::FinishLauncherWipe {
			committed: val.get( "committed" )
				.and_then( |c| c.as_bool() )
				.unwrap_or( false ),
			progress: val.get( "progress" )
				.and_then( |p| p.as_f64() )
				.unwrap_or( 0.0 ) as f32,
		},
		"open_quick_settings"                 => Message::OpenQuickSettings,
		"start_wipe_down"                     => Message::StartQuickSettingsWipe,
		"wiping"                              => Message::QuickSettingsWiping(
			val.get( "progress" )
				.and_then( |p| p.as_f64() )
				.unwrap_or( 0.0 ) as f32,
		),
		"wipe_done"                           => Message::FinishQuickSettingsWipe {
			committed: val.get( "committed" )
				.and_then( |c| c.as_bool() )
				.unwrap_or( false ),
			progress: val.get( "progress" )
				.and_then( |p| p.as_f64() )
				.unwrap_or( 0.0 ) as f32,
		},
		"toggle_calendar"                     => Message::ToggleCalendar,
		"reveal_dock"                         => Message::RevealDock,
		"hide_dock"                           => Message::HideDock,
		"app_activated"                       => Message::AppActivated {
			token: val.get( "token" )
				.and_then( |t| t.as_str() )
				.unwrap_or_default()
				.to_string(),
			app_id: val.get( "app_id" )
				.and_then( |a| a.as_str() )
				.unwrap_or_default()
				.to_string(),
		},
		_ => return,
	};

	if let Err( e ) = sender.send( msg )
	{
		tracing::error!( "Failed to dispatch IPC action '{}': {}", action, e );
	}
}
