#nullable enable

using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;

namespace MBN_STOCK_WEBVIEW.Core.Playout;

public sealed record PlayoutBridgeCommand(
    string RequestId,
    string Command,
    PlayoutCue? Cue);

public sealed record PlayoutBridgeCommandParseResult(
    bool IsValid,
    PlayoutBridgeCommand Request,
    string Error);

public sealed record PlayoutBridgeWorkflowCommand(
    string RequestId,
    string Command,
    IReadOnlyList<LegacyPlaylistEntry>? Playlist,
    int SelectedIndexZeroBased);

public sealed record PlayoutBridgeWorkflowCommandParseResult(
    bool IsValid,
    PlayoutBridgeWorkflowCommand Request,
    string Error);

public sealed record PlayoutBridgeTimeoutQuarantine(
    string RequestId,
    string Command);

public sealed record PlayoutBridgeTimeoutQuarantineParseResult(
    bool IsValid,
    PlayoutBridgeTimeoutQuarantine Request,
    string Error);

public sealed record PlayoutBridgePagePlanRequest(
    string RequestId,
    IReadOnlyList<LegacyPlaylistEntry>? Entries);

public sealed record PlayoutBridgePagePlanRequestParseResult(
    bool IsValid,
    PlayoutBridgePagePlanRequest Request,
    string Error);

/// <summary>
/// Strict, COM-neutral parser for the local WebView playout command boundary.
/// Presentation text is intentionally not accepted as scene mutation data.
/// </summary>
public static class PlayoutBridgeProtocol
{
    private const int MaximumRequestIdLength = 128;
    private const int MaximumSceneCodeLength = 64;
    private const int MaximumPlaylistLength = 1_000;
    private const int MaximumLookupTextLength = 256;

    public static bool IsTrustedSource(string? source, string expectedHost) =>
        !string.IsNullOrWhiteSpace(source) &&
        !string.IsNullOrWhiteSpace(expectedHost) &&
        Uri.TryCreate(source, UriKind.Absolute, out var uri) &&
        uri.Scheme == Uri.UriSchemeHttps &&
        uri.IsDefaultPort &&
        string.IsNullOrEmpty(uri.UserInfo) &&
        uri.Host.Equals(expectedHost, StringComparison.OrdinalIgnoreCase);

    public static PlayoutBridgeCommandParseResult ParseCommand(JsonElement payload)
    {
        var empty = new PlayoutBridgeCommand(string.Empty, string.Empty, null);
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId", "command", "cue"))
        {
            return Invalid(empty, "송출 요청에 허용되지 않은 필드가 있습니다.");
        }

        if (!TryGetSafeToken(payload, "requestId", MaximumRequestIdLength, out var requestId))
        {
            return Invalid(empty, "송출 요청 식별자가 올바르지 않습니다.");
        }

        var command = GetString(payload, "command") ?? string.Empty;
        if (command is not ("prepare" or "take-in" or "next" or "take-out"))
        {
            return Invalid(
                new PlayoutBridgeCommand(requestId, string.Empty, null),
                "지원하지 않는 송출 명령입니다.");
        }

        var hasCue = payload.TryGetProperty("cue", out var cueElement);
        if (command is "prepare" or "next")
        {
            if (!hasCue || !TryParseCue(cueElement, out var cue))
            {
                return Invalid(
                    new PlayoutBridgeCommand(requestId, command, null),
                    "그래픽 cue 형식이 올바르지 않습니다.");
            }

            return Valid(new PlayoutBridgeCommand(requestId, command, cue));
        }

        if (hasCue)
        {
            return Invalid(
                new PlayoutBridgeCommand(requestId, command, null),
                "이 송출 명령에는 cue를 지정할 수 없습니다.");
        }

        return Valid(new PlayoutBridgeCommand(requestId, command, null));
    }

