#if defined (__linux__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)

#include "XDGBasedAppProvider.hpp"
#include "AppProvider.hpp"
#include "common.hpp"
#include "lng.hpp"
#include "KeyFileHelper.h"
#include "utils.h"
#include "WideMB.h"
#include <algorithm>
#include <cstring>
#include <fstream>
#include <map>
#include <set>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>
#include <dirent.h>
#include <fnmatch.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

namespace openwith
{

	constexpr const char* INI_SECTION_XDG_PROVIDER = "Settings.XDG";

	// ****************************** Public API ******************************


	XDGBasedAppProvider::XDGBasedAppProvider()
	{
		_platform_settings_definitions = {
			{ "UseXdgMimeTool",            MsgID::UseXdgMimeTool,            &XDGBasedAppProvider::_use_xdg_mime_tool,            true,  true },
			{ "UseFileTool",               MsgID::UseFileTool,               &XDGBasedAppProvider::_use_file_tool,                true,  true },
			{ "UseMagikaTool",             MsgID::UseMagikaTool,             &XDGBasedAppProvider::_use_magika_tool,              false, true },
			{ "UseGlobRules",              MsgID::UseGlobRules,              &XDGBasedAppProvider::_use_glob_rules,               false, true },
			{ "UseExtensionBasedFallback", MsgID::UseExtensionBasedFallback, &XDGBasedAppProvider::_use_extension_based_fallback, false, true },
			{ "LoadMimeTypeAliases",       MsgID::LoadMimeTypeAliases,       &XDGBasedAppProvider::_load_mimetype_aliases,        true,  true },
			{ "LoadMimeTypeSubclasses",    MsgID::LoadMimeTypeSubclasses,    &XDGBasedAppProvider::_load_mimetype_subclasses,     true,  true },
			{ "ResolveStructuredSuffixes", MsgID::ResolveStructuredSuffixes, &XDGBasedAppProvider::_resolve_structured_suffixes,  true,  true },
			{ "UseGenericMimeFallbacks",   MsgID::UseGenericMimeFallbacks,   &XDGBasedAppProvider::_use_generic_mime_fallbacks,   true,  true },
			{ "ShowUniversalHandlers",     MsgID::ShowUniversalHandlers,     &XDGBasedAppProvider::_show_universal_handlers,      true,  true },
			{ "QueryXdgMimeDefault",       MsgID::QueryXdgMimeDefault,       &XDGBasedAppProvider::_query_xdg_mime_default,       false,  true },
			{ "IgnoreRemovedAssociations", MsgID::IgnoreRemovedAssociations, &XDGBasedAppProvider::_ignore_removed_associations,  false, true },
			{ "UseMimeinfoCache",          MsgID::UseMimeinfoCache,          &XDGBasedAppProvider::_use_mimeinfo_cache,           true,  true },
			{ "FilterByShowIn",            MsgID::FilterByShowIn,            &XDGBasedAppProvider::_filter_by_show_in,            true,  true },
			{ "ValidateTryExec",           MsgID::ValidateTryExec,           &XDGBasedAppProvider::_validate_try_exec,            true,  true },
			{ "SortAlphabetically",        MsgID::SortAlphabetically,        &XDGBasedAppProvider::_sort_alphabetically,          false, true },
			{ "TreatUrlsAsPaths",          MsgID::TreatUrlsAsPaths,          &XDGBasedAppProvider::_treat_urls_as_paths,          false, false },
			{ "ShowPackageTags",           MsgID::ShowPackageTags,           &XDGBasedAppProvider::_show_package_tags,            true,  true }
		};

		for (const auto& def : _platform_settings_definitions) {
			_key_wide_to_member_map[StrMB2Wide(def.internal_key)] = def.member_variable;
			this->*(def.member_variable) = def.default_value;
		}
	}


	void XDGBasedAppProvider::LoadPlatformSettings(const KeyFileReadHelper &key_reader)
	{
		for (const auto& def : _platform_settings_definitions) {
			this->*(def.member_variable) = key_reader.GetInt(INI_SECTION_XDG_PROVIDER, def.internal_key, def.default_value) != 0;
		}
	}


	void XDGBasedAppProvider::SavePlatformSettings(KeyFileHelper& key_writer)
	{
		for (const auto& def : _platform_settings_definitions) {
			key_writer.SetInt(INI_SECTION_XDG_PROVIDER, def.internal_key, this->*(def.member_variable));
		}
	}


	std::vector<ProviderSetting> XDGBasedAppProvider::GetPlatformSettings()
	{
		std::vector<ProviderSetting> settings;
		settings.reserve(_platform_settings_definitions.size());
		for (const auto& def : _platform_settings_definitions) {

			// Disable settings that require an external tool if that tool is not installed.
			bool is_disabled = false;
			if (auto it = s_tool_key_map.find(def.internal_key); it != s_tool_key_map.end()) {
				const std::string& tool_name = it->second;
				is_disabled = !IsExecutableAvailable(tool_name);
			}

			settings.push_back({StrMB2Wide(def.internal_key), GetMsg(def.display_name_id), this->*(def.member_variable),
								is_disabled, def.affects_candidates});
		}
		return settings;
	}


	void XDGBasedAppProvider::SetPlatformSettings(const std::vector<ProviderSetting>& settings)
	{
		for (const auto& s : settings) {
			if (auto it = _key_wide_to_member_map.find(s.internal_key); it != _key_wide_to_member_map.end()) {
				this->*(it->second) = s.value;
			}
		}
	}


	AppProvider::GetCandidatesResult XDGBasedAppProvider::GetAppCandidates(const std::vector<std::wstring>& filepaths_wide, ProgressCallback progress,
															  const std::atomic<bool>* cancel_flag)
	{
		if (filepaths_wide.empty()) {
			return {};
		}

		ClearLastQueryCaches();
		OperationGuard op_guard(*this, std::move(progress), cancel_flag);

		try {
			OperationContext op_context(*this);
			CandidateMap final_candidates;

			if (filepaths_wide.size() == 1) {

				// --- Single file logic ---

				ReportProgress({GetMsg(MsgID::IdentifyingMimes), GetMsg(MsgID::PleaseWait)});

				auto profile = GetRawMimeProfile(StrWide2MB(filepaths_wide[0]));
				_last_unique_mime_profiles.insert(profile);

				ReportProgress({GetMsg(MsgID::DiscoveringApplications), GetMsg(MsgID::MatchingFilteringRanking)});
				CheckCancellation();

				auto expanded_mimes = ExpandAndPrioritizeMimeTypes(profile);
				final_candidates = DiscoverCandidatesForExpandedMimes(expanded_mimes);

			} else {

				// --- Multiple files intersection logic ---

				const size_t files_total = filepaths_wide.size();
				size_t files_processed = 0;

				// 1. Profile Deduplication. Group N files into K unique MIME profiles.

				ReportProgress({GetMsg(MsgID::IdentifyingMimes), GetMsg(MsgID::PleaseWait)});

				for (const auto& filepath_wide : filepaths_wide) {
					wchar_t status_buf[256];
					swprintf(status_buf, std::size(status_buf), GetMsg(MsgID::ProcessingFiles), ++files_processed, files_total);
					ReportProgress({nullptr, status_buf});
					CheckCancellation();

					_last_unique_mime_profiles.insert(GetRawMimeProfile(StrWide2MB(filepath_wide)));
				}

				// 2. Candidate gathering. Resolve applications for each unique profile.
				std::unordered_map<RawMimeProfile, CandidateMap, RawMimeProfile::Hash> raw_mime_profile_to_candidate_map;
				if (_last_unique_mime_profiles.empty()) {
					return {};
				}

				ReportProgress({GetMsg(MsgID::DiscoveringApplications), GetMsg(MsgID::MatchingFilteringRanking)});

				for (const auto& profile : _last_unique_mime_profiles) {
					CheckCancellation();
					auto expanded_mimes_for_profile = ExpandAndPrioritizeMimeTypes(profile);
					auto candidates_for_profile = DiscoverCandidatesForExpandedMimes(expanded_mimes_for_profile);
					// Fail-fast: If one profile has no handlers, the intersection for all files is empty.
					if (candidates_for_profile.empty()) {
						return {};
					}
					raw_mime_profile_to_candidate_map.try_emplace(profile, std::move(candidates_for_profile));
				}

				// 3. Intersection. Find common applications that exist in ALL profiles.

				// 3a. Optimization.
				// Start with the most restrictive set to minimize lookups and removals.
				auto smallest_set_it = _last_unique_mime_profiles.begin();
				size_t min_size = raw_mime_profile_to_candidate_map.at(*smallest_set_it).size();
				for (auto it = std::next(smallest_set_it); it != _last_unique_mime_profiles.end(); ++it) {
					size_t current_size = raw_mime_profile_to_candidate_map.at(*it).size();
					if (current_size < min_size) {
						min_size = current_size;
						smallest_set_it = it;
					}
				}

				// 3b. Establish the baseline survivors set.
				const RawMimeProfile base_profile = *smallest_set_it;
				// std::move is safe because we explicitly skip 'base_profile' in the filtering loop below.
				final_candidates = std::move(raw_mime_profile_to_candidate_map.at(base_profile));

				// 3c. Filter survivors against other profiles.
				for (const auto& filter_profile : _last_unique_mime_profiles) {
					if (filter_profile == base_profile) {
						continue;
					}
					const CandidateMap& filter_candidates = raw_mime_profile_to_candidate_map.at(filter_profile);
					for (auto survivor_it = final_candidates.begin(); survivor_it != final_candidates.end(); ) {
						const auto& survivor_key = survivor_it->first;
						auto& survivor_candidate = survivor_it->second;
						auto filter_match_it = filter_candidates.find(survivor_key);
						if (filter_match_it == filter_candidates.end()) {
							// The app is missing in the filter profile, so it cannot handle all files.
							// It fails the intersection test -> remove it.
							survivor_it = final_candidates.erase(survivor_it);
						} else {
							// The app exists in both lists. It survives this round.
							// Check if the match in the filter profile offers a better (higher) rank.
							const RankedCandidate& filter_candidate = filter_match_it->second;
							if (filter_candidate.rank > survivor_candidate.rank) {
								survivor_candidate.rank = filter_candidate.rank;
							}
							++survivor_it;
						}
					}
					if (final_candidates.empty()) {
						return {};
					}
				}
			}

			// --- Common post-processing (for both 1 and >1 file cases) ---

			auto final_candidates_sorted = BuildSortedCandidates(final_candidates);
			// Only store source info (for F3) when a single file is selected,
			// as sources vary per file in multi-selection mode.
			return { FormatCandidatesForUI(final_candidates_sorted, /* store_source_info = */ (filepaths_wide.size() == 1)), false };

		} catch (const OperationCancelledException&) {
			ClearLastQueryCaches();
			return {{}, true};
		}
	}


