#pragma once

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

#include "AppProvider.hpp"
#include "common.hpp"
#include "lng.hpp"
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include <sys/types.h>

class KeyFileReadHelper;
class KeyFileHelper;

namespace openwith
{
	class XDGBasedAppProvider : public AppProvider
	{

	public:

		XDGBasedAppProvider();
		GetCandidatesResult GetAppCandidates(const std::vector<std::wstring>& filepaths_wide, ProgressCallback progress = nullptr,
											 const std::atomic<bool>* cancel_flag = nullptr) override;
		std::vector<std::wstring> ConstructLaunchCommands(const CandidateInfo& candidate, const std::vector<std::wstring>& filepaths_wide) override;
		std::vector<std::wstring> GetFileTypes() override;
		std::vector<Field> GetCandidateDetails(const CandidateInfo& candidate) override;
		std::vector<CandidateContextLocation> GetCandidateContextLocations(const CandidateInfo& candidate) override;

		// Platform-specific settings API
		std::vector<ProviderSetting> GetPlatformSettings() override;
		void SetPlatformSettings(const std::vector<ProviderSetting>& settings) override;
		void LoadPlatformSettings(const KeyFileReadHelper& key_reader) override;
		void SavePlatformSettings(KeyFileHelper& key_writer) override;

	private:

		// ******************************************************************************
		// Group 1: Core XDG Desktop Entry Types
		// Structures representing the content and logic of .desktop files.
		// ******************************************************************************

		// Defines how the application expects arguments based on Field Codes in the Exec key.
		enum class ExecutionModel
		{
			ImplicitFileAppend, // no field codes found; append files to the end of the command.
			PerFile,            // %f or %u found; launch a separate process for each file.
			FileList            // %F or %U found; pass all files as a list to a single process.
		};


		// Represents a parsed argument template from the Exec key, potentially containing field codes.
		struct ExecArgTemplate
		{
			std::string value;
			bool is_quoted_literal = false; // if true, field codes inside this argument must be ignored.
		};

		// Identifies the packaging system or runtime environment an application originates from.
		enum class PackageType { None, Snap, Flatpak };

		// Represents a parsed .desktop file from the XDG specifications.
		struct DesktopEntry
		{
			std::string id;
			std::string desktop_filepath;
			PackageType package_type = PackageType::None;

			std::string name;
			std::string generic_name;
			std::string comment;
			std::string categories;
			std::string exec;
			std::string try_exec;
			std::string mimetype;
			std::string only_show_in;
			std::string not_show_in;
			std::string terminal;
			std::string x_flatpak;
			std::string x_snap_instance_name;

			// Mutable cache for lazy parsing: command line analysis is performed once when converting the entry to a menu candidate.
			mutable bool is_exec_parsed = false;
			mutable ExecutionModel execution_model = ExecutionModel::ImplicitFileAppend;
			mutable std::vector<ExecArgTemplate> arg_templates;
		};


		// ******************************************************************************
		// Group 2: MIME Detection Types
		// Structures used for identifying file types before application lookup.
		// ******************************************************************************

		// Represents the "raw" MIME profile of a file, derived from all available detection tools before any expansion.
		struct RawMimeProfile
		{
			std::string xdg_mime;		// result from 'xdg-mime query filetype'
			std::string file_mime;		// result from 'file --mime-type'
			std::string magika_mime;	// result from 'magika --format %m'
			std::string globs2_mime;	// result from XDG glob rules matching (globs2)
			std::string ext_mime;		// result from internal extension fallback map
			std::string stat_mime;		// result from internal stat() analysis (e.g., inode/directory)

			bool is_regular_file = false; // true if S_ISREG

			bool operator==(const RawMimeProfile& other) const
			{
				return std::tie(is_regular_file, xdg_mime, file_mime, magika_mime, globs2_mime, ext_mime, stat_mime) ==
					   std::tie(other.is_regular_file, other.xdg_mime, other.file_mime, other.magika_mime, other.globs2_mime, other.ext_mime, other.stat_mime);
			}

