using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Threading;
using DragonISO.App.Services;

namespace DragonISO.App.ViewModels;

/// <summary>
/// View-model for the v2 Ctrl+K command palette. Owns the static list of
/// commands the operator can invoke, plus a free-text filter that whittles
/// the visible list down.
///
/// The palette is the v2 redesign's navigation surface â€” it replaces the
/// v1 rail's launch / hide / settings buttons (still discoverable in the
/// 32px header) AND the buried-in-tabs operator actions like "Apply
/// transcoder topology" or "Stop all ISOs" that previously needed
/// hunting through menus. Type two letters, press Enter, action invokes.
///
/// Match shape: case-insensitive Contains across Label + Category + the
/// optional Keywords list. Fuzzy (Sublime / Linear style) matching is a
/// future evolution if Contains proves insufficient; broadcasters have
/// short attention budgets and Contains is the predictable answer.
/// </summary>
public sealed class CommandPaletteViewModel : ObservableObject
{
    private readonly MainViewModel _main;
    private readonly Dispatcher _dispatcher;
    private readonly List<PaletteCommand> _all;
    private string _filter = string.Empty;
    private PaletteCommand? _selected;

    public CommandPaletteViewModel(MainViewModel main, Dispatcher dispatcher)
    {
        _main = main;
        _dispatcher = dispatcher;
        _all = BuildCommands();
        Visible = new ObservableCollection<PaletteCommand>(_all);
        Selected = Visible.FirstOrDefault();
    }

    /// <summary>Free-text filter. Empty string shows all commands.</summary>
    public string Filter
    {
        get => _filter;
        set
        {
            if (!SetField(ref _filter, value)) return;
            ApplyFilter();
        }
    }

    /// <summary>Filtered command list, bound to the palette ListBox.</summary>
    public ObservableCollection<PaletteCommand> Visible { get; }

    /// <summary>Currently-highlighted command. Driven by â†‘/â†“ in the search box and by mouse hover.</summary>
    public PaletteCommand? Selected
    {
        get => _selected;
        set => SetField(ref _selected, value);
    }

    /// <summary>
    /// Move the selection up or down within the visible list, wrapping at the
    /// edges. Called from the palette's PreviewKeyDown when the operator
    /// presses â†‘ / â†“ while focus is in the search box.
    /// </summary>
    public void MoveSelection(int direction)
    {
        if (Visible.Count == 0) return;
        var idx = Selected is null ? -1 : Visible.IndexOf(Selected);
        idx = (idx + direction + Visible.Count) % Visible.Count;
        Selected = Visible[idx];
    }

    /// <summary>Invoke the current selection's action. Returns true if something fired.</summary>
    public bool InvokeSelection()
    {
        var sel = Selected;
        if (sel is null) return false;
        try { sel.Invoke(); }
        catch (Exception ex)
        {
            _main.Toast.Warn($"{sel.Label}: {ex.Message}");
            return false;
        }
        return true;
    }

    private void ApplyFilter()
    {
        var query = _filter.Trim();
        var prevSelected = Selected;
        Visible.Clear();
        if (string.IsNullOrEmpty(query))
        {
            foreach (var c in _all) Visible.Add(c);
        }
        else
        {
            foreach (var c in _all)
            {
                if (Matches(c, query)) Visible.Add(c);
            }
        }
        Selected = Visible.Contains(prevSelected!)
            ? prevSelected
            : Visible.FirstOrDefault();
    }