	std::vector<std::wstring> XDGBasedAppProvider::ConstructLaunchCommands(const CandidateInfo& candidate, const std::vector<std::wstring>& filepaths_wide)
	{
		if (filepaths_wide.empty()) {
			return {};
		}

		const std::string desktop_id = StrWide2MB(candidate.id);
		auto it = _desktop_id_to_desktop_entry_cache.find(desktop_id);

		if (it == _desktop_id_to_desktop_entry_cache.end() || !it->second.has_value()) {
			return {};
		}

		const DesktopEntry& desktop_entry = it->second.value();
		if (desktop_entry.exec.empty()) {
			return {};
		}
		AnalyzeExecLine(desktop_entry);
		if (desktop_entry.arg_templates.empty()) {
			return {};
		}
		std::vector<std::string> filepaths;
		filepaths.reserve(filepaths_wide.size());
		for (const auto& filepath_wide : filepaths_wide) {
			filepaths.push_back(StrWide2MB(filepath_wide));
		}
		std::vector<std::wstring> launch_commands_wide;
		auto add_command_from_batch = [&](const std::vector<std::string>& batch) {
			auto command = AssembleLaunchCommand(desktop_entry, batch);
			if (!command.empty()) {
				launch_commands_wide.push_back(StrMB2Wide(command));
			}
		};
		if (desktop_entry.execution_model == ExecutionModel::PerFile) {
			// The application uses %f or %u field codes and accepts only one file per invocation.
			// Generate a separate command line for each selected file.
			launch_commands_wide.reserve(filepaths.size());
			for (const auto& filepath : filepaths) {
				add_command_from_batch({ filepath });
			}
		} else {
			// The application uses %F, %U, or has no field codes (Legacy).
			// It accepts the entire list of files in a single invocation.
			add_command_from_batch(filepaths);
		}
		return launch_commands_wide;
	}


	std::vector<Field> XDGBasedAppProvider::GetCandidateDetails(const CandidateInfo& candidate)
	{
		std::vector<Field> details;
		std::string desktop_id = StrWide2MB(candidate.id);

		auto it = _desktop_id_to_desktop_entry_cache.find(desktop_id);
		if (it == _desktop_id_to_desktop_entry_cache.end() || !it->second.has_value()) {
			return details;
		}
		const DesktopEntry& desktop_entry = it->second.value();

		details.push_back({GetMsg(MsgID::DesktopFile), StrMB2Wide(desktop_entry.desktop_filepath)});

		// Retrieve the source info (e.g., 'mimeapps.list') stored during GetAppCandidates.
		// This is only available for single-file selections.
		if (auto source_it = _last_association_facts.find(desktop_id);
				 source_it != _last_association_facts.end()) {
			const AssociationFact& fact = source_it->second;
			details.push_back({GetMsg(MsgID::Source), FormatFactForUI(fact)});
		}

		static constexpr std::pair<const wchar_t*, std::string DesktopEntry::*> field_map[] = {
			{L"Name =",               &DesktopEntry::name},
			{L"GenericName =",        &DesktopEntry::generic_name},
			{L"Comment =",            &DesktopEntry::comment},
			{L"Categories =",         &DesktopEntry::categories},
			{L"Exec =",               &DesktopEntry::exec},
			{L"TryExec =",            &DesktopEntry::try_exec},
			{L"Terminal =",           &DesktopEntry::terminal},
			{L"MimeType =",           &DesktopEntry::mimetype},
			{L"OnlyShowIn =",         &DesktopEntry::only_show_in},
			{L"NotShowIn =",          &DesktopEntry::not_show_in},
			{L"X-Flatpak =",          &DesktopEntry::x_flatpak},
			{L"X-SnapInstanceName =", &DesktopEntry::x_snap_instance_name}
		};

		for (const auto& [label_wide, member_ptr] : field_map) {
			const std::string& val = desktop_entry.*member_ptr;
			if (!val.empty()) {
				details.push_back({ label_wide, StrMB2Wide(val) });
			}
		}

		return details;
	}


	// Generates "GoTo" targets for the Details dialog: the .desktop file, the TryExec binary,
	// and the association source file (mimeapps.list or mimeinfo.cache).
	std::vector<CandidateContextLocation> XDGBasedAppProvider::GetCandidateContextLocations(const CandidateInfo& candidate)
	{
		std::vector<CandidateContextLocation> locations;
		std::string desktop_id = StrWide2MB(candidate.id);

		if (auto it = _desktop_id_to_desktop_entry_cache.find(desktop_id);
				 it != _desktop_id_to_desktop_entry_cache.end() && it->second.has_value()) {

			const DesktopEntry& entry = it->second.value();
			locations.push_back({GetMsg(MsgID::GotoDesktop), StrMB2Wide(entry.desktop_filepath)});

			const std::string try_exec_path = UnescapeGKeyFileString(entry.try_exec);
			std::string tryexec_filepath;
			if (IsExecutableAvailable(try_exec_path, &tryexec_filepath)) {
				locations.push_back({GetMsg(MsgID::GotoTryExec), StrMB2Wide(tryexec_filepath)});
			}
		}

		if (auto it = _last_association_facts.find(desktop_id);
				 it != _last_association_facts.end()) {
			const AssociationFact& fact = it->second;
			if (!fact.filepath.empty()) {
				locations.push_back({GetMsg(MsgID::GotoSource), StrMB2Wide(fact.filepath)});
			}
		}

		return locations;
	}


	// Formats the unique MIME profiles collected during the last candidate search
	// into human-readable strings for display in the Details dialog.
	std::vector<std::wstring> XDGBasedAppProvider::GetFileTypes()
	{
		std::set<std::wstring> unique_representations_wide;
		bool has_none = false;

		for (const auto& profile : _last_unique_mime_profiles) {
			std::set<std::string> profile_mimes;

			auto add_if_present = [&](const std::string& s) {
				if (!s.empty()) profile_mimes.insert(s);
			};

			add_if_present(profile.xdg_mime);
			add_if_present(profile.file_mime);
			add_if_present(profile.magika_mime);
			add_if_present(profile.globs2_mime);
			add_if_present(profile.stat_mime);
			add_if_present(profile.ext_mime);

			if (profile_mimes.empty()) {
				has_none = true;
			} else if (profile_mimes.size() == 1) {
				unique_representations_wide.insert(StrMB2Wide(*profile_mimes.begin()));
			} else {
				std::string repr = "(";
				bool is_first = true;
				for (const auto& mime : profile_mimes) {
					if (!is_first) repr += ';';
					repr += mime;
					is_first = false;
				}
				repr += ')';
				unique_representations_wide.insert(StrMB2Wide(repr));
			}
		}

		std::vector<std::wstring> result_representations_wide;
		if (has_none) {
			result_representations_wide.push_back(L"(none)");
		}
		result_representations_wide.insert(result_representations_wide.end(), unique_representations_wide.begin(), unique_representations_wide.end());
		return result_representations_wide;
	}



	// ****************************** Candidate search and ranking ******************************


	void XDGBasedAppProvider::ClearLastQueryCaches()
	{
		_desktop_id_to_desktop_entry_cache.clear();
		_last_association_facts.clear();
		_last_unique_mime_profiles.clear();
	}


	// Queries all candidate sources (system default, mimeapps.list, mimeinfo.cache, full scan)
	// for a prioritized list of MIME types and ranks results by specificity and association source.
	XDGBasedAppProvider::CandidateMap XDGBasedAppProvider::DiscoverCandidatesForExpandedMimes(const std::vector<std::string>& expanded_mimes)
	{
		CandidateMap unique_candidates;

		if (_query_xdg_mime_default && _op_state->xdg_mime_exists) {
			const auto mime_for_default = expanded_mimes.empty() ? "" : expanded_mimes[0];
			std::string desktop_id = QuerySystemDefaultApplication(mime_for_default);
			if (!desktop_id.empty()) {
				if (!IsAssociationRemoved(mime_for_default, desktop_id, _op_state->mimeapps_lists_cache)) {
					// The system default always receives the top specificity rank (index 0 -> highest).
					int rank = static_cast<int>(expanded_mimes.size()) * Ranking::SPECIFICITY_MULTIPLIER + Ranking::SOURCE_RANK_GLOBAL_DEFAULT;
					RegisterCandidateById(unique_candidates, desktop_id, rank,
											AssociationFact{AssociationOriginTier::SystemDefault, "", mime_for_default});
				}
			}
		}

		AppendCandidatesFromMimeappsLists(expanded_mimes, unique_candidates);

		if (!_op_state->mime_to_desktop_associations_index.empty()) {
			AppendCandidatesFromMimeinfoCache(expanded_mimes, unique_candidates);
		} else {
			AppendCandidatesFromFullScan(expanded_mimes, unique_candidates);
		}

		return unique_candidates;
	}


	std::string XDGBasedAppProvider::QuerySystemDefaultApplication(const std::string& mime)
	{
		if (mime.empty()) {
			return "";
		}

		if (auto it = _op_state->mime_to_default_desktop_id_cache.find(mime);
				 it != _op_state->mime_to_default_desktop_id_cache.end()) {
			return it->second;
		}

		std::string cmd = "xdg-mime query default " + QuoteArgForShell(mime) + " 2>/dev/null";

		CheckCancellation();
		auto desktop_id = RunCommandAndCaptureOutput(cmd);
		_op_state->mime_to_default_desktop_id_cache.try_emplace(mime, desktop_id);
		return desktop_id;
	}


	void XDGBasedAppProvider::AppendCandidatesFromMimeappsLists(const std::vector<std::string>& expanded_mimes, CandidateMap& unique_candidates)
	{
		const int total_mimes = static_cast<int>(expanded_mimes.size());

		for (int mime_idx = 0; mime_idx < total_mimes; ++mime_idx) {
			const auto& mime = expanded_mimes[mime_idx];
			int mime_specificity_weight = (total_mimes - mime_idx);

			// 1. Process [Default Applications].
			if (auto it = _op_state->mimeapps_lists_cache.defaults.find(mime);
					 it != _op_state->mimeapps_lists_cache.defaults.end()) {

				const auto& default_associations = it->second;
				int rank_penalty = 0;
				for (const DesktopAssociation& default_association : default_associations) {
					int rank = mime_specificity_weight * Ranking::SPECIFICITY_MULTIPLIER + Ranking::SOURCE_RANK_MIMEAPPS_DEFAULT
							   - std::min(rank_penalty, Ranking::MAX_PENALTY_FOR_DEFAULT);
					RegisterCandidateById(unique_candidates, default_association.desktop_id, rank,
										  AssociationFact{AssociationOriginTier::MimeappsDefault, default_association.source_filepath, mime});
					++rank_penalty;
				}
			}

			// 2. Process [Added Associations].
			if (auto it = _op_state->mimeapps_lists_cache.added.find(mime);
					 it != _op_state->mimeapps_lists_cache.added.end()) {

				const auto& added_associations = it->second;
				int rank_penalty = 0;
				for (const DesktopAssociation& added_association : added_associations) {
					const int rank = mime_specificity_weight * Ranking::SPECIFICITY_MULTIPLIER + Ranking::SOURCE_RANK_MIMEAPPS_ADDED
									 - std::min(rank_penalty, Ranking::MAX_PENALTY_FOR_ADDED);
					RegisterCandidateById(unique_candidates, added_association.desktop_id, rank,
											AssociationFact{AssociationOriginTier::MimeappsAdded, added_association.source_filepath, mime});
					++rank_penalty;
				}
			}
		}
	}