			// Custom hash functor required to use RawMimeProfile as a key in std::unordered_map and std::unordered_set.
			struct Hash
			{
				std::size_t operator()(const RawMimeProfile& s) const noexcept
				{
					auto hash_combine = [](std::size_t& seed, std::size_t hash_val) {
						seed ^= hash_val + 0x9e3779b9 + (seed << 6) + (seed >> 2);
					};
					std::size_t seed = std::hash<std::string>{}(s.xdg_mime);
					hash_combine(seed, std::hash<std::string>{}(s.file_mime));
					hash_combine(seed, std::hash<std::string>{}(s.magika_mime));
					hash_combine(seed, std::hash<std::string>{}(s.globs2_mime));
					hash_combine(seed, std::hash<std::string>{}(s.ext_mime));
					hash_combine(seed, std::hash<std::string>{}(s.stat_mime));
					hash_combine(seed, std::hash<bool>{}(s.is_regular_file));
					return seed;
				}
			};
		};


		// Represents a single pattern matching rule from parsed 'globs2' file.
		struct GlobRule
		{
			int weight = 50;
			std::string mime_type;
			std::string pattern;
			bool case_sensitive = false;
			int source_rank = 0;
			bool is_literal = false;

			// Defines the sorting order for glob matching.
			// Returns true if 'this' rule should be checked BEFORE 'other'.
			bool operator<(const GlobRule& other) const
			{
				// Higher weights are checked first.
				if (weight != other.weight) {
					return weight > other.weight;
				}

				// Exact filenames (e.g., 'Makefile') take precedence over globs (*.txt).
				if (is_literal != other.is_literal) {
					return is_literal;
				}

				// Longer patterns are more specific (e.g., '*.tar.gz' > '*.gz').
				if (pattern.length() != other.pattern.length()) {
					return pattern.length() > other.pattern.length();
				}

				// User-defined rules (higher rank) override system rules.
				return source_rank > other.source_rank;
			}
		};


		// ******************************************************************************
		// Group 3: Association Database Types
		// Structures representing parsed data from 'mimeapps.list', 'mimeinfo.cache', etc.
		// ******************************************************************************

		// Links a Desktop ID to the source file ('mimeapps.list' or 'mimeinfo.cache') defining the association.
		struct DesktopAssociation
		{
			std::string desktop_id;
			std::string source_filepath;

			DesktopAssociation() = default;
			DesktopAssociation(const std::string& did, const std::string& sfp)
				: desktop_id(did), source_filepath(sfp) {}
		};


		// Represents the merged content of all parsed 'mimeapps.list' files found in XDG directories.
		struct MimeappsListsConfig
		{
			// MIME type -> list of apps from [Default Applications].
			std::unordered_map<std::string, std::vector<DesktopAssociation>> defaults;
			// MIME type -> list of apps from [Added Associations].
			std::unordered_map<std::string, std::vector<DesktopAssociation>> added;
			// MIME type -> set of apps from [Removed Associations].
			std::unordered_map<std::string, std::unordered_set<std::string>> removed;
		};

		enum class AssociationOriginTier
		{
			SystemDefault,   // xdg-mime query default
			MimeappsDefault, // [Default Applications] in mimeapps.list
			MimeappsAdded,   // [Added Associations] in mimeapps.list
			MimeinfoCache,   // mimeinfo.cache
			FullScan         // fallback scan of all .desktop files
		};

		struct AssociationFact
		{
			AssociationOriginTier tier = AssociationOriginTier::FullScan;
			std::string filepath;
			std::string mime_type;
		};


		// ******************************************************************************
		// Group 4: Ranking & Candidate Identification
		// Structures used in the logic for selecting and sorting the best applications.
		// ******************************************************************************

		// Constants for the tiered ranking system.
		struct Ranking
		{
			// Specificity determines the base tier (Specific MIME > Generic MIME > Fallback).
			static constexpr int SPECIFICITY_MULTIPLIER = 100'000;

			// Within the same specificity tier, the source determines priority.
			static constexpr int SOURCE_RANK_GLOBAL_DEFAULT = 5'000;   // 'xdg-mime query default'
			static constexpr int SOURCE_RANK_MIMEAPPS_DEFAULT = 4'000; // [Default Applications] in 'mimeapps.list'
			static constexpr int SOURCE_RANK_MIMEAPPS_ADDED = 3'000;   // [Added Associations] in 'mimeapps.list'
			static constexpr int SOURCE_RANK_CACHE_OR_SCAN = 2'000;    // 'mimeinfo.cache' or full .desktop scan