    /// <summary>
    /// Parses the native workflow boundary used by the migrated MainForm state machine.
    /// PREPARE supplies an immutable playlist snapshot; NEXT has no caller-selected cue so
    /// native PageN/playlist rules remain authoritative. Lookup text can only reach
    /// parameterized scene-data loaders and is never interpreted as a K3D control surface.
    /// </summary>
    public static PlayoutBridgeWorkflowCommandParseResult ParseWorkflowCommand(
        JsonElement payload)
    {
        var empty = new PlayoutBridgeWorkflowCommand(
            string.Empty,
            string.Empty,
            null,
            -1);
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId", "command", "playlist", "selectedIndex"))
        {
            return InvalidWorkflow(empty, "The playout workflow request shape is invalid.");
        }

        if (!TryGetSafeToken(payload, "requestId", MaximumRequestIdLength, out var requestId))
        {
            return InvalidWorkflow(empty, "The playout workflow request identifier is invalid.");
        }

        var command = GetString(payload, "command") ?? string.Empty;
        if (command is not ("prepare" or "take-in" or "next" or "take-out"))
        {
            return InvalidWorkflow(
                new PlayoutBridgeWorkflowCommand(requestId, string.Empty, null, -1),
                "The playout workflow command is not supported.");
        }

        var hasPlaylist = payload.TryGetProperty("playlist", out var playlistElement);
        var hasSelectedIndex = payload.TryGetProperty("selectedIndex", out var indexElement);
        if (command == "prepare")
        {
            if (!hasPlaylist || !hasSelectedIndex ||
                !TryParsePlaylist(playlistElement, out var playlist) ||
                indexElement.ValueKind != JsonValueKind.Number ||
                !indexElement.TryGetInt32(out var selectedIndex) ||
                selectedIndex < 0 || selectedIndex >= playlist!.Count)
            {
                return InvalidWorkflow(
                    new PlayoutBridgeWorkflowCommand(requestId, command, null, -1),
                    "The PREPARE playlist selection is invalid.");
            }

            return ValidWorkflow(new PlayoutBridgeWorkflowCommand(
                requestId,
                command,
                playlist,
                selectedIndex));
        }

        if (hasPlaylist || hasSelectedIndex)
        {
            return InvalidWorkflow(
                new PlayoutBridgeWorkflowCommand(requestId, command, null, -1),
                "Only PREPARE can supply a playlist selection.");
        }

        return ValidWorkflow(new PlayoutBridgeWorkflowCommand(
            requestId,
            command,
            null,
            -1));
    }

    /// <summary>
    /// Parses the read-only PageN preflight boundary. It accepts only the three paged
    /// cut aliases and never accepts a stored page number, cue, mutation, or asset path.
    /// </summary>
    public static PlayoutBridgePagePlanRequestParseResult ParsePagePlanRequest(
        JsonElement payload)
    {
        var empty = new PlayoutBridgePagePlanRequest(string.Empty, null);
        if (payload.ValueKind != JsonValueKind.Object)
        {
            return InvalidPagePlan(empty, "The playlist page-plan request is invalid.");
        }

        if (!TryGetSafeToken(payload, "requestId", MaximumRequestIdLength, out var requestId))
        {
            return InvalidPagePlan(empty, "The playlist page-plan request identifier is invalid.");
        }

        var correlated = new PlayoutBridgePagePlanRequest(requestId, null);
        if (!HasOnlyProperties(payload, "requestId", "entries") ||
            !payload.TryGetProperty("entries", out var entriesElement) ||
            !TryParsePlaylist(entriesElement, out var entries) ||
            entries!.Any(entry => entry.CutCode is not ("5074" or "5077" or "5088")))
        {
            return InvalidPagePlan(correlated, "The playlist page-plan entries are invalid.");
        }

        return ValidPagePlan(new PlayoutBridgePagePlanRequest(requestId, entries));
    }