	void XDGBasedAppProvider::AppendCandidatesFromMimeinfoCache(const std::vector<std::string>& expanded_mimes, CandidateMap& unique_candidates)
	{
		if (_op_state->mime_to_desktop_associations_index.empty()) {
			return;
		}
		const int total_mimes = static_cast<int>(expanded_mimes.size());

		std::unordered_map<std::string, RankedCandidate> staged_candidates;

		for (int mime_idx = 0; mime_idx < total_mimes; ++mime_idx) {
			const auto& mime = expanded_mimes[mime_idx];
			auto it = _op_state->mime_to_desktop_associations_index.find(mime);
			if (it == _op_state->mime_to_desktop_associations_index.end()) {
				continue;
			}
			const auto& associations = it->second;
			const int base_rank = (total_mimes - mime_idx) * Ranking::SPECIFICITY_MULTIPLIER + Ranking::SOURCE_RANK_CACHE_OR_SCAN;
			int rank_penalty = 0;
			for (const DesktopAssociation& association : associations) {
				const auto& desktop_id = association.desktop_id;
				if (desktop_id.empty() || (IsAssociationRemoved(mime, desktop_id, _op_state->mimeapps_lists_cache))) {
					continue;
				}
				const int rank = base_rank - std::min(rank_penalty, Ranking::MAX_PENALTY_FOR_MIMEINFO_CACHE);
				AssociationFact fact{AssociationOriginTier::MimeinfoCache, association.source_filepath, mime};
				auto [it, inserted] = staged_candidates.try_emplace(desktop_id, rank, std::move(fact));
				if (inserted) {
					++rank_penalty;
				}
			}
		}
		for (auto& [desktop_id, candidate] : staged_candidates) {
			RegisterCandidateById(unique_candidates, desktop_id, candidate.rank, candidate.origin_fact);
		}
	}


	void XDGBasedAppProvider::AppendCandidatesFromFullScan(const std::vector<std::string>& expanded_mimes, CandidateMap& unique_candidates)
	{
		if (_op_state->mime_to_desktop_entry_index.empty()) {
			return;
		}
		const int total_mimes = static_cast<int>(expanded_mimes.size());
		std::unordered_map<const DesktopEntry*, RankedCandidate> staged_candidates;
		for (int mime_idx = 0; mime_idx < total_mimes; ++mime_idx) {
			const auto& mime = expanded_mimes[mime_idx];
			auto it = _op_state->mime_to_desktop_entry_index.find(mime);
			if (it == _op_state->mime_to_desktop_entry_index.end()) {
				continue;
			}
			const auto& entries = it->second;
			int rank = (total_mimes - mime_idx) * Ranking::SPECIFICITY_MULTIPLIER + Ranking::SOURCE_RANK_CACHE_OR_SCAN;
			for (const DesktopEntry* entry : entries) {
				if (!entry || IsAssociationRemoved(mime, entry->id, _op_state->mimeapps_lists_cache)) {
					continue;
				}
				AssociationFact fact{AssociationOriginTier::FullScan, "", mime};
				staged_candidates.try_emplace(entry, entry, rank, std::move(fact));
			}

		}
		for (const auto& [entry, candidate] : staged_candidates) {
			RegisterCandidateFromDesktopEntry(unique_candidates, *candidate.desktop_entry, candidate.rank, candidate.origin_fact);
		}
	}


	void XDGBasedAppProvider::RegisterCandidateById(CandidateMap& unique_candidates, const std::string& desktop_id,
													int rank, const AssociationFact& origin_fact)
	{
		if (desktop_id.empty()) {
			return;
		}
		const auto& desktop_entry_opt = GetOrLoadDesktopEntry(desktop_id);
		if (!desktop_entry_opt) {
			return;
		}

		RegisterCandidateFromDesktopEntry(unique_candidates, *desktop_entry_opt, rank, origin_fact);
	}


	// Validates a desktop entry against TryExec and OnlyShowIn/NotShowIn constraints, then forwards it to AddOrUpdateCandidate().
	// Entries incompatible with the current desktop environment are silently discarded.
	void XDGBasedAppProvider::RegisterCandidateFromDesktopEntry(CandidateMap& unique_candidates, const DesktopEntry& desktop_entry,
																int rank, const AssociationFact& origin_fact)
	{
		if (_validate_try_exec && !desktop_entry.try_exec.empty()) {
			std::string try_exec_path = UnescapeGKeyFileString(desktop_entry.try_exec);
			if (!IsExecutableAvailable(try_exec_path)) {
				return;
			}
		}

		if (_filter_by_show_in && !_op_state->current_desktop_names.empty()) {

			bool only_show_in_present = !desktop_entry.only_show_in.empty();
			bool not_show_in_present = !desktop_entry.not_show_in.empty();

			if (only_show_in_present || not_show_in_present) {

				auto is_desktop_listed = [](std::string_view env_list, std::string_view desktop_name) noexcept {
					while (!env_list.empty()) {
						size_t semicolon_pos = env_list.find(';');
						std::string_view extracted_env = TrimToStrV(env_list.substr(0, semicolon_pos));
						if (extracted_env == desktop_name) {
							return true;
						}
						if (semicolon_pos == std::string_view::npos) {
							break;
						}
						env_list.remove_prefix(semicolon_pos + 1);
					}
					return false;
				};

				bool explicitly_allowed = false;
				for (const auto& current_desktop_name : _op_state->current_desktop_names) {
					if (current_desktop_name.empty()) {
						continue;
					}
					if (only_show_in_present && is_desktop_listed(desktop_entry.only_show_in, current_desktop_name)) {
						explicitly_allowed = true;
						break; // Found a high-priority match explicitly allowing the app. Stop checking.
					}
					if (not_show_in_present && is_desktop_listed(desktop_entry.not_show_in, current_desktop_name)) {
						return; // Found a high-priority match explicitly forbidding the app. Reject immediately.
					}
				}
				if (only_show_in_present && !explicitly_allowed) {
					return;
				}
			}
		}

		AddOrUpdateCandidate(unique_candidates, desktop_entry, rank, origin_fact);
	}



	// Deduplicates candidates by (Name, Exec). If the same application is found via multiple
	// sources, the instance with the highest rank is retained.
	void XDGBasedAppProvider::AddOrUpdateCandidate(CandidateMap& unique_candidates, const DesktopEntry& desktop_entry,
												   int rank, const AssociationFact& origin_fact)
	{
		// Composite key (Name + Exec) used to deduplicate candidates defined across multiple .desktop files or overlays.
		AppUniqueKey unique_key{desktop_entry.name, desktop_entry.exec};

		auto [it, inserted] = unique_candidates.try_emplace(unique_key, RankedCandidate{&desktop_entry, rank, origin_fact});

		// If the candidate already exists, we keep the version with the HIGHEST rank.
		if (!inserted) {
			RankedCandidate& existing = it->second;
			if (rank > existing.rank) {
				existing.rank = rank;
				existing.desktop_entry = &desktop_entry;
				existing.origin_fact = origin_fact;
			}
		}
	}


	bool XDGBasedAppProvider::IsAssociationRemoved(const std::string& mime, const std::string& desktop_id, const MimeappsListsConfig& mimeapps_lists) const
	{
		if (_ignore_removed_associations) {
			return false;
		}

		// 1. Check for an exact match (e.g., "image/jpeg")
		if (auto exact_it = mimeapps_lists.removed.find(mime); exact_it != mimeapps_lists.removed.end()) {
			if (exact_it->second.count(desktop_id)) {
				return true;
			}
		}

		// 2. Check for a wildcard match (e.g., "image/*") that bans entire categories
		const auto major = GetMajorMimeType(mime);
		if (!major.empty()) {
			const auto wildcard_mime = std::string(major) + "/*";
			if (auto wildcard_it = mimeapps_lists.removed.find(wildcard_mime); wildcard_it != mimeapps_lists.removed.end()) {
				if (wildcard_it->second.count(desktop_id)) {
					return true;
				}
			}
		}

		return false;
	}


	std::vector<XDGBasedAppProvider::RankedCandidate> XDGBasedAppProvider::BuildSortedCandidates(const CandidateMap& candidate_map)
	{
		std::vector<RankedCandidate> sorted_candidates;
		sorted_candidates.reserve(candidate_map.size());
		for (const auto& [key, ranked_candidate] : candidate_map) {
			sorted_candidates.push_back(ranked_candidate);
		}

		if (_sort_alphabetically) {
			std::sort(sorted_candidates.begin(), sorted_candidates.end(), [](const RankedCandidate& a, const RankedCandidate& b) {
				if (!a.desktop_entry || !b.desktop_entry) {
					return b.desktop_entry != nullptr;
				}
				return a.desktop_entry->name < b.desktop_entry->name;
			});
		} else {
			std::sort(sorted_candidates.begin(), sorted_candidates.end());
		}

		return sorted_candidates;
	}


	std::vector<CandidateInfo> XDGBasedAppProvider::FormatCandidatesForUI(
		const std::vector<RankedCandidate>& ranked_candidates,
		bool store_source_info)
	{
		std::vector<CandidateInfo> result;
		result.reserve(ranked_candidates.size());

		for (const auto& ranked_candidate : ranked_candidates) {
			CandidateInfo ci = ConvertDesktopEntryToCandidateInfo(*ranked_candidate.desktop_entry);

			if (_show_package_tags) {
				ci.name += GetPackageTag(ranked_candidate.desktop_entry->package_type);
			}

			if (store_source_info) {
				_last_association_facts.try_emplace(ranked_candidate.desktop_entry->id, ranked_candidate.origin_fact);
			}
			result.push_back(ci);
		}
		return result;
	}


	std::wstring_view XDGBasedAppProvider::GetPackageTag(PackageType type)
	{
		switch (type) {
			case PackageType::Flatpak:
				return L" [*Flatpak]";
			case PackageType::Snap:
				return L" [*Snap]";
			default:
				return L"";
		}
	}


	CandidateInfo XDGBasedAppProvider::ConvertDesktopEntryToCandidateInfo(const DesktopEntry& desktop_entry)
	{
		CandidateInfo candidate;
		candidate.terminal = (desktop_entry.terminal == "true" || desktop_entry.terminal == "1");
		candidate.name = StrMB2Wide(UnescapeGKeyFileString(desktop_entry.name));
		candidate.id = StrMB2Wide(desktop_entry.id);

		// Ensure the execution model is determined before checking capabilities.
		AnalyzeExecLine(desktop_entry);

		// The 'multi_file_aware' flag indicates if the plugin can pass a list of files to this app.
		// If the model is PerFile (%f/%u), the app strictly accepts one argument, so it is *not* multi-file aware
		// from the perspective of a single invocation. FileList (%F/%U) and ImplicitFileAppend modes support lists.
		candidate.multi_file_aware = (desktop_entry.execution_model != ExecutionModel::PerFile);

		return candidate;
	}


	std::wstring XDGBasedAppProvider::FormatFactForUI(const AssociationFact& fact) const
	{
		switch (fact.tier) {
			case AssociationOriginTier::SystemDefault:
				return StrMB2Wide("xdg-mime query default " + fact.mime_type);

			case AssociationOriginTier::MimeappsDefault:
				return StrMB2Wide(fact.filepath) + GetMsg(MsgID::In) + L"[Default Applications]" + GetMsg(MsgID::For) + StrMB2Wide(fact.mime_type);

			case AssociationOriginTier::MimeappsAdded:
				return StrMB2Wide(fact.filepath) + GetMsg(MsgID::In) + L"[Added Associations]" + GetMsg(MsgID::For) + StrMB2Wide(fact.mime_type);

			case AssociationOriginTier::MimeinfoCache:
				return StrMB2Wide(fact.filepath) + GetMsg(MsgID::For) + StrMB2Wide(fact.mime_type);

			case AssociationOriginTier::FullScan:
				return GetMsg(MsgID::FullScanFor) + StrMB2Wide(fact.mime_type);
		}
		return L"";
	}



