using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Registration;
using MBN_STOCK_WEBVIEW.Playout.Runtime;
using MBN_STOCK_WEBVIEW.Playout.Safety;

namespace MBN_STOCK_WEBVIEW.Playout;

internal sealed class TornadoPlayoutEngine : IPlayoutEngine
{
    private const string ProcessChangedMessage =
        "Tornado 프로세스가 변경되어 안전한 재연결이 필요합니다.";

    private readonly ValidatedPlayoutOptions _options;
    private readonly IK3dSessionFactory _sessionFactory;
    private readonly IK3dRegistrationProbe _registrationProbe;
    private readonly ITornadoProcessProbe _processProbe;
    private readonly IStaDispatcher? _dispatcher;
    private readonly ILiveAuthorization _liveAuthorization;
    private readonly PlayoutLaunchAuthorization _launchAuthorization;
    private readonly TimeProvider _timeProvider;
    private readonly Func<bool>? _finalConnectSafetyCheck;
    private readonly bool _abandonOnTargetMismatch;
    private readonly Action? _beforeSdkDispatchClaim;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly CancellationTokenSource _monitorCancellation = new();
    private readonly object _statusSync = new();
    private readonly Task _monitorTask;

    private IK3dSession? _session;
    private TornadoProcessSnapshot? _connectedProcessSnapshot;
    private PlayoutCue? _preparedCue;
    private string? _onAirSceneName;
    private TornadoProcessSnapshot _processSnapshot = new(0, 0, 0);
    private PlayoutConnectionState _connectionState;
    private DateTimeOffset _nextReconnectAt;
    private int _reconnectAttempts;
    private long _sequence;
    private int _lastKtapConnectState;
    private int _ktapHelloObservedState;
    private bool _connectionRequested;
    private volatile bool _outcomeUnknown;
    private volatile bool _quarantineIncomplete;
    private volatile bool _disposed;
    private int _disposeStarted;

    internal TornadoPlayoutEngine(
        ValidatedPlayoutOptions options,
        IK3dSessionFactory sessionFactory,
        IK3dRegistrationProbe registrationProbe,
        ITornadoProcessProbe processProbe,
        IStaDispatcher? dispatcher,
        ILiveAuthorization liveAuthorization,
        TimeProvider timeProvider,
        bool startMonitor = true,
        Func<bool>? finalConnectSafetyCheck = null,
        bool abandonOnTargetMismatch = false,
        Action? beforeSdkDispatchClaim = null,
        PlayoutLaunchAuthorization? launchAuthorization = null)
    {
        _options = options;
        _sessionFactory = sessionFactory;
        _registrationProbe = registrationProbe;
        _processProbe = processProbe;
        _dispatcher = dispatcher;
        _liveAuthorization = liveAuthorization;
        _launchAuthorization = launchAuthorization ??
            PlayoutLaunchAuthorization.Unrestricted;
        _timeProvider = timeProvider;
        _finalConnectSafetyCheck = finalConnectSafetyCheck;
        _abandonOnTargetMismatch = abandonOnTargetMismatch;
        _beforeSdkDispatchClaim = beforeSdkDispatchClaim;
        _connectionState = options.Mode switch
        {
            PlayoutMode.Disabled => PlayoutConnectionState.Disabled,
            PlayoutMode.DryRun => PlayoutConnectionState.DryRunReady,
            _ => PlayoutConnectionState.Disconnected
        };
        if (startMonitor && options.Mode != PlayoutMode.Disabled)
        {
            _processSnapshot = CaptureProcessSnapshot();
        }

        Status = BuildStatus(InitialMessage(options.Mode));
        _monitorTask = startMonitor && options.Mode != PlayoutMode.Disabled
            ? Task.Run(MonitorLoopAsync)
            : Task.CompletedTask;
    }

    public PlayoutStatus Status { get; private set; }

    public event EventHandler<PlayoutStatusChangedEventArgs>? StatusChanged;

    public Task<PlayoutResult> ConnectAsync(CancellationToken cancellationToken = default) =>
        SerializedAsync(PlayoutOperation.Connect, cancellationToken, ConnectCoreAsync);

    public Task<PlayoutResult> DisconnectAsync(CancellationToken cancellationToken = default) =>
        SerializedAsync(PlayoutOperation.Disconnect, cancellationToken, DisconnectCoreAsync);

    public Task<PlayoutResult> PrepareAsync(
        PlayoutCue cue,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(cue);
        return SerializedAsync(
            PlayoutOperation.Prepare,
            cancellationToken,
            token => PrepareCoreAsync(cue, token),
            cue);
    }

    public Task<PlayoutResult> TakeInAsync(CancellationToken cancellationToken = default) =>
        SerializedAsync(PlayoutOperation.TakeIn, cancellationToken, TakeInCoreAsync);

    public Task<PlayoutResult> NextAsync(
        PlayoutCue cue,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(cue);
        return SerializedAsync(
            PlayoutOperation.Next,
            cancellationToken,
            token => NextCoreAsync(cue, token),
            cue);
    }

    public Task<PlayoutResult> UpdateOnAirAsync(
        PlayoutCue cue,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(cue);
        return SerializedAsync(
            PlayoutOperation.UpdateOnAir,
            cancellationToken,
            token => UpdateOnAirCoreAsync(cue, token),
            cue);
    }

    public Task<PlayoutResult> TakeOutAsync(
        PlayoutTakeOutScope scope,
        CancellationToken cancellationToken = default) =>
        SerializedAsync(
            PlayoutOperation.TakeOut,
            cancellationToken,
            token => TakeOutCoreAsync(scope, token));

