using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using TeamsISO.App.Services;
using TeamsISO.App.ViewModels;
using TeamsISO.Engine.Controller;

namespace TeamsISO.App;

/// <summary>
/// Modal dialog for saving and loading operator presets. Owned by
/// <see cref="MainWindow"/>; given a snapshot of the current
/// <see cref="ParticipantViewModel"/> list and the
/// <see cref="IIsoController"/> so it can re-apply assignments
/// (which requires calling EnableIsoAsync/DisableIsoAsync on the engine).
/// </summary>
public partial class PresetsDialog : Window
{
    private readonly IIsoController _controller;
    private readonly IReadOnlyList<ParticipantViewModel> _participants;
    private readonly ToastViewModel? _toast;

    /// <summary>
    /// Display-side wrapper for an <see cref="OperatorPresetStore.Preset"/>.
    /// Adds derived presentation-only properties so the ListBox template can
    /// render without inline converters or value-conversion logic.
    /// </summary>
    public sealed class PresetRow
    {
        public OperatorPresetStore.Preset Preset { get; }
        public string Name => Preset.Name;
        public string SavedAtDisplay => Preset.SavedAt.LocalDateTime.ToString("yyyy-MM-dd HH:mm");
        public int AssignmentCount => Preset.Assignments.Count(a => a.Enabled);
        public PresetRow(OperatorPresetStore.Preset preset) => Preset = preset;
    }

    public ObservableCollection<PresetRow> Rows { get; } = new();

    public PresetsDialog(
        IIsoController controller,
        IReadOnlyList<ParticipantViewModel> participants,
        ToastViewModel? toast = null)
    {
        InitializeComponent();
        _controller = controller;
        _participants = participants;
        _toast = toast;
        PresetsList.ItemsSource = Rows;
        ReloadPresets();
    }

    /// <summary>Refresh the ListBox from disk and reflect emptiness in the empty-state TextBlock.</summary>
    private void ReloadPresets()
    {
        Rows.Clear();
        foreach (var p in OperatorPresetStore.LoadAll().OrderByDescending(p => p.SavedAt))
            Rows.Add(new PresetRow(p));
        EmptyState.Visibility = Rows.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
        UpdateButtonStates();
    }

    private void UpdateButtonStates()
    {
        var hasSelection = PresetsList.SelectedItem is PresetRow;
        ApplyButton.IsEnabled = hasSelection;
        DeleteButton.IsEnabled = hasSelection;
        DuplicateButton.IsEnabled = hasSelection;
    }