	// ****************************** File MIME type detection and expansion ******************************

	// Probes a single file with every enabled detection method and returns an unexpanded
	// RawMimeProfile. Content-based CLI tools are invoked only for readable regular files.
	XDGBasedAppProvider::RawMimeProfile XDGBasedAppProvider::GetRawMimeProfile(const std::string& filepath)
	{
		RawMimeProfile profile{};
		struct stat st;

		if (stat(filepath.c_str(), &st) != 0) {
			return profile;
		}

		if (S_ISREG(st.st_mode)) {
			profile.is_regular_file = true;

			// Filename-based detection methods are only meaningful for regular files.
			if (_use_glob_rules) {
				profile.globs2_mime = DetectMimeTypeViaGlobRules(filepath);
			}
			if (_use_extension_based_fallback) {
				profile.ext_mime = DetectMimeTypeByExtension(filepath);
			}

			bool should_run_cli_tools = ((_use_xdg_mime_tool && _op_state->xdg_mime_exists) || _op_state->file_tool_enabled_and_exists || _op_state->magika_tool_enabled_and_exists);

			// Run expensive external tools ONLY for accessible regular files.
			if (should_run_cli_tools && access(filepath.c_str(), R_OK) == 0) {

				auto filepath_quoted = QuoteArgForShell(filepath);

				if (_use_xdg_mime_tool && _op_state->xdg_mime_exists) {
					CheckCancellation();
					profile.xdg_mime = DetectMimeTypeWithXdgMimeTool(filepath_quoted);
				}
				if (_op_state->file_tool_enabled_and_exists) {
					CheckCancellation();
					profile.file_mime = DetectMimeTypeWithFileTool(filepath_quoted);
				}
				if (_op_state->magika_tool_enabled_and_exists) {
					CheckCancellation();
					profile.magika_mime = DetectMimeTypeWithMagikaTool(filepath_quoted);
				}
			}

		} else if (S_ISDIR(st.st_mode)) {
			profile.stat_mime = "inode/directory";
		} else if (S_ISFIFO(st.st_mode)) {
			profile.stat_mime = "inode/fifo";
		} else if (S_ISSOCK(st.st_mode)) {
			profile.stat_mime = "inode/socket";
		} else if (S_ISCHR(st.st_mode)) {
			profile.stat_mime = "inode/chardevice";
		} else if (S_ISBLK(st.st_mode)) {
			profile.stat_mime = "inode/blockdevice";
		}

		return profile;
	}


	std::vector<std::string> XDGBasedAppProvider::ExpandAndPrioritizeMimeTypes(const RawMimeProfile& profile)
	{
		std::vector<std::string> mimes;
		std::unordered_set<std::string> seen_mimes;

		static const std::string s_octet_stream = "application/octet-stream";
		bool octet_stream_detected = false;

		// Adds a MIME type only if it's valid and not already present.
		auto add_unique = [&](std::string mime) {
			mime = TrimToStr(mime);
			if (mime.empty()) {
				return;
			}
			if (mime == s_octet_stream) {
				octet_stream_detected = true;
				return;
			}
			if (mime.find('/') != std::string::npos && seen_mimes.insert(mime).second) {
				mimes.push_back(std::move(mime));
			}
		};

		// --- Step 1: initial, most specific MIME types ---
		add_unique(profile.xdg_mime);
		add_unique(profile.file_mime);
		add_unique(profile.magika_mime);
		add_unique(profile.globs2_mime);
		add_unique(profile.stat_mime);
		add_unique(profile.ext_mime);

		// --- Step 2: iteratively expand via aliases and subclass hierarchy ---
		if (!_op_state->subclass_to_parent_cache.empty() || !_op_state->alias_to_canonical_cache.empty()) {

			// Index-based loop intentional: newly added MIME types are appended to
			// 'mimes' during iteration and must also be expanded by subsequent passes.
			for (size_t i = 0; i < mimes.size(); ++i) {
				const std::string current_mime = mimes[i];

				// Expansion A: process aliases
				if (!_op_state->alias_to_canonical_cache.empty()) {

					// --- Standard forward lookup (alias -> canonical) ---
					if (auto canonical_it = _op_state->alias_to_canonical_cache.find(current_mime);
							 canonical_it != _op_state->alias_to_canonical_cache.end()) {
						add_unique(canonical_it->second);
					}

					// --- Reverse lookup (canonical -> aliases) using major-type filtered map ---
					if (auto aliases_it = _op_state->canonical_to_aliases_cache.find(current_mime);
							 aliases_it != _op_state->canonical_to_aliases_cache.end()) {
						for (const auto& alias : aliases_it->second) {
							add_unique(alias);
						}
					}
				}

				// Expansion B: Add parent from subclass hierarchy.
				if (!_op_state->subclass_to_parent_cache.empty()) {
					auto it = _op_state->subclass_to_parent_cache.find(current_mime);
					if (it != _op_state->subclass_to_parent_cache.end()) {
						add_unique(it->second);
					}
				}
			}
		}

		// --- Step 3: add base types for structured suffixes (+xml, etc) ---
		// This is a reliable fallback that works even if the subclass hierarchy is incomplete in the system's MIME database.
		if (_resolve_structured_suffixes) {

			static const std::unordered_map<std::string_view, std::string_view> s_suffix_to_base_mime = {
				{"xml",         "application/xml"},
				{"json",        "application/json"},
				{"cbor",        "application/cbor"},
				{"fastinfoset", "application/fastinfoset"},
				{"wbxml",       "application/vnd.wap.wbxml"},
				{"zip",         "application/zip"},
				{"json-seq",    "application/json-seq"},
				{"sqlite3",     "application/vnd.sqlite3"},
				{"jwt",         "application/jwt"},
				{"gzip",        "application/gzip"},
				{"cbor-seq",    "application/cbor-seq"},
				{"zstd",        "application/zstd"},
				{"yaml",        "application/yaml"},
				{"cose",        "application/cose"},
				{"cwt",         "application/cwt"},
				{"sd-jwt",      "application/sd-jwt"},
				{"csv",         "text/csv"},
				{"sd-cwt",      "application/sd-cwt"},
			};

			const auto size_before_suffixes = mimes.size();
			for (size_t i = 0; i < size_before_suffixes; ++i) {
				std::string_view mime_v = mimes[i];
				size_t plus_pos = mime_v.rfind('+');
				if (plus_pos != std::string_view::npos && plus_pos < mime_v.length() - 1) {
					auto suffix = mime_v.substr(plus_pos + 1);
					if (auto it = s_suffix_to_base_mime.find(suffix); it != s_suffix_to_base_mime.end()) {
						add_unique(std::string(it->second));
					}
				}
			}
		}

		// --- Step 4: add generic fallbacks (text/plain, image/*, etc.) ---
		if (_use_generic_mime_fallbacks) {
			const auto size_before_generic_types = mimes.size();
			for (size_t i = 0; i < size_before_generic_types; ++i) {
				const auto major = GetMajorMimeType(mimes[i]);
				if (major.empty()) {
					continue;
				}
				const auto wildcard_mime = std::string(major) + "/*";
				if (major == "text") {
					// WARNING: add_unique() may reallocate the vector, invalidating the 'major' string_view.
					// Do not use 'major' after this call.
					add_unique("text/plain");
				}
				add_unique(wildcard_mime);
			}
		}

		// --- Step 5: append application/octet-stream if applicable ---
		if (profile.is_regular_file) {
			if (_show_universal_handlers || octet_stream_detected) {
				mimes.push_back(s_octet_stream);
			}
		}

		return mimes;
	}


	std::string XDGBasedAppProvider::DetectMimeTypeWithXdgMimeTool(const std::string& filepath_quoted)
	{
		return RunCommandAndCaptureOutput("xdg-mime query filetype " + filepath_quoted + " 2>/dev/null");
	}


	std::string XDGBasedAppProvider::DetectMimeTypeWithFileTool(const std::string& filepath_quoted)
	{
		return RunCommandAndCaptureOutput("file --brief --dereference --mime-type " + filepath_quoted + " 2>/dev/null");
	}


	std::string XDGBasedAppProvider::DetectMimeTypeWithMagikaTool(const std::string& filepath_quoted)
	{
		return RunCommandAndCaptureOutput("magika --no-colors --format '%m' " + filepath_quoted + " 2>/dev/null");
	}


	std::string XDGBasedAppProvider::DetectMimeTypeViaGlobRules(const std::string& filepath)
	{
		std::string filename = GetBaseName(filepath);
		if (filename.empty()) {
			return "";
		}

		for (const auto& rule : _op_state->glob_rules_cache) {
			if (GlobMatch(filename, rule.pattern, rule.case_sensitive)) {
				return rule.mime_type;
			}
		}

		return "";
	}


	bool XDGBasedAppProvider::GlobMatch(const std::string &text, const std::string &pattern, bool case_sensitive)
	{
		int flags = 0;

		if (!case_sensitive) {
#ifdef FNM_CASEFOLD
			flags = FNM_CASEFOLD;
#elif defined(FNM_IGNORECASE)
			flags = FNM_IGNORECASE;
#endif
		}

		if (UNLIKELY(flags == 0 && !case_sensitive)) {
			return fnmatch(ToLowerASCII(pattern).c_str(),
						   ToLowerASCII(text).c_str(), 0) == 0;
		}

		return fnmatch(pattern.c_str(), text.c_str(), flags) == 0;
	}


	std::string_view XDGBasedAppProvider::DetectMimeTypeByExtension(const std::string& filepath)
	{
		auto filename = GetBaseName(filepath);
		auto dot_pos = filename.rfind('.');
		if (dot_pos != std::string::npos && dot_pos > 0 && dot_pos + 1 < filename.size()) {
			std::string ext = ToLowerASCII(filename.substr(dot_pos + 1));
			const auto& map = GetExtMimeMap();
			if (auto it = map.find(ext); it != map.end()) {
				return it->second;
			}
		}
		return "";
	}



	// ****************************** XDG database parsing and caching ******************************


	// Recursively scans all XDG application directories to build an index mapping Desktop File IDs to their absolute file paths.
	std::unordered_map<std::string, std::string> XDGBasedAppProvider::IndexAllDesktopFiles()
	{
		if (_op_state->desktop_file_dirpaths.empty()) {
			return {};
		}

		std::unordered_map<std::string, std::string> desktop_id_to_path_map;

		VisitedInodeSet visited_inodes;

		for (const auto& dirpath : _op_state->desktop_file_dirpaths) {
			CheckCancellation();
			IndexDirectoryRecursively(desktop_id_to_path_map, dirpath, dirpath, visited_inodes);
		}

		return desktop_id_to_path_map;
	}