			// Maximum intra-tier rank penalty (applied per-position within a single source).
			// Capped so that even the lowest-ranked entry within a tier cannot drop below
			// the base rank of the next lower-priority tier, preserving strict tier ordering.
			static constexpr int MAX_PENALTY_FOR_DEFAULT = SOURCE_RANK_MIMEAPPS_DEFAULT - SOURCE_RANK_MIMEAPPS_ADDED - 1;
			static constexpr int MAX_PENALTY_FOR_ADDED = SOURCE_RANK_MIMEAPPS_ADDED - SOURCE_RANK_CACHE_OR_SCAN - 1;
			static constexpr int MAX_PENALTY_FOR_MIMEINFO_CACHE = SOURCE_RANK_CACHE_OR_SCAN - 1;
		};


		// Holds a non-owning pointer to a cached DesktopEntry and its calculated rank.
		// Used for sorting candidates before display.
		struct RankedCandidate
		{
			const DesktopEntry* desktop_entry = nullptr;
			int rank = 0;

			AssociationFact origin_fact;

			RankedCandidate() = default;
			RankedCandidate(const DesktopEntry* d, int r, AssociationFact f)
				: desktop_entry(d), rank(r), origin_fact(std::move(f)) {}

			RankedCandidate(int r, AssociationFact f)
					: desktop_entry(nullptr), rank(r), origin_fact(std::move(f)) {}

			bool operator<(const RankedCandidate& other) const {
				if (rank != other.rank) {
					return rank > other.rank; // Descending by rank.
				}
				if (desktop_entry && other.desktop_entry) {
					return desktop_entry->name < other.desktop_entry->name;	// Ascending by name.
				}
				return desktop_entry == nullptr && other.desktop_entry != nullptr;
			}
		};


		// Unique identifier for a candidate in the final map.
		// Used to deduplicate identical applications that might be defined in different .desktop files.
		struct AppUniqueKey
		{
			std::string_view name;
			std::string_view exec;

			bool operator==(const AppUniqueKey& other) const {
				return name == other.name && exec == other.exec;
			}

			// Custom hash function to allow AppUniqueKey to be used as a key in std::unordered_map.
			struct Hash
			{
				size_t operator()(const AppUniqueKey& k) const {
					const auto h1 = std::hash<std::string_view>{}(k.name);
					const auto h2 = std::hash<std::string_view>{}(k.exec);
					return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
				}
			};
		};





		// ******************************************************************************
		// Group 5: Plugin Internal Configuration
		// ******************************************************************************

		struct PlatformSettingDefinition
		{
			std::string internal_key;                    // persistent INI key and internal identifier
			MsgID display_name_id;                       // ID to fetch the localized UI label
			bool XDGBasedAppProvider::* member_variable; // pointer to the linked boolean class member
			bool default_value;                          // fallback value if missing in the INI file
			bool affects_candidates;                     // true if changing this setting affects the contents or order of the candidate list
		};


		// ******************************************************************************
		// Group 6: Lifecycle & State Management
		// ******************************************************************************

		using CandidateMap = std::unordered_map<AppUniqueKey, RankedCandidate, AppUniqueKey::Hash>;
		using MimeToDesktopEntryIndex = std::unordered_map<std::string, std::vector<const DesktopEntry*>>;
		using MimeToDesktopAssociationsIndex = std::unordered_map<std::string, std::vector<DesktopAssociation>>;
		using VisitedInodeSet = std::set<std::pair<dev_t, ino_t>>;

		struct OpState
		{
			std::vector<std::string> locale_suffixes;
			std::vector<std::string> current_desktop_names;  // from $XDG_CURRENT_DESKTOP
			bool xdg_mime_exists = false;
			bool file_tool_enabled_and_exists = false;
			bool magika_tool_enabled_and_exists = false;
			std::vector<std::string> desktop_file_dirpaths;
			std::vector<std::string> mimeapps_list_filepaths;
			std::vector<std::string> mime_database_dirpaths;
			std::unordered_map<std::string, std::string> desktop_id_to_path_index;
			std::vector<GlobRule> glob_rules_cache; // from 'globs2'
			std::unordered_map<std::string, std::string> alias_to_canonical_cache;  // from 'aliases'
			std::unordered_map<std::string, std::vector<std::string>> canonical_to_aliases_cache;  // from 'aliases'
			std::unordered_map<std::string, std::string> subclass_to_parent_cache;  // from 'subclasses'
			MimeappsListsConfig mimeapps_lists_cache;  // from 'mimeapps.list'
			std::unordered_map<std::string, std::string> mime_to_default_desktop_id_cache;  // from 'xdg-mime query default'
			MimeToDesktopAssociationsIndex mime_to_desktop_associations_index; 	// from 'mimeinfo.cache'
			MimeToDesktopEntryIndex mime_to_desktop_entry_index;  // from full .desktop scan
		};


