// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Text;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;
using Windows.Foundation;
using WyHash;

namespace Microsoft.CmdPal.UI.ViewModels;

[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IExtendedAttributesProvider, IPrecomputedListItem
{
    private readonly ISettingsService _settingsService;
    private readonly ProviderSettings _providerSettings;
    private readonly IServiceProvider _serviceProvider;
    private readonly CommandItemViewModel _commandItemViewModel;
    private readonly IContextMenuFactory _contextMenuFactory;

    public ICommandProviderContext ProviderContext { get; private set; }

    private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id;

    private string _fallbackId = string.Empty;

    private string _generatedId = string.Empty;

    private HotkeySettings? _hotkey;
    private IIconInfo? _initialIcon;

    private FuzzyTargetCache _titleCache;
    private FuzzyTargetCache _subtitleCache;
    private FuzzyTargetCache _extensionNameCache;

    private CommandAlias? Alias { get; set; }

    public bool IsFallback { get; private set; }

    [ObservableProperty]
    public partial ObservableCollection<Tag> Tags { get; set; } = [];

    public string Id => string.IsNullOrWhiteSpace(IdFromModel) ? _generatedId : IdFromModel;

    public CommandPaletteHost ExtensionHost { get; private set; }

    public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;

    public CommandViewModel CommandViewModel => _commandItemViewModel.Command;

    public CommandItemViewModel ItemViewModel => _commandItemViewModel;

    public string CommandProviderId => ProviderContext.ProviderId;

    public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;

    ////// ICommandItem
    public string Title => _commandItemViewModel.Title;

    public string Subtitle => _commandItemViewModel.Subtitle;

    public IIconInfo Icon => (IIconInfo)IconViewModel;

    public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;

    ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;

    IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();

    ////// IListItem
    ITag[] IListItem.Tags => Tags.ToArray();

    IDetails? IListItem.Details => null;

    string IListItem.Section => string.Empty;

    string IListItem.TextToSuggest => string.Empty;

    ////// INotifyPropChanged
    public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;

    // Fallback items
    public string DisplayTitle { get; private set; } = string.Empty;

    public HotkeySettings? Hotkey
    {
        get => _hotkey;
        set
        {
            _serviceProvider.GetService<HotkeyManager>()!.UpdateHotkey(Id, value);
            UpdateHotkey();
            UpdateTags();
            Save();
        }
    }

    public bool HasAlias => !string.IsNullOrEmpty(AliasText);

    public string AliasText
    {
        get => Alias?.Alias ?? string.Empty;
        set
        {
            var previousAlias = Alias?.Alias ?? string.Empty;

            if (string.IsNullOrEmpty(value))
            {
                Alias = null;
            }
            else
            {
                if (Alias is CommandAlias a)
                {
                    Alias = a with { Alias = value };
                }
                else
                {
                    Alias = new CommandAlias(value, Id);
                }
            }

            // Only call HandleChangeAlias if there was an actual change.
            if (previousAlias != Alias?.Alias)
            {
                HandleChangeAlias();
                OnPropertyChanged(nameof(AliasText));
                OnPropertyChanged(nameof(IsDirectAlias));
            }
        }
    }

    public bool IsDirectAlias
    {
        get => Alias?.IsDirect ?? false;
        set
        {
            if (Alias is CommandAlias a)
            {
                Alias = a with { IsDirect = value };
            }

            HandleChangeAlias();
            OnPropertyChanged(nameof(IsDirectAlias));
        }
    }

    public bool IsEnabled
    {
        get
        {
            if (IsFallback)
            {
                if (_providerSettings.FallbackCommands.TryGetValue(_fallbackId, out var fallbackSettings))
                {
                    return fallbackSettings.IsEnabled;
                }

                return true;
            }
            else
            {
                return _providerSettings.IsEnabled;
            }
        }
    }

    // Dock properties
    public bool IsDockBand { get; private set; }

    public DockBandSettings? DockBandSettings
    {
        get
        {
            if (!IsDockBand)
            {
                return null;
            }

            var bandSettings = _settingsService.Settings.DockSettings.StartBands
                .Concat(_settingsService.Settings.DockSettings.CenterBands)
                .Concat(_settingsService.Settings.DockSettings.EndBands)
                .FirstOrDefault(band => band.CommandId == this.Id);
            if (bandSettings is null)
            {
                return new DockBandSettings()
                {
                    ProviderId = this.CommandProviderId,
                    CommandId = this.Id,
                    ShowLabels = true,
                };
            }

            return bandSettings;
        }
    }

    public TopLevelViewModel(
        CommandItemViewModel item,
        TopLevelType topLevelType,
        CommandPaletteHost extensionHost,
        ICommandProviderContext commandProviderContext,
        ProviderSettings providerSettings,
        IServiceProvider serviceProvider,
        ICommandItem? commandItem,
        IContextMenuFactory? contextMenuFactory)
    {
        _serviceProvider = serviceProvider;
        _settingsService = serviceProvider.GetRequiredService<ISettingsService>();
        _providerSettings = providerSettings;
        ProviderContext = commandProviderContext;
        _commandItemViewModel = item;

        _contextMenuFactory = contextMenuFactory ?? DefaultContextMenuFactory.Instance;

        IsFallback = topLevelType == TopLevelType.Fallback;
        IsDockBand = topLevelType == TopLevelType.DockBand;
        ExtensionHost = extensionHost;
        if (IsFallback && commandItem is FallbackCommandItem fallback)
        {
            _fallbackId = fallback.Id;
        }

        item.PropertyChangedBackground += Item_PropertyChanged;
    }

    internal void InitializeProperties()
    {
        // Init first, so that we get the ID & titles,
        // then generate the ID,
        // then slow init for the context menu
        ItemViewModel.InitializeProperties();
        GenerateId();
        ItemViewModel.SlowInitializeProperties();

        if (IsFallback)
        {
            var model = _commandItemViewModel.Model.Unsafe;

            // RPC to check type
            if (model is IFallbackCommandItem fallback)
            {
                DisplayTitle = fallback.DisplayTitle;
            }

            UpdateInitialIcon(false);
        }
    }

    private void Item_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (!string.IsNullOrEmpty(e.PropertyName))
        {
            PropChanged?.Invoke(this, new PropChangedEventArgs(e.PropertyName));

            if (e.PropertyName is nameof(CommandItemViewModel.Title) or nameof(CommandItemViewModel.Name))
            {
                _titleCache.Invalidate();
            }
            else if (e.PropertyName is nameof(CommandItemViewModel.Subtitle))
            {
                _subtitleCache.Invalidate();
            }

            if (e.PropertyName is "IsInitialized" or nameof(CommandItemViewModel.Command))
            {
                GenerateId();

                FetchAliasFromAliasManager();
                UpdateHotkey();
                UpdateTags();
                UpdateInitialIcon();
            }
            else if (e.PropertyName == nameof(CommandItem.Icon))
            {
                UpdateInitialIcon();
            }
            else if (e.PropertyName == nameof(CommandItem.DataPackage))
            {
                DoOnUiThread(() =>
                {
                    OnPropertyChanged(nameof(CommandItem.DataPackage));
                });
            }
        }
    }

    private void UpdateInitialIcon(bool raiseNotification = true)
    {
        if (_initialIcon != null || !_commandItemViewModel.Icon.IsSet)
        {
            return;
        }

        _initialIcon = (IIconInfo?)_commandItemViewModel.Icon;

        if (raiseNotification)
        {
            DoOnUiThread(
                () =>
                {
                    PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(InitialIcon)));
                });
        }
    }

    private void Save() => _settingsService.Save();

    private void HandleChangeAlias()
    {
        SetAlias();
        Save();
    }

    public void SetAlias()
    {
        var commandAlias = Alias is null
                ? null
                : new CommandAlias(Alias.Alias, Alias.CommandId, Alias.IsDirect);

        _serviceProvider.GetService<AliasManager>()!.UpdateAlias(Id, commandAlias);
        UpdateTags();
    }

    private void FetchAliasFromAliasManager()
    {
        var am = _serviceProvider.GetService<AliasManager>();
        if (am is not null)
        {
            var commandAlias = am.AliasFromId(Id);
            if (commandAlias is not null)
            {
                // Decouple from the alias manager alias object
                Alias = new CommandAlias(commandAlias.Alias, commandAlias.CommandId, commandAlias.IsDirect);
            }
        }
    }

    private void UpdateHotkey()
    {
        var hotkey = _settingsService.Settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault();
        if (hotkey is not null)
        {
            _hotkey = hotkey.Hotkey;
        }
    }

    private void UpdateTags()
    {
        List<Tag> tags = [];

        if (Hotkey is not null)
        {
            tags.Add(new Tag() { Text = Hotkey.ToString() });
        }

        if (Alias is not null)
        {
            tags.Add(new Tag() { Text = Alias.SearchPrefix });
        }

        DoOnUiThread(
            () =>
            {
                ListHelpers.InPlaceUpdateList(Tags, tags);
                PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(Tags)));
            });
    }

    private void GenerateId()
    {
        // Use WyHash64 to generate stable ID hashes.
        // manually seeding with 0, so that the hash is stable across launches
        var result = WyHash64.ComputeHash64(CommandProviderId + DisplayTitle + Title + Subtitle, seed: 0);
        _generatedId = $"{CommandProviderId}{result}";
    }

    private void DoOnUiThread(Action action)
    {
        if (_commandItemViewModel.PageContext.TryGetTarget(out var pageContext))
        {
            Task.Factory.StartNew(
                action,
                CancellationToken.None,
                TaskCreationOptions.None,
                pageContext.Scheduler);
        }
    }

    internal bool SafeUpdateFallbackTextSynchronous(string newQuery)
    {
        if (!IsFallback)
        {
            return false;
        }

        if (!IsEnabled)
        {
            return false;
        }

        try
        {
            return UnsafeUpdateFallbackSynchronous(newQuery);
        }
        catch (Exception ex)
        {
            Logger.LogError(ex.ToString());
        }

        return false;
    }

    /// <summary>
    /// Calls UpdateQuery on our command, if we're a fallback item. This does
    /// RPC work, so make sure you're calling it on a BG thread.
    /// </summary>
    /// <param name="newQuery">The new search text to pass to the extension</param>
    /// <returns>true if our Title changed across this call</returns>
    private bool UnsafeUpdateFallbackSynchronous(string newQuery)
    {
        var model = _commandItemViewModel.Model.Unsafe;

        // RPC to check type
        if (model is IFallbackCommandItem fallback)
        {
            var wasEmpty = string.IsNullOrEmpty(Title);

            // RPC for method
            fallback.FallbackHandler.UpdateQuery(newQuery);
            var isEmpty = string.IsNullOrEmpty(Title);
            return wasEmpty != isEmpty;
        }

        return false;
    }

    public PerformCommandMessage GetPerformCommandMessage()
    {
        return new PerformCommandMessage(this.CommandViewModel.Model, new Models.ExtensionObject<IListItem>(this));
    }

    public override string ToString()
    {
        return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}";
    }

    public IDictionary<string, object?> GetProperties()
    {
        return new Dictionary<string, object?>
        {
            [WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
        };
    }

    public FuzzyTarget GetTitleTarget(IPrecomputedFuzzyMatcher matcher)
        => _titleCache.GetOrUpdate(matcher, Title);

    public FuzzyTarget GetSubtitleTarget(IPrecomputedFuzzyMatcher matcher)
        => _subtitleCache.GetOrUpdate(matcher, Subtitle);

    public FuzzyTarget GetExtensionNameTarget(IPrecomputedFuzzyMatcher matcher)
        => _extensionNameCache.GetOrUpdate(matcher, ExtensionName);

    private string GetDebuggerDisplay()
    {
        return ToString();
    }

    /// <summary>
    /// Helper to convert our context menu viewmodels back into the API
    /// interfaces that ICommandItem expects.
    /// </summary>
    private IContextItem?[] BuildContextMenu()
    {
        List<IContextItem?> contextItems = new();

        foreach (var item in _commandItemViewModel.MoreCommands)
        {
            if (item is ISeparatorContextItem)
            {
                contextItems.Add(item as IContextItem);
            }
            else if (item is CommandContextItemViewModel commandItem)
            {
                contextItems.Add(commandItem.Model.Unsafe);
            }
        }

        _contextMenuFactory.AddMoreCommandsToTopLevel(this, this.ProviderContext, contextItems);

        return contextItems.ToArray();
    }

    internal ICommandItem ToPinnedDockBandItem()
    {
        var item = new PinnedDockItem(item: this, id: Id);

        return item;
    }
}

public enum TopLevelType
{
    Normal,
    Fallback,
    DockBand,
}