	void XDGBasedAppProvider::IndexDirectoryRecursively(std::unordered_map<std::string, std::string>& desktop_id_to_path_map, const std::string& current_path, const std::string& base_dir_prefix, VisitedInodeSet &visited_inodes)
	{
		struct stat st;
		if (stat(current_path.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
			return;
		}

		// Loop protection using device and inode numbers.
		if (!visited_inodes.insert({st.st_dev, st.st_ino}).second) {
			return;
		}

		struct DirCloser
		{
			void operator()(DIR* d) const
			{
				if (d) {
					closedir(d);
				}
			}
		};
		using UniqueDir = std::unique_ptr<DIR, DirCloser>;

		UniqueDir dir_stream(opendir(current_path.c_str()));
		if (!dir_stream) {
			return;
		}

		const dirent *dir_entry;

		while ((dir_entry = readdir(dir_stream.get()))) {
			std::string_view name = dir_entry->d_name;
			if (name == "." || name == "..") {
				continue;
			}

			bool is_dir = false;
			bool is_reg = false;
			bool need_stat = true;

			// Optimization: Use d_type from dirent to avoid expensive stat() calls when possible.
#ifdef DT_UNKNOWN
			// We MUST stat symlinks because we need to know what they point to (follow semantics).
			if (dir_entry->d_type != DT_UNKNOWN && dir_entry->d_type != DT_LNK) {
				need_stat = false;
				if (dir_entry->d_type == DT_DIR) {
					is_dir = true;
				} else if (dir_entry->d_type == DT_REG) {
					is_reg = true;
				} else {
					// Skip special file types (e.g., sockets, pipes, devices).
					continue;
				}
			}
#endif

			std::string full_path = current_path + "/" + name.data();

			if (need_stat) {
				// Fallback to stat():
				// 1. If d_type is not supported by the OS or filesystem.
				// 2. If the entry is a symlink (we need to follow it).
				if (stat(full_path.c_str(), &st) != 0) {
					continue;
				}

				if (S_ISDIR(st.st_mode)) {
					is_dir = true;
				} else if (S_ISREG(st.st_mode)) {
					is_reg = true;
				}
			}

			if (is_dir) {
				IndexDirectoryRecursively(desktop_id_to_path_map, full_path, base_dir_prefix, visited_inodes);
			} else if (is_reg) {
				if (name.size() > 8 && name.compare(name.size() - 8, 8, ".desktop") == 0) {
					// Calculate ID: relative path from base_dir_prefix, with '/' replaced by '-'.
					if (full_path.size() > base_dir_prefix.size() + 1) {
						std::string id = full_path.substr(base_dir_prefix.size() + 1); // +1 for the separator
						std::replace(id.begin(), id.end(), '/', '-');
						// Store in the map. First found wins.
						desktop_id_to_path_map.try_emplace(std::move(id), full_path);
					}
				}
			}
		}
	}


	// Retrieves a DesktopEntry from the cache or parses it from disk if not present.
	const std::optional<XDGBasedAppProvider::DesktopEntry>& XDGBasedAppProvider::GetOrLoadDesktopEntry(const std::string& desktop_id)
	{
		if (auto it = _desktop_id_to_desktop_entry_cache.find(desktop_id); it != _desktop_id_to_desktop_entry_cache.end()) {
			return it->second;
		}

		if (auto path_it = _op_state->desktop_id_to_path_index.find(desktop_id); path_it != _op_state->desktop_id_to_path_index.end()) {
			const std::string& filepath = path_it->second;
			CheckCancellation();
			if (auto desktop_entry = ParseDesktopFile(filepath)) {
				// Set the ID on the object, as ParseDesktopFile does not know it.
				desktop_entry->id = desktop_id;
				auto [it, inserted] = _desktop_id_to_desktop_entry_cache.try_emplace(desktop_id, std::move(desktop_entry));
				return it->second;
			}
		}

		// Cache a nullopt if the file cannot be found or parsed.
		auto [it, inserted] = _desktop_id_to_desktop_entry_cache.try_emplace(desktop_id, std::nullopt);
		return it->second;
	}


	// Builds a reverse index by iterating through all discovered Desktop IDs and parsing their fields.
	XDGBasedAppProvider::MimeToDesktopEntryIndex XDGBasedAppProvider::ParseAllDesktopFiles()
	{
		MimeToDesktopEntryIndex index;

		for (const auto& [id, filepath] : _op_state->desktop_id_to_path_index) {

			// Parsed entries are stored in the persistent _desktop_id_to_desktop_entry_cache
			// so they can later be looked up cheaply by RegisterCandidateById().
			const auto& desktop_entry_opt = GetOrLoadDesktopEntry(id);
			if (!desktop_entry_opt.has_value()) {
				continue;
			}

			const DesktopEntry* desktop_entry_ptr = &desktop_entry_opt.value();

			std::vector<std::string> entry_mimes = SplitString(desktop_entry_ptr->mimetype, ';');
			for (const auto& mime : entry_mimes) {
				if (!mime.empty()) {
					index[mime].push_back(desktop_entry_ptr);
				}
			}
		}
		return index;
	}


	XDGBasedAppProvider::MimeToDesktopAssociationsIndex XDGBasedAppProvider::ParseMimeinfoCaches()
	{
		if (_op_state->desktop_file_dirpaths.empty()) {
			return {};
		}

		MimeToDesktopAssociationsIndex mime_to_desktop_associations_index;
		for (const auto& dirpath : _op_state->desktop_file_dirpaths) {
			CheckCancellation();
			std::string filepath = dirpath + "/mimeinfo.cache";
			if (IsReadableFile(filepath)) {
				ParseMimeinfoCache(filepath, mime_to_desktop_associations_index);
			}
		}
		return mime_to_desktop_associations_index;
	}


	void XDGBasedAppProvider::ParseMimeinfoCache(const std::string& filepath, MimeToDesktopAssociationsIndex& mime_to_desktop_associations_index)
	{
		std::ifstream file(filepath);
		if (!file.is_open()) {
			return;
		}

		std::string line;
		bool in_cache_section = false;
		while (std::getline(file, line)) {
			std::string_view line_v = TrimToStrV(line);
			if (line_v.empty() || line_v[0] == '#') {
				continue;
			}

			if (line_v == "[MIME Cache]") {
				in_cache_section = true;
				continue;
			}

			if (line_v[0] == '[') {
				in_cache_section = false;
				continue;
			}

			if (!in_cache_section) {
				continue;
			}

			size_t eq_pos = line_v.find('=');
			if (eq_pos == std::string_view::npos) {
				continue;
			}

			std::string mime = TrimToStr(line_v.substr(0, eq_pos));
			auto desktop_ids = SplitString(line_v.substr(eq_pos + 1), ';');

			if (mime.empty() || desktop_ids.empty()) {
				continue;
			}

			std::vector<DesktopAssociation>& existing = mime_to_desktop_associations_index[mime];
			for (const std::string& desktop_id : desktop_ids) {
				existing.push_back(DesktopAssociation(desktop_id, filepath));
			}
		}
	}


	XDGBasedAppProvider::MimeappsListsConfig XDGBasedAppProvider::ParseMimeappsLists()
	{
		if (_op_state->mimeapps_list_filepaths.empty()) {
			return {};
		}
		MimeappsListsConfig mimeapps_lists;
		for (const auto& filepath : _op_state->mimeapps_list_filepaths) {
			CheckCancellation();
			ParseMimeappsList(filepath, mimeapps_lists);
		}
		return mimeapps_lists;
	}


	void XDGBasedAppProvider::ParseMimeappsList(const std::string& filepath, MimeappsListsConfig& mimeapps_lists)
	{
		std::ifstream file(filepath);
		if (!file.is_open()) {
			return;
		}

		std::string line, current_section;
		while (std::getline(file, line)) {
			std::string_view line_v = TrimToStrV(line);
			if (line_v.empty() || line_v[0] == '#') {
				continue;
			}

			if (line_v[0] == '[' && line_v.back() == ']') {
				current_section = std::string(line_v);
				continue;
			}

			size_t eq_pos = line_v.find('=');
			if (eq_pos == std::string_view::npos) {
				continue;
			}

			std::string mime = TrimToStr(line_v.substr(0, eq_pos));
			auto desktop_ids = SplitString(line_v.substr(eq_pos + 1), ';');

			if (mime.empty() || desktop_ids.empty()) {
				continue;
			}

			// By passing the partially built 'mimeapps_lists' to IsAssociationRemoved(), we check the candidate application
			// against only those records from [Removed Associations] that have been accumulated from higher-priority files
			// parsed up to this point - preventing later system-level removals from overriding earlier user-level additions.

			if (current_section == "[Default Applications]") {
				for (const std::string& desktop_id : desktop_ids) {
					if (!IsAssociationRemoved(mime, desktop_id, mimeapps_lists)) {
						mimeapps_lists.defaults[mime].push_back(DesktopAssociation(desktop_id, filepath));
					}
				}
			} else if (current_section == "[Added Associations]") {
				for (const std::string& desktop_id : desktop_ids) {
					if (!IsAssociationRemoved(mime, desktop_id, mimeapps_lists)) {
						mimeapps_lists.added[mime].push_back(DesktopAssociation(desktop_id, filepath));
					}
				}
			} else if (current_section == "[Removed Associations]") {
				for (const std::string& desktop_id : desktop_ids) {
					mimeapps_lists.removed[mime].insert(desktop_id);
				}
			}
		}
	}


	// Parses a .desktop file, validates Type=Application, Hidden, Exec, and Name,
	// and extracts optional fields with localization support. Returns nullopt on any validation failure.
	std::optional<XDGBasedAppProvider::DesktopEntry> XDGBasedAppProvider::ParseDesktopFile(const std::string& filepath)
	{
		std::ifstream file(filepath);
		if (!file.is_open()) {
			return std::nullopt;
		}

		std::string line;
		bool in_main_section = false;
		DesktopEntry desktop_entry;
		desktop_entry.desktop_filepath = filepath;

		std::unordered_map<std::string, std::string> kv_entries;

		// Parse key-value pairs from the [Desktop Entry] section only.
		while (std::getline(file, line)) {
			std::string_view line_v = TrimToStrV(line);

			if (line_v.empty() || line_v[0] == '#') {
				continue;
			}
			if (line_v == "[Desktop Entry]") {
				in_main_section = true;
				continue;
			}
			if (line_v[0] == '[') {
				in_main_section = false;
				continue;
			}
			if (!in_main_section) {
				continue;
			}
			size_t eq_pos = line_v.find('=');
			if (eq_pos == std::string_view::npos) {
				continue;
			}

			std::string_view key_v = TrimToStrV(line_v.substr(0, eq_pos));
			std::string_view value_v = TrimToStrV(line_v.substr(eq_pos + 1));
			if (!key_v.empty()) {
				kv_entries.emplace(key_v, value_v);
			}
		}

		// Validate required fields and application type according to the spec.
		bool is_application = false;
		if (auto it = kv_entries.find("Type"); it != kv_entries.end() && it->second == "Application") {
			is_application = true;
		}

		bool hidden = false;
		if (auto it = kv_entries.find("Hidden"); it != kv_entries.end() && it->second == "true") {
			hidden = true;
		}

		// Ignore hidden entries and non-applications
		if (hidden || !is_application) {
			return std::nullopt;
		}

		// An application must have a non-empty Exec field.
		if (auto it = kv_entries.find("Exec"); it == kv_entries.end() || it->second.empty()) {
			return std::nullopt;
		} else {
			desktop_entry.exec = it->second;
		}

		// The Name field is required for a valid desktop entry.
		desktop_entry.name = GetLocalizedValue(kv_entries, "Name");
		if (desktop_entry.name.empty()) {
			return std::nullopt;
		}

		// Extract optional fields with localization support.
		desktop_entry.generic_name = GetLocalizedValue(kv_entries, "GenericName");
		desktop_entry.comment = GetLocalizedValue(kv_entries, "Comment");

		static constexpr std::pair<const char*, std::string DesktopEntry::*> field_map[] = {
			{"Categories",         &DesktopEntry::categories},
			{"TryExec",            &DesktopEntry::try_exec},
			{"Terminal",           &DesktopEntry::terminal},
			{"MimeType",           &DesktopEntry::mimetype},
			{"OnlyShowIn",         &DesktopEntry::only_show_in},
			{"NotShowIn",          &DesktopEntry::not_show_in},
			{"X-Flatpak",          &DesktopEntry::x_flatpak},
			{"X-SnapInstanceName", &DesktopEntry::x_snap_instance_name}
		};

		for (const auto& [key, member_ptr] : field_map) {
			if (auto it = kv_entries.find(key); it != kv_entries.end()) {
				desktop_entry.*member_ptr = it->second;
			}
		}

		if (_show_package_tags) {
			desktop_entry.package_type = DetectPackageType(desktop_entry);
		}

		return desktop_entry;
	}


	XDGBasedAppProvider::PackageType XDGBasedAppProvider::DetectPackageType(const DesktopEntry& entry) const
	{
		if (!entry.x_flatpak.empty())            return PackageType::Flatpak;
		if (!entry.x_snap_instance_name.empty()) return PackageType::Snap;
		std::string exec = UnescapeGKeyFileString(entry.exec);
		std::string_view ev = exec;
		if (ev.find("flatpak run") != std::string_view::npos) return PackageType::Flatpak;
		if (ev.find("snap run")    != std::string_view::npos) return PackageType::Snap;
		if (ev.find("/snap/bin/")  != std::string_view::npos) return PackageType::Snap;

		return PackageType::None;
	}


	// Resolves the value of a key based on the current locale settings.
	// If no localized key is found, it falls back to the default (unlocalized) key.
	std::string XDGBasedAppProvider::GetLocalizedValue(const std::unordered_map<std::string, std::string>& kv_entries,
													   const std::string& base_key) const
	{
		std::string key_buffer;
		key_buffer.reserve(base_key.size() + 16);
		key_buffer = base_key;
		const size_t base_len = base_key.size();
		for (const auto& suffix : _op_state->locale_suffixes) {
			key_buffer.push_back('[');
			key_buffer.append(suffix);
			key_buffer.push_back(']');
			if (auto it = kv_entries.find(key_buffer); it != kv_entries.end()) {
				return it->second;
			}
			key_buffer.resize(base_len);
		}
		auto it = kv_entries.find(base_key);
		return (it != kv_entries.end()) ? it->second : "";
	}


	std::vector<std::string> XDGBasedAppProvider::GenerateLocaleSuffixes()
	{
		const char* locale_env_vars[] = {"LC_ALL", "LC_MESSAGES", "LANG"};
		std::string locale;
		for (const char* var : locale_env_vars) {
			if (const char* val = std::getenv(var); val && *val) {
				locale = val;
				break;
			}
		}
		if (locale.empty()) {
			return {};
		}
		// Locale must be of the form: lang[_COUNTRY][.ENCODING][@MODIFIER]
		// According to the Desktop Entry Specification the encoding part (e.g., ".UTF-8") is ignored when matching.
		if (size_t dot = locale.find('.'); dot != std::string::npos) {
			size_t at = locale.find('@');
			if (at != std::string::npos && at > dot) {
				locale.erase(dot, at - dot); // Remove encoding between '.' and '@'
			} else {
				locale.resize(dot); // Remove trailing encoding
			}
		}
		std::vector<std::string> suffixes;
		suffixes.reserve(4);
		suffixes.push_back(locale); // 1. Full locale match
		size_t at = locale.find('@');
		size_t under = locale.find('_');
		if (at != std::string::npos) {
			suffixes.push_back(locale.substr(0, at)); // 2. Match without modifier
		}
		if (under != std::string::npos) {
			if (at != std::string::npos) {
				suffixes.push_back(locale.substr(0, under) + locale.substr(at)); // 3. Match language with modifier
			}
			suffixes.push_back(locale.substr(0, under)); // 4. Match language only
		}
		return suffixes;
	}


	std::unordered_map<std::string, std::string> XDGBasedAppProvider::LoadMimeAliases()
	{
		if (_op_state->mime_database_dirpaths.empty()) {
			return {};
		}

		std::unordered_map<std::string, std::string> alias_to_canonical_map;

		// Iterate paths from high priority (user) to low priority (system).
		for (const auto& dirpath : _op_state->mime_database_dirpaths) {
			CheckCancellation();
			std::string alias_filepath = dirpath + "/aliases";
			std::ifstream file(alias_filepath);
			if (!file.is_open()) {
				continue;
			}

			std::string line;
			while (std::getline(file, line)) {
				std::string_view line_v = TrimToStrV(line);
				if (line_v.empty() || line_v[0] == '#') {
					continue;
				}
				size_t space_pos = line_v.find_first_of(" \t");
				if (space_pos != std::string_view::npos) {
					std::string alias_mime = TrimToStr(line_v.substr(0, space_pos));
					std::string canonical_mime = TrimToStr(line_v.substr(space_pos + 1));
					if (!alias_mime.empty() && !canonical_mime.empty()) {
						// Use try_emplace to ensure that only the first-encountered alias
						// (from the highest-priority file) is inserted.
						alias_to_canonical_map.try_emplace(std::move(alias_mime), std::move(canonical_mime));
					}
				}
			}
		}
		return alias_to_canonical_map;
	}


	std::string_view XDGBasedAppProvider::GetMajorMimeType(std::string_view mime)
	{
		size_t slash_pos = mime.find('/');
		if (slash_pos == std::string_view::npos || slash_pos == 0) {
			return {};
		}
		// image/jpeg -> image
		return mime.substr(0, slash_pos);
	}


	std::unordered_map<std::string, std::string> XDGBasedAppProvider::LoadMimeSubclasses()
	{
		if (_op_state->mime_database_dirpaths.empty()) {
			return {};
		}

		std::unordered_map<std::string, std::string> subclass_to_parent_map;

		// Iterate in reverse order (system -> user) so that user-defined entries,
		// inserted last, overwrite system ones in the map.
		for (auto it = _op_state->mime_database_dirpaths.rbegin(); it != _op_state->mime_database_dirpaths.rend(); ++it) {
			CheckCancellation();
			std::string subclasses_filepath = *it + "/subclasses";
			std::ifstream file(subclasses_filepath);
			if (!file.is_open()) {
				continue;
			}

			std::string line;
			while (std::getline(file, line)) {
				std::string_view line_v = TrimToStrV(line);
				if (line_v.empty() || line_v[0] == '#') {
					continue;
				}
				size_t space_pos = line_v.find_first_of(" \t");
				if (space_pos != std::string_view::npos) {
					std::string child_mime = TrimToStr(line_v.substr(0, space_pos));
					std::string parent_mime = TrimToStr(line_v.substr(space_pos + 1));
					if (!child_mime.empty() && !parent_mime.empty()) {
						subclass_to_parent_map[std::move(child_mime)] = std::move(parent_mime);
					}
				}
			}
		}
		return subclass_to_parent_map;
	}


	std::vector<XDGBasedAppProvider::GlobRule> XDGBasedAppProvider::LoadGlobRules()
	{
		if (_op_state->mime_database_dirpaths.empty()) {
			return {};
		}

		std::vector<GlobRule> rules;
		int current_source_rank = 0;

		// Iterate in reverse order (system -> user). This assigns a higher source rank to user-specific directories,
		// ensuring that rules defined by the user take precedence over system rules when weights are equal.
		for (auto it = _op_state->mime_database_dirpaths.rbegin(); it != _op_state->mime_database_dirpaths.rend(); ++it) {
			CheckCancellation();
			std::string globs_path = *it + "/globs2";
			if (IsReadableFile(globs_path)) {
				ParseGlobs2File(globs_path, rules, current_source_rank);
			}
			current_source_rank++;
		}

		std::sort(rules.begin(), rules.end());
		return rules;
	}


	// Parses a single 'globs2' file and updates the accumulated rules vector.
	// Handles the 'weight:mime:pattern:flags' format and the special '__NOGLOBS__' directive.
	void XDGBasedAppProvider::ParseGlobs2File(const std::string& filepath, std::vector<GlobRule>& rules, int source_rank)
	{
		std::ifstream file(filepath);
		if (!file.is_open()) {
			return;
		}

		std::string line;
		while (std::getline(file, line)) {
			if (line.empty() || line[0] == '#') {
				continue;
			}

			size_t first_colon = line.find(':');
			if (first_colon == std::string::npos) {
				continue;
			}

			size_t second_colon = line.find(':', first_colon + 1);
			if (second_colon == std::string::npos) {
				continue;
			}

			int weight = 50;
			try {
				weight = std::stoi(line.substr(0, first_colon));
			} catch (const std::exception&) {
				continue;
			}

			std::string_view mime_v = TrimToStrV(std::string_view(line).substr(first_colon + 1, second_colon - first_colon - 1));

			std::string pattern;
			std::string flags_str;

			size_t third_colon = line.find(':', second_colon + 1);

			if (third_colon == std::string::npos) {
				pattern = line.substr(second_colon + 1);
			} else {
				pattern = line.substr(second_colon + 1, third_colon - second_colon - 1);

				size_t fourth_colon = line.find(':', third_colon + 1);
				if (fourth_colon == std::string::npos) {
					flags_str = line.substr(third_colon + 1);
				} else {
					flags_str = line.substr(third_colon + 1, fourth_colon - third_colon - 1);
				}
			}

			// Handle the 'glob-deleteall' mechanism.
			// If the pattern is "__NOGLOBS__", we must discard all previously accumulated rules
			// for this specific MIME type (simulating a reset/override from a higher-priority directory).
			if (pattern == "__NOGLOBS__") {
				rules.erase(std::remove_if(rules.begin(), rules.end(),
										   [mime_v](const GlobRule& r) { return r.mime_type == mime_v; }),
							rules.end());
				continue;
			}

			bool case_sensitive = false;
			if (!flags_str.empty()) {
				const auto flags = SplitString(flags_str, ',');
				case_sensitive = (std::find(flags.begin(), flags.end(), "cs") != flags.end());
			}

			if (!mime_v.empty() && !pattern.empty()) {
				// Pre-calculate is_literal to optimize sorting performance later.
				bool is_literal = IsLiteralPattern(pattern);
				rules.push_back({weight, std::string(mime_v), std::move(pattern), case_sensitive, source_rank, is_literal});
			}
		}
	}


	bool XDGBasedAppProvider::IsLiteralPattern(const std::string& pattern)
	{
		return pattern.find_first_of("*?[") == std::string::npos;
	}


	std::vector<std::string> XDGBasedAppProvider::GetDesktopFileDirpaths()
	{
		std::vector<std::string> dirpaths;
		std::unordered_set<std::string> seen_dirpaths;

		auto add_path = [&](const std::string& p) {
			if (!p.empty() && seen_dirpaths.insert(p).second && IsTraversableDirectory(p)) {
				dirpaths.push_back(p);
			}
		};

		std::string home = GetEnv("HOME");
		std::string xdg_data_home = GetEnv("XDG_DATA_HOME");
		std::string xdg_data_dirs = GetEnv("XDG_DATA_DIRS", "/usr/local/share:/usr/share");

		// User-specific data directory ($XDG_DATA_HOME or ~/.local/share) has the highest priority.
		if (!xdg_data_home.empty() && xdg_data_home[0] == '/') {
			add_path(xdg_data_home + "/applications");
		} else if (!home.empty()) {
			add_path(home + "/.local/share/applications");
		}

		// System-wide data directories ($XDG_DATA_DIRS) have lower priority.
		for (const auto& dir : SplitString(xdg_data_dirs, ':')) {
			if (dir.empty() || dir[0] != '/') {
				continue;
			}
			add_path(dir + "/applications");
		}

		// Additional common paths for Flatpak and Snap applications.
		if (!home.empty()) {
			add_path(home + "/.local/share/flatpak/exports/share/applications");
		}
		add_path("/var/lib/flatpak/exports/share/applications");
		add_path("/var/lib/snapd/desktop/applications");

		return dirpaths;
	}


	std::vector<std::string> XDGBasedAppProvider::GetMimeappsListFilepaths()
	{
		std::vector<std::string> filepaths;
		std::unordered_set<std::string> seen_filepaths;
		filepaths.reserve(16);

		auto add_path = [&](std::string path) {
			if (!path.empty() && seen_filepaths.insert(path).second && IsReadableFile(path)) {
				filepaths.push_back(std::move(path));
			}
		};

		struct SearchBase
		{
			std::string colon_paths;
			bool is_legacy; // If true, implies XDG_DATA_* dirs where "/applications" suffix is required.
		};

		std::vector<SearchBase> bases;
		bases.reserve(4);

		// Level 1: User configuration (XDG_CONFIG_HOME)
		std::string xdg_config_home = GetEnv("XDG_CONFIG_HOME");
		if (!xdg_config_home.empty() && xdg_config_home[0] != '/') {
			xdg_config_home.clear();
		}
		if (xdg_config_home.empty()) {
			std::string home = GetEnv("HOME");
			if (!home.empty()) {
				xdg_config_home = home + "/.config";
			}
		}
		if (!xdg_config_home.empty() && xdg_config_home[0] == '/') {
			bases.push_back({std::move(xdg_config_home), false});
		}

		// Level 2: System configuration (XDG_CONFIG_DIRS)
		bases.push_back({GetEnv("XDG_CONFIG_DIRS", "/etc/xdg"), false});

		// Level 3: User data (legacy) (XDG_DATA_HOME)
		std::string xdg_data_home = GetEnv("XDG_DATA_HOME");
		if (xdg_data_home.empty()) {
			std::string home = GetEnv("HOME");
			if (!home.empty()) {
				xdg_data_home = home + "/.local/share";
			}
		}
		if (!xdg_data_home.empty()) {
			bases.push_back({std::move(xdg_data_home), true});
		}

		// Level 4: Distribution data (legacy) (XDG_DATA_DIRS)
		bases.push_back({GetEnv("XDG_DATA_DIRS", "/usr/local/share:/usr/share"), true});

		// Files are collected in strict priority order (highest first: user -> system).
		// ParseMimeappsList() processes them sequentially in this order, which is essential:
		// [Removed Associations] from a lower-priority file cannot retroactively invalidate
		// [Added Associations] from a higher-priority one already processed.
		for (const SearchBase& base : bases) {
			for (const auto& raw_dir : SplitString(base.colon_paths, ':')) {
				if (raw_dir.empty() || raw_dir[0] != '/') {
					continue;
				}
				std::string dir = raw_dir;
				if (dir.back() != '/') {
					dir += '/';
				}
				// For XDG_DATA_HOME and XDG_DATA_DIRS, the spec requires the files to be in the "applications" subdirectory.
				if (base.is_legacy) {
					dir += "applications/";
				}
				// 1. Desktop-specific associations: $desktop-mimeapps.list
				for (const auto& current_desktop_name : _op_state->current_desktop_names) {
					if (current_desktop_name.empty()) {
						continue;
					}
					std::string filename = ToLowerASCII(current_desktop_name) + "-mimeapps.list";
					add_path(dir + filename);
				}
				// 2. Generic associations: mimeapps.list
				add_path(dir + "mimeapps.list");
			}
		}
		return filepaths;
	}


	// Returns XDG-compliant search paths for MIME database files ('aliases', 'subclasses', etc.)
	std::vector<std::string> XDGBasedAppProvider::GetMimeDbDirpaths()
	{
		std::vector<std::string> dirpaths;
		std::unordered_set<std::string> seen_dirpaths;

		auto add_path = [&](const std::string& p) {
			if (!p.empty() && seen_dirpaths.insert(p).second && IsTraversableDirectory(p)) {
				dirpaths.push_back(p);
			}
		};

		std::string home = GetEnv("HOME");
		std::string xdg_data_home = GetEnv("XDG_DATA_HOME");
		std::string xdg_data_dirs = GetEnv("XDG_DATA_DIRS", "/usr/local/share:/usr/share");

		if (!xdg_data_home.empty() && xdg_data_home[0] == '/') {
			add_path(xdg_data_home + "/mime");
		} else if (!home.empty()) {
			add_path(home + "/.local/share/mime");
		}

		for (const auto& dir : SplitString(xdg_data_dirs, ':')) {
			if (dir.empty() || dir[0] != '/') {
				continue;
			}
			add_path(dir + "/mime");
		}
		return dirpaths;
	}



	// ****************************** Launch command construction ******************************


	// Performs lazy analysis of the Exec string: unescapes GKeyFile sequences, tokenizes,
	// and scans for field codes to determine the ExecutionModel.
	void XDGBasedAppProvider::AnalyzeExecLine(const DesktopEntry& desktop_entry)
	{
		if (desktop_entry.is_exec_parsed) {
			return;
		}

		// 1. Unescape string values (e.g., \s, \n) as per GKeyFile format rules.
		// Note: This must happen *before* parsing arguments and quotes.
		std::string exec_unescaped = UnescapeGKeyFileString(desktop_entry.exec);

		// 2. Tokenize the command line adhering to the specific quoting rules.
		desktop_entry.arg_templates = TokenizeExecString(exec_unescaped);

		// 3. Determine the ExecutionModel by scanning for specific field codes (%f, %F, etc.).
		desktop_entry.execution_model = ExecutionModel::ImplicitFileAppend;

		for (const ExecArgTemplate& arg_template : desktop_entry.arg_templates) {
			// Spec: field codes must not be used inside a quoted argument.
			if (arg_template.is_quoted_literal) {
				continue;
			}

			// Check for FileList codes (%F, %U) which dictate a single process invocation for multiple files.
			if (arg_template.value == "%F" || arg_template.value == "%U") {
				desktop_entry.execution_model = ExecutionModel::FileList;
				break;
			}

			// Check PerFile codes (%f, %u) only if FileList was not detected above.
			size_t pos = 0;
			while ((pos = arg_template.value.find('%', pos)) != std::string::npos) {
				if (pos + 1 < arg_template.value.length()) {
					char next = arg_template.value[pos + 1];
					if (next == 'f' || next == 'u') {
						desktop_entry.execution_model = ExecutionModel::PerFile;
						break;
					}
					// Skip escaped percent "%%" to avoid false positives.
					if (next == '%') pos++;
				}
				pos++;
			}
		}

		desktop_entry.is_exec_parsed = true;
	}


	// Parses the GKeyFile-unescaped 'Exec' key string into a sequence of argument templates.
	std::vector<XDGBasedAppProvider::ExecArgTemplate> XDGBasedAppProvider::TokenizeExecString(const std::string& exec_value)
	{
		if (exec_value.empty()) {
			return {};
		}

		std::vector<ExecArgTemplate> tokens;
		tokens.reserve(4);

		std::string token_buffer;
		token_buffer.reserve(32);

		bool escape_pending = false;
		bool is_inside_quotes = false;

		// Tracks if the current token involved quotes. This allows distinguishing
		// an explicit empty string ("") from a mere whitespace delimiter.
		bool contains_quoted_part = false;

		auto flush_token = [&]() {
			// Collapses consecutive whitespace into a single delimiter while preserving explicitly quoted empty strings.
			if (!token_buffer.empty() || contains_quoted_part) {
				tokens.push_back({std::move(token_buffer), contains_quoted_part});
				token_buffer.clear();
				contains_quoted_part = false;
			}
		};

		for (char c : exec_value) {
			if (escape_pending) {
				// Per Spec: inside quotes, backslash acts as an escape only for ` " $ \.
				// Otherwise, it is preserved as a literal char.
				if (is_inside_quotes && (c != '`' && c != '"' && c != '$' && c != '\\')) {
					token_buffer += '\\';
				}
				token_buffer += c;
				escape_pending = false;
				continue;
			}

			if (c == '\\') {
				escape_pending = true;
				continue;
			}

			if (is_inside_quotes) {
				if (c == '"') {
					is_inside_quotes = false;
				} else {
					token_buffer += c;
				}
			} else {
				if (c == '"') {
					is_inside_quotes = true;
					// Mark this token as semantically meaningful, even if empty.
					contains_quoted_part = true;
				} else if (c == ' ') {
					flush_token();
				} else {
					token_buffer += c;
				}
			}
		}

		// Flush the final argument.
		flush_token();

		return tokens;
	}


	// Constructs a final, shell-safe command line string for a specific invocation.
	// This function handles argument expansion and the fallback logic for implicit file passing.
	std::string XDGBasedAppProvider::AssembleLaunchCommand(const DesktopEntry& desktop_entry, const std::vector<std::string>& filepaths) const
	{
		std::string command_line;

		auto append_argument = [&](const std::string& argument) {
			if (!command_line.empty()) {
				command_line += ' ';
			}
			command_line += argument;
		};

		// 1. Process the explicit command line template from the .desktop Exec key.
		for (const ExecArgTemplate& arg_template : desktop_entry.arg_templates) {
			for (const auto& expanded_arg : ExpandFieldCodes(arg_template, filepaths, desktop_entry)) {
				append_argument(expanded_arg);
			}
		}

		// 2. Legacy behavior: If no execution model was deduced from field codes, explicitly append native file paths.
		if (desktop_entry.execution_model == ExecutionModel::ImplicitFileAppend) {
			for (const auto& filepath : filepaths) {
				append_argument(QuoteArgForShell(filepath));
			}
		}

		return command_line;
	}


	// Expands a single argument template by substituting field codes with actual data.
	std::vector<std::string> XDGBasedAppProvider::ExpandFieldCodes(const ExecArgTemplate& arg_template, const std::vector<std::string>& filepaths, const DesktopEntry& desktop_entry) const
	{
		// Fast-path: skip expansion for quoted arguments or if no field codes are present.
		if (arg_template.is_quoted_literal || arg_template.value.find('%') == std::string::npos) {
			return { QuoteArgForShell(arg_template.value) };
		}

		const std::string& tmpl = arg_template.value;

		// Handle list codes (%F, %U).
		// These expand into multiple separate arguments, one for each file.
		if (tmpl == "%F" || tmpl == "%U") {
			std::vector<std::string> result;
			result.reserve(filepaths.size());
			const bool should_convert_to_uri = (tmpl == "%U" && !_treat_urls_as_paths);
			for (const auto& filepath : filepaths) {
				result.push_back(QuoteArgForShell(should_convert_to_uri ? PathToUri(filepath) : filepath));
			}
			return result;
		}

		// Handle other codes (%f, %u, %c, etc.).
		// These expand into a single argument string, potentially concatenating data.

		const std::string context_filepath = filepaths.empty() ? std::string() : filepaths.front();
		std::string expanded_token;

		expanded_token.reserve(tmpl.size() + 64);

		for (size_t i = 0; i < tmpl.size(); ++i) {
			if (tmpl[i] != '%' || i + 1 >= tmpl.size()) {
				expanded_token += tmpl[i];
				continue;
			}

			char code = tmpl[++i]; // advance to the character following '%'

			switch (code) {
			case '%':
				// "%%" expands to a literal "%".
				expanded_token += '%';
				break;

			case 'f':
				expanded_token += context_filepath;
				break;

			case 'u':
				expanded_token += (_treat_urls_as_paths ? context_filepath : PathToUri(context_filepath));
				break;

			case 'c':
				expanded_token += UnescapeGKeyFileString(desktop_entry.name);
				break;

			case 'k':
				expanded_token += desktop_entry.desktop_filepath;
				break;

			case 'i':
				// The %i code is supposed to expand to two arguments: "--icon" and the Icon key value.
				// We don't support icons, so we remove the entire argument.
				return {};

			case 'd': case 'D': case 'n': case 'N': case 'v': case 'm':
				// Deprecated codes: the spec states these should be removed and ignored.
				break;

			default:
				// Deviation from Spec: unknown codes are preserved as literals rather than
				// invalidating the command (for robustness).
				expanded_token += '%';
				expanded_token += code;
				break;
			}
		}

		if (expanded_token.empty()) {
			return {};
		}

		return { QuoteArgForShell(expanded_token) };
	}


	std::string XDGBasedAppProvider::PathToUri(const std::string& path)
	{
		if (path.empty() || path[0] != '/') {
			return {};
		}

		static constexpr char hex_digits[] = "0123456789ABCDEF";
		std::string uri;
		uri.reserve(7 + path.size() * 3 / 2);
		uri.append("file://");
		for (const unsigned char c : path) {
			// Perform a locale-independent check for unreserved characters (RFC 3986) plus the path separator '/'.
			if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
				|| c == '-' || c == '_' || c == '.' || c == '~' || c == '/') {
				uri.push_back(static_cast<char>(c));
			} else {
				// Percent-encode all other characters.
				uri.push_back('%');
				uri.push_back(hex_digits[(c >> 4) & 0xF]);
				uri.push_back(hex_digits[c & 0xF]);
			}
		}
		return uri;
	}


