using System.Windows.Threading;
using DragonISO.App.ViewModels;
using DragonISO.Engine.Controller;

namespace DragonISO.App.Services;

/// <summary>
/// Shared preset-application logic. Originally lived inline in
/// <c>PresetsDialog.OnApply</c>; lifted out so the REST control surface
/// (<see cref="ControlSurfaceServer"/>) and the auto-apply-on-launch path
/// (<see cref="MainViewModel.TryAutoApplyPendingPreset"/>) can call the same
/// implementation. Single source of truth for "what does Apply mean."
///
/// Application proceeds participant-by-participant, matching by display name
/// (the only stable join key across meetings since Ids regen each session).
/// For each match, the custom output name is updated and IsEnabled is
/// reconciled with the preset's value via <see cref="IIsoController.EnableIsoAsync"/>
/// / <see cref="IIsoController.DisableIsoAsync"/>. Per-participant failures are
/// caught and counted; one bad row never aborts applying the rest.
/// </summary>
public static class PresetApplier
{
    /// <summary>Result counts from an apply pass.</summary>
    public sealed record ApplyResult(int Matched, int Changed, int Skipped);

    /// <summary>
    /// Apply <paramref name="preset"/> to the live <paramref name="participants"/>
    /// list. <paramref name="dispatcher"/>, when supplied, is used to marshal
    /// IsEnabled / CustomName property writes onto the UI thread; pass null in
    /// contexts that already run on the UI thread (e.g. the dialog's button click).
    /// </summary>
    public static async Task<ApplyResult> ApplyAsync(
        Services.OperatorPresetStore.Preset preset,
        IReadOnlyList<ParticipantViewModel> participants,
        IIsoController controller,
        Dispatcher? dispatcher = null,
        CancellationToken cancellationToken = default)
    {
        // Build the lookup once, case-insensitive â€” Teams display names are
        // human-typed, so "Jane" and "jane" should match the same row.
        var byName = preset.Assignments.ToDictionary(
            a => a.DisplayName,
            StringComparer.OrdinalIgnoreCase);

        var matched = 0;
        var changed = 0;

        foreach (var p in participants)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (!byName.TryGetValue(p.DisplayName, out var assignment)) continue;
            matched++;

            await SetOnUiAsync(dispatcher, () => p.CustomName = assignment.CustomOutputName ?? string.Empty);

            if (assignment.Enabled && !p.IsEnabled)
            {
                try
                {
                    await controller.EnableIsoAsync(
                        p.Id,
                        string.IsNullOrWhiteSpace(p.CustomName) ? null : p.CustomName,
                        cancellationToken);
                    await SetOnUiAsync(dispatcher, () => p.IsEnabled = true);
                    changed++;
                }
                catch
                {
                    // Per-participant best-effort: the rest still get applied.
                }
            }
            else if (!assignment.Enabled && p.IsEnabled)
            {
                try
                {
                    await controller.DisableIsoAsync(p.Id, cancellationToken);
                    await SetOnUiAsync(dispatcher, () => p.IsEnabled = false);
                    changed++;
                }
                catch
                {
                    /* defensive */
                }
            }
        }

        // Mark applied so auto-apply-on-launch picks the right preset next time.
        try { Services.OperatorPresetStore.MarkApplied(preset.Name); }
        catch { /* preference write is best-effort */ }

        var skipped = preset.Assignments.Count - matched;
        return new ApplyResult(matched, changed, skipped);
    }

    private static Task SetOnUiAsync(Dispatcher? dispatcher, Action action)
    {
        if (dispatcher is null || dispatcher.CheckAccess())
        {
            action();
            return Task.CompletedTask;
        }
        return dispatcher.InvokeAsync(action).Task;
    }
}
