using System.Collections.Specialized;
using System.Text.Json;
using DragonISO.Engine.Domain;

namespace DragonISO.App.Services;

// /participants/* route handlers. Anything that reads or writes
// participant + per-pipeline state lives here.
//
//   GET    /participants                     â†’ GetParticipants
//   POST   /participants/{id}/iso            â†’ ToggleIsoByIdAsync
//   POST   /participants/iso                 â†’ ToggleIsoByNameAsync
//   POST   /participants/{id}/override       â†’ SetIsoOverrideByIdAsync
//   DELETE /participants/{id}/override       â†’ ClearIsoOverrideByIdAsync
public sealed partial class ControlSurfaceServer
{
    private object GetParticipants()
    {
        var vm = _viewModel();
        if (vm is null) return new { participants = Array.Empty<object>() };
        // Synchronously snapshot on the UI thread â€” ObservableCollection
        // isn't safe to enumerate from this request handler's thread-pool
        // task, and the ParticipantViewModel property reads chase
        // data-binding state.
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (dispatcher is null) return new { participants = Array.Empty<object>() };
        var globals = _controller.GlobalSettings;
        var list = dispatcher.Invoke(() => vm.Participants.Select(p => {
            var ovr = _controller.GetIsoOverride(p.Id);
            return (object)new
            {
                id = p.Id,
                displayName = p.DisplayName,
                isOnline = p.IsOnline,
                isEnabled = p.IsEnabled,
                customName = string.IsNullOrEmpty(p.CustomName) ? null : p.CustomName,
                stateLabel = p.StateLabel,
                // Effective settings = override if set, else globals. The
                // web UI uses this to show the current per-row values
                // without a separate round-trip to /global.
                effective = new
                {
                    framerate = (ovr ?? globals).Framerate.ToString(),
                    resolution = (ovr ?? globals).Resolution.ToString(),
                    aspect = (ovr ?? globals).Aspect.ToString(),
                    audio = (ovr ?? globals).Audio.ToString(),
                    isOverride = ovr is not null,
                },
            };
        }).ToArray());
        return new { participants = list, globals = new {
            framerate = globals.Framerate.ToString(),
            resolution = globals.Resolution.ToString(),
            aspect = globals.Aspect.ToString(),
            audio = globals.Audio.ToString(),
        } };
    }

    /// <summary>
    /// POST /participants/{id}/override â€” set or replace the per-pipeline
    /// override. Body fields: framerate (enum string), resolution (enum
    /// string), aspect (enum string), audio (enum string). All fields are
    /// optional; missing fields fall back to the current global value.
    /// </summary>
    private async Task<object> SetIsoOverrideByIdAsync(string path, JsonElement body)
    {
        var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length != 3 || segments[0] != "participants" || segments[2] != "override")
            return new { ok = false, error = "expected /participants/{id}/override" };
        if (!Guid.TryParse(segments[1], out var id))
            return new { ok = false, error = "invalid id" };