		struct OperationContext
		{
			XDGBasedAppProvider& provider;
			explicit OperationContext(XDGBasedAppProvider& p);
			~OperationContext();

			OperationContext(const OperationContext &) = delete;
			OperationContext &operator=(const OperationContext &) = delete;
		};


		// ******************************************************************************
		// METHODS
		// ******************************************************************************

		// --- Candidate search and ranking ---
		void ClearLastQueryCaches();
		CandidateMap DiscoverCandidatesForExpandedMimes(const std::vector<std::string>& expanded_mimes);
		std::string QuerySystemDefaultApplication(const std::string& mime);
		void AppendCandidatesFromMimeappsLists(const std::vector<std::string>& expanded_mimes, CandidateMap& unique_candidates);
		void AppendCandidatesFromMimeinfoCache(const std::vector<std::string>& expanded_mimes, CandidateMap& unique_candidates);
		void AppendCandidatesFromFullScan(const std::vector<std::string>& expanded_mimes, CandidateMap& unique_candidates);
		void RegisterCandidateById(CandidateMap& unique_candidates, const std::string& desktop_id, int rank, const AssociationFact& origin_fact);
		void RegisterCandidateFromDesktopEntry(CandidateMap& unique_candidates, const DesktopEntry& desktop_entry, int rank, const AssociationFact& origin_fact);
		void AddOrUpdateCandidate(CandidateMap& unique_candidates, const DesktopEntry& desktop_entry, int rank, const AssociationFact& origin_fact);
		bool IsAssociationRemoved(const std::string& mime, const std::string& desktop_id, const MimeappsListsConfig& mimeapps_lists) const;
		std::vector<RankedCandidate> BuildSortedCandidates(const CandidateMap& candidate_map);
		std::vector<CandidateInfo> FormatCandidatesForUI(const std::vector<RankedCandidate>& ranked_candidates, bool store_source_info);
		static std::wstring_view GetPackageTag(PackageType type);
		static CandidateInfo ConvertDesktopEntryToCandidateInfo(const DesktopEntry& desktop_entry);
		std::wstring FormatFactForUI(const AssociationFact& fact) const;

		// --- File MIME type detection and expansion ---
		RawMimeProfile GetRawMimeProfile(const std::string& filepath);
		std::vector<std::string> ExpandAndPrioritizeMimeTypes(const RawMimeProfile& profile);
		static std::string DetectMimeTypeWithXdgMimeTool(const std::string& filepath_quoted);
		static std::string DetectMimeTypeWithFileTool(const std::string& filepath_quoted);
		static std::string DetectMimeTypeWithMagikaTool(const std::string& filepath_quoted);
		std::string DetectMimeTypeViaGlobRules(const std::string& filepath);
		static bool GlobMatch(const std::string &text, const std::string &pattern, bool case_sensitive);
		static std::string_view DetectMimeTypeByExtension(const std::string& filepath);
		static const std::unordered_map<std::string_view, std::string_view>& GetExtMimeMap();

		// --- XDG database parsing and caching ---
		std::unordered_map<std::string, std::string> IndexAllDesktopFiles();
		void 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);
		const std::optional<XDGBasedAppProvider::DesktopEntry>& GetOrLoadDesktopEntry(const std::string& desktop_id);
		MimeToDesktopEntryIndex ParseAllDesktopFiles();
		MimeToDesktopAssociationsIndex ParseMimeinfoCaches();
		static void ParseMimeinfoCache(const std::string& filepath, MimeToDesktopAssociationsIndex& mime_to_desktop_associations_index);
		MimeappsListsConfig ParseMimeappsLists();
		void ParseMimeappsList(const std::string& filepath, MimeappsListsConfig& mimeapps_lists);
		std::optional<XDGBasedAppProvider::DesktopEntry> ParseDesktopFile(const std::string& filepath);
		PackageType DetectPackageType(const DesktopEntry& entry) const;
		std::string GetLocalizedValue(const std::unordered_map<std::string, std::string>& kv_entries, const std::string& base_key) const;
		static std::vector<std::string> GenerateLocaleSuffixes();
		std::unordered_map<std::string, std::string> LoadMimeAliases();
		static std::string_view GetMajorMimeType(std::string_view mime);
		std::unordered_map<std::string, std::string> LoadMimeSubclasses();
		std::vector<GlobRule> LoadGlobRules();
		void ParseGlobs2File(const std::string& filepath, std::vector<GlobRule>& rules, int source_rank);
		static bool IsLiteralPattern(const std::string& pattern);
		static std::vector<std::string> GetDesktopFileDirpaths();
		std::vector<std::string> GetMimeappsListFilepaths();
		static std::vector<std::string> GetMimeDbDirpaths();

