using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Flow.Launcher.Core.ExternalPlugins;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using IRemovable = Flow.Launcher.Core.Storage.IRemovable;
using ISavable = Flow.Launcher.Plugin.ISavable;

namespace Flow.Launcher.Core.Plugin
{
    /// <summary>
    /// Class for co-ordinating and managing all plugin lifecycle.
    /// </summary>
    public static class PluginManager
    {
        private static readonly string ClassName = nameof(PluginManager);

        private static readonly ConcurrentDictionary<string, PluginPair> _allLoadedPlugins = [];
        private static readonly ConcurrentDictionary<string, PluginPair> _allInitializedPlugins = [];
        private static readonly ConcurrentDictionary<string, PluginPair> _initFailedPlugins = [];
        private static readonly ConcurrentDictionary<string, PluginPair> _globalPlugins = [];
        private static readonly ConcurrentDictionary<string, PluginPair> _nonGlobalPlugins = [];

        private static PluginsSettings Settings;
        private static readonly ConcurrentBag<string> ModifiedPlugins = [];

        private static readonly ConcurrentBag<PluginPair> _contextMenuPlugins = [];
        private static readonly ConcurrentBag<PluginPair> _homePlugins = [];
        private static readonly ConcurrentBag<PluginPair> _translationPlugins = [];
        private static readonly ConcurrentBag<PluginPair> _externalPreviewPlugins = [];

        /// <summary>
        /// Directories that will hold Flow Launcher plugin directory
        /// </summary>
        public static readonly string[] Directories =
        [
            Constant.PreinstalledDirectory, DataLocation.PluginsDirectory
        ];

        #region Save & Dispose & Reload Plugin

        /// <summary>
        /// Save json and ISavable
        /// </summary>
        public static void Save()
        {
            foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: false))
            {
                var savable = pluginPair.Plugin as ISavable;
                try
                {
                    savable?.Save();
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogException(ClassName, $"Failed to save plugin {pluginPair.Metadata.Name}", e);
                }
            }

