use std::collections::HashMap;
use std::path::{ Path, PathBuf };
use std::process::Command;
use std::sync::OnceLock;

/// Global icon index built lazily on first call to `find_icon`.
/// Maps icon stem (e.g. "kcalc") to the best resolved path found on disk.
static ICON_INDEX: OnceLock<HashMap<String, PathBuf>> = OnceLock::new();

#[derive( Debug, Clone )]
pub struct AppEntry
{
	/// Stable identifier: the `.desktop` filename **with** its extension
	/// (e.g. `firefox.desktop`, `org.gnome.Console.desktop`). Callers pin
	/// apps to the dock / homescreen grid by this id so renames of the
	/// displayed `Name` don't break existing layouts. Future APK entries
	/// coming through the Android translation layer will use `.apk`-rooted
	/// ids on the same field — the extension disambiguates app-type
	/// dispatch for the launcher backend.
	pub app_id: String,
	pub name: String,
	pub exec: String,
	pub icon: Option<PathBuf>,
	pub description: Option<String>,
	pub terminal: bool,
	/// `StartupWMClass=` value from the `.desktop` entry. The Wayland
	/// `app_id` of legacy apps (`firefox`, `code`, `chromium`) does not
	/// match the `.desktop` basename, so the running-app matcher needs
	/// this hint to associate a live toplevel with its dock entry.
	/// `None` when the key is absent — the matcher falls back to the
	/// basename of `app_id` (without the `.desktop` extension), which
	/// is what GNOME-style reverse-DNS desktop files already line up
	/// with on the Wayland side.
	pub startup_wm_class: Option<String>,
	pub is_terminal_emulator: bool,
}

/// Scan for .desktop files and parse them
pub fn scan_applications() -> Vec<AppEntry> {
    let mut apps = Vec::new();

    let app_dirs = [
        PathBuf::from("/usr/share/applications"),
        PathBuf::from("/usr/local/share/applications"),
        dirs::data_dir()
            .map(|d| d.join("applications"))
            .unwrap_or_else(|| PathBuf::from("~/.local/share/applications")),
        PathBuf::from("/var/lib/flatpak/exports/share/applications"),
        dirs::home_dir()
            .map(|d| d.join(".local/share/flatpak/exports/share/applications"))
            .unwrap_or_default(),
    ];

    for dir in app_dirs {
        if !dir.exists() {
            continue;
        }

        if let Ok(entries) = std::fs::read_dir(&dir) {
            for entry in entries.flatten() {
                let path = entry.path();
                if path.extension().map(|e| e == "desktop").unwrap_or(false) {
                    if let Some(app) = parse_desktop_file(&path) {
                        apps.push(app);
                    }
                }
            }
        }
    }

    apps.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
    apps.dedup_by(|a, b| a.app_id == b.app_id);
    apps
}

fn parse_desktop_file(path: &PathBuf) -> Option<AppEntry> {
    let content = std::fs::read_to_string(path).ok()?;
    let app_id = path.file_name().and_then(|n| n.to_str())?.to_string();

    let mut name = None;
    let mut exec = None;
    let mut icon: Option<PathBuf> = None;
    let mut description = None;
    let mut terminal = false;
    let mut startup_wm_class: Option<String> = None;
    let mut is_terminal_emulator = false;
    let mut no_display = false;
    let mut hidden = false;
    let mut in_desktop_entry = false;

    for line in content.lines() {
        let line = line.trim();

        if line.starts_with('[') {
            in_desktop_entry = line == "[Desktop Entry]";
            continue;
        }

        if !in_desktop_entry {
            continue;
        }

        if let Some((key, value)) = line.split_once('=') {
            let key = key.trim();
            let value = value.trim();

            match key {
                "Name" => name = Some(value.to_string()),
                "Exec" => {
                    let exec_clean = value
                        .split_whitespace()
                        .filter(|s| !s.starts_with('%'))
                        .collect::<Vec<_>>()
                        .join(" ");
                    exec = Some(exec_clean);
                }
                "Icon" => icon = find_icon(value),
                "Comment" => description = Some(value.to_string()),
                "Terminal" => terminal = value.eq_ignore_ascii_case( "true" ),
                "StartupWMClass" => startup_wm_class = Some( value.to_string() ),
                "Categories" => is_terminal_emulator = value
                    .split( ';' )
                    .any( |c| c.trim().eq_ignore_ascii_case( "TerminalEmulator" ) ),
                "NoDisplay" => no_display = value.eq_ignore_ascii_case( "true" ),
                "Hidden" => hidden = value.eq_ignore_ascii_case( "true" ),
                _ => {}
            }
        }
    }

    if no_display || hidden {
        return None;
    }

    let exec_str = exec.as_deref().unwrap_or( "" );
    if exec_str.starts_with( "waydroid " ) && !is_command_available( "waydroid" )
    {
        return None;
    }
    if exec_str.starts_with( "steam " ) && !is_command_available( "steam" )
    {
        return None;
    }

    Some( AppEntry {
        app_id,
        name: name?,
        exec: exec?,
        icon,
        description,
        terminal,
        startup_wm_class,
        is_terminal_emulator,
    } )
}

pub fn find_icon( icon_name: &str ) -> Option<PathBuf>
{
	if icon_name.starts_with( '/' )
	{
		let path = PathBuf::from( icon_name );
		if path.exists()
		{
			return Some( path );
		}
	}

	// Check the active theme's app icon directory first so Liberux-branded
	// icons take precedence over whatever the system icon theme has installed.
	if let Some( path ) = ltk::theme_app_icon( icon_name )
	{
		return Some( path );
	}

	let index = ICON_INDEX.get_or_init( build_icon_index );
	index.get( icon_name ).cloned()
}

/// Walk all icon directories once and build a stem→best-path index.
/// Themes use different layouts (hicolor: 48x48/apps/foo.svg, breeze: apps/48/foo.svg)
/// so we walk each tree recursively. This runs once on first lookup.
fn build_icon_index() -> HashMap<String, PathBuf>
{
	let mut index: HashMap<String, PathBuf> = HashMap::new();

	let mut theme_bases: Vec<PathBuf> = vec![
		PathBuf::from( "/usr/share/icons" ),
		PathBuf::from( "/usr/local/share/icons" ),
		PathBuf::from( "/var/lib/flatpak/exports/share/icons" ),
		PathBuf::from( "/usr/share/pixmaps" ),
	];
	if let Some( data ) = dirs::data_dir()
	{
		theme_bases.push( data.join( "icons" ) );
	}
	if let Some( home ) = dirs::home_dir()
	{
		theme_bases.push( home.join( ".local/share/flatpak/exports/share/icons" ) );
	}

	for base in &theme_bases
	{
		walk_collect_icons( base, &mut index, 6 );
	}

	index
}

fn walk_collect_icons( dir: &PathBuf, index: &mut HashMap<String, PathBuf>, max_depth: u32 )
{
	if max_depth == 0 || !dir.is_dir()
	{
		return;
	}
	let entries = match std::fs::read_dir( dir )
	{
		Ok( e ) => e,
		Err( _ ) => return,
	};
	for entry in entries.filter_map( |e| e.ok() )
	{
		let path = entry.path();
		// Use metadata() (follows symlinks) instead of file_type() so symlinked
		// icons like /usr/share/pixmaps/torcs.png → ../games/torcs/Ticon.png are
		// recognized as files.
		let meta = match path.metadata()
		{
			Ok( m ) => m,
			Err( _ ) => continue,
		};
		if meta.is_dir()
		{
			walk_collect_icons( &path, index, max_depth - 1 );
		} else if meta.is_file() {
			let ext = match path.extension().and_then( |e| e.to_str() )
			{
				Some( e ) => e,
				None => continue,
			};
			if ext != "svg" && ext != "png" && ext != "xpm"
			{
				continue;
			}
			let stem = match path.file_stem().and_then( |s| s.to_str() )
			{
				Some( s ) => s.to_string(),
				None => continue,
			};

			insert_icon( index, stem.clone(), &path, ext );

			// Legacy pixmap convention: "mini.xterm_48x48.xpm" should also be
			// reachable as "mini.xterm". Strip a trailing "_NNxNN" suffix.
			if let Some( base ) = strip_size_suffix( &stem )
			{
				insert_icon( index, base.to_string(), &path, ext );
			}
		}
	}
}

/// Strip a trailing `_NNxNN` (e.g. "_48x48") from an icon stem if present.
fn strip_size_suffix( stem: &str ) -> Option<&str>
{
	let underscore = stem.rfind( '_' )?;
	let suffix = &stem[underscore + 1..];
	let ( w, h ) = suffix.split_once( 'x' )?;
	if !w.is_empty() && !h.is_empty()
		&& w.chars().all( |c| c.is_ascii_digit() )
		&& h.chars().all( |c| c.is_ascii_digit() )
	{
		Some( &stem[..underscore] )
	} else {
		None
	}
}

fn insert_icon( index: &mut HashMap<String, PathBuf>, stem: String, path: &PathBuf, ext: &str )
{
	use std::collections::hash_map::Entry;
	let new_score = icon_score( path, ext );
	match index.entry( stem )
	{
		Entry::Vacant( v ) => { v.insert( path.clone() ); }
		Entry::Occupied( mut o ) =>
		{
			let existing = o.get().clone();
			let existing_ext = existing.extension().and_then( |e| e.to_str() ).unwrap_or( "" );
			let existing_score = icon_score( &existing, existing_ext );
			if new_score > existing_score
			{
				o.insert( path.clone() );
			}
		}
	}
}