    internal static bool Matches(PaletteCommand c, string query)
    {
        if (c.Label.Contains(query, StringComparison.OrdinalIgnoreCase)) return true;
        if (c.Category.Contains(query, StringComparison.OrdinalIgnoreCase)) return true;
        // Keywords is a single space-separated string of synonyms â€” Contains
        // over the whole blob suffices for the operator's short-token typing.
        if (!string.IsNullOrEmpty(c.Keywords) &&
            c.Keywords.Contains(query, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }
        return false;
    }

    /// <summary>
    /// Build the static command list. Order within a category matters for
    /// keyboard-only operators: the most-frequent command of each category
    /// goes first.
    /// </summary>
    private List<PaletteCommand> BuildCommands()
    {
        var vm = _main;
        return new List<PaletteCommand>
        {
            // â”€â”€â”€ QUICK â”€â”€â”€ operator's top-of-mind verbs
            new("Quick", "Enable all online", "ISOs enable everyone start everything live", "Ctrl+E",
                () => InvokeIfReady(vm.EnableAllOnlineCommand)),
            new("Quick", "Stop all ISOs",  "panic stop everything kill disable", "Ctrl+Shift+S",
                () => InvokeIfReady(vm.StopAllIsosCommand)),
            new("Quick", "Refresh discovery", "rediscover sources rebuild finder NDI", "Ctrl+R",
                () => InvokeIfReady(vm.RefreshDiscoveryCommand)),

            // â”€â”€â”€ TEAMS â”€â”€â”€ direct UIA orchestration
            new("Teams", "Mute / unmute", "microphone audio silence toggle", null,
                () => InvokeIfReady(vm.ToggleMuteCommand)),
            new("Teams", "Toggle camera", "video webcam on off", null,
                () => InvokeIfReady(vm.ToggleCameraCommand)),
            new("Teams", "Open share tray", "screen share present", null,
                () => InvokeIfReady(vm.OpenShareTrayCommand)),
            new("Teams", "Leave call", "exit end disconnect quit", null,
                () => InvokeIfReady(vm.LeaveCallCommand)),
            new("Teams", "Launch Microsoft Teams", "start open run app", null,
                () => RunOnUi(() =>
                {
                    if (!TeamsLauncher.IsRunning() && TeamsLauncher.TryLaunch(out _))
                        vm.Toast.Show("Launching Microsoft Teamsâ€¦");
                    else
                        TeamsLauncher.ShowWindows();
                })),
            new("Teams", "Hide Teams windows", "minimize cloak", null,
                () => RunOnUi(() =>
                {
                    var n = TeamsLauncher.HideWindows();
                    vm.Toast.Show(n > 0 ? $"Hid {n} Teams window(s)" : "No Teams windows to hide");
                })),
            new("Teams", "Show Teams windows", "restore unhide", null,
                () => RunOnUi(() =>
                {
                    var n = TeamsLauncher.ShowWindows();
                    vm.Toast.Show(n > 0 ? $"Restored {n} Teams window(s)" : "No Teams windows to restore");
                })),

            // â”€â”€â”€ NETWORK â”€â”€â”€
            new("Network", "Apply transcoder topology", "ndi groups isolate Dragon-ISO-input private", null,
                () => InvokeIfReady(vm.Settings.ApplyTranscoderTopologyCommand)),

            // â”€â”€â”€ APP â”€â”€â”€
            new("App", "Theme: dark",   "appearance night mode", null,
                () => RunOnUi(() => ThemeManager.Current.Set("Dark"))),
            new("App", "Theme: light",  "appearance day mode bright", null,
                () => RunOnUi(() => ThemeManager.Current.Set("Light"))),
            new("App", "Theme: follow Windows", "system auto", null,
                () => RunOnUi(() => ThemeManager.Current.Set("System"))),
            new("App", "Help",         "shortcuts cheatsheet f1", "F1",
                () => InvokeIfReady(vm.ShowHelpCommand)),
            new("App", "Show notes",   "show notes daily journal", null,
                () => InvokeIfReady(vm.ShowNotesCommand)),
        };
    }

    private void RunOnUi(Action action) => _dispatcher.BeginInvoke(action);

    private static void InvokeIfReady(System.Windows.Input.ICommand cmd)
    {
        if (cmd?.CanExecute(null) == true) cmd.Execute(null);
    }
}

/// <summary>
/// One command in the Ctrl+K palette. <see cref="Keywords"/> is an optional
/// space of additional search terms â€” the operator might type "ndi" or
/// "private" and still match "Apply transcoder topology".
/// </summary>
public sealed record PaletteCommand(
    string Category,
    string Label,
    string? Keywords,
    string? Shortcut,
    Action Invoke);