    public async ValueTask QuarantineAsync()
    {
        await _gate.WaitAsync(CancellationToken.None).ConfigureAwait(false);
        try
        {
            if (_disposed)
            {
                return;
            }

            _outcomeUnknown = true;
            _connectionRequested = false;
            _connectedProcessSnapshot = null;
            _preparedCue = null;
            _onAirSceneName = null;
            _connectionState = PlayoutConnectionState.OutcomeUnknown;

            var session = _session;
            _session = null;
            var quarantineCompleted = !_quarantineIncomplete;
            if (session is not null)
            {
                quarantineCompleted =
                    await AbandonSessionOnStaAsync(session).ConfigureAwait(false) &&
                    quarantineCompleted;
            }

            if (!quarantineCompleted || _quarantineIncomplete)
            {
                throw new InvalidOperationException(
                    "The playout session could not be quarantined.");
            }

            PublishStatus("송출 결과를 확인할 수 없어 연결을 격리했습니다. 출력 상태를 직접 확인하세요.");
        }
        finally
        {
            _gate.Release();
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (Interlocked.CompareExchange(ref _disposeStarted, 1, 0) != 0)
        {
            return;
        }

        _disposed = true;
        _connectionRequested = false;
        _monitorCancellation.Cancel();
        try
        {
            await _monitorTask.ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
        }

        await _gate.WaitAsync().ConfigureAwait(false);
        try
        {
            if (_session is not null && _dispatcher is not null && !_dispatcher.IsQuarantined)
            {
                if (_session.HasPendingLifecycleCallbacks &&
                    !await ProcessLifecycleCallbacksAsync(CancellationToken.None).ConfigureAwait(false))
                {
                    _outcomeUnknown = true;
                }

                if (_session?.HasPendingLifecycleCallbacks == true)
                {
                    _outcomeUnknown = true;
                    await AbandonCurrentSessionAsync().ConfigureAwait(false);
                }
                else if (_onAirSceneName is not null || _outcomeUnknown)
                {
                    _outcomeUnknown = true;
                    await AbandonCurrentSessionAsync().ConfigureAwait(false);
                }
                else
                {
                    await ReleaseSessionAsync(CancellationToken.None).ConfigureAwait(false);
                }
            }

            _preparedCue = null;
            _onAirSceneName = null;
            _connectionState = PlayoutConnectionState.Disposed;
            PublishStatus("송출 어댑터가 종료되었습니다.");
        }
        finally
        {
            _gate.Release();
        }

        if (_dispatcher is not null)
        {
            await _dispatcher.DisposeAsync().ConfigureAwait(false);
        }

        _monitorCancellation.Dispose();
    }

    internal async Task PollProcessOnceAsync(CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_disposed)
            {
                return;
            }

            var previousSnapshot = _processSnapshot;
            _processSnapshot = CaptureProcessSnapshot();
            if (_options.Mode == PlayoutMode.DryRun)
            {
                if (_processSnapshot != previousSnapshot)
                {
                    PublishStatus(DryRunMonitoringMessage);
                }

                return;
            }

            if (_options.Mode is not (PlayoutMode.Test or PlayoutMode.Live))
            {
                return;
            }

            if (_outcomeUnknown)
            {
                _connectionState = PlayoutConnectionState.OutcomeUnknown;
                PublishStatus("송출 결과를 확인할 수 없습니다. 앱을 다시 시작하고 출력 상태를 확인하세요.");
                return;
            }

            var eligible = IsProcessEligible(_processSnapshot);
            if (!eligible)
            {
                _reconnectAttempts = 0;
                if (_onAirSceneName is not null)
                {
                    await AbandonCurrentSessionAsync().ConfigureAwait(false);
                    MarkOutcomeUnknown(PlayoutOperation.Disconnect);
                    return;
                }

                if (_session is not null && !_outcomeUnknown)
                {
                    if (!await ReleaseSessionAsync(cancellationToken).ConfigureAwait(false))
                    {
                        MarkOutcomeUnknown(PlayoutOperation.Disconnect);
                        return;
                    }
                }

                _preparedCue = null;
                _onAirSceneName = null;
                ScheduleReconnect();
                _connectionState = _connectionRequested
                    ? PlayoutConnectionState.Reconnecting
                    : PlayoutConnectionState.Disconnected;
                PublishStatus(ProcessUnavailableMessage(_processSnapshot));
                return;
            }

            if (HasConnectedProcessGenerationChanged(_processSnapshot))
            {
                _reconnectAttempts = 0;
                if (_onAirSceneName is not null)
                {
                    await AbandonCurrentSessionAsync().ConfigureAwait(false);
                    MarkOutcomeUnknown(PlayoutOperation.Disconnect);
                    return;
                }

                if (!await ReleaseSessionAsync(cancellationToken).ConfigureAwait(false))
                {
                    MarkOutcomeUnknown(PlayoutOperation.Disconnect);
                    return;
                }

                _preparedCue = null;
                _onAirSceneName = null;
                ScheduleReconnect();
                _connectionState = _connectionRequested
                    ? PlayoutConnectionState.Reconnecting
                    : PlayoutConnectionState.Disconnected;
            }

            if (_session is not null &&
                !await ProcessLifecycleCallbacksAsync(cancellationToken).ConfigureAwait(false))
            {
                MarkOutcomeUnknown(PlayoutOperation.Disconnect);
                return;
            }

            if (_session is not null || !_connectionRequested ||
                !_launchAuthorization.AllowsAutomaticReconnect ||
                !_options.ReconnectEnabled ||
                _outcomeUnknown || _reconnectAttempts >= _options.MaximumReconnectAttempts ||
                _timeProvider.GetUtcNow() < _nextReconnectAt)
            {
                PublishStatus(CurrentMessage());
                return;
            }

            _reconnectAttempts++;
            var result = await ConnectActualAsync(reconnecting: true, cancellationToken)
                .ConfigureAwait(false);
            if (!result.IsSuccess)
            {
                ScheduleReconnect();
            }
        }
        finally
        {
            _gate.Release();
        }
    }

    private async Task<PlayoutResult> SerializedAsync(
        PlayoutOperation operation,
        CancellationToken cancellationToken,
        Func<CancellationToken, Task<PlayoutResult>> callback,
        PlayoutCue? cue = null)
    {
        try
        {
            await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            // A caller can be cancelled while another serialized command is still
            // deciding whether its COM outcome is known. Wait for that decision so an
            // existing OutcomeUnknown latch can never be downgraded to retryable Cancelled.
            await _gate.WaitAsync(CancellationToken.None).ConfigureAwait(false);
            try
            {
                if (_disposed)
                {
                    return _outcomeUnknown
                        ? ExistingOutcomeUnknown(operation)
                        : Result(operation, PlayoutResultCode.Unavailable, "송출 어댑터가 종료되었습니다.");
                }

                return _outcomeUnknown
                    ? ExistingOutcomeUnknown(operation)
                    : Result(operation, PlayoutResultCode.Cancelled, "송출 요청이 취소되었습니다.");
            }
            finally
            {
                _gate.Release();
            }
        }

        try
        {
            if (_disposed)
            {
                return _outcomeUnknown
                    ? ExistingOutcomeUnknown(operation)
                    : Result(operation, PlayoutResultCode.Unavailable, "송출 어댑터가 종료되었습니다.");
            }

            if (_outcomeUnknown)
            {
                return ExistingOutcomeUnknown(operation);
            }

            if (!_launchAuthorization.TryClaimEngineCommand(operation, cue))
            {
                return Result(
                    operation,
                    PlayoutResultCode.Rejected,
                    _launchAuthorization.RejectionMessage(operation));
            }

            return await callback(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    private Task<PlayoutResult> ConnectCoreAsync(CancellationToken cancellationToken)
    {
        if (_options.Mode == PlayoutMode.Disabled)
        {
            return Task.FromResult(Result(
                PlayoutOperation.Connect,
                PlayoutResultCode.Rejected,
                "송출 기능이 비활성화되어 있습니다."));
        }

        if (_options.Mode == PlayoutMode.DryRun)
        {
            _connectionState = PlayoutConnectionState.DryRunReady;
            var message = DryRunOperationMessage("연결 요청을 확인했습니다.");
            PublishStatus(message);
            return Task.FromResult(Result(
                PlayoutOperation.Connect,
                PlayoutResultCode.Success,
                message));
        }

        _connectionRequested = true;
        _reconnectAttempts = 0;
        if (_options.Mode == PlayoutMode.Live && !IsLiveAuthorized())
        {
            _connectionRequested = false;
            _connectionState = PlayoutConnectionState.Disconnected;
            PublishStatus("라이브 송출 승인이 필요합니다.");
            return Task.FromResult(Result(
                PlayoutOperation.Connect,
                PlayoutResultCode.Rejected,
                "라이브 송출 승인이 필요합니다."));
        }

        return ConnectActualAsync(reconnecting: false, cancellationToken);
    }

    private async Task<PlayoutResult> ConnectActualAsync(
        bool reconnecting,
        CancellationToken cancellationToken)
    {
        if (_outcomeUnknown || _dispatcher is null || _dispatcher.IsQuarantined)
        {
            return MarkOutcomeUnknown(PlayoutOperation.Connect);
        }

        _processSnapshot = CaptureProcessSnapshot();
        if (!IsProcessEligible(_processSnapshot))
        {
            if (_onAirSceneName is not null)
            {
                await AbandonCurrentSessionAsync().ConfigureAwait(false);
                return MarkOutcomeUnknown(PlayoutOperation.Connect);
            }

            if (_session is not null)
            {
                if (!await ReleaseSessionAsync(cancellationToken).ConfigureAwait(false))
                {
                    return MarkOutcomeUnknown(PlayoutOperation.Disconnect);
                }

                _preparedCue = null;
                _onAirSceneName = null;
            }

            _connectionState = reconnecting
                ? PlayoutConnectionState.Reconnecting
                : PlayoutConnectionState.Disconnected;
            PublishStatus(ProcessUnavailableMessage(_processSnapshot));
            return Result(
                PlayoutOperation.Connect,
                PlayoutResultCode.Unavailable,
                ProcessUnavailableMessage(_processSnapshot));
        }

        if (_session is not null && !HasConnectedProcessGenerationChanged(_processSnapshot))
        {
            _connectionState = PlayoutConnectionState.Connected;
            PublishStatus("Tornado 테스트 송출에 연결되었습니다.");
            return Result(PlayoutOperation.Connect, PlayoutResultCode.Success, "송출 연결이 준비되었습니다.");
        }

        if (_session is not null)
        {
            if (_onAirSceneName is not null)
            {
                await AbandonCurrentSessionAsync().ConfigureAwait(false);
                return MarkOutcomeUnknown(PlayoutOperation.Connect);
            }

            if (!await ReleaseSessionAsync(cancellationToken).ConfigureAwait(false))
            {
                return MarkOutcomeUnknown(PlayoutOperation.Disconnect);
            }

            _preparedCue = null;
            _onAirSceneName = null;
        }

        var processSnapshotBeforeConnect = _processSnapshot;

        K3dRegistrationReport registration;
        try
        {
            registration = _registrationProbe.Probe();
        }
        catch (Exception)
        {
            _connectionState = PlayoutConnectionState.Faulted;
            PublishStatus("K3D 송출 구성 요소를 확인할 수 없습니다.");
            return Result(
                PlayoutOperation.Connect,
                PlayoutResultCode.Unavailable,
                "K3D 송출 구성 요소를 확인할 수 없습니다.");
        }

        if (!registration.IsReady)
        {
            _connectionState = PlayoutConnectionState.Faulted;
            PublishStatus(registration.Message);
            return Result(PlayoutOperation.Connect, PlayoutResultCode.Unavailable, registration.Message);
        }

        _connectionState = reconnecting
            ? PlayoutConnectionState.Reconnecting
            : PlayoutConnectionState.Connecting;
        PublishStatus(reconnecting ? "Tornado 테스트 송출에 다시 연결하는 중입니다." : "Tornado 송출에 연결하는 중입니다.");

        IK3dSession? pendingSession = null;
        var dispatchLatch = new SdkDispatchLatch(_beforeSdkDispatchClaim);
        try
        {
            var session = _sessionFactory.Create();
            Volatile.Write(ref pendingSession, session);
            _session = await _dispatcher.InvokeAsync(
                () =>
                {
                    try
                    {
                        if (_finalConnectSafetyCheck is null)
                        {
                            session.Connect(_options);
                        }
                        else if (session is IK3dGuardedConnectSession guardedSession)
                        {
                            guardedSession.ConnectGuarded(
                                _options,
                                IsExactTargetSafeForSdkDispatch,
                                dispatchLatch);
                        }
                        else
                        {
                            throw new K3dConnectSafetyException();
                        }

                        return session;
                    }
                    catch
                    {
                        if (_abandonOnTargetMismatch)
                        {
                            AbandonSessionInline(session);
                        }
                        else
                        {
                            session.Dispose();
                        }

                        throw;
                    }
                },
                _options.ConnectTimeout,
                cancellationToken,
                lateSession =>
                {
                    if (_abandonOnTargetMismatch)
                    {
                        AbandonSessionInline(lateSession);
                    }
                    else
                    {
                        lateSession.Dispose();
                    }
                },
                () => Volatile.Read(ref pendingSession)?.TryPreventKtapConnect(),
                dispatchLatch.PreventFutureDispatch).ConfigureAwait(false);

            CaptureKtapEvidence(_session);

            _processSnapshot = CaptureProcessSnapshot();
            if (!IsProcessEligible(_processSnapshot) ||
                !processSnapshotBeforeConnect.HasSameProcessGeneration(_processSnapshot))
            {
                if (_abandonOnTargetMismatch)
                {
                    await AbandonCurrentSessionAsync().ConfigureAwait(false);
                    return MarkOutcomeUnknown(PlayoutOperation.Connect);
                }

                if (!await ReleaseSessionAsync(CancellationToken.None).ConfigureAwait(false))
                {
                    return MarkOutcomeUnknown(PlayoutOperation.Disconnect);
                }

                ScheduleReconnect();
                _connectionState = reconnecting
                    ? PlayoutConnectionState.Reconnecting
                    : PlayoutConnectionState.Disconnected;
                var processMessage = IsProcessEligible(_processSnapshot)
                    ? ProcessChangedMessage
                    : ProcessUnavailableMessage(_processSnapshot);
                PublishStatus(processMessage);
                return Result(
                    PlayoutOperation.Connect,
                    PlayoutResultCode.Unavailable,
                    processMessage);
            }

            _connectedProcessSnapshot = _processSnapshot;
            _connectionState = PlayoutConnectionState.Connected;
            _reconnectAttempts = 0;
            _preparedCue = null;
            _onAirSceneName = null;
            PublishStatus("KTAPConnect가 수락되었습니다. OnHello 및 Network Monitoring [R]/[S]는 아직 확인되지 않았습니다.");
            return Result(
                PlayoutOperation.Connect,
                PlayoutResultCode.Success,
                "KTAPConnect 요청이 수락되었습니다. 서버 콜백은 운영자 확인이 필요합니다.");
        }
        catch (OperationCanceledException)
        {
            var cancelledSession = Volatile.Read(ref pendingSession);
            PreventLateKtapDispatchAndCapture(cancelledSession);
            if (_abandonOnTargetMismatch &&
                cancelledSession is not null &&
                cancelledSession.LastKtapConnectState != PlayoutKtapConnectState.NotAttempted)
            {
                _session = null;
                return MarkOutcomeUnknown(PlayoutOperation.Connect);
            }

            _connectionState = PlayoutConnectionState.Disconnected;
            PublishStatus("송출 연결 요청이 취소되었습니다.");
            return Result(PlayoutOperation.Connect, PlayoutResultCode.Cancelled, "송출 요청이 취소되었습니다.");
        }
        catch (StaOperationTimedOutException)
        {
            if (_abandonOnTargetMismatch)
            {
                _quarantineIncomplete = true;
            }

            PreventLateKtapDispatchAndCapture(Volatile.Read(ref pendingSession));
            return MarkOutcomeUnknown(PlayoutOperation.Connect);
        }
        catch (StaDispatcherQuarantinedException)
        {
            if (_abandonOnTargetMismatch)
            {
                _quarantineIncomplete = true;
            }

            PreventLateKtapDispatchAndCapture(Volatile.Read(ref pendingSession));
            return MarkOutcomeUnknown(PlayoutOperation.Connect);
        }
        catch (Exception)
        {
            var failedSession = Volatile.Read(ref pendingSession);
            CaptureKtapEvidence(failedSession);
            _session = null;
            if (_abandonOnTargetMismatch &&
                failedSession is not null &&
                failedSession.LastKtapConnectState != PlayoutKtapConnectState.NotAttempted)
            {
                return MarkOutcomeUnknown(PlayoutOperation.Connect);
            }

            ScheduleReconnect();
            _connectionState = reconnecting
                ? PlayoutConnectionState.Reconnecting
                : PlayoutConnectionState.Faulted;
            PublishStatus("Tornado 송출에 연결할 수 없습니다.");
            return Result(PlayoutOperation.Connect, PlayoutResultCode.Failed, "Tornado 송출에 연결할 수 없습니다.");
        }
    }

    private async Task<PlayoutResult> DisconnectCoreAsync(CancellationToken cancellationToken)
    {
        _connectionRequested = false;
        _reconnectAttempts = 0;
        if (_options.Mode == PlayoutMode.Disabled)
        {
            return Result(PlayoutOperation.Disconnect, PlayoutResultCode.Rejected, "송출 기능이 비활성화되어 있습니다.");
        }

        if (_options.Mode == PlayoutMode.DryRun)
        {
            _preparedCue = null;
            _onAirSceneName = null;
            _connectionState = PlayoutConnectionState.DryRunReady;
            var message = DryRunOperationMessage("연결 해제를 확인했습니다.");
            PublishStatus(message);
            return Result(PlayoutOperation.Disconnect, PlayoutResultCode.Success, message);
        }

        if (_outcomeUnknown || _dispatcher?.IsQuarantined == true)
        {
            return MarkOutcomeUnknown(PlayoutOperation.Disconnect);
        }

        if (_onAirSceneName is not null)
        {
            await AbandonCurrentSessionAsync().ConfigureAwait(false);
            return MarkOutcomeUnknown(PlayoutOperation.Disconnect);
        }

        if (_session?.HasPendingLifecycleCallbacks == true)
        {
            if (!await ProcessLifecycleCallbacksAsync(cancellationToken).ConfigureAwait(false))
            {
                return MarkOutcomeUnknown(PlayoutOperation.Disconnect);
            }

            if (_session?.HasPendingLifecycleCallbacks == true)
            {
                _connectionState = PlayoutConnectionState.Connected;
                const string pendingMessage =
                    "Tornado 장면 완료 이벤트를 기다리는 중이므로 연결을 해제하지 않았습니다.";
                PublishStatus(pendingMessage);
                return Result(
                    PlayoutOperation.Disconnect,
                    PlayoutResultCode.Unavailable,
                    pendingMessage);
            }
        }

        _connectionState = PlayoutConnectionState.Disconnecting;
        PublishStatus("Tornado 송출 연결을 해제하는 중입니다.");
        if (!await ReleaseSessionAsync(cancellationToken).ConfigureAwait(false))
        {
            return MarkOutcomeUnknown(PlayoutOperation.Disconnect);
        }

        _preparedCue = null;
        _onAirSceneName = null;
        _connectionState = PlayoutConnectionState.Disconnected;
        PublishStatus("Tornado 송출 연결이 해제되었습니다.");
        return Result(PlayoutOperation.Disconnect, PlayoutResultCode.Success, "송출 연결이 해제되었습니다.");
    }

    private Task<PlayoutResult> PrepareCoreAsync(PlayoutCue cue, CancellationToken cancellationToken)
    {
        if (_options.Mode is PlayoutMode.Disabled)
        {
            return Task.FromResult(Result(PlayoutOperation.Prepare, PlayoutResultCode.Rejected, "송출 기능이 비활성화되어 있습니다."));
        }

        if (_options.Mode == PlayoutMode.DryRun)
        {
            _preparedCue = cue with { SceneName = SafeSceneName(cue) };
            var message = DryRunOperationMessage("장면 준비를 확인했습니다.");
            PublishStatus(message);
            return Task.FromResult(Result(PlayoutOperation.Prepare, PlayoutResultCode.Success, message));
        }

        if (_options.Mode == PlayoutMode.Live && !IsLiveAuthorized())
        {
            return Task.FromResult(Result(
                PlayoutOperation.Prepare,
                PlayoutResultCode.Rejected,
                "라이브 송출 승인이 필요합니다."));
        }

        PlayoutCue resolved;
        try
        {
            resolved = _options.ResolveCue(cue);
        }
        catch (PlayoutRequestException exception)
        {
            return Task.FromResult(Result(PlayoutOperation.Prepare, PlayoutResultCode.Rejected, exception.Message));
        }

        if (_options.Mode is PlayoutMode.Test or PlayoutMode.Live &&
            !_options.IsOutputSceneAllowed(resolved))
        {
            return Task.FromResult(Result(PlayoutOperation.Prepare, PlayoutResultCode.Rejected, "승인되지 않은 장면입니다."));
        }

        return PrepareActualAsync(resolved, cancellationToken);
    }

    private async Task<PlayoutResult> PrepareActualAsync(
        PlayoutCue cue,
        CancellationToken cancellationToken)
    {
        var availability = await EnsureCommandAvailableAsync(PlayoutOperation.Prepare, cancellationToken)
            .ConfigureAwait(false);
        if (availability is not null)
        {
            return availability;
        }

        var result = await RunSessionOperationAsync(
            PlayoutOperation.Prepare,
            (session, dispatchLatch) => ExecuteSdkDispatch(
                dispatchLatch,
                () => session.Prepare(cue, _options.LayoutIndex)),
            partialOutcomeUnknown: false,
            cancellationToken).ConfigureAwait(false);
        if (result.IsSuccess)
        {
            _preparedCue = cue;
            PublishStatus("장면이 준비되었습니다.");
        }

        return result;
    }

    private async Task<PlayoutResult> TakeInCoreAsync(CancellationToken cancellationToken)
    {
        if (_options.Mode == PlayoutMode.Disabled)
        {
            return Result(PlayoutOperation.TakeIn, PlayoutResultCode.Rejected, "송출 기능이 비활성화되어 있습니다.");
        }

        if (_preparedCue is null)
        {
            return Result(PlayoutOperation.TakeIn, PlayoutResultCode.Rejected, "먼저 장면을 준비해야 합니다.");
        }

        if (_options.Mode == PlayoutMode.DryRun)
        {
            _onAirSceneName = SafeSceneName(_preparedCue);
            _preparedCue = null;
            var message = DryRunOperationMessage("TAKE IN을 확인했습니다.");
            PublishStatus(message);
            return Result(PlayoutOperation.TakeIn, PlayoutResultCode.Success, message);
        }

        if (!CanTakeIn())
        {
            return Result(PlayoutOperation.TakeIn, PlayoutResultCode.Rejected, "라이브 송출 승인이 필요합니다.");
        }

        if (_options.Mode is PlayoutMode.Test or PlayoutMode.Live &&
            !_options.IsOutputSceneAllowed(_preparedCue))
        {
            return Result(PlayoutOperation.TakeIn, PlayoutResultCode.Rejected, "승인되지 않은 장면입니다.");
        }

        var availability = await EnsureCommandAvailableAsync(PlayoutOperation.TakeIn, cancellationToken)
            .ConfigureAwait(false);
        if (availability is not null)
        {
            return availability;
        }

        var prepared = _preparedCue;
        var result = await RunSessionOperationAsync(
            PlayoutOperation.TakeIn,
            (session, dispatchLatch) => ExecuteSdkDispatch(
                dispatchLatch,
                () => session.Play(_options.LayoutIndex)),
            partialOutcomeUnknown: true,
            cancellationToken).ConfigureAwait(false);
        if (result.IsSuccess)
        {
            _onAirSceneName = SafeSceneName(prepared);
            _preparedCue = null;
            PublishStatus("장면이 송출되었습니다.");
        }

        return result;
    }

    private async Task<PlayoutResult> NextCoreAsync(PlayoutCue cue, CancellationToken cancellationToken)
    {
        if (_onAirSceneName is null)
        {
            return Result(
                PlayoutOperation.Next,
                PlayoutResultCode.Rejected,
                "NEXT는 송출 중인 장면이 있을 때만 사용할 수 있습니다.");
        }

        if (_options.Mode == PlayoutMode.Disabled)
        {
            return Result(PlayoutOperation.Next, PlayoutResultCode.Rejected, "송출 기능이 비활성화되어 있습니다.");
        }

        if (_options.Mode == PlayoutMode.DryRun)
        {
            _onAirSceneName = SafeSceneName(cue);
            _preparedCue = null;
            var message = DryRunOperationMessage("NEXT를 확인했습니다.");
            PublishStatus(message);
            return Result(PlayoutOperation.Next, PlayoutResultCode.Success, message);
        }

        if (!CanTakeIn())
        {
            return Result(PlayoutOperation.Next, PlayoutResultCode.Rejected, "라이브 송출 승인이 필요합니다.");
        }

        PlayoutCue resolved;
        try
        {
            resolved = _options.ResolveCue(cue);
        }
        catch (PlayoutRequestException exception)
        {
            return Result(PlayoutOperation.Next, PlayoutResultCode.Rejected, exception.Message);
        }

        if (_options.Mode is PlayoutMode.Test or PlayoutMode.Live &&
            !_options.IsOutputSceneAllowed(resolved))
        {
            return Result(PlayoutOperation.Next, PlayoutResultCode.Rejected, "승인되지 않은 장면입니다.");
        }

        var availability = await EnsureCommandAvailableAsync(PlayoutOperation.Next, cancellationToken)
            .ConfigureAwait(false);
        if (availability is not null)
        {
            return availability;
        }

        var result = await RunSessionOperationAsync(
            PlayoutOperation.Next,
            (session, dispatchLatch) =>
            {
                ExecuteSdkDispatch(
                    dispatchLatch,
                    () => session.Prepare(resolved, _options.LayoutIndex));
                ExecuteSdkDispatch(
                    dispatchLatch,
                    () => session.Play(_options.LayoutIndex));
            },
            partialOutcomeUnknown: true,
            cancellationToken).ConfigureAwait(false);
        if (result.IsSuccess)
        {
            _onAirSceneName = SafeSceneName(resolved);
            _preparedCue = null;
            PublishStatus("다음 장면이 송출되었습니다.");
        }

        return result;
    }

    private async Task<PlayoutResult> UpdateOnAirCoreAsync(
        PlayoutCue cue,
        CancellationToken cancellationToken)
    {
        if (_onAirSceneName is null)
        {
            return Result(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Rejected,
                "Same-scene refresh requires a scene that is already on air.");
        }

        if (_options.Mode == PlayoutMode.Disabled)
        {
            return Result(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Rejected,
                "Playout is disabled.");
        }

        if (_options.Mode == PlayoutMode.DryRun)
        {
            if (!IsCurrentOnAirCue(cue))
            {
                return Result(
                    PlayoutOperation.UpdateOnAir,
                    PlayoutResultCode.Rejected,
                    "Same-scene refresh can update only the scene currently on air.");
            }

            _preparedCue = null;
            var message = DryRunOperationMessage("Same-scene refresh was validated.");
            PublishStatus(message);
            return Result(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Success,
                message);
        }

        if (!CanTakeIn())
        {
            return Result(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Rejected,
                "Live playout authorization is required.");
        }

        PlayoutCue resolved;
        try
        {
            resolved = _options.ResolveCue(cue);
        }
        catch (PlayoutRequestException exception)
        {
            return Result(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Rejected,
                exception.Message);
        }

        if (_options.Mode is PlayoutMode.Test or PlayoutMode.Live &&
            !_options.IsOutputSceneAllowed(resolved))
        {
            return Result(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Rejected,
                "The scene is not approved for output.");
        }

        if (!IsCurrentOnAirCue(resolved))
        {
            return Result(
                PlayoutOperation.UpdateOnAir,
                PlayoutResultCode.Rejected,
                "Same-scene refresh can update only the scene currently on air.");
        }

        var availability = await EnsureCommandAvailableAsync(
            PlayoutOperation.UpdateOnAir,
            cancellationToken).ConfigureAwait(false);
        if (availability is not null)
        {
            return availability;
        }

        var result = await RunSessionOperationAsync(
            PlayoutOperation.UpdateOnAir,
            (session, dispatchLatch) => ExecuteSdkDispatch(
                dispatchLatch,
                () => session.UpdateOnAir(resolved, _options.LayoutIndex)),
            partialOutcomeUnknown: true,
            cancellationToken).ConfigureAwait(false);
        if (result.IsSuccess)
        {
            _onAirSceneName = SafeSceneName(resolved);
            _preparedCue = null;
            PublishStatus("The on-air scene page was updated.");
        }

        return result;
    }

    private async Task<PlayoutResult> TakeOutCoreAsync(
        PlayoutTakeOutScope scope,
        CancellationToken cancellationToken)
    {
        if (!Enum.IsDefined(scope))
        {
            return Result(PlayoutOperation.TakeOut, PlayoutResultCode.Rejected, "TAKE OUT 범위가 올바르지 않습니다.");
        }

        if (_options.Mode == PlayoutMode.Disabled)
        {
            return Result(PlayoutOperation.TakeOut, PlayoutResultCode.Rejected, "송출 기능이 비활성화되어 있습니다.");
        }

        if (_options.Mode == PlayoutMode.DryRun)
        {
            _preparedCue = null;
            _onAirSceneName = null;
            var message = DryRunOperationMessage("TAKE OUT을 확인했습니다.");
            PublishStatus(message);
            return Result(PlayoutOperation.TakeOut, PlayoutResultCode.Success, message);
        }

        if (_options.Mode == PlayoutMode.Live && !IsLiveAuthorized())
        {
            return Result(PlayoutOperation.TakeOut, PlayoutResultCode.Rejected, "라이브 송출 승인이 필요합니다.");
        }

        var availability = await EnsureCommandAvailableAsync(PlayoutOperation.TakeOut, cancellationToken)
            .ConfigureAwait(false);
        if (availability is not null)
        {
            return availability;
        }

        var result = await RunSessionOperationAsync(
            PlayoutOperation.TakeOut,
            (session, dispatchLatch) => ExecuteSdkDispatch(
                dispatchLatch,
                () => session.TakeOut(_options.LayoutIndex, scope)),
            partialOutcomeUnknown: true,
            cancellationToken).ConfigureAwait(false);
        if (result.IsSuccess)
        {
            _preparedCue = null;
            _onAirSceneName = null;
            PublishStatus(_session?.HasPendingLifecycleCallbacks == true
                ? "TAKE OUT 명령이 수락되어 Tornado 완료 이벤트를 기다리고 있습니다."
                : "장면 송출이 종료되었습니다.");
        }

        return result;
    }

    private async Task<PlayoutResult?> EnsureCommandAvailableAsync(
        PlayoutOperation operation,
        CancellationToken cancellationToken)
    {
        if (_outcomeUnknown || _dispatcher?.IsQuarantined == true)
        {
            return MarkOutcomeUnknown(operation);
        }

        _processSnapshot = CaptureProcessSnapshot();
        if (!IsProcessEligible(_processSnapshot))
        {
            if (_abandonOnTargetMismatch && _session is not null)
            {
                await AbandonCurrentSessionAsync().ConfigureAwait(false);
                return MarkOutcomeUnknown(operation);
            }

            if (_onAirSceneName is not null)
            {
                await AbandonCurrentSessionAsync().ConfigureAwait(false);
                return MarkOutcomeUnknown(operation);
            }

            if (_session is not null)
            {
                if (!await ReleaseSessionAsync(cancellationToken).ConfigureAwait(false))
                {
                    return MarkOutcomeUnknown(operation);
                }
            }

            _preparedCue = null;
            _onAirSceneName = null;
            _connectionState = PlayoutConnectionState.Reconnecting;
            PublishStatus(ProcessUnavailableMessage(_processSnapshot));
            return Result(operation, PlayoutResultCode.Unavailable, ProcessUnavailableMessage(_processSnapshot));
        }

        if (HasConnectedProcessGenerationChanged(_processSnapshot))
        {
            if (_abandonOnTargetMismatch)
            {
                await AbandonCurrentSessionAsync().ConfigureAwait(false);
                return MarkOutcomeUnknown(operation);
            }

            if (_onAirSceneName is not null)
            {
                await AbandonCurrentSessionAsync().ConfigureAwait(false);
                return MarkOutcomeUnknown(operation);
            }

            if (!await ReleaseSessionAsync(cancellationToken).ConfigureAwait(false))
            {
                return MarkOutcomeUnknown(operation);
            }

            _preparedCue = null;
            _onAirSceneName = null;
            ScheduleReconnect();
            _connectionState = _connectionRequested
                ? PlayoutConnectionState.Reconnecting
                : PlayoutConnectionState.Disconnected;
            PublishStatus(ProcessChangedMessage);
            return Result(
                operation,
                PlayoutResultCode.Unavailable,
                ProcessChangedMessage);
        }

        if (_session is null)
        {
            return Result(operation, PlayoutResultCode.Unavailable, "Tornado 송출 연결이 필요합니다.");
        }

        if (!await ProcessLifecycleCallbacksAsync(cancellationToken).ConfigureAwait(false))
        {
            return MarkOutcomeUnknown(operation);
        }

        if (_session?.HasPendingTakeOutCallbacks == true)
        {
            const string pendingTakeOutMessage =
                "The previous SDK CutOut/StopAll completion callback is still pending. " +
                "No further scene command is allowed until Tornado confirms the result.";
            PublishStatus(pendingTakeOutMessage);
            return Result(
                operation,
                PlayoutResultCode.Unavailable,
                pendingTakeOutMessage);
        }

        if (RequiresCompletedPlay(operation) &&
            _session?.HasPendingPlayCallbacks == true)
        {
            const string pendingPlayMessage =
                "The previous SDK Play completion callback is still pending. " +
                "Only TAKE OUT is allowed until Tornado confirms the result.";
            PublishStatus(pendingPlayMessage);
            return Result(
                operation,
                PlayoutResultCode.Unavailable,
                pendingPlayMessage);
        }

        return null;
    }

    private static bool RequiresCompletedPlay(PlayoutOperation operation) => operation is
        PlayoutOperation.Prepare or
        PlayoutOperation.TakeIn or
        PlayoutOperation.Next or
        PlayoutOperation.UpdateOnAir;

    private async Task<bool> ProcessLifecycleCallbacksAsync(
        CancellationToken cancellationToken)
    {
        var session = _session;
        if (session is null || !session.SupportsLifecycleCallbacks)
        {
            CaptureKtapEvidence(session);
            return true;
        }

        if (_dispatcher is null || _dispatcher.IsQuarantined)
        {
            return false;
        }

        try
        {
            await _dispatcher.InvokeAsync(
                () => session.ProcessPendingCallbacks(_options.LayoutIndex),
                _options.OperationTimeout,
                CancellationToken.None,
                _ => AbandonSessionInline(session),
                () => AbandonSessionInline(session)).ConfigureAwait(false);
            CaptureKtapEvidence(session);
            return true;
        }
        catch
        {
            _session = null;
            _connectedProcessSnapshot = null;
            if (_dispatcher.IsQuarantined)
            {
                _quarantineIncomplete = true;
            }
            else if (!await AbandonSessionOnStaAsync(session).ConfigureAwait(false))
            {
                _quarantineIncomplete = true;
            }

            return false;
        }
    }

    private async Task<PlayoutResult> RunSessionOperationAsync(
        PlayoutOperation operation,
        Action<IK3dSession, SdkDispatchLatch> callback,
        bool partialOutcomeUnknown,
        CancellationToken cancellationToken)
    {
        var session = _session!;
        var dispatchLatch = new SdkDispatchLatch(_beforeSdkDispatchClaim);
        var callbackStarted = 0;
        var preserveActiveOutput =
            _abandonOnTargetMismatch ||
            partialOutcomeUnknown ||
            _onAirSceneName is not null;
        void CleanupAbandonedOperation()
        {
            if (preserveActiveOutput)
            {
                AbandonSessionInline(session);
            }
            else
            {
                session.Dispose();
            }
        }

        try
        {
            await _dispatcher!.InvokeAsync(
                () =>
                {
                    Volatile.Write(ref callbackStarted, 1);
                    try
                    {
                        callback(session, dispatchLatch);
                    }
                    finally
                    {
                        if (_dispatcher.IsQuarantined)
                        {
                            CleanupAbandonedOperation();
                        }
                    }

                    return true;
                },
                _options.OperationTimeout,
                cancellationToken,
                _ => CleanupAbandonedOperation(),
                CleanupAbandonedOperation,
                dispatchLatch.PreventFutureDispatch).ConfigureAwait(false);
            return Result(operation, PlayoutResultCode.Success, SuccessMessage(operation));
        }
        catch (OperationCanceledException)
        {
            if (preserveActiveOutput && Volatile.Read(ref callbackStarted) != 0)
            {
                _session = null;
                _connectedProcessSnapshot = null;
                await AbandonSessionOnStaAsync(session).ConfigureAwait(false);
                return MarkOutcomeUnknown(operation);
            }

            return Result(operation, PlayoutResultCode.Cancelled, "송출 요청이 취소되었습니다.");
        }
        catch (StaOperationTimedOutException)
        {
            if (preserveActiveOutput)
            {
                _quarantineIncomplete = true;
            }

            return MarkOutcomeUnknown(operation);
        }
        catch (StaDispatcherQuarantinedException)
        {
            if (preserveActiveOutput)
            {
                _quarantineIncomplete = true;
            }

            return MarkOutcomeUnknown(operation);
        }
        catch (Exception)
        {
            if (preserveActiveOutput)
            {
                _session = null;
                _connectedProcessSnapshot = null;
                await AbandonSessionOnStaAsync(session).ConfigureAwait(false);
                return MarkOutcomeUnknown(operation);
            }

            _session = session;
            if (!await RecycleFailedSessionAsync().ConfigureAwait(false))
            {
                return MarkOutcomeUnknown(operation);
            }

            return Result(
                operation,
                PlayoutResultCode.Failed,
                "송출 명령을 처리할 수 없습니다.");
        }
    }

    private async Task<bool> RecycleFailedSessionAsync()
    {
        _preparedCue = null;
        _onAirSceneName = null;
        var released = await ReleaseSessionAsync(CancellationToken.None).ConfigureAwait(false);
        if (!released)
        {
            return false;
        }

        _connectionState = _connectionRequested
            ? PlayoutConnectionState.Reconnecting
            : PlayoutConnectionState.Faulted;
        ScheduleReconnect();
        PublishStatus("Tornado 송출 세션을 다시 연결해야 합니다.");
        return true;
    }

    private async Task<bool> ReleaseSessionAsync(CancellationToken cancellationToken)
    {
        if (_onAirSceneName is not null)
        {
            await AbandonCurrentSessionAsync().ConfigureAwait(false);
            return false;
        }

        if (_session is not null && !_launchAuthorization.AllowsSdkDisconnect)
        {
            await AbandonCurrentSessionAsync().ConfigureAwait(false);
            return false;
        }

        var session = _session;
        if (session?.HasPendingLifecycleCallbacks == true)
        {
            // A late OnScenePlayed/OnCutOut/OnStopAll callback still owns scene lifetime.
            // Never issue Disconnect while that result is unresolved.
            await AbandonCurrentSessionAsync().ConfigureAwait(false);
            return false;
        }

        _session = null;
        _connectedProcessSnapshot = null;
        if (session is null)
        {
            return true;
        }

        if (_dispatcher is null || _dispatcher.IsQuarantined)
        {
            if (_abandonOnTargetMismatch)
            {
                _quarantineIncomplete = true;
            }

            return false;
        }

        try
        {
            await CleanupSessionOnStaAsync(session, cancellationToken).ConfigureAwait(false);
            return true;
        }
        catch (OperationCanceledException)
        {
            if (_dispatcher.IsQuarantined)
            {
                if (_abandonOnTargetMismatch)
                {
                    _quarantineIncomplete = true;
                }

                return false;
            }

            try
            {
                await CleanupSessionOnStaAsync(session, CancellationToken.None).ConfigureAwait(false);
                return true;
            }
            catch (Exception)
            {
                if (_abandonOnTargetMismatch)
                {
                    _quarantineIncomplete = true;
                }

                return false;
            }
        }
        catch (Exception)
        {
            if (_abandonOnTargetMismatch)
            {
                _quarantineIncomplete = true;
            }

            return false;
        }
    }

    private async Task<bool> AbandonSessionOnStaAsync(IK3dSession session)
    {
        if (_dispatcher is null || _dispatcher.IsQuarantined)
        {
            _quarantineIncomplete = true;
            return false;
        }

        try
        {
            await _dispatcher.InvokeAsync(
                () =>
                {
                    AbandonSessionInline(session);
                    return true;
                },
                _options.DisconnectTimeout,
                CancellationToken.None,
                _ => AbandonSessionInline(session),
                () => AbandonSessionInline(session)).ConfigureAwait(false);
            return true;
        }
        catch
        {
            // Never fall back to Disconnect when the physical output state is unknown.
            _quarantineIncomplete = true;
            return false;
        }
    }

    private async Task AbandonCurrentSessionAsync()
    {
        var session = _session;
        _session = null;
        _connectedProcessSnapshot = null;
        if (session is not null)
        {
            await AbandonSessionOnStaAsync(session).ConfigureAwait(false);
        }
    }

    private Task<bool> CleanupSessionOnStaAsync(
        IK3dSession session,
        CancellationToken cancellationToken)
    {
        if (!_launchAuthorization.AllowsSdkDisconnect)
        {
            throw new K3dConnectSafetyException();
        }

        var dispatchLatch = new SdkDispatchLatch(_beforeSdkDispatchClaim);
        return _dispatcher!.InvokeAsync(
            () =>
            {
                if (_abandonOnTargetMismatch)
                {
                    var disconnected = false;
                    try
                    {
                        ExecuteSdkDispatch(dispatchLatch, session.Disconnect);
                        disconnected = true;
                    }
                    catch
                    {
                        AbandonSessionInline(session);
                        throw;
                    }
                    finally
                    {
                        if (disconnected)
                        {
                            session.Dispose();
                        }
                    }

                    return true;
                }

                try
                {
                    ExecuteSdkDispatch(dispatchLatch, session.Disconnect);
                }
                finally
                {
                    session.Dispose();
                }

                return true;
            },
            _options.DisconnectTimeout,
            cancellationToken,
            preventFutureDispatch: dispatchLatch.PreventFutureDispatch);
    }

    private void EnsureExactTarget()
    {
        if (_finalConnectSafetyCheck is not null &&
            !IsExactTargetSafeForSdkDispatch())
        {
            throw new K3dConnectSafetyException();
        }
    }

    private void ExecuteSdkDispatch(
        SdkDispatchLatch dispatchLatch,
        Action sdkCall)
    {
        EnsureExactTarget();
        using var dispatchClaim = dispatchLatch.TryClaim();
        if (dispatchClaim is null)
        {
            throw new K3dConnectSafetyException();
        }

        sdkCall();
        EnsureExactTarget();
    }

    private bool IsExactTargetSafeForSdkDispatch()
    {
        var dispatcher = _dispatcher;
        if (dispatcher is null || dispatcher.IsQuarantined)
        {
            return false;
        }

        try
        {
            return _finalConnectSafetyCheck is not null &&
                _finalConnectSafetyCheck() &&
                !dispatcher.IsQuarantined;
        }
        catch
        {
            return false;
        }
    }

    private void AbandonSessionInline(IK3dSession session)
    {
        try
        {
            session.Abandon();
        }
        catch
        {
            _quarantineIncomplete = true;
            throw;
        }
    }

    private sealed class SdkDispatchLatch(Action? beforeClaim) : IK3dSdkDispatchLatch
    {
        private const int Open = 0;
        private const int Claimed = 1;
        private const int Prevented = 2;
        private const int ClaimedAndPrevented = 3;

        private int _state;

        public IDisposable? TryClaim()
        {
            beforeClaim?.Invoke();
            return Interlocked.CompareExchange(ref _state, Claimed, Open) == Open
                ? new DispatchClaim(this)
                : null;
        }

        public void PreventFutureDispatch()
        {
            while (true)
            {
                var state = Volatile.Read(ref _state);
                var next = state switch
                {
                    Open => Prevented,
                    Claimed => ClaimedAndPrevented,
                    _ => state
                };
                if (next == state ||
                    Interlocked.CompareExchange(ref _state, next, state) == state)
                {
                    return;
                }
            }
        }

        private void Release()
        {
            while (true)
            {
                var state = Volatile.Read(ref _state);
                var next = state switch
                {
                    Claimed => Open,
                    ClaimedAndPrevented => Prevented,
                    _ => state
                };
                if (next == state ||
                    Interlocked.CompareExchange(ref _state, next, state) == state)
                {
                    return;
                }
            }
        }

        private sealed class DispatchClaim(SdkDispatchLatch owner) : IDisposable
        {
            private SdkDispatchLatch? _owner = owner;

            public void Dispose() =>
                Interlocked.Exchange(ref _owner, null)?.Release();
        }
    }

    private async Task MonitorLoopAsync()
    {
        var cancellationToken = _monitorCancellation.Token;
        try
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                try
                {
                    await PollProcessOnceAsync(cancellationToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
                {
                    break;
                }
                catch (Exception)
                {
                    // Process inspection must never terminate the app or the monitor loop.
                }

                await Task.Delay(
                    _options.ProcessPollInterval,
                    _timeProvider,
                    cancellationToken).ConfigureAwait(false);
            }
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
        }
    }

    private TornadoProcessSnapshot CaptureProcessSnapshot()
    {
        try
        {
            return _processProbe.Capture(
                _options.Mode == PlayoutMode.Test
                    ? _options.TestProcessWindowTitleRegex
                    : null);
        }
        catch (Exception)
        {
            return new TornadoProcessSnapshot(0, 0, 0)
            {
                IdentityInspectionSucceeded = false
            };
        }
    }

    private bool IsProcessEligible(TornadoProcessSnapshot snapshot) => _options.Mode switch
    {
        PlayoutMode.Test =>
            snapshot.IdentityInspectionSucceeded &&
            snapshot.TotalProcessCount == 1 &&
            snapshot.EligibleProcessCount == 1 &&
            snapshot.ProgramProcessCount == 0,
        PlayoutMode.Live => snapshot.IdentityInspectionSucceeded && snapshot.AnyTornadoProcess,
        _ => false
    };

    private bool HasConnectedProcessGenerationChanged(TornadoProcessSnapshot snapshot) =>
        _session is not null &&
        (_connectedProcessSnapshot is null ||
         !_connectedProcessSnapshot.HasSameProcessGeneration(snapshot));

    private bool CanTakeIn() => _options.Mode switch
    {
        PlayoutMode.Test => true,
        PlayoutMode.Live =>
            IsLiveAuthorized() && _launchAuthorization.CanTakeIn,
        _ => false
    };

    private bool IsLiveAuthorized() =>
        _options.TrustedLiveOutputEnabled &&
        _liveAuthorization.IsAuthorizedForThisLaunch;

    private PlayoutResult MarkOutcomeUnknown(PlayoutOperation operation)
    {
        _outcomeUnknown = true;
        _connectionRequested = false;
        _session = null;
        _connectedProcessSnapshot = null;
        _preparedCue = null;
        _onAirSceneName = null;
        _connectionState = PlayoutConnectionState.OutcomeUnknown;
        PublishStatus("송출 결과를 확인할 수 없습니다. 앱을 다시 시작하고 출력 상태를 확인하세요.");
        return ExistingOutcomeUnknown(operation);
    }

    private PlayoutResult ExistingOutcomeUnknown(PlayoutOperation operation) =>
        Result(
            operation,
            PlayoutResultCode.OutcomeUnknown,
            "송출 결과를 확인할 수 없습니다. 앱을 다시 시작하고 출력 상태를 확인하세요.");

    private void ScheduleReconnect() =>
        _nextReconnectAt = _timeProvider.GetUtcNow() + _options.ReconnectDelay;

    private void PublishStatus(string message)
    {
        PlayoutStatus previous;
        PlayoutStatus current;
        lock (_statusSync)
        {
            previous = Status;
            current = BuildStatus(message);
            Status = current;
        }

        var handlers = StatusChanged;
        if (handlers is null)
        {
            return;
        }

        foreach (EventHandler<PlayoutStatusChangedEventArgs> handler in handlers.GetInvocationList())
        {
            try
            {
                handler(this, new PlayoutStatusChangedEventArgs(previous, current));
            }
            catch (Exception)
            {
                // A UI subscriber cannot break the playout state machine.
            }
        }
    }

    private PlayoutStatus BuildStatus(string message)
    {
        var connected = _session is not null && !_outcomeUnknown;
        var commandAvailable = (_options.Mode == PlayoutMode.DryRun ||
            (connected && IsProcessEligible(_processSnapshot))) &&
            _session?.HasPendingTakeOutCallbacks != true;
        var liveAllowed = connected &&
            IsProcessEligible(_processSnapshot) &&
            CanTakeIn();
        return new PlayoutStatus(
            _options.Mode,
            _connectionState,
            connected,
            _processSnapshot.AnyTornadoProcess,
            commandAvailable,
            liveAllowed,
            _preparedCue is null ? null : SafeSceneName(_preparedCue),
            _onAirSceneName,
            message,
            _timeProvider.GetUtcNow(),
            Interlocked.Increment(ref _sequence))
        {
            OperationTimeoutMilliseconds = (int)_options.OperationTimeout.TotalMilliseconds,
            IsPlayCompletionPending = _session?.HasPendingPlayCallbacks == true,
            IsTakeOutCompletionPending = _session?.HasPendingTakeOutCallbacks == true,
            LastKtapConnectState = (PlayoutKtapConnectState)Volatile.Read(
                ref _lastKtapConnectState),
            KtapHelloObserved = Volatile.Read(ref _ktapHelloObservedState) switch
            {
                1 => false,
                2 => true,
                _ => null
            }
        };
    }

    private void CaptureKtapEvidence(IK3dSession? session)
    {
        if (session is null)
        {
            return;
        }

        var state = session.LastKtapConnectState;
        if (state != PlayoutKtapConnectState.NotAttempted)
        {
            Volatile.Write(ref _lastKtapConnectState, (int)state);
        }

        if (session.SupportsLifecycleCallbacks)
        {
            Volatile.Write(
                ref _ktapHelloObservedState,
                session.KtapHelloObserved == true ? 2 : 1);
        }
    }

    private void PreventLateKtapDispatchAndCapture(IK3dSession? session)
    {
        if (session is null)
        {
            return;
        }

        session.TryPreventKtapConnect();
        CaptureKtapEvidence(session);
    }

    private PlayoutResult Result(
        PlayoutOperation operation,
        PlayoutResultCode code,
        string message) => new(
            operation,
            code,
            _options.Mode == PlayoutMode.DryRun,
            message,
            _timeProvider.GetUtcNow());

    private static string SafeSceneName(PlayoutCue cue)
    {
        var value = cue.SceneName?.Trim();
        if (string.IsNullOrEmpty(value) || value.Length > 64)
        {
            return "장면";
        }

        foreach (var character in value)
        {
            if (!((character >= 'A' && character <= 'Z') ||
                  (character >= 'a' && character <= 'z') ||
                  (character >= '0' && character <= '9') ||
                  character is '_' or '-'))
            {
                return "장면";
            }
        }

        return value;
    }

    private bool IsCurrentOnAirCue(PlayoutCue cue)
    {
        var sceneName = cue.SceneName?.Trim();
        return !string.IsNullOrEmpty(sceneName) &&
            string.Equals(sceneName, _onAirSceneName, StringComparison.OrdinalIgnoreCase);
    }

    private string CurrentMessage() => _connectionState switch
    {
        PlayoutConnectionState.Connected =>
            "KTAPConnect가 수락되었습니다. OnHello 및 Network Monitoring [R]/[S]는 아직 확인되지 않았습니다.",
        PlayoutConnectionState.Reconnecting => "Tornado 송출에 다시 연결하는 중입니다.",
        PlayoutConnectionState.Faulted => "Tornado 송출 연결을 확인하세요.",
        _ => InitialMessage(_options.Mode)
    };

    private static string InitialMessage(PlayoutMode mode) => mode switch
    {
        PlayoutMode.Disabled => "송출 기능이 비활성화되어 있습니다.",
        PlayoutMode.DryRun => DryRunMonitoringMessage,
        _ => "Tornado 송출 연결이 필요합니다."
    };

    private const string DryRunMonitoringMessage =
        "DryRun: COM/KTAPConnect를 호출하지 않았습니다. Network Monitoring 기록 없음이 정상입니다.";

    private static string DryRunOperationMessage(string operation) =>
        $"DryRun: {operation} COM/KTAPConnect는 호출하지 않았으며 Network Monitoring 기록 없음이 정상입니다.";

    private string ProcessUnavailableMessage(TornadoProcessSnapshot snapshot)
    {
        if (_options.Mode == PlayoutMode.Test)
        {
            if (snapshot.ProgramProcessCount > 0)
            {
                return "PROGRAM Tornado가 실행 중이므로 테스트 연결을 차단했습니다.";
            }

            if (snapshot.TotalProcessCount > 1)
            {
                return "Tornado 인스턴스가 여러 개여서 테스트 연결을 차단했습니다.";
            }

            return "허용된 테스트 Tornado 인스턴스를 찾을 수 없습니다.";
        }

        return "Tornado 프로세스를 찾을 수 없습니다.";
    }

    private static string SuccessMessage(PlayoutOperation operation) => operation switch
    {
        PlayoutOperation.UpdateOnAir => "The on-air scene page was updated.",
        PlayoutOperation.Prepare => "장면이 준비되었습니다.",
        PlayoutOperation.TakeIn => "장면이 송출되었습니다.",
        PlayoutOperation.Next => "다음 장면이 송출되었습니다.",
        PlayoutOperation.TakeOut => "장면 송출이 종료되었습니다.",
        _ => "송출 명령이 완료되었습니다."
    };
}