	// Performs the first-pass un-escaping for a raw string from a .desktop file.
	// This handles general GKeyFile-style escape sequences like '\\' -> '\', '\s' -> ' ', etc.
	std::string XDGBasedAppProvider::UnescapeGKeyFileString(const std::string& str)
	{
		std::string unescaped;
		unescaped.reserve(str.length());

		for (size_t i = 0; i < str.length(); ++i) {
			if (str[i] == '\\') {
				if (i + 1 < str.length()) {
					i++; // move to the character after the backslash
					switch (str[i]) {
					case 's': unescaped += ' '; break;
					case 'n': unescaped += '\n'; break;
					case 't': unescaped += '\t'; break;
					case 'r': unescaped += '\r'; break;
					case '\\': unescaped += '\\'; break;
					default:
						// For unknown escape sequences, the backslash is dropped
						// and the character is preserved.
						unescaped += str[i];
						break;
					}
				} else {
					// A trailing backslash at the end of the string is treated as a literal.
					unescaped += '\\';
				}
			} else {
				unescaped += str[i];
			}
		}
		return unescaped;
	}



	// ****************************** System, environment, and common helpers ******************************


	bool XDGBasedAppProvider::IsReadableFile(const std::string& filepath)
	{
		if (filepath.empty()) {
			return false;
		}
		struct stat st;
		return stat(filepath.c_str(), &st) == 0
			&& S_ISREG(st.st_mode)
			&& access(filepath.c_str(), R_OK) == 0;
	}