		// --- Launch command construction ---
		static void AnalyzeExecLine(const DesktopEntry& desktop_entry);
		static std::vector<XDGBasedAppProvider::ExecArgTemplate> TokenizeExecString(const std::string& exec_value);
		std::string AssembleLaunchCommand(const DesktopEntry& desktop_entry, const std::vector<std::string>& files) const;
		std::vector<std::string> ExpandFieldCodes(const ExecArgTemplate& arg_template, const std::vector<std::string>& files, const DesktopEntry& desktop_entry) const;
		static std::string PathToUri(const std::string &path);
		static std::string UnescapeGKeyFileString(const std::string& str);

		// --- System, environment, and common helpers ---
		static bool IsReadableFile(const std::string& filepath);
		static bool IsTraversableDirectory(const std::string& dirpath);
		static bool IsExecutableAvailable(const std::string& command, std::string* out_resolved_path = nullptr);
		static std::string RunCommandAndCaptureOutput(const std::string& cmd);
		static std::string GetEnv(const char* var, const char* default_val = "");
		static std::string QuoteArgForShell(const std::string& arg);
		static std::string GetBaseName(const std::string& filepath);
		static std::string_view TrimToStrV(std::string_view sv) noexcept;
		static std::string TrimToStr(std::string_view sv);

		enum class SplitBehavior { KeepEmpty, SkipEmpty };
		static std::vector<std::string> SplitString(std::string_view str, char delimiter, SplitBehavior behavior = SplitBehavior::SkipEmpty);

		static std::string ToLowerASCII(std::string str);

		// ******************************************************************************
		// DATA MEMBERS
		// ******************************************************************************

		std::map<std::string, std::optional<DesktopEntry>> _desktop_id_to_desktop_entry_cache;

		// Maps a candidate's ID to its source info, i.e. where the association came from (e.g., 'mimeapps.list').
		std::unordered_map<std::string, AssociationFact> _last_association_facts;

		// Caches all unique RawMimeProfile objects collected during the last GetAppCandidates() call.
		// This is used by GetMimeTypes() to avoid redundant work.
		std::unordered_set<RawMimeProfile, RawMimeProfile::Hash> _last_unique_mime_profiles;

		// --- Platform-specific settings (values are loaded from INI) ---
		bool _use_xdg_mime_tool;
		bool _use_file_tool;
		bool _use_magika_tool;
		bool _use_glob_rules;
		bool _use_extension_based_fallback;
		bool _load_mimetype_aliases;
		bool _load_mimetype_subclasses;
		bool _resolve_structured_suffixes;
		bool _use_generic_mime_fallbacks;
		bool _show_universal_handlers;
		bool _query_xdg_mime_default;
		bool _ignore_removed_associations;
		bool _use_mimeinfo_cache;
		bool _filter_by_show_in;
		bool _validate_try_exec;
		bool _sort_alphabetically;
		bool _treat_urls_as_paths;
		bool _show_package_tags;

		// Holds all setting definitions. Initialized once in the constructor.
		std::vector<PlatformSettingDefinition> _platform_settings_definitions;

		// A pre-calculated lookup map (Key -> MemberPtr) for efficient updates in SetPlatformSettings.
		std::map<std::wstring, bool XDGBasedAppProvider::*> _key_wide_to_member_map;

		// Maps internal setting keys to required command-line tools.
		// Used to disable settings in the UI if the required tool is missing.
		inline static const std::map<std::string, std::string> s_tool_key_map = {
			{ "UseXdgMimeTool",      "xdg-mime" },
			{ "UseFileTool",         "file" },
			{ "UseMagikaTool",       "magika" },
			{ "QueryXdgMimeDefault", "xdg-mime" }
		};

		// --- Operation-Scoped State ---
		// Valid only during a GetAppCandidates call. Populated by OperationContext.
		std::optional<OpState> _op_state;

	};

} // namespace openwith

#endif