    /// <summary>
    /// Parses the browser watchdog notification. A valid notification permanently
    /// quarantines the native playout runtime for the remainder of the app process;
    /// it is deliberately not a playout command and carries no scene data.
    /// </summary>
    public static PlayoutBridgeTimeoutQuarantineParseResult ParseTimeoutQuarantine(
        JsonElement payload)
    {
        var empty = new PlayoutBridgeTimeoutQuarantine(string.Empty, string.Empty);
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "requestId", "command"))
        {
            return InvalidTimeoutQuarantine(
                empty,
                "The playout timeout quarantine request shape is invalid.");
        }

        if (!TryGetSafeToken(payload, "requestId", MaximumRequestIdLength, out var requestId))
        {
            return InvalidTimeoutQuarantine(
                empty,
                "The playout timeout quarantine request identifier is invalid.");
        }

        var command = GetString(payload, "command") ?? string.Empty;
        if (command is not ("prepare" or "take-in" or "next" or "take-out"))
        {
            return InvalidTimeoutQuarantine(
                new PlayoutBridgeTimeoutQuarantine(requestId, string.Empty),
                "The timed-out playout command is not supported.");
        }

        return ValidTimeoutQuarantine(
            new PlayoutBridgeTimeoutQuarantine(requestId, command));
    }

    private static bool TryParseCue(JsonElement payload, out PlayoutCue? cue)
    {
        cue = null;
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(payload, "code") ||
            !TryGetSafeToken(payload, "code", MaximumSceneCodeLength, out var code))
        {
            return false;
        }

        cue = new PlayoutCue($"{code}.t2s", code);
        return true;
    }

    private static bool TryParsePlaylist(
        JsonElement payload,
        out IReadOnlyList<LegacyPlaylistEntry>? playlist)
    {
        playlist = null;
        if (payload.ValueKind != JsonValueKind.Array ||
            payload.GetArrayLength() is < 1 or > MaximumPlaylistLength)
        {
            return false;
        }

        var entries = new List<LegacyPlaylistEntry>(payload.GetArrayLength());
        var identifiers = new HashSet<string>(StringComparer.Ordinal);
        foreach (var element in payload.EnumerateArray())
        {
            if (element.ValueKind != JsonValueKind.Object ||
                !HasOnlyProperties(element, "id", "code", "enabled", "fadeDuration", "selection") ||
                !TryGetSafeToken(element, "id", MaximumRequestIdLength, out var id) ||
                !TryGetSafeToken(element, "code", MaximumSceneCodeLength, out var code) ||
                !identifiers.Add(id) ||
                !element.TryGetProperty("enabled", out var enabledElement) ||
                enabledElement.ValueKind is not (JsonValueKind.True or JsonValueKind.False))
            {
                return false;
            }

            int? fadeDuration = null;
            if (element.TryGetProperty("fadeDuration", out var fadeElement))
            {
                if (fadeElement.ValueKind != JsonValueKind.Number ||
                    !fadeElement.TryGetInt32(out var parsedFade) ||
                    parsedFade is < 0 or > 19)
                {
                    return false;
                }

                fadeDuration = parsedFade;
            }

            LegacySceneSelection? selection = null;
            if (element.TryGetProperty("selection", out var selectionElement) &&
                !TryParseSelection(selectionElement, out selection))
            {
                return false;
            }

            entries.Add(new LegacyPlaylistEntry(
                id,
                code,
                enabledElement.GetBoolean(),
                fadeDuration,
                selection));
        }

        playlist = Array.AsReadOnly(entries.ToArray());
        return true;
    }

    private static bool TryParseSelection(
        JsonElement payload,
        out LegacySceneSelection? selection)
    {
        selection = null;
        if (payload.ValueKind != JsonValueKind.Object ||
            !HasOnlyProperties(
                payload,
                "groupCode",
                "subject",
                "graphicType",
                "subtype",
                "dataCode") ||
            !TryGetLookupText(payload, "groupCode", out var groupCode) ||
            !TryGetLookupText(payload, "subject", out var subject) ||
            !TryGetLookupText(payload, "graphicType", out var graphicType) ||
            !TryGetLookupText(payload, "subtype", out var subtype) ||
            !TryGetLookupText(payload, "dataCode", out var dataCode))
        {
            return false;
        }

        selection = new LegacySceneSelection(
            groupCode,
            subject,
            graphicType,
            subtype,
            dataCode);
        return true;
    }

    private static bool TryGetLookupText(
        JsonElement payload,
        string propertyName,
        out string value)
    {
        value = GetString(payload, propertyName) ?? string.Empty;
        return payload.TryGetProperty(propertyName, out var element) &&
            element.ValueKind == JsonValueKind.String &&
            value.Length <= MaximumLookupTextLength &&
            !value.Any(char.IsControl);
    }

    private static bool TryGetSafeToken(
        JsonElement payload,
        string propertyName,
        int maximumLength,
        out string value)
    {
        value = GetString(payload, propertyName) ?? string.Empty;
        return value.Length is > 0 && value.Length <= maximumLength &&
            value.All(character => char.IsAsciiLetterOrDigit(character) || character is '_' or '-');
    }

    private static bool HasOnlyProperties(JsonElement payload, params string[] allowed)
    {
        var seen = new HashSet<string>(StringComparer.Ordinal);
        foreach (var property in payload.EnumerateObject())
        {
            if (!seen.Add(property.Name) ||
                !allowed.Contains(property.Name, StringComparer.Ordinal))
            {
                return false;
            }
        }

        return true;
    }

    private static string? GetString(JsonElement payload, string propertyName) =>
        payload.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
            ? value.GetString()
            : null;

    private static PlayoutBridgeCommandParseResult Valid(PlayoutBridgeCommand request) =>
        new(true, request, string.Empty);

    private static PlayoutBridgeCommandParseResult Invalid(
        PlayoutBridgeCommand request,
        string error) => new(false, request, error);

    private static PlayoutBridgeWorkflowCommandParseResult ValidWorkflow(
        PlayoutBridgeWorkflowCommand request) => new(true, request, string.Empty);

    private static PlayoutBridgeWorkflowCommandParseResult InvalidWorkflow(
        PlayoutBridgeWorkflowCommand request,
        string error) => new(false, request, error);

    private static PlayoutBridgeTimeoutQuarantineParseResult ValidTimeoutQuarantine(
        PlayoutBridgeTimeoutQuarantine request) => new(true, request, string.Empty);

    private static PlayoutBridgeTimeoutQuarantineParseResult InvalidTimeoutQuarantine(
        PlayoutBridgeTimeoutQuarantine request,
        string error) => new(false, request, error);

    private static PlayoutBridgePagePlanRequestParseResult ValidPagePlan(
        PlayoutBridgePagePlanRequest request) => new(true, request, string.Empty);

    private static PlayoutBridgePagePlanRequestParseResult InvalidPagePlan(
        PlayoutBridgePagePlanRequest request,
        string error) => new(false, request, error);
}