            PublicApi.Instance.SavePluginSettings();
            PublicApi.Instance.SavePluginCaches();
        }

        public static async ValueTask DisposePluginsAsync()
        {
            // Still call dispose for all plugins even if initialization failed, so that we can clean up resources
            foreach (var pluginPair in GetAllInitializedPlugins(includeFailed: true))
            {
                await DisposePluginAsync(pluginPair);
            }
        }

        private static async Task DisposePluginAsync(PluginPair pluginPair)
        {
            try
            {
                switch (pluginPair.Plugin)
                {
                    case IDisposable disposable:
                        disposable.Dispose();
                        break;
                    case IAsyncDisposable asyncDisposable:
                        await asyncDisposable.DisposeAsync();
                        break;
                }
            }
            catch (Exception e)
            {
                PublicApi.Instance.LogException(ClassName, $"Failed to dispose plugin {pluginPair.Metadata.Name}", e);
            }
        }

        public static async Task ReloadDataAsync()
        {
            await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
            {
                IReloadable p => Task.Run(p.ReloadData),
                IAsyncReloadable p => p.ReloadDataAsync(),
                _ => Task.CompletedTask,
            })]);
        }

        #endregion

        #region External Preview

        public static async Task OpenExternalPreviewAsync(string path, bool sendFailToast = true)
        {
            await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
            {
                IAsyncExternalPreview p => p.OpenPreviewAsync(path, sendFailToast),
                _ => Task.CompletedTask,
            })]);
        }

        public static async Task CloseExternalPreviewAsync()
        {
            await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
            {
                IAsyncExternalPreview p => p.ClosePreviewAsync(),
                _ => Task.CompletedTask,
            })]);
        }

        public static async Task SwitchExternalPreviewAsync(string path, bool sendFailToast = true)
        {
            await Task.WhenAll([.. GetAllInitializedPlugins(includeFailed: false).Select(plugin => plugin.Plugin switch
            {
                IAsyncExternalPreview p => p.SwitchPreviewAsync(path, sendFailToast),
                _ => Task.CompletedTask,
            })]);
        }

        public static bool UseExternalPreview()
        {
            return GetExternalPreviewPlugins().Any(x => !x.Metadata.Disabled);
        }

        public static bool AllowAlwaysPreview()
        {
            var plugin = GetExternalPreviewPlugins().FirstOrDefault(x => !x.Metadata.Disabled);

            if (plugin is null)
                return false;

            return ((IAsyncExternalPreview)plugin.Plugin).AllowAlwaysPreview();
        }

        private static IList<PluginPair> GetExternalPreviewPlugins()
        {
            return [.. _externalPreviewPlugins.Where(p => !PluginModified(p.Metadata.ID))];
        }

        #endregion

        #region Constructor

        static PluginManager()
        {
            // validate user directory
            Directory.CreateDirectory(DataLocation.PluginsDirectory);
            // force old plugins use new python binding
            DeletePythonBinding();
        }

        private static void DeletePythonBinding()
        {
            const string binding = "flowlauncher.py";
            foreach (var subDirectory in Directory.GetDirectories(DataLocation.PluginsDirectory))
            {
                try
                {
                    File.Delete(Path.Combine(subDirectory, binding));
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogDebug(ClassName, $"Failed to delete {binding} in {subDirectory}: {e.Message}");
                }
            }
        }

        #endregion

        #region Load & Initialize Plugins

        /// <summary>
        /// Load plugins from the directories specified in Directories.
        /// </summary>
        /// <param name="settings"></param>
        public static void LoadPlugins(PluginsSettings settings)
        {
            var metadatas = PluginConfig.Parse(Directories);
            Settings = settings;
            Settings.UpdatePluginSettings(metadatas);

            // Load plugins
            var allLoadedPlugins = PluginsLoader.Plugins(metadatas, Settings);
            foreach (var plugin in allLoadedPlugins)
            {
                if (plugin != null)
                {
                    if (!_allLoadedPlugins.TryAdd(plugin.Metadata.ID, plugin))
                    {
                        PublicApi.Instance.LogError(ClassName, $"Plugin with ID {plugin.Metadata.ID} already loaded");
                    }
                }
            }

            // Since dotnet plugins need to get assembly name first, we should update plugin directory after loading plugins
            UpdatePluginDirectory(metadatas);
        }

        private static void UpdatePluginDirectory(List<PluginMetadata> metadatas)
        {
            foreach (var metadata in metadatas)
            {
                if (AllowedLanguage.IsDotNet(metadata.Language))
                {
                    if (string.IsNullOrEmpty(metadata.AssemblyName))
                    {
                        PublicApi.Instance.LogWarn(ClassName, $"AssemblyName is empty for plugin with metadata: {metadata.Name}");
                        continue; // Skip if AssemblyName is not set, which can happen for erroneous plugins
                    }
                    metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.AssemblyName);
                    metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.AssemblyName);
                }
                else
                {
                    if (string.IsNullOrEmpty(metadata.Name))
                    {
                        PublicApi.Instance.LogWarn(ClassName, $"Name is empty for plugin with metadata: {metadata.Name}");
                        continue; // Skip if Name is not set, which can happen for erroneous plugins
                    }
                    metadata.PluginSettingsDirectoryPath = Path.Combine(DataLocation.PluginSettingsDirectory, metadata.Name);
                    metadata.PluginCacheDirectoryPath = Path.Combine(DataLocation.PluginCacheDirectory, metadata.Name);
                }
            }
        }

        /// <summary>
        /// Initialize all plugins asynchronously.
        /// </summary>
        /// <param name="register">The register to register results updated event for each plugin.</param>
        /// <returns>return the list of failed to init plugins or null for none</returns>
        public static async Task InitializePluginsAsync(IResultUpdateRegister register)
        {
            var initTasks = _allLoadedPlugins.Select(x => Task.Run(async () =>
            {
                var pair = x.Value;

                // Register plugin action keywords so that plugins can be queried in results
                RegisterPluginActionKeywords(pair);

                try
                {
                    var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Init method time cost for <{pair.Metadata.Name}>",
                        () => pair.Plugin.InitAsync(new PluginInitContext(pair.Metadata, PublicApi.Instance)));

                    pair.Metadata.InitTime += milliseconds;
                    PublicApi.Instance.LogInfo(ClassName,
                        $"Total init cost for <{pair.Metadata.Name}> is <{pair.Metadata.InitTime}ms>");
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogException(ClassName, $"Fail to Init plugin: {pair.Metadata.Name}", e);
                    if (pair.Metadata.Disabled && pair.Metadata.HomeDisabled)
                    {
                        // If this plugin is already disabled, do not show error message again
                        // Or else it will be shown every time
                        PublicApi.Instance.LogDebug(ClassName, $"Skipped init for <{pair.Metadata.Name}> due to error");
                    }
                    else
                    {
                        pair.Metadata.Disabled = true;
                        pair.Metadata.HomeDisabled = true;
                        PublicApi.Instance.LogDebug(ClassName, $"Disable plugin <{pair.Metadata.Name}> because init failed");
                    }

                    // Even if the plugin cannot be initialized, we still need to add it in all plugin list so that
                    // we can remove the plugin from Plugin or Store page or Plugin Manager plugin.
                    _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
                    _initFailedPlugins.TryAdd(pair.Metadata.ID, pair);
                    return;
                }

                // Register ResultsUpdated event so that plugin query can use results updated interface
                register.RegisterResultsUpdatedEvent(pair);

                // Update plugin metadata translation after the plugin is initialized with IPublicAPI instance
                Internationalization.UpdatePluginMetadataTranslation(pair);

                // Add plugin to Dialog Jump plugin list after the plugin is initialized
                DialogJump.InitializeDialogJumpPlugin(pair);

                // Add plugin to lists after the plugin is initialized
                AddPluginToLists(pair);
            }));

            await Task.WhenAll(initTasks);

            if (!_initFailedPlugins.IsEmpty)
            {
                var failed = string.Join(",", _initFailedPlugins.Values.Select(x => x.Metadata.Name));
                PublicApi.Instance.ShowMsg(
                    Localize.failedToInitializePluginsTitle(),
                    Localize.failedToInitializePluginsMessage(failed),
                    "",
                    false
                );
            }
        }

        private static void RegisterPluginActionKeywords(PluginPair pair)
        {
            // set distinct on each plugin's action keywords helps only firing global(*) and action keywords once where a plugin
            // has multiple global and action keywords because we will only add them here once.
            foreach (var actionKeyword in pair.Metadata.ActionKeywords.Distinct())
            {
                switch (actionKeyword)
                {
                    case Query.GlobalPluginWildcardSign:
                        _globalPlugins.TryAdd(pair.Metadata.ID, pair);
                        break;
                    default:
                        _nonGlobalPlugins.TryAdd(actionKeyword, pair);
                        break;
                }
            }
        }

        private static void AddPluginToLists(PluginPair pair)
        {
            if (pair.Plugin is IContextMenu)
            {
                _contextMenuPlugins.Add(pair);
            }
            if (pair.Plugin is IAsyncHomeQuery)
            {
                _homePlugins.Add(pair);
            }
            if (pair.Plugin is IPluginI18n)
            {
                _translationPlugins.Add(pair);
            }
            if (pair.Plugin is IAsyncExternalPreview)
            {
                _externalPreviewPlugins.Add(pair);
            }
            _allInitializedPlugins.TryAdd(pair.Metadata.ID, pair);
        }

        #endregion

        #region Validate & Query Plugins

        public static ICollection<PluginPair> ValidPluginsForQuery(Query query, bool dialogJump)
        {
            if (query is null)
                return Array.Empty<PluginPair>();

            if (!_nonGlobalPlugins.TryGetValue(query.ActionKeyword, out var plugin))
            {
                if (dialogJump)
                    return [.. GetGlobalPlugins().Where(p => p.Plugin is IAsyncDialogJump && !PluginModified(p.Metadata.ID))];
                else
                    return [.. GetGlobalPlugins().Where(p => !PluginModified(p.Metadata.ID))];
            }

            if (dialogJump && plugin.Plugin is not IAsyncDialogJump)
                return Array.Empty<PluginPair>();

            if (PluginModified(plugin.Metadata.ID))
                return Array.Empty<PluginPair>();

            return [plugin];
        }

        public static ICollection<PluginPair> ValidPluginsForHomeQuery()
        {
            return [.. _homePlugins.Where(p => !PluginModified(p.Metadata.ID))];
        }

        public static async Task<List<Result>> QueryForPluginAsync(PluginPair pair, Query query, CancellationToken token)
        {
            var results = new List<Result>();
            var metadata = pair.Metadata;

            if (IsPluginInitializing(metadata))
            {
                Result r = new()
                {
                    Title = Localize.pluginStillInitializing(metadata.Name),
                    SubTitle = Localize.pluginStillInitializingSubtitle(),
                    AutoCompleteText = query.TrimmedQuery,
                    IcoPath = metadata.IcoPath,
                    PluginDirectory = metadata.PluginDirectory,
                    ActionKeywordAssigned = query.ActionKeyword,
                    PluginID = metadata.ID,
                    OriginQuery = query,
                    Action = _ =>
                    {
                        PublicApi.Instance.ReQuery();
                        return false;
                    }
                };
                results.Add(r);
                return results;
            }

            try
            {
                var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
                    async () => results = await pair.Plugin.QueryAsync(query, token).ConfigureAwait(false));

                token.ThrowIfCancellationRequested();
                if (results == null)
                    return null;
                UpdatePluginMetadata(results, metadata, query);

                metadata.QueryCount += 1;
                metadata.AvgQueryTime =
                    metadata.QueryCount == 1 ? milliseconds : (metadata.AvgQueryTime + milliseconds) / 2;
                token.ThrowIfCancellationRequested();
            }
            catch (OperationCanceledException)
            {
                // null will be fine since the results will only be added into queue if the token hasn't been cancelled
                return null;
            }
            catch (Exception e)
            {
                Result r = new()
                {
                    Title = Localize.pluginFailedToRespond(metadata.Name),
                    SubTitle = Localize.pluginFailedToRespondSubtitle(),
                    AutoCompleteText = query.TrimmedQuery,
                    IcoPath = Constant.ErrorIcon,
                    PluginDirectory = metadata.PluginDirectory,
                    ActionKeywordAssigned = query.ActionKeyword,
                    PluginID = metadata.ID,
                    OriginQuery = query,
                    Action = _ => { throw new FlowPluginException(metadata, e);}
                };
                results.Add(r);
            }
            return results;
        }

        public static async Task<List<Result>> QueryHomeForPluginAsync(PluginPair pair, Query query, CancellationToken token)
        {
            var results = new List<Result>();
            var metadata = pair.Metadata;

            if (IsPluginInitializing(metadata))
            {
                Result r = new()
                {
                    Title = Localize.pluginStillInitializing(metadata.Name),
                    SubTitle = Localize.pluginStillInitializingSubtitle(),
                    AutoCompleteText = query.TrimmedQuery,
                    IcoPath = metadata.IcoPath,
                    PluginDirectory = metadata.PluginDirectory,
                    ActionKeywordAssigned = query.ActionKeyword,
                    PluginID = metadata.ID,
                    OriginQuery = query,
                    Action = _ =>
                    {
                        PublicApi.Instance.ReQuery();
                        return false;
                    }
                };
                results.Add(r);
                return results;
            }

            try
            {
                var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
                    async () => results = await ((IAsyncHomeQuery)pair.Plugin).HomeQueryAsync(token).ConfigureAwait(false));

                token.ThrowIfCancellationRequested();
                if (results == null)
                    return null;
                UpdatePluginMetadata(results, metadata, query);

                token.ThrowIfCancellationRequested();
            }
            catch (OperationCanceledException)
            {
                // null will be fine since the results will only be added into queue if the token hasn't been cancelled
                return null;
            }
            catch (Exception e)
            {
                PublicApi.Instance.LogException(ClassName, $"Failed to query home for plugin: {metadata.Name}", e);
                return null;
            }
            return results;
        }

        public static async Task<List<DialogJumpResult>> QueryDialogJumpForPluginAsync(PluginPair pair, Query query, CancellationToken token)
        {
            var results = new List<DialogJumpResult>();
            var metadata = pair.Metadata;

            if (IsPluginInitializing(metadata))
            {
                // null will be fine since the results will only be added into queue if the token hasn't been cancelled
                return null;
            }

            try
            {
                var milliseconds = await PublicApi.Instance.StopwatchLogDebugAsync(ClassName, $"Cost for {metadata.Name}",
                    async () => results = await ((IAsyncDialogJump)pair.Plugin).QueryDialogJumpAsync(query, token).ConfigureAwait(false));

                token.ThrowIfCancellationRequested();
                if (results == null)
                    return null;
                UpdatePluginMetadata(results, metadata, query);

                token.ThrowIfCancellationRequested();
            }
            catch (OperationCanceledException)
            {
                // null will be fine since the results will only be added into queue if the token hasn't been cancelled
                return null;
            }
            catch (Exception e)
            {
                PublicApi.Instance.LogException(ClassName, $"Failed to query Dialog Jump for plugin: {metadata.Name}", e);
                return null;
            }
            return results;
        }

        private static bool IsPluginInitializing(PluginMetadata metadata)
        {
            return !_allInitializedPlugins.ContainsKey(metadata.ID);
        }

        #endregion

        #region Get Plugin List

        public static List<PluginPair> GetAllLoadedPlugins()
        {
            return [.. _allLoadedPlugins.Values];
        }

        public static List<PluginPair> GetAllInitializedPlugins(bool includeFailed)
        {
            if (includeFailed)
            {
                return [.. _allInitializedPlugins.Values];
            }
            else
            {
                return [.. _allInitializedPlugins.Values
                    .Where(p => !_initFailedPlugins.ContainsKey(p.Metadata.ID))];
            }
        }

        private static List<PluginPair> GetGlobalPlugins()
        {
            return [.. _globalPlugins.Values];
        }

        public static Dictionary<string, PluginPair> GetNonGlobalPlugins()
        {
            return _nonGlobalPlugins.ToDictionary();
        }

        public static List<PluginPair> GetTranslationPlugins()
        {
            return [.. _translationPlugins.Where(p => !PluginModified(p.Metadata.ID))];
        }

        #endregion

        #region Update Metadata & Get Plugin

        public static void UpdatePluginMetadata(IReadOnlyList<Result> results, PluginMetadata metadata, Query query)
        {
            foreach (var r in results)
            {
                r.PluginDirectory = metadata.PluginDirectory;
                r.PluginID = metadata.ID;
                r.OriginQuery = query;

                // ActionKeywordAssigned is used for constructing MainViewModel's query text auto-complete suggestions
                // Plugins may have multi-actionkeywords eg. WebSearches. In this scenario it needs to be overriden on the plugin level
                if (metadata.ActionKeywords.Count == 1)
                    r.ActionKeywordAssigned = query.ActionKeyword;
            }
        }

        /// <summary>
        /// get specified plugin, return null if not found
        /// </summary>
        /// <remarks>
        /// Plugin may not be initialized, so do not use its plugin model to execute any commands
        /// </remarks>
        /// <param name="id"></param>
        /// <returns></returns>
        public static PluginPair GetPluginForId(string id)
        {
            return GetAllLoadedPlugins().FirstOrDefault(o => o.Metadata.ID == id);
        }

        #endregion

        #region Get Context Menus

        public static List<Result> GetContextMenusForPlugin(Result result)
        {
            var results = new List<Result>();
            var pluginPair = _contextMenuPlugins.Where(p => !PluginModified(p.Metadata.ID)).FirstOrDefault(o => o.Metadata.ID == result.PluginID);
            if (pluginPair != null)
            {
                var plugin = (IContextMenu)pluginPair.Plugin;

                try
                {
                    results = plugin.LoadContextMenus(result) ?? results;
                    foreach (var r in results)
                    {
                        r.PluginDirectory = pluginPair.Metadata.PluginDirectory;
                        r.PluginID = pluginPair.Metadata.ID;
                        r.OriginQuery = result.OriginQuery;
                    }
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogException(ClassName, 
                        $"Can't load context menus for plugin <{pluginPair.Metadata.Name}>",
                        e);
                }
            }

            return results;
        }

        #endregion

        #region Check Home Plugin

        public static bool IsHomePlugin(string id)
        {
            return _homePlugins.Where(p => !PluginModified(p.Metadata.ID)).Any(p => p.Metadata.ID == id);
        }

        #endregion

        #region Check Initializing & Init Failed

        public static bool IsInitializingOrInitFailed(string id)
        {
            // Id does not exist in loaded plugins
            if (!_allLoadedPlugins.ContainsKey(id)) return false;

            // Plugin initialized already
            if (_allInitializedPlugins.ContainsKey(id))
            {
                // Check if the plugin initialization failed
                return _initFailedPlugins.ContainsKey(id);
            }
            // Plugin is still initializing
            else
            {
                return true;
            }
        }

        public static bool IsInitializing(string id)
        {
            // Id does not exist in loaded plugins
            if (!_allLoadedPlugins.ContainsKey(id)) return false;

            // Plugin initialized already
            if (_allInitializedPlugins.ContainsKey(id))
            {
                return false;
            }
            // Plugin is still initializing
            else
            {
                return true;
            }
        }

        public static bool IsInitializationFailed(string id)
        {
            // Id does not exist in loaded plugins
            if (!_allLoadedPlugins.ContainsKey(id)) return false;

            // Plugin initialized already
            if (_allInitializedPlugins.ContainsKey(id))
            {
                // Check if the plugin initialization failed
                return _initFailedPlugins.ContainsKey(id);
            }
            // Plugin is still initializing
            else
            {
                return false;
            }
        }

        #endregion

        #region Plugin Action Keyword

        public static bool ActionKeywordRegistered(string actionKeyword)
        {
            // this method is only checking for action keywords (defined as not '*') registration
            // hence the actionKeyword != Query.GlobalPluginWildcardSign logic
            return actionKeyword != Query.GlobalPluginWildcardSign 
                && _nonGlobalPlugins.ContainsKey(actionKeyword);
        }

        /// <summary>
        /// used to add action keyword for multiple action keyword plugin
        /// e.g. web search
        /// </summary>
        public static void AddActionKeyword(string id, string newActionKeyword)
        {
            var plugin = GetPluginForId(id);
            if (newActionKeyword == Query.GlobalPluginWildcardSign)
            {
                _globalPlugins.TryAdd(id, plugin);
            }
            else
            {
                _nonGlobalPlugins.AddOrUpdate(newActionKeyword, plugin, (key, oldValue) => plugin);
            }

            // Update action keywords and action keyword in plugin metadata
            plugin.Metadata.ActionKeywords.Add(newActionKeyword);
            if (plugin.Metadata.ActionKeywords.Count > 0)
            {
                plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
            }
            else
            {
                plugin.Metadata.ActionKeyword = string.Empty;
            }
        }

        /// <summary>
        /// used to remove action keyword for multiple action keyword plugin
        /// e.g. web search
        /// </summary>
        public static void RemoveActionKeyword(string id, string oldActionkeyword)
        {
            var plugin = GetPluginForId(id);
            if (oldActionkeyword == Query.GlobalPluginWildcardSign
                && // Plugins may have multiple ActionKeywords that are global, eg. WebSearch
                plugin.Metadata.ActionKeywords
                    .Count(x => x == Query.GlobalPluginWildcardSign) == 1)
            {
                _globalPlugins.TryRemove(id, out _);
            }

            if (oldActionkeyword != Query.GlobalPluginWildcardSign)
            {
                _nonGlobalPlugins.TryRemove(oldActionkeyword, out _);
            }

            // Update action keywords and action keyword in plugin metadata
            plugin.Metadata.ActionKeywords.Remove(oldActionkeyword);
            if (plugin.Metadata.ActionKeywords.Count > 0)
            {
                plugin.Metadata.ActionKeyword = plugin.Metadata.ActionKeywords[0];
            }
            else
            {
                plugin.Metadata.ActionKeyword = string.Empty;
            }
        }

        #endregion

        #region Plugin Install & Uninstall & Update

        #region Private Functions

        private static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath)
        {
            var unzippedFolderCount = Directory.GetDirectories(unzippedParentFolderPath).Length;
            var unzippedFilesCount = Directory.GetFiles(unzippedParentFolderPath).Length;

            // adjust path depending on how the plugin is zipped up
            // the recommended should be to zip up the folder not the contents
            if (unzippedFolderCount == 1 && unzippedFilesCount == 0)
                // folder is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/pluginFolderName/
                return Directory.GetDirectories(unzippedParentFolderPath)[0];

            if (unzippedFilesCount > 1)
                // content is zipped up, unzipped plugin directory structure: tempPath/unzippedParentPluginFolder/
                return unzippedParentFolderPath;

            return string.Empty;
        }

        private static bool SameOrLesserPluginVersionExists(PluginMetadata metadata)
        {
            if (!Version.TryParse(metadata.Version, out var newVersion))
                return true; // If version is not valid, we assume it is lesser than any existing version

            // Get all plugins even if initialization failed so that we can check if the plugin with the same ID exists
            return GetAllInitializedPlugins(includeFailed: true).Any(x => x.Metadata.ID == metadata.ID
                && Version.TryParse(x.Metadata.Version, out var version)
                && newVersion <= version);
        }

        #endregion

        #region Public Functions

        public static bool PluginModified(string id)
        {
            return ModifiedPlugins.Contains(id);
        }

        public static async Task<bool> UpdatePluginAsync(PluginMetadata existingVersion, UserPlugin newVersion, string zipFilePath)
        {
            if (PluginModified(existingVersion.ID))
            {
                PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(existingVersion.Name),
                    Localize.pluginModifiedAlreadyMessage());
                return false;
            }

            var installSuccess = InstallPlugin(newVersion, zipFilePath, checkModified: false);
            if (!installSuccess) return false;

            var uninstallSuccess = await UninstallPluginAsync(existingVersion, removePluginFromSettings: false, removePluginSettings: false, checkModified: false);
            if (!uninstallSuccess) return false;

            ModifiedPlugins.Add(existingVersion.ID);
            return true;
        }

        public static bool InstallPlugin(UserPlugin plugin, string zipFilePath)
        {
            return InstallPlugin(plugin, zipFilePath, checkModified: true);
        }

        public static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginSettings = false)
        {
            return await UninstallPluginAsync(plugin, removePluginFromSettings: true, removePluginSettings: removePluginSettings, checkModified: true);
        }

        #endregion

        #region Internal Functions

        internal static bool InstallPlugin(UserPlugin plugin, string zipFilePath, bool checkModified)
        {
            if (checkModified && PluginModified(plugin.ID))
            {
                PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
                    Localize.pluginModifiedAlreadyMessage());
                return false;
            }

            // Unzip plugin files to temp folder
            var tempFolderPluginPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
            System.IO.Compression.ZipFile.ExtractToDirectory(zipFilePath, tempFolderPluginPath);

            try
            {
                if (!plugin.IsFromLocalInstallPath)
                    File.Delete(zipFilePath);

                var pluginFolderPath = GetContainingFolderPathAfterUnzip(tempFolderPluginPath);

                var metadataJsonFilePath = string.Empty;
                if (File.Exists(Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName)))
                    metadataJsonFilePath = Path.Combine(pluginFolderPath, Constant.PluginMetadataFileName);

                if (string.IsNullOrEmpty(metadataJsonFilePath) || string.IsNullOrEmpty(pluginFolderPath))
                {
                    PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
                        Localize.fileNotFoundMessage(pluginFolderPath));
                    return false;
                }

                PluginMetadata newMetadata;
                try
                {
                    newMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(metadataJsonFilePath)) ??
                        throw new JsonException("Deserialized metadata is null");
                }
                catch (Exception ex)
                {
                    PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
                        Localize.pluginJsonInvalidOrCorrupted());
                    PublicApi.Instance.LogException(ClassName,
                        $"Failed to deserialize plugin metadata for plugin {plugin.Name} from file {metadataJsonFilePath}", ex);
                    return false;
                }

                if (SameOrLesserPluginVersionExists(newMetadata))
                {
                    PublicApi.Instance.ShowMsgError(Localize.failedToInstallPluginTitle(plugin.Name),
                        Localize.pluginExistAlreadyMessage());
                    return false;
                }

                if (!IsMinimumAppVersionSatisfied(newMetadata.Name, newMetadata.MinimumAppVersion))
                {
                    // Ask users if they want to install the plugin that doesn't satisfy the minimum app version requirement
                    if (PublicApi.Instance.ShowMsgBox(
                        Localize.pluginMinimumAppVersionUnsatisfiedMessage(newMetadata.Name, Environment.NewLine),
                        Localize.pluginMinimumAppVersionUnsatisfiedTitle(newMetadata.Name, newMetadata.MinimumAppVersion),
                        MessageBoxButton.YesNo) == MessageBoxResult.No)
                    {
                        return false;
                    }
                }

                var folderName = string.IsNullOrEmpty(plugin.Version) ? $"{plugin.Name}-{Guid.NewGuid()}" : $"{plugin.Name}-{plugin.Version}";

                var defaultPluginIDs = new List<string>
                    {
                        "0ECADE17459B49F587BF81DC3A125110", // BrowserBookmark
                        "CEA0FDFC6D3B4085823D60DC76F28855", // Calculator
                        "572be03c74c642baae319fc283e561a8", // Explorer
                        "6A122269676E40EB86EB543B945932B9", // PluginIndicator
                        "9f8f9b14-2518-4907-b211-35ab6290dee7", // PluginsManager
                        "b64d0a79-329a-48b0-b53f-d658318a1bf6", // ProcessKiller
                        "791FC278BA414111B8D1886DFE447410", // Program
                        "D409510CD0D2481F853690A07E6DC426", // Shell
                        "CEA08895D2544B019B2E9C5009600DF4", // Sys
                        "0308FD86DE0A4DEE8D62B9B535370992", // URL
                        "565B73353DBF4806919830B9202EE3BF", // WebSearch
                        "5043CETYU6A748679OPA02D27D99677A" // WindowsSettings
                    };

                // Treat default plugin differently, it needs to be removable along with each flow release
                var installDirectory = !defaultPluginIDs.Any(x => x == plugin.ID)
                                        ? DataLocation.PluginsDirectory
                                        : Constant.PreinstalledDirectory;

                var newPluginPath = Path.Combine(installDirectory, folderName);

                FilesFolders.CopyAll(pluginFolderPath, newPluginPath, (s) => PublicApi.Instance.ShowMsgBox(s));

                // Check if marker file exists and delete it
                try
                {
                    var markerFilePath = Path.Combine(newPluginPath, DataLocation.PluginDeleteFile);
                    if (File.Exists(markerFilePath))
                        File.Delete(markerFilePath);
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin marker file in {newPluginPath}", e);
                }

                if (checkModified)
                {
                    ModifiedPlugins.Add(plugin.ID);
                }

                return true;
            }
            finally
            {
                try
                {
                    if (Directory.Exists(tempFolderPluginPath))
                        Directory.Delete(tempFolderPluginPath, true);
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogException(ClassName, $"Failed to delete temp folder {tempFolderPluginPath}", e);
                }
            }
        }

        internal static async Task<bool> UninstallPluginAsync(PluginMetadata plugin, bool removePluginFromSettings, bool removePluginSettings, bool checkModified)
        {
            if (checkModified && PluginModified(plugin.ID))
            {
                PublicApi.Instance.ShowMsgError(Localize.pluginModifiedAlreadyTitle(plugin.Name),
                    Localize.pluginModifiedAlreadyMessage());
                return false;
            }

            if (removePluginSettings || removePluginFromSettings)
            {
                // If we want to remove plugin from AllPlugins,
                // we need to dispose them so that they can release file handles
                // which can help FL to delete the plugin settings & cache folders successfully
                var pluginPairs = GetAllInitializedPlugins(includeFailed: true).Where(p => p.Metadata.ID == plugin.ID).ToList();
                foreach (var pluginPair in pluginPairs)
                {
                    await DisposePluginAsync(pluginPair);
                }
            }

            if (removePluginSettings)
            {
                // For dotnet plugins, we need to remove their PluginJsonStorage and PluginBinaryStorage instances
                if (AllowedLanguage.IsDotNet(plugin.Language) && PublicApi.Instance is IRemovable removable)
                {
                    removable.RemovePluginSettings(plugin.AssemblyName);
                    removable.RemovePluginCaches(plugin.PluginCacheDirectoryPath);
                }

                try
                {
                    var pluginSettingsDirectory = plugin.PluginSettingsDirectoryPath;
                    if (Directory.Exists(pluginSettingsDirectory))
                        Directory.Delete(pluginSettingsDirectory, true);
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin settings folder for {plugin.Name}", e);
                    PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginSettingsTitle(),
                        Localize.failedToRemovePluginSettingsMessage(plugin.Name));
                }
            }

            if (removePluginFromSettings)
            {
                try
                {
                    var pluginCacheDirectory = plugin.PluginCacheDirectoryPath;
                    if (Directory.Exists(pluginCacheDirectory))
                        Directory.Delete(pluginCacheDirectory, true);
                }
                catch (Exception e)
                {
                    PublicApi.Instance.LogException(ClassName, $"Failed to delete plugin cache folder for {plugin.Name}", e);
                    PublicApi.Instance.ShowMsgError(Localize.failedToRemovePluginCacheTitle(),
                        Localize.failedToRemovePluginCacheMessage(plugin.Name));
                }
                Settings.RemovePluginSettings(plugin.ID);
                {
                    _allLoadedPlugins.TryRemove(plugin.ID, out var _);
                }
                {
                    _allInitializedPlugins.TryRemove(plugin.ID, out var _);
                }
                {
                    _initFailedPlugins.TryRemove(plugin.ID, out var _);
                }
                {
                    _globalPlugins.TryRemove(plugin.ID, out var _);
                }
                var keysToRemove = _nonGlobalPlugins.Where(p => p.Value.Metadata.ID == plugin.ID).Select(p => p.Key).ToList();
                foreach (var key in keysToRemove)
                {
                    _nonGlobalPlugins.TryRemove(key, out var _);
                }
            }

            // Marked for deletion. Will be deleted on next start up
            using var _ = File.CreateText(Path.Combine(plugin.PluginDirectory, DataLocation.PluginDeleteFile));

            if (checkModified)
            {
                ModifiedPlugins.Add(plugin.ID);
            }

            return true;
        }

        internal static bool IsMinimumAppVersionSatisfied(string pluginName, string minimumAppVersion)
        {
            // If the minimum app version is not specified in plugin.json, this plugin is compatible with all app versions
            if (string.IsNullOrEmpty(minimumAppVersion))
                return true;

            var appVersion = Version.Parse(Constant.Version);

            if (!Version.TryParse(minimumAppVersion, out var minimumVersion))
            {
                PublicApi.Instance.LogError(ClassName,
                    $"Failed to parse the minimum app version {minimumAppVersion} for plugin {pluginName}.");
                return false;  // If the minimum app version specified in plugin.json is invalid, we assume it is not satisfied
            }

            if (appVersion >= minimumVersion)
                return true;

            return false;
        }

        #endregion

        #endregion
    }
}