using System.Windows.Threading;
using DragonISO.App.Services;

namespace DragonISO.App.ViewModels;

// Auto-apply-last-preset path. Split out of MainViewModel.cs so the
// pending-preset bookkeeping doesn't clutter the main file.
//
// Lifecycle:
//   â€¢ InitializeAsync (in main file) reads operator preference + last-applied
//     name from OperatorPresetStore and sets _pendingPresetName + deadline.
//   â€¢ OnParticipantsChanged (in main file) calls TryAutoApplyPendingPreset
//     once participants populate.
//   â€¢ RequestApplyPresetOnStartup is the CLI hook: `--apply-preset NAME`.
public sealed partial class MainViewModel
{
    // Set on InitializeAsync from disk; cleared once we successfully apply
    // (so we don't re-apply when the participant list later mutates). The
    // grace deadline gives Teams enough time to publish all initial sources
    // after engine start before we attempt the apply â€” applying before
    // everyone's visible would partially-restore the routing and silently
    // drop assignments for late-appearing participants.
    private string? _pendingPresetName;
    private DateTimeOffset _pendingPresetDeadline;
    private bool _pendingPresetApplied;

    /// <summary>
    /// CLI override for the launch-time auto-apply. Called from App.OnStartup
    /// after parsing <c>--apply-preset</c>. Sets the same pending-preset state
    /// that the user-toggled auto-apply path uses, so a single trigger flow
    /// covers both. Wins over the persisted preference (operator's CLI intent
    /// is more recent than what's on disk).
    /// </summary>
    public void RequestApplyPresetOnStartup(string presetName)
    {
        _pendingPresetName = presetName;
        _pendingPresetDeadline = DateTimeOffset.UtcNow.AddSeconds(30);
        _pendingPresetApplied = false;
    }

    /// <summary>
    /// Reads the operator's auto-apply preference + last-applied preset name
    /// from disk and seeds the pending-preset state. Called by InitializeAsync
    /// during engine startup. Failures are swallowed â€” a preset read fault
    /// should never block the engine from coming up.
    /// </summary>
    private void LoadPendingPresetFromPreferences()
    {
        try
        {
            var pref = OperatorPresetStore.GetStartupPreference();
            if (pref.AutoApplyOnStartup && !string.IsNullOrEmpty(pref.LastAppliedName))
            {
                _pendingPresetName = pref.LastAppliedName;
                // 30s grace window is generous: Teams typically advertises all
                // existing participants within 5â€“10s of NDI discovery starting.
                // After this deadline we apply with whoever is visible.
                _pendingPresetDeadline = DateTimeOffset.UtcNow.AddSeconds(30);
            }
        }
        catch { /* preset read failures shouldn't block engine startup */ }
    }

    /// <summary>
    /// Attempts to apply <c>_pendingPresetName</c> if either every preset
    /// assignment matches a live participant, or the grace deadline has
    /// passed. Idempotent â€” repeat calls without state change are no-ops;
    /// once we fire we flag <c>_pendingPresetApplied</c> so subsequent
    /// participant churn doesn't trigger a second apply. Failures (missing
    /// preset on disk, preset that no longer matches anyone) are swallowed:
    /// the operator can always re-apply manually via the dialog. Delegates
    /// to <see cref="PresetApplier.ApplyAsync"/> for the actual
    /// reconciliation so the dialog, REST surface, and this auto-apply path
    /// all share a single implementation.
    /// </summary>
    private void TryAutoApplyPendingPreset()
    {
        OperatorPresetStore.Preset? preset;
        try { preset = OperatorPresetStore.Find(_pendingPresetName!); }
        catch { preset = null; }
        if (preset is null)
        {
            _pendingPresetApplied = true; // give up; nothing on disk to apply
            return;
        }

        var liveNames = new HashSet<string>(
            Participants.Select(p => p.DisplayName),
            StringComparer.OrdinalIgnoreCase);
        var allPresent = preset.Assignments.All(a => liveNames.Contains(a.DisplayName));
        if (!allPresent && DateTimeOffset.UtcNow < _pendingPresetDeadline)
            return; // wait for the rest of the meeting to populate

        _pendingPresetApplied = true;
        var captured = preset;
        // Snapshot the participants list since we're about to await on a
        // worker thread; the live ObservableCollection isn't safe to
        // enumerate from outside the dispatcher.
        var snapshot = Participants.ToList();
        _ = Task.Run(async () =>
        {
            var result = await PresetApplier.ApplyAsync(
                captured, snapshot, _controller, _dispatcher);
            await _dispatcher.InvokeAsync(() =>
                Toast.Show($"Auto-applied preset \"{captured.Name}\" â€” {result.Matched} participant(s)"));
        });
    }
}