/// Lexicographic ranking on `( format, resolution_hint )` so the index
/// picks the "best" source when several files share a stem.
///
/// Format dominates — an SVG always beats any raster, no matter the
/// raster's resolution, because vectors render crisp at any target
/// size and the renderer's resvg pass produces a 1:1 sample-to-pixel
/// mapping at decode time. Among files of the same format, the higher
/// resolution hint wins: this is what fixes the apparent blurriness
/// of icons that ship many density variants (Signal Desktop's
/// `hicolor/{16..1024}x{N}/apps/signal-desktop.png` chain). Without
/// the resolution tiebreaker `read_dir` order — inode-driven on ext4
/// — could pick any of the nine sizes, including 16 px, which the
/// renderer would then have to upscale ~4× to fit the homescreen
/// grid. With it, the 1024 px (or `scalable/`, when present) source
/// wins and downscales cleanly with Lanczos3.
fn icon_score( path: &Path, ext: &str ) -> ( u32, u32 )
{
	let format_rank = if ext == "svg" { 1 } else { 0 };
	( format_rank, resolution_hint( path ) )
}

/// Parse a pixel-size hint out of an icon-theme path. Recognises:
///
/// * `NxN`      — hicolor / Adwaita / most GNOME themes:
///                `…/hicolor/256x256/apps/foo.png`
/// * `N`        — breeze / KDE flat-number components:
///                `…/breeze/apps/256/foo.svg`
/// * `scalable` — vector trees: ranks at `u32::MAX` so a vector
///                under `/scalable/apps/` outranks any raster
///                size. Belt-and-braces for SVGs that are also
///                already promoted by [`icon_score`]'s format rank.
///
/// Returns `0` when no component matches — `/usr/share/pixmaps/foo.png`
/// and similar flat trees end up here. The caller treats `0` as a
/// fallback: it's still inserted, but any later registration with a
/// real size hint replaces it.
fn resolution_hint( path: &Path ) -> u32
{
	for component in path.components()
	{
		let name = match component.as_os_str().to_str()
		{
			Some( s ) => s,
			None      => continue,
		};
		if name == "scalable"
		{
			return u32::MAX;
		}
		if let Some( ( w, h ) ) = name.split_once( 'x' )
		{
			if let ( Ok( wn ), Ok( hn ) ) = ( w.parse::<u32>(), h.parse::<u32>() )
			{
				return wn.max( hn );
			}
		}
		if let Ok( n ) = name.parse::<u32>()
		{
			return n;
		}
	}
	0
}

pub fn launch_app( exec: &str, terminal: bool, token: Option<&str> )
{
	tracing::info!( "Launching: {}", exec );

	if terminal
	{
		if let Some( term ) = find_terminal_emulator()
		{
			let mut cmd = Command::new( &term );
			cmd.args( ["-e", exec] );
			set_activation_token( &mut cmd, token );
			match cmd.spawn()
			{
				Ok( child ) => tracing::info!( "Started terminal process with PID: {}", child.id() ),
				Err( e ) => tracing::error!( "Failed to launch {} -e {}: {}", term, exec, e ),
			}
		} else {
			tracing::error!( "No terminal emulator found for: {}", exec );
		}
		return;
	}

	let parts: Vec<&str> = exec.split_whitespace().collect();
	if parts.is_empty()
	{
		return;
	}

	let program = parts[0];
	let args: Vec<&str> = parts[1..].to_vec();

	let mut cmd = Command::new( program );
	cmd.args( &args );
	set_activation_token( &mut cmd, token );
	match cmd.spawn()
	{
		Ok( child ) =>
		{
			tracing::info!( "Started process with PID: {}", child.id() );
		}
		Err( e ) =>
		{
			tracing::error!( "Failed to launch {}: {}", exec, e );
		}
	}
}

fn set_activation_token( cmd: &mut Command, token: Option<&str> )
{
	match token
	{
		Some( t ) if !t.is_empty() => { cmd.env( "XDG_ACTIVATION_TOKEN", t ); }
		_                          => { cmd.env_remove( "XDG_ACTIVATION_TOKEN" ); }
	}
}

fn find_terminal_emulator() -> Option<String>
{
	let candidates = ["alacritty", "foot", "konsole", "gnome-terminal", "xterm"];
	for name in candidates
	{
		if is_command_available( name )
		{
			return Some( name.to_string() );
		}
	}
	None
}

fn is_command_available( name: &str ) -> bool
{
	Command::new( "which" ).arg( name ).output()
		.map( |o| o.status.success() ).unwrap_or( false )
}