        var g = _controller.GlobalSettings;
        var framerate = TryParseEnum(body, "framerate", g.Framerate);
        var resolution = TryParseEnum(body, "resolution", g.Resolution);
        var aspect = TryParseEnum(body, "aspect", g.Aspect);
        var audio = TryParseEnum(body, "audio", g.Audio);
        var ovr = new FrameProcessingSettings(framerate, resolution, aspect, audio);
        await _controller.SetIsoOverrideAsync(id, ovr, CancellationToken.None);
        return new { ok = true, id, effective = new
        {
            framerate = ovr.Framerate.ToString(),
            resolution = ovr.Resolution.ToString(),
            aspect = ovr.Aspect.ToString(),
            audio = ovr.Audio.ToString(),
            isOverride = true,
        } };
    }

    /// <summary>DELETE /participants/{id}/override â€” pipeline reverts to global settings.</summary>
    private async Task<object> ClearIsoOverrideByIdAsync(string path)
    {
        var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length != 3 || segments[0] != "participants" || segments[2] != "override")
            return new { ok = false, error = "expected /participants/{id}/override" };
        if (!Guid.TryParse(segments[1], out var id))
            return new { ok = false, error = "invalid id" };
        await _controller.SetIsoOverrideAsync(id, null, CancellationToken.None);
        return new { ok = true, id, cleared = true };
    }

    /// <summary>
    /// Parse an enum value from a JSON body, falling back to a default when
    /// the field is missing or the value doesn't match any enum member.
    /// Case-insensitive. Used by SetIsoOverrideByIdAsync for the four
    /// FrameProcessingSettings enums.
    /// </summary>
    private static TEnum TryParseEnum<TEnum>(JsonElement body, string field, TEnum fallback)
        where TEnum : struct, Enum
    {
        if (body.ValueKind != JsonValueKind.Object) return fallback;
        if (!body.TryGetProperty(field, out var prop)) return fallback;
        if (prop.ValueKind != JsonValueKind.String) return fallback;
        var s = prop.GetString();
        if (string.IsNullOrEmpty(s)) return fallback;
        return Enum.TryParse<TEnum>(s, ignoreCase: true, out var result) ? result : fallback;
    }

    private async Task<object> ToggleIsoByIdAsync(string path, JsonElement body, NameValueCollection query)
    {
        // path = /participants/<guid>/iso
        var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
        if (segments.Length != 3 || segments[0] != "participants" || segments[2] != "iso")
            return NotFound();
        if (!Guid.TryParse(segments[1], out var id))
            return new { ok = false, error = "invalid id" };
        return await ToggleByIdAsync(id, body, query);
    }

    private async Task<object> ToggleIsoByNameAsync(JsonElement body, NameValueCollection query)
    {
        var displayName = TryGetString(body, query, "displayName");
        if (string.IsNullOrWhiteSpace(displayName))
            return new { ok = false, error = "displayName required" };
        var vm = _viewModel();
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (vm is null || dispatcher is null)
            return new { ok = false, error = "view-model not ready" };
        var p = await dispatcher.InvokeAsync(() => vm.Participants.FirstOrDefault(x =>
            string.Equals(x.DisplayName, displayName, StringComparison.OrdinalIgnoreCase)));
        if (p is null) return new { ok = false, error = "participant not found", displayName };
        return await ToggleByIdAsync(p.Id, body, query);
    }

    private async Task<object> ToggleByIdAsync(Guid id, JsonElement body, NameValueCollection query)
    {
        var enabled = TryGetBool(body, query, "enabled");
        var customName = TryGetString(body, query, "customName");
        var vm = _viewModel();
        var dispatcher = System.Windows.Application.Current?.Dispatcher;
        if (vm is null || dispatcher is null)
            return new { ok = false, error = "view-model not ready" };

        // Look up the VM and snapshot its current state on the UI thread â€”
        // ObservableCollection enumeration and view-model property reads
        // both need to happen there.
        var lookup = await dispatcher.InvokeAsync(() =>
        {
            var p = vm.Participants.FirstOrDefault(x => x.Id == id);
            return p is null
                ? null
                : new { Pvm = p, p.IsEnabled, p.CustomName };
        });
        if (lookup is null) return new { ok = false, error = "participant not found", id };

        var target = enabled ?? !lookup.IsEnabled;
        var nameToUse = !string.IsNullOrEmpty(customName) ? customName : lookup.CustomName;

        if (target == lookup.IsEnabled && string.IsNullOrEmpty(customName))
            return new { ok = true, id, enabled = lookup.IsEnabled, action = "noop" };

        // Apply CustomName change first (if any) on the UI thread so a
        // subsequent EnableIsoAsync sees the new name.
        if (!string.IsNullOrEmpty(customName))
            await dispatcher.InvokeAsync(() => lookup.Pvm.CustomName = customName);

        if (target)
        {
            await _controller.EnableIsoAsync(id,
                string.IsNullOrWhiteSpace(nameToUse) ? null : nameToUse,
                CancellationToken.None);
            await dispatcher.InvokeAsync(() => lookup.Pvm.IsEnabled = true);
        }
        else
        {
            await _controller.DisableIsoAsync(id, CancellationToken.None);
            await dispatcher.InvokeAsync(() => lookup.Pvm.IsEnabled = false);
        }
        return new { ok = true, id, enabled = target };
    }
}