    private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (PresetsList.SelectedItem is PresetRow row)
        {
            // Mirror the selected name into the textbox so a re-save overwrites
            // by default; operator can still type a new name to fork.
            NameBox.Text = row.Name;
        }
        UpdateButtonStates();
    }

    private void OnSave(object sender, RoutedEventArgs e)
    {
        var name = NameBox.Text?.Trim();
        if (string.IsNullOrWhiteSpace(name))
        {
            _toast?.Warn("Enter a name for the preset");
            NameBox.Focus();
            return;
        }

        var assignments = _participants
            .Select(p => new OperatorPresetStore.Assignment(
                DisplayName: p.DisplayName,
                CustomOutputName: string.IsNullOrWhiteSpace(p.CustomName) ? null : p.CustomName,
                Enabled: p.IsEnabled))
            .ToList();

        var existing = OperatorPresetStore.Find(name);
        if (existing is not null)
        {
            var confirm = MessageBox.Show(
                this,
                $"A preset named \"{name}\" already exists. Overwrite it?",
                "TeamsISO — Overwrite preset",
                MessageBoxButton.YesNo,
                MessageBoxImage.Question,
                MessageBoxResult.No);
            if (confirm != MessageBoxResult.Yes) return;
        }

        try
        {
            OperatorPresetStore.Save(new OperatorPresetStore.Preset(
                Name: name,
                SavedAt: DateTimeOffset.Now,
                Assignments: assignments));
            _toast?.Show($"Saved preset \"{name}\"");
            ReloadPresets();
        }
        catch (Exception ex)
        {
            MessageBox.Show(
                this,
                $"Could not save preset.\n\n{ex.Message}",
                "TeamsISO — Save preset",
                MessageBoxButton.OK,
                MessageBoxImage.Warning);
        }
    }

    /// <summary>
    /// Apply the selected preset: walks the current participants list, matching
    /// by display name (the only stable join key across meetings — Ids are
    /// regenerated each meeting). For each match, set the custom output name and
    /// reconcile its enabled state with the preset by calling EnableIsoAsync /
    /// DisableIsoAsync as needed. Participants in the preset who aren't in the
    /// current meeting are silently skipped (and reported in the toast).
    /// </summary>
    private async void OnApply(object sender, RoutedEventArgs e)
    {
        if (PresetsList.SelectedItem is not PresetRow row) return;

        ApplyButton.IsEnabled = false;
        try
        {
            // PresetApplier owns the apply loop — same code path the REST control
            // surface and auto-apply-on-launch use. Dialog passes null dispatcher
            // since OnApply already runs on the UI thread.
            var result = await PresetApplier.ApplyAsync(
                row.Preset, _participants, _controller, dispatcher: null);

            var summary = result.Skipped > 0
                ? $"Applied \"{row.Name}\" — {result.Changed} change(s); {result.Skipped} not in meeting"
                : $"Applied \"{row.Name}\" — {result.Changed} change(s)";
            _toast?.Show(summary);
            DialogResult = true;
            Close();
        }
        finally
        {
            ApplyButton.IsEnabled = true;
        }
    }

    /// <summary>
    /// Duplicate the selected preset under a new name. We auto-suggest
    /// "&lt;original&gt; (copy)" but pop a tiny input dialog so the operator
    /// can pick something meaningful. WPF doesn't ship an InputBox; we
    /// use a quick custom prompt below.
    /// </summary>
    private void OnDuplicate(object sender, RoutedEventArgs e)
    {
        if (PresetsList.SelectedItem is not PresetRow row) return;

        var defaultName = SuggestCopyName(row.Name);
        var newName = PromptForName("Duplicate preset", "New name:", defaultName);
        if (string.IsNullOrWhiteSpace(newName)) return;

        try
        {
            var existing = OperatorPresetStore.Find(newName);
            if (existing is not null)
            {
                var confirm = MessageBox.Show(
                    this,
                    $"A preset named \"{newName}\" already exists. Overwrite it?",
                    "TeamsISO — Duplicate preset",
                    MessageBoxButton.YesNo,
                    MessageBoxImage.Question,
                    MessageBoxResult.No);
                if (confirm != MessageBoxResult.Yes) return;
            }

            // Re-using Save() with a fresh SavedAt timestamp — Save's overwrite
            // semantics handle the name-collision case cleanly.
            OperatorPresetStore.Save(new OperatorPresetStore.Preset(
                Name: newName,
                SavedAt: DateTimeOffset.Now,
                Assignments: row.Preset.Assignments));
            _toast?.Show($"Duplicated to \"{newName}\"");
            ReloadPresets();
        }
        catch (Exception ex)
        {
            MessageBox.Show(this,
                $"Could not duplicate preset.\n\n{ex.Message}",
                "TeamsISO — Duplicate preset",
                MessageBoxButton.OK,
                MessageBoxImage.Warning);
        }
    }

    /// <summary>
    /// Suggest a name for a copy: "Foo" → "Foo (copy)", "Foo (copy)" → "Foo (copy 2)".
    /// Bumps the digit if the operator iterates from a copy.
    /// </summary>
    private static string SuggestCopyName(string original)
    {
        if (!original.EndsWith(")", StringComparison.Ordinal))
            return original + " (copy)";
        var match = System.Text.RegularExpressions.Regex.Match(original, @" \(copy(?: (\d+))?\)$");
        if (!match.Success) return original + " (copy)";
        var n = match.Groups[1].Success && int.TryParse(match.Groups[1].Value, out var parsed) ? parsed + 1 : 2;
        return original[..(original.Length - match.Length)] + $" (copy {n})";
    }

    /// <summary>
    /// Quick input dialog for a single string. WPF doesn't ship one, so we
    /// build a minimal modal here. Keeps the dialog dependency-free.
    /// </summary>
    private string? PromptForName(string title, string prompt, string defaultValue)
    {
        var dlg = new System.Windows.Window
        {
            Title = title,
            Owner = this,
            Width = 400,
            Height = 170,
            WindowStartupLocation = System.Windows.WindowStartupLocation.CenterOwner,
            ResizeMode = System.Windows.ResizeMode.NoResize,
            Background = (System.Windows.Media.Brush)FindResource("Wd.Canvas"),
        };
        var stack = new System.Windows.Controls.StackPanel { Margin = new System.Windows.Thickness(20) };
        stack.Children.Add(new System.Windows.Controls.TextBlock
        {
            Text = prompt,
            Margin = new System.Windows.Thickness(0, 0, 0, 8),
            Foreground = (System.Windows.Media.Brush)FindResource("Wd.Text.Primary"),
        });
        var tb = new System.Windows.Controls.TextBox { Text = defaultValue, Padding = new System.Windows.Thickness(8, 6, 8, 6) };
        stack.Children.Add(tb);
        var buttons = new System.Windows.Controls.StackPanel
        {
            Orientation = System.Windows.Controls.Orientation.Horizontal,
            HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
            Margin = new System.Windows.Thickness(0, 16, 0, 0),
        };
        var ok = new System.Windows.Controls.Button { Content = "OK", IsDefault = true, Padding = new System.Windows.Thickness(20, 6, 20, 6), Style = (System.Windows.Style)FindResource("Wd.Button.Primary") };
        var cancel = new System.Windows.Controls.Button { Content = "Cancel", IsCancel = true, Padding = new System.Windows.Thickness(14, 6, 14, 6), Margin = new System.Windows.Thickness(0, 0, 8, 0), Style = (System.Windows.Style)FindResource("Wd.Button.Ghost") };
        ok.Click += (_, _) => { dlg.DialogResult = true; dlg.Close(); };
        buttons.Children.Add(cancel);
        buttons.Children.Add(ok);
        stack.Children.Add(buttons);
        dlg.Content = stack;
        tb.Focus();
        tb.SelectAll();
        var result = dlg.ShowDialog();
        return result == true ? tb.Text.Trim() : null;
    }

    private void OnDelete(object sender, RoutedEventArgs e)
    {
        if (PresetsList.SelectedItem is not PresetRow row) return;

        var confirm = MessageBox.Show(
            this,
            $"Delete preset \"{row.Name}\"? This cannot be undone.",
            "TeamsISO — Delete preset",
            MessageBoxButton.YesNo,
            MessageBoxImage.Warning,
            MessageBoxResult.No);
        if (confirm != MessageBoxResult.Yes) return;

        try
        {
            OperatorPresetStore.Delete(row.Name);
            _toast?.Show($"Deleted preset \"{row.Name}\"");
            ReloadPresets();
        }
        catch (Exception ex)
        {
            MessageBox.Show(
                this,
                $"Could not delete preset.\n\n{ex.Message}",
                "TeamsISO — Delete preset",
                MessageBoxButton.OK,
                MessageBoxImage.Warning);
        }
    }

    private void OnCancel(object sender, RoutedEventArgs e)
    {
        DialogResult = false;
        Close();
    }

    /// <summary>
    /// Save every preset as a single .json bundle to a path the user picks via
    /// SaveFileDialog. We use Microsoft.Win32.SaveFileDialog because it doesn't
    /// drag in WinForms; the WPF host doesn't ship a built-in alternative.
    /// </summary>
    private void OnExport(object sender, RoutedEventArgs e)
    {
        var dlg = new Microsoft.Win32.SaveFileDialog
        {
            Title = "Export TeamsISO presets",
            FileName = $"teamsiso-presets-{DateTimeOffset.Now:yyyy-MM-dd}.json",
            Filter = "TeamsISO preset bundle (*.json)|*.json",
            DefaultExt = "json",
        };
        if (dlg.ShowDialog(this) != true) return;

        try
        {
            var json = OperatorPresetStore.ExportAllAsJson();
            System.IO.File.WriteAllText(dlg.FileName, json);
            _toast?.Show($"Exported {Rows.Count} preset(s)");
        }
        catch (Exception ex)
        {
            MessageBox.Show(this,
                $"Could not export presets.\n\n{ex.Message}",
                "TeamsISO — Export presets",
                MessageBoxButton.OK,
                MessageBoxImage.Warning);
        }
    }

    /// <summary>
    /// Load a bundle from a path the user picks. On name collision we ask once
    /// (covering all collisions) whether to overwrite — a per-preset prompt would
    /// be exhausting for a 20-preset bundle. The Y/N here drives the overwrite
    /// flag passed to <see cref="OperatorPresetStore.ImportBundle"/>.
    /// </summary>
    private void OnImport(object sender, RoutedEventArgs e)
    {
        var dlg = new Microsoft.Win32.OpenFileDialog
        {
            Title = "Import TeamsISO presets",
            Filter = "TeamsISO preset bundle (*.json)|*.json|All files (*.*)|*.*",
        };
        if (dlg.ShowDialog(this) != true) return;

        string json;
        try { json = System.IO.File.ReadAllText(dlg.FileName); }
        catch (Exception ex)
        {
            MessageBox.Show(this,
                $"Could not read the file.\n\n{ex.Message}",
                "TeamsISO — Import presets",
                MessageBoxButton.OK,
                MessageBoxImage.Warning);
            return;
        }

        // Quick parse to sniff for collisions before asking the operator anything.
        OperatorPresetStore.Bundle? bundle;
        try { bundle = System.Text.Json.JsonSerializer.Deserialize<OperatorPresetStore.Bundle>(json); }
        catch
        {
            MessageBox.Show(this,
                "That file isn't a valid TeamsISO preset bundle.",
                "TeamsISO — Import presets",
                MessageBoxButton.OK,
                MessageBoxImage.Warning);
            return;
        }
        if (bundle is null || bundle.Presets is null || bundle.Presets.Count == 0)
        {
            MessageBox.Show(this,
                "The bundle is empty.",
                "TeamsISO — Import presets",
                MessageBoxButton.OK,
                MessageBoxImage.Information);
            return;
        }

        var existingNames = OperatorPresetStore.LoadAll()
            .Select(p => p.Name)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        var collisions = bundle.Presets.Count(p => existingNames.Contains(p.Name));

        var overwrite = false;
        if (collisions > 0)
        {
            var choice = MessageBox.Show(
                this,
                $"{collisions} preset name(s) in this bundle already exist on this machine.\n\n" +
                "Yes = overwrite local copies with the bundle's versions.\n" +
                "No = keep local copies; only import new presets.",
                "TeamsISO — Import presets",
                MessageBoxButton.YesNoCancel,
                MessageBoxImage.Question,
                MessageBoxResult.No);
            if (choice == MessageBoxResult.Cancel) return;
            overwrite = choice == MessageBoxResult.Yes;
        }

        var result = OperatorPresetStore.ImportBundle(json, overwrite);
        if (result.Error is not null)
        {
            MessageBox.Show(this,
                $"Import failed.\n\n{result.Error}",
                "TeamsISO — Import presets",
                MessageBoxButton.OK,
                MessageBoxImage.Warning);
            return;
        }

        var summary = $"Imported — {result.Added} new";
        if (result.Overwritten > 0) summary += $", {result.Overwritten} overwritten";
        if (result.Skipped > 0) summary += $", {result.Skipped} skipped";
        _toast?.Show(summary);
        ReloadPresets();
    }
}