	bool XDGBasedAppProvider::IsTraversableDirectory(const std::string& dirpath)
	{
		if (dirpath.empty()) {
			return false;
		}
		struct stat st;
		return stat(dirpath.c_str(), &st) == 0
			&& S_ISDIR(st.st_mode)
			&& access(dirpath.c_str(), X_OK) == 0;
	}


	bool XDGBasedAppProvider::IsExecutableAvailable(const std::string& command, std::string *out_resolved_filepath)
	{
		if (command.empty()) {
			return false;
		}

		auto check_and_save = [out_resolved_filepath](const std::string& filepath) {
			struct stat st;
			if (stat(filepath.c_str(), &st) == 0 && S_ISREG(st.st_mode) && access(filepath.c_str(), X_OK) == 0) {
				if (out_resolved_filepath) {
					*out_resolved_filepath = filepath;
				}
				return true;
			}
			return false;
		};

		// Absolute path? Check it directly.
		if (command[0] == '/') {
			return check_and_save(command);
		}

		// Otherwise, look up in $PATH.
		auto dirs = SplitString(GetEnv("PATH"), ':');
		for (const auto& dir : dirs) {
			if (dir.empty()) {
				continue;
			}
			std::string filepath = (dir.back() == '/') ? (dir + command) : (dir + '/' + command);
			if (check_and_save(filepath)) {
				return true;
			}
		}
		return false;
	}


	std::string XDGBasedAppProvider::RunCommandAndCaptureOutput(const std::string& cmd)
	{
		std::string result;
		if (cmd.empty() || !POpen(result, cmd.c_str())) {
			return "";
		}
		return TrimToStr(result);
	}


	std::string XDGBasedAppProvider::GetEnv(const char* var, const char* default_val)
	{
		const char* val = getenv(var);
		if (val && *val != '\0') {
			return val;
		}
		return default_val;
	}


	std::string XDGBasedAppProvider::QuoteArgForShell(const std::string& arg)
	{
		if (arg.empty()) {
			// An empty string is represented as '' in the shell.
			return "''";
		}

		bool is_safe = true;
		for (const unsigned char c : arg) {
			// Perform a locale-independent check for 7-bit ASCII characters.
			if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
				  || c == '/' || c == '.' || c == '_' || c == '-'))
			{
				is_safe = false;
				break;
			}
		}

		if (is_safe) {
			return arg;
		}

		std::string out;
		out.push_back('\'');
		for (const unsigned char uc : arg) {
			if (uc == '\'') {
				// Single quotes cannot be nested. Close the string, add an escaped literal quote,
				// and reopen. Ex: "it's" -> 'it'\''s'.
				out.append("'\\''");
			} else {
				out.push_back(static_cast<char>(uc));
			}
		}
		out.push_back('\'');
		return out;
	}


	std::string XDGBasedAppProvider::GetBaseName(const std::string& filepath)
	{
		size_t pos = filepath.find_last_of('/');
		if (pos == std::string::npos) {
			return filepath;
		}
		return filepath.substr(pos + 1);
	}


	std::string_view XDGBasedAppProvider::TrimToStrV(std::string_view sv) noexcept
	{
		constexpr std::string_view spaces = " \t\n\r\f\v";
		const size_t first = sv.find_first_not_of(spaces);
		if (first == std::string_view::npos) {
			return {};
		}
		const size_t last = sv.find_last_not_of(spaces);
		return sv.substr(first, (last - first) + 1);
	}


	std::string XDGBasedAppProvider::TrimToStr(std::string_view sv)
	{
		return std::string(TrimToStrV(sv));
	}


	std::vector<std::string> XDGBasedAppProvider::SplitString(std::string_view str, char delimiter, SplitBehavior behavior)
	{
		std::vector<std::string> tokens;
		while (true) {
			const size_t pos = str.find(delimiter);
			std::string_view token = TrimToStrV(str.substr(0, pos));
			if (behavior == SplitBehavior::KeepEmpty || !token.empty()) {
				tokens.emplace_back(token);
			}
			if (pos == std::string_view::npos) {
				break;
			}
			str.remove_prefix(pos + 1);
		}
		return tokens;
	}


	std::string XDGBasedAppProvider::ToLowerASCII(std::string str)
	{
		for (char& c : str) {
			if (c >= 'A' && c <= 'Z') {
				c += ('a' - 'A');
			}
		}
		return str;
	}



	// ****************************** RAII cache helper ******************************


	XDGBasedAppProvider::OperationContext::OperationContext(XDGBasedAppProvider& p) : provider(p)
	{
		try {
			p._op_state = OpState{};
			auto& op = *provider._op_state;

			// ----- Phase 1: Environment setup & tool availability checks -----
			op.locale_suffixes = provider.GenerateLocaleSuffixes();
			op.current_desktop_names = provider.SplitString(provider.GetEnv("XDG_CURRENT_DESKTOP"), ':');
			op.xdg_mime_exists = provider.IsExecutableAvailable("xdg-mime");
			op.file_tool_enabled_and_exists = provider._use_file_tool && provider.IsExecutableAvailable("file");
			op.magika_tool_enabled_and_exists = provider._use_magika_tool && provider.IsExecutableAvailable("magika");

			// ----- Phase 2: Path resolution & desktop file indexing -----
			op.desktop_file_dirpaths = provider.GetDesktopFileDirpaths();
			op.mimeapps_list_filepaths = provider.GetMimeappsListFilepaths();
			op.mime_database_dirpaths = provider.GetMimeDbDirpaths();
			op.desktop_id_to_path_index = provider.IndexAllDesktopFiles();

			// ----- Phase 3: Auxiliary MIME databases & user configuration -----

			if (provider._load_mimetype_aliases) {
				op.alias_to_canonical_cache = provider.LoadMimeAliases();
				for (const auto& [alias, canonical] : op.alias_to_canonical_cache) {
					auto alias_major = GetMajorMimeType(alias);
					auto canonical_major = GetMajorMimeType(canonical);
					// Add the alias ONLY if its major type matches the canonical one.
					// This prevents adding, for example, "text/ico" for "image/vnd.microsoft.icon",
					// but allows adding "image/x-icon".
					if (!alias_major.empty() && alias_major == canonical_major) {
						op.canonical_to_aliases_cache[canonical].push_back(alias);
					}
				}
			}

			if (provider._load_mimetype_subclasses) {
				op.subclass_to_parent_cache = provider.LoadMimeSubclasses();
			}

			if (provider._use_glob_rules) {
				op.glob_rules_cache = provider.LoadGlobRules();
			}

			op.mimeapps_lists_cache = provider.ParseMimeappsLists();


			// ----- Phase 4: Association database construction -----
			if (provider._use_mimeinfo_cache) {
				// Attempt to load from 'mimeinfo.cache' first, as per user setting.
				op.mime_to_desktop_associations_index = provider.ParseMimeinfoCaches();
			}

			// If caching is disabled (by user) OR cache was empty...
			if (op.mime_to_desktop_associations_index.empty()) {
				// ...populate the index by performing a full scan.
				op.mime_to_desktop_entry_index = provider.ParseAllDesktopFiles();
			}

		} catch (...) {
			p._op_state.reset();
			throw;
		}
	}


	XDGBasedAppProvider::OperationContext::~OperationContext()
	{
		provider._op_state.reset();  // destroys OpState and all its fields
	}

} // namespace openwith

#endif
