using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using KAsyncEngineLib;

namespace Tornado3_2026Election.Services;

public sealed class TornadoManager : IDisposable
{
    private static readonly TimeSpan AutoReconnectInterval = TimeSpan.FromSeconds(5);
    private static readonly TimeSpan AsyncOperationTimeout = TimeSpan.FromSeconds(30);

    private readonly string _host;
    private readonly int _port;
    private readonly LogService _logService;
    private readonly StaDispatcher _dispatcher;
    private readonly Timer _reconnectTimer;
    private readonly SemaphoreSlim _asyncOperationLock = new(1, 1);
    private readonly Dictionary<string, KAScene> _scenes = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, string> _scenePaths = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, DateTime> _sceneWriteTimes = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<int, KAScenePlayer> _scenePlayers = new();

    private IKAEngine? _engine;
    private KarismaEventHandler? _eventHandler;
    private bool _connected;
    private bool _isTcpConnected;
    private bool _autoReconnectEnabled;
    private bool _disposed;
    private int _reconnectInProgress;

    public bool IsConnected => _isTcpConnected;

    public event EventHandler? ConnectionChanged;

    public TornadoManager(string host, int port, LogService logService)
    {
        _host = string.IsNullOrWhiteSpace(host)
            ? throw new ArgumentException("Host is required.", nameof(host))
            : host;
        _port = port > 0
            ? port
            : throw new ArgumentOutOfRangeException(nameof(port), "Port must be a positive integer.");
        _logService = logService;
        _dispatcher = new StaDispatcher("Karisma Async Engine");
        _reconnectTimer = new Timer(OnReconnectTimerTick, null, AutoReconnectInterval, AutoReconnectInterval);
    }

    public Task EnsureConnectedAsync(CancellationToken cancellationToken)
    {
        return EnsureConnectedInternalAsync(cancellationToken);
    }

    public Task<string> LoadSceneAsync(string scenePath, string sceneAlias, CancellationToken cancellationToken)
    {
        return LoadSceneAsync(scenePath, sceneAlias, forceReload: false, cancellationToken);
    }

    public async Task<string> LoadSceneAsync(
        string scenePath,
        string sceneAlias,
        bool forceReload,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(scenePath))
        {
            throw new ArgumentException("Scene path is required.", nameof(scenePath));
        }

        if (string.IsNullOrWhiteSpace(sceneAlias))
        {
            throw new ArgumentException("Scene alias is required.", nameof(sceneAlias));
        }

        await _asyncOperationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var sceneWriteTime = ResolveSceneWriteTime(scenePath);
            var existingAlias = await _dispatcher.InvokeAsync(() =>
            {
                ThrowIfDisposed();
                EnsureConnectedCore();

                if (!forceReload &&
                    _scenePaths.TryGetValue(sceneAlias, out var existingPath) &&
                    string.Equals(existingPath, scenePath, StringComparison.OrdinalIgnoreCase) &&
                    _sceneWriteTimes.TryGetValue(sceneAlias, out var existingWriteTime) &&
                    existingWriteTime == sceneWriteTime)
                {
                    return sceneAlias;
                }

                return string.Empty;
            }, cancellationToken).ConfigureAwait(false);

            if (!string.IsNullOrWhiteSpace(existingAlias))
            {
                return existingAlias;
            }

            var eventHandler = GetEventHandlerCore();
            var completion = eventHandler.BeginLoadSceneWait(sceneAlias);

            try
            {
                await _dispatcher.InvokeAsync(() =>
                {
                    ThrowIfDisposed();
                    EnsureConnectedCore();

                    var shouldForceReload = forceReload || _scenePaths.ContainsKey(sceneAlias);
                    var scene = shouldForceReload
                        ? _engine!.LoadSceneForce(scenePath, sceneAlias)
                        : _engine!.LoadScene(scenePath, sceneAlias);

                    _logService.Info(
                        $"Karisma {(shouldForceReload ? "LoadSceneForce" : "LoadScene")}() return={(scene is null ? "null" : "scene-handle")} alias={sceneAlias} path={scenePath}");

                    _scenes[sceneAlias] = scene ?? throw new InvalidOperationException($"Failed to load Karisma scene: {scenePath}");
                    _scenePaths[sceneAlias] = scenePath;
                    _sceneWriteTimes[sceneAlias] = sceneWriteTime;
                }, cancellationToken).ConfigureAwait(false);

                var result = await WaitForKarismaCallbackAsync(
                    completion,
                    cancellationToken,
                    $"LoadScene '{sceneAlias}'").ConfigureAwait(false);
                if (result != eKResult.RESULT_SUCCESS)
                {
                    throw new InvalidOperationException($"LoadScene failed for '{sceneAlias}': {result} ({(int)result})");
                }

                _logService.Info($"Karisma scene loaded: alias={sceneAlias} path={scenePath}");
                return sceneAlias;
            }
            catch (Exception ex)
            {
                eventHandler.CancelPendingLoadScene(sceneAlias, ex);
                try
                {
                    await _dispatcher.InvokeAsync(() =>
                    {
                        _scenes.Remove(sceneAlias);
                        _scenePaths.Remove(sceneAlias);
                        _sceneWriteTimes.Remove(sceneAlias);
                    }, CancellationToken.None).ConfigureAwait(false);
                }
                catch
                {
                }

                throw;
            }
        }
        finally
        {
            _asyncOperationLock.Release();
        }
    }

    public async Task ApplyValuesAsync(
        string sceneAlias,
        IReadOnlyList<KarismaVisibilityUpdate> visibilityUpdatesBeforeValue,
        IReadOnlyDictionary<string, string> values,
        IReadOnlyList<KarismaCounterNumberKeyUpdate> counterNumberKeys,
        IReadOnlyList<KarismaChartCellUpdate> chartCellUpdates,
        IReadOnlyList<KarismaPositionUpdate> positionUpdates,
        IReadOnlyList<KarismaCropKeyUpdate> cropKeyUpdates,
        IReadOnlyList<KarismaStyleColorUpdate> styleColorUpdates,
        IReadOnlyList<KarismaVisibilityUpdate> visibilityUpdatesAfterValue,
        CancellationToken cancellationToken)
    {
        await _asyncOperationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            Task<eKResult>? transactionCompletion = null;
            var eventHandler = GetEventHandlerCore();

            try
            {
                await _dispatcher.InvokeAsync(() =>
                {
                    ThrowIfDisposed();
                    EnsureConnectedCore();

                    var scene = GetSceneCore(sceneAlias);
                    var counterNumberKeyObjectNames = counterNumberKeys
                        .Where(update => update.KeyIndex != 0 && !string.IsNullOrWhiteSpace(update.ObjectName))
                        .Select(update => update.ObjectName)
                        .ToHashSet(StringComparer.OrdinalIgnoreCase);
                    var counterSetValueObjectNames = counterNumberKeys
                        .Where(update => update.AllowSetValue && !string.IsNullOrWhiteSpace(update.ObjectName))
                        .Select(update => update.ObjectName)
                        .ToHashSet(StringComparer.OrdinalIgnoreCase);
                    _engine!.BeginTransaction();
                    try
                    {
                        ApplyVisibilityUpdates(sceneAlias, scene, visibilityUpdatesBeforeValue);

                        foreach (var pair in values)
                        {
                            if (string.IsNullOrWhiteSpace(pair.Key))
                            {
                                continue;
                            }

                            try
                            {
                                var sceneObject = scene.GetObject(pair.Key);
                                if (sceneObject is null)
                                {
                                    continue;
                                }

                                if (counterNumberKeyObjectNames.Contains(pair.Key) &&
                                    !counterSetValueObjectNames.Contains(pair.Key) &&
                                    sceneObject is IKACounter)
                                {
                                    continue;
                                }

                                sceneObject.SetValue(pair.Value ?? string.Empty);
                            }
                            catch (Exception ex)
                            {
                                _logService.Warning($"Karisma object update skipped: scene={sceneAlias} object={pair.Key} reason={ex.Message}");
                            }
                        }

                        foreach (var counterNumberKey in counterNumberKeys)
                        {
                            if (string.IsNullOrWhiteSpace(counterNumberKey.ObjectName))
                            {
                                continue;
                            }

                            try
                            {
                                if (counterNumberKey.KeyIndex == 0 && !counterNumberKey.AllowKeyZero)
                                {
                                    continue;
                                }

                                var sceneObject = scene.GetObject(counterNumberKey.ObjectName);
                                if (sceneObject is not IKACounter counter)
                                {
                                    _logService.Warning(
                                        $"Karisma counter update skipped: scene={sceneAlias} object={counterNumberKey.ObjectName} reason=object is not a counter");
                                    continue;
                                }

                                counter.SetCounterNumberKey(counterNumberKey.KeyIndex, counterNumberKey.Number);
                            }
                            catch (Exception ex)
                            {
                                _logService.Warning(
                                    $"Karisma counter update skipped: scene={sceneAlias} object={counterNumberKey.ObjectName} keyIndex={counterNumberKey.KeyIndex} reason={ex.Message}");
                            }
                        }

                        foreach (var chartCellUpdate in chartCellUpdates)
                        {
                            if (string.IsNullOrWhiteSpace(chartCellUpdate.ObjectName))
                            {
                                continue;
                            }

                            try
                            {
                                var sceneObject = scene.GetObject(chartCellUpdate.ObjectName);
                                if (sceneObject is not IKAChart chart)
                                {
                                    _logService.Warning(
                                        $"Karisma chart cell update skipped: scene={sceneAlias} object={chartCellUpdate.ObjectName} reason=object is not a chart");
                                    continue;
                                }

                                chart.SetChartCellData(chartCellUpdate.Row, chartCellUpdate.Column, chartCellUpdate.Value);
                            }
                            catch (Exception ex)
                            {
                                _logService.Warning(
                                    $"Karisma chart cell update skipped: scene={sceneAlias} object={chartCellUpdate.ObjectName} row={chartCellUpdate.Row} column={chartCellUpdate.Column} reason={ex.Message}");
                            }
                        }

                        foreach (var positionUpdate in positionUpdates)
                        {
                            if (string.IsNullOrWhiteSpace(positionUpdate.ObjectName))
                            {
                                continue;
                            }

                            try
                            {
                                var sceneObject = scene.GetObject(positionUpdate.ObjectName);
                                if (sceneObject is null)
                                {
                                    continue;
                                }

                                if (positionUpdate.KeyIndex >= 0)
                                {
                                    sceneObject.SetPositionKey(
                                        positionUpdate.KeyIndex,
                                        positionUpdate.X,
                                        positionUpdate.Y,
                                        positionUpdate.Z,
                                        positionUpdate.VectorType);
                                }
                                else
                                {
                                    sceneObject.SetPosition(
                                        positionUpdate.X,
                                        positionUpdate.Y,
                                        positionUpdate.Z,
                                        positionUpdate.VectorType);
                                }
                            }
                            catch (Exception ex)
                            {
                                _logService.Warning(
                                    $"Karisma position update skipped: scene={sceneAlias} object={positionUpdate.ObjectName} keyIndex={positionUpdate.KeyIndex} reason={ex.Message}");
                            }
                        }

                        foreach (var cropKeyUpdate in cropKeyUpdates)
                        {
                            if (string.IsNullOrWhiteSpace(cropKeyUpdate.ObjectName))
                            {
                                continue;
                            }

                            try
                            {
                                var sceneObject = scene.GetObject(cropKeyUpdate.ObjectName);
                                if (sceneObject is null)
                                {
                                    continue;
                                }

                                sceneObject.SetCropKey(
                                    cropKeyUpdate.KeyIndex,
                                    cropKeyUpdate.Left,
                                    cropKeyUpdate.Top,
                                    cropKeyUpdate.Right,
                                    cropKeyUpdate.Bottom,
                                    cropKeyUpdate.CropKey);
                            }
                            catch (Exception ex)
                            {
                                _logService.Warning(
                                    $"Karisma crop-key update skipped: scene={sceneAlias} object={cropKeyUpdate.ObjectName} keyIndex={cropKeyUpdate.KeyIndex} reason={ex.Message}");
                            }
                        }

                        foreach (var styleColorUpdate in styleColorUpdates)
                        {
                            if (string.IsNullOrWhiteSpace(styleColorUpdate.ObjectName))
                            {
                                continue;
                            }

                            try
                            {
                                var sceneObject = scene.GetObject(styleColorUpdate.ObjectName);
                                if (sceneObject is not IKAStyle style)
                                {
                                    _logService.Warning(
                                        $"Karisma style color update skipped: scene={sceneAlias} object={styleColorUpdate.ObjectName} reason=object does not implement IKAStyle");
                                    continue;
                                }

                                style.SetStyleColor(
                                    styleColorUpdate.StyleType,
                                    styleColorUpdate.Order,
                                    styleColorUpdate.R,
                                    styleColorUpdate.G,
                                    styleColorUpdate.B,
                                    styleColorUpdate.A);
                            }
                            catch (Exception ex)
                            {
                                _logService.Warning(
                                    $"Karisma style color update skipped: scene={sceneAlias} object={styleColorUpdate.ObjectName} style={styleColorUpdate.StyleType} order={styleColorUpdate.Order} reason={ex.Message}");
                            }
                        }

                        ApplyVisibilityUpdates(sceneAlias, scene, visibilityUpdatesAfterValue);
                    }
                    finally
                    {
                        transactionCompletion = eventHandler.BeginEndTransactionWait();
                        _engine!.EndTransaction();
                    }
                }, cancellationToken).ConfigureAwait(false);

                if (transactionCompletion is not null)
                {
                    var transactionResult = await WaitForKarismaResultAsync(
                        transactionCompletion,
                        cancellationToken,
                        $"EndTransaction scene={sceneAlias}").ConfigureAwait(false);
                    ThrowIfKarismaFailed(transactionResult, $"EndTransaction failed for scene={sceneAlias}");
                }

                await UpdateSceneTexturesAsync(sceneAlias, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                eventHandler.CancelPendingEndTransaction(ex);
                eventHandler.CancelPendingUpdateTextures(sceneAlias, ex);
                throw;
            }
        }
        finally
        {
            _asyncOperationLock.Release();
        }
    }

    private static DateTime ResolveSceneWriteTime(string scenePath)
    {
        try
        {
            return File.GetLastWriteTimeUtc(scenePath);
        }
        catch
        {
            return DateTime.MinValue;
        }
    }

    private void ApplyVisibilityUpdates(string sceneAlias, KAScene scene, IReadOnlyList<KarismaVisibilityUpdate> visibilityUpdates)
    {
        foreach (var visibilityUpdate in visibilityUpdates)
        {
            if (string.IsNullOrWhiteSpace(visibilityUpdate.ObjectName))
            {
                continue;
            }

            try
            {
                var sceneObject = scene.GetObject(visibilityUpdate.ObjectName);
                if (sceneObject is null)
                {
                    continue;
                }

                sceneObject.SetVisible(visibilityUpdate.IsVisible ? 1 : 0);
            }
            catch (Exception ex)
            {
                _logService.Warning(
                    $"Karisma visibility update skipped: scene={sceneAlias} object={visibilityUpdate.ObjectName} visible={visibilityUpdate.IsVisible} reason={ex.Message}");
            }
        }
    }

    private async Task UpdateSceneTexturesAsync(string sceneAlias, CancellationToken cancellationToken)
    {
        var eventHandler = GetEventHandlerCore();
        var completion = eventHandler.BeginUpdateTexturesWait(sceneAlias);

        try
        {
            await _dispatcher.InvokeAsync(() =>
            {
                ThrowIfDisposed();
                EnsureConnectedCore();
                GetSceneCore(sceneAlias).UpdateTextures();
            }, cancellationToken).ConfigureAwait(false);

            var result = await WaitForKarismaResultAsync(
                completion,
                cancellationToken,
                $"UpdateTextures scene={sceneAlias}").ConfigureAwait(false);
            ThrowIfKarismaFailed(result, $"UpdateTextures failed for scene={sceneAlias}");
        }
        catch (Exception ex)
        {
            eventHandler.CancelPendingUpdateTextures(sceneAlias, ex);
            throw;
        }
    }

    public async Task PrepareAsync(int outputChannelIndex, int layerNo, string sceneAlias, CancellationToken cancellationToken)
    {
        await _asyncOperationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var eventHandler = GetEventHandlerCore();
            var completion = eventHandler.BeginScenePrepareWait(outputChannelIndex, layerNo);

            try
            {
                await _dispatcher.InvokeAsync(() =>
                {
                    ThrowIfDisposed();
                    EnsureConnectedCore();
                    GetScenePlayerCore(outputChannelIndex).Prepare(layerNo, GetSceneCore(sceneAlias));
                }, cancellationToken).ConfigureAwait(false);

                var result = await WaitForKarismaResultAsync(
                    completion,
                    cancellationToken,
                    $"Prepare output={outputChannelIndex} layer={layerNo} scene={sceneAlias}").ConfigureAwait(false);
                ThrowIfKarismaFailed(result, $"Prepare failed for output={outputChannelIndex} layer={layerNo} scene={sceneAlias}");

                // Give Karisma one render tick after Prepare before a mixed-preview screenshot is requested.
                await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                eventHandler.CancelPendingScenePrepare(outputChannelIndex, layerNo, ex);
                throw;
            }
        }
        finally
        {
            _asyncOperationLock.Release();
        }
    }

    public async Task PlayAsync(int outputChannelIndex, int layerNo, bool cutIn, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();
            EnsureConnectedCore();

            var scenePlayer = GetScenePlayerCore(outputChannelIndex);
            if (cutIn)
            {
                scenePlayer.CutIn(layerNo);
                return;
            }

            scenePlayer.Play(layerNo);
        }, cancellationToken);
    }

    public async Task PauseAsync(int outputChannelIndex, int layerNo, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();
            EnsureConnectedCore();
            GetScenePlayerCore(outputChannelIndex).Pause(layerNo);
        }, cancellationToken);
    }

    public async Task ResumeAsync(int outputChannelIndex, int layerNo, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();
            EnsureConnectedCore();
            GetScenePlayerCore(outputChannelIndex).Resume(layerNo);
        }, cancellationToken);
    }

    public async Task StopAsync(int outputChannelIndex, int layerNo, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();
            EnsureConnectedCore();
            GetScenePlayerCore(outputChannelIndex).Stop(layerNo);
        }, cancellationToken);
    }

    public async Task PlayOutAsync(int outputChannelIndex, int layerNo, bool cutOut, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();
            EnsureConnectedCore();

            var scenePlayer = GetScenePlayerCore(outputChannelIndex);
            if (cutOut)
            {
                scenePlayer.CutOut(layerNo);
                return;
            }

            scenePlayer.PlayOut(layerNo);
        }, cancellationToken);
    }

    public async Task ClearNextPreviewAsync(int outputChannelIndex, int layerNo, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();
            EnsureConnectedCore();
            GetScenePlayerCore(outputChannelIndex).ClearNextPreview(layerNo);
        }, cancellationToken);
    }

    public async Task SaveMixedPreviewImageAsync(
        int outputChannelIndex,
        int layerNo,
        string fileName,
        int width,
        int height,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(fileName))
        {
            throw new ArgumentException("Image file path is required.", nameof(fileName));
        }

        await _asyncOperationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await _dispatcher.InvokeAsync(() =>
            {
                ThrowIfDisposed();
                EnsureConnectedCore();
            }, cancellationToken).ConfigureAwait(false);

            var eventHandler = GetEventHandlerCore();
            var completion = eventHandler.BeginSaveMixedPreviewImageWait();

            try
            {
                await _dispatcher.InvokeAsync(() =>
                {
                    ThrowIfDisposed();
                    EnsureConnectedCore();

                    var directory = Path.GetDirectoryName(fileName);
                    if (!string.IsNullOrWhiteSpace(directory))
                    {
                        Directory.CreateDirectory(directory);
                    }

                    GetScenePlayerCore(outputChannelIndex).SaveMixedPreviewImage(fileName, width, height);
                }, cancellationToken).ConfigureAwait(false);

                var result = await WaitForKarismaCallbackAsync(
                    completion,
                    cancellationToken,
                    $"SaveMixedPreviewImage output={outputChannelIndex} layer={layerNo}").ConfigureAwait(false);

                if (result.Result != eKResult.RESULT_SUCCESS)
                {
                    throw new InvalidOperationException(
                        $"SaveMixedPreviewImage failed for output={outputChannelIndex} layer={layerNo}: {result.Result} ({(int)result.Result})");
                }

                await WaitForFileAsync(fileName, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                eventHandler.CancelPendingSaveMixedPreviewImage(ex);
                throw;
            }
        }
        finally
        {
            _asyncOperationLock.Release();
        }
    }

    public async Task SaveSceneImageAsync(
        string sceneAlias,
        string fileName,
        int width,
        int height,
        int frame,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(sceneAlias))
        {
            throw new ArgumentException("Scene alias is required.", nameof(sceneAlias));
        }

        if (string.IsNullOrWhiteSpace(fileName))
        {
            throw new ArgumentException("Image file path is required.", nameof(fileName));
        }

        await _asyncOperationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await _dispatcher.InvokeAsync(() =>
            {
                ThrowIfDisposed();
                EnsureConnectedCore();
            }, cancellationToken).ConfigureAwait(false);

            var eventHandler = GetEventHandlerCore();
            var completion = eventHandler.BeginSaveSceneImageWait();

            try
            {
                await _dispatcher.InvokeAsync(() =>
                {
                    ThrowIfDisposed();
                    EnsureConnectedCore();

                    var directory = Path.GetDirectoryName(fileName);
                    if (!string.IsNullOrWhiteSpace(directory))
                    {
                        Directory.CreateDirectory(directory);
                    }

                    GetSceneCore(sceneAlias).SaveSceneImage(fileName, width, height, frame);
                }, cancellationToken).ConfigureAwait(false);

                var result = await WaitForKarismaCallbackAsync(
                    completion,
                    cancellationToken,
                    $"SaveSceneImage scene={sceneAlias}").ConfigureAwait(false);

                if (result.Result != eKResult.RESULT_SUCCESS)
                {
                    throw new InvalidOperationException($"SaveSceneImage failed for '{sceneAlias}': {result.Result} ({(int)result.Result})");
                }

                await WaitForFileAsync(fileName, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                eventHandler.CancelPendingSaveSceneImage(ex);
                throw;
            }
        }
        finally
        {
            _asyncOperationLock.Release();
        }
    }

    public async Task UnloadSceneAsync(string sceneAlias, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();

            if (!_scenes.TryGetValue(sceneAlias, out var scene))
            {
                return;
            }

            scene.UnloadScene();
            _scenes.Remove(sceneAlias);
            _scenePaths.Remove(sceneAlias);
        }, cancellationToken);
    }

    public async Task TriggerAsync(int outputChannelIndex, int layerNo, string animationName, CancellationToken cancellationToken)
    {
        await InvokeExclusiveAsync(() =>
        {
            ThrowIfDisposed();
            EnsureConnectedCore();

            if (string.IsNullOrWhiteSpace(animationName))
            {
                return;
            }

            GetScenePlayerCore(outputChannelIndex).Trigger(layerNo, animationName);
        }, cancellationToken);
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;
        _autoReconnectEnabled = false;
        _reconnectTimer.Dispose();

        try
        {
            _dispatcher.InvokeAsync(() =>
            {
                foreach (var scene in _scenes.Values)
                {
                    try
                    {
                        scene.UnloadScene();
                    }
                    catch
                    {
                    }
                }

                try
                {
                    if (_connected)
                    {
                        _engine?.Disconnect();
                    }
                }
                catch
                {
                }

                _scenes.Clear();
                _scenePaths.Clear();
                _scenePlayers.Clear();
                _engine = null;
                _eventHandler = null;
                _connected = false;
            }, CancellationToken.None).GetAwaiter().GetResult();
        }
        finally
        {
            _dispatcher.Dispose();
        }
    }

    private void EnsureConnectedCore()
    {
        ThrowIfDisposed();

        if (_connected)
        {
            return;
        }

        _engine ??= (IKAEngine)new KAEngineClass();
        _eventHandler ??= new KarismaEventHandler(_logService, HandleConnect, HandleClose);
        _autoReconnectEnabled = true;

        var requested = _engine.Connect(_host, _port, _eventHandler);
        _logService.Info($"Karisma Connect() return={(requested != 0 ? "TRUE" : "FALSE")} raw={requested} target={_host}:{_port}");
        if (requested == 0)
        {
            throw new InvalidOperationException($"Karisma Async Engine connection request failed: {_host}:{_port}");
        }

        _connected = true;
        _logService.Info($"Karisma Async Engine connect requested: {_host}:{_port}");
    }

    private void HandleConnect(int errorCode)
    {
        if (errorCode == 0)
        {
            _connected = true;
            UpdateTcpConnectionState(true);
            return;
        }

        _connected = false;
        UpdateTcpConnectionState(false);
    }

    private void HandleClose(int errorCode)
    {
        _connected = false;
        UpdateTcpConnectionState(false);
        if (_eventHandler is not null)
        {
            var error = new IOException($"Karisma connection closed while waiting for a callback (errorCode={errorCode}).");
            _eventHandler.CancelPendingConnect(error);
            _eventHandler.CancelAllPendingLoadScenes(error);
            _eventHandler.CancelPendingSaveSceneImage(error);
        }
    }

    private void UpdateTcpConnectionState(bool isConnected)
    {
        if (_isTcpConnected == isConnected)
        {
            return;
        }

        _isTcpConnected = isConnected;
        ConnectionChanged?.Invoke(this, EventArgs.Empty);
    }

    private void OnReconnectTimerTick(object? state)
    {
        if (_disposed || !_autoReconnectEnabled || _connected)
        {
            return;
        }

        if (Interlocked.Exchange(ref _reconnectInProgress, 1) == 1)
        {
            return;
        }

        _ = TryAutoReconnectAsync();
    }

    private async Task TryAutoReconnectAsync()
    {
        try
        {
            if (_disposed || !_autoReconnectEnabled || _connected)
            {
                return;
            }

            await _dispatcher.InvokeAsync(() =>
            {
                if (_disposed || _connected)
                {
                    return;
                }

                try
                {
                    EnsureConnectedCore();
                }
                catch (Exception ex)
                {
                    _logService.Warning($"Karisma auto reconnect attempt failed: {ex.Message}");
                }
            }, CancellationToken.None).ConfigureAwait(false);
        }
        finally
        {
            Interlocked.Exchange(ref _reconnectInProgress, 0);
        }
    }

    private KAScene GetSceneCore(string sceneAlias)
    {
        if (_scenes.TryGetValue(sceneAlias, out var scene))
        {
            return scene;
        }

        throw new InvalidOperationException($"Karisma scene is not loaded: {sceneAlias}");
    }

    private KAScenePlayer GetScenePlayerCore(int outputChannelIndex)
    {
        if (_scenePlayers.TryGetValue(outputChannelIndex, out var scenePlayer))
        {
            return scenePlayer;
        }

        scenePlayer = _engine!.GetScenePlayer(outputChannelIndex)
            ?? throw new InvalidOperationException($"Karisma scene player is unavailable: output={outputChannelIndex}");
        _scenePlayers[outputChannelIndex] = scenePlayer;
        return scenePlayer;
    }

    private KarismaEventHandler GetEventHandlerCore()
    {
        return _eventHandler ?? throw new InvalidOperationException("Karisma event handler is unavailable.");
    }

    private async Task InvokeExclusiveAsync(Action action, CancellationToken cancellationToken)
    {
        await _asyncOperationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await _dispatcher.InvokeAsync(action, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _asyncOperationLock.Release();
        }
    }

    private static Task<eKResult> WaitForKarismaResultAsync(
        Task<eKResult> completion,
        CancellationToken cancellationToken,
        string operationName)
    {
        return WaitForKarismaCallbackAsync(completion, cancellationToken, operationName);
    }

    private static async Task<T> WaitForKarismaCallbackAsync<T>(
        Task<T> completion,
        CancellationToken cancellationToken,
        string operationName)
    {
        using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeoutCts.CancelAfter(AsyncOperationTimeout);

        try
        {
            return await completion.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
        {
            throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout.TotalSeconds:0.#} seconds.");
        }
    }

    private static void ThrowIfKarismaFailed(eKResult result, string message)
    {
        if (result != eKResult.RESULT_SUCCESS)
        {
            throw new InvalidOperationException($"{message}: {result} ({(int)result})");
        }
    }

    private async Task EnsureConnectedInternalAsync(CancellationToken cancellationToken)
    {
        await _asyncOperationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            Task<int>? connectCompletion = null;
            try
            {
                await _dispatcher.InvokeAsync(() =>
                {
                    ThrowIfDisposed();

                    if (_isTcpConnected)
                    {
                        return;
                    }

                    _engine ??= (IKAEngine)new KAEngineClass();
                    _eventHandler ??= new KarismaEventHandler(_logService, HandleConnect, HandleClose);
                    _autoReconnectEnabled = true;
                    connectCompletion = _eventHandler.BeginConnectWait();

                    if (_connected)
                    {
                        return;
                    }

                    var requested = _engine.Connect(_host, _port, _eventHandler);
                    _logService.Info($"Karisma Connect() return={(requested != 0 ? "TRUE" : "FALSE")} raw={requested} target={_host}:{_port}");
                    if (requested == 0)
                    {
                        throw new InvalidOperationException($"Karisma Async Engine connection request failed: {_host}:{_port}");
                    }

                    _connected = true;
                    _logService.Info($"Karisma Async Engine connect requested: {_host}:{_port}");
                }, cancellationToken).ConfigureAwait(false);

                if (connectCompletion is null)
                {
                    return;
                }

                var errorCode = await WaitForKarismaCallbackAsync(
                    connectCompletion,
                    cancellationToken,
                    "Connect").ConfigureAwait(false);
                if (errorCode != 0)
                {
                    throw new InvalidOperationException($"Karisma Async Engine OnConnect failed: errorCode={errorCode}");
                }
            }
            catch (Exception ex)
            {
                if (_eventHandler is not null)
                {
                    _eventHandler.CancelPendingConnect(ex);
                }

                try
                {
                    await _dispatcher.InvokeAsync(() =>
                    {
                        _connected = false;
                        _isTcpConnected = false;
                    }, CancellationToken.None).ConfigureAwait(false);
                }
                catch
                {
                }

                throw;
            }
        }
        finally
        {
            _asyncOperationLock.Release();
        }
    }

    private static async Task WaitForFileAsync(string fileName, CancellationToken cancellationToken)
    {
        for (var attempt = 0; attempt < 20; attempt++)
        {
            if (File.Exists(fileName))
            {
                var fileInfo = new FileInfo(fileName);
                if (fileInfo.Length > 0)
                {
                    return;
                }
            }

            await Task.Delay(150, cancellationToken).ConfigureAwait(false);
        }

        throw new IOException($"Saved scene image was not created: {fileName}");
    }

    private void ThrowIfDisposed()
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
    }

    private sealed class StaDispatcher : IDisposable
    {
        private const uint WmApp = 0x8000;
        private const uint WmDispatch = WmApp + 1;

        private readonly ConcurrentQueue<Action> _queue = new();
        private readonly Thread _thread;
        private readonly ManualResetEventSlim _threadReady = new();
        private bool _disposed;
        private uint _threadId;

        public StaDispatcher(string threadName)
        {
            _thread = new Thread(Run)
            {
                IsBackground = true,
                Name = threadName
            };
            _thread.SetApartmentState(ApartmentState.STA);
            _thread.Start();
            _threadReady.Wait();
        }

        public Task InvokeAsync(Action action, CancellationToken cancellationToken)
        {
            return InvokeAsync(() =>
            {
                action();
                return true;
            }, cancellationToken);
        }

        public Task<T> InvokeAsync<T>(Func<T> action, CancellationToken cancellationToken)
        {
            ObjectDisposedException.ThrowIf(_disposed, this);
            cancellationToken.ThrowIfCancellationRequested();

            var completion = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
            _queue.Enqueue(() =>
            {
                if (cancellationToken.IsCancellationRequested)
                {
                    completion.TrySetCanceled(cancellationToken);
                    return;
                }

                try
                {
                    completion.TrySetResult(action());
                }
                catch (Exception ex)
                {
                    completion.TrySetException(ex);
                }
            });

            if (!PostThreadMessage(_threadId, WmDispatch, UIntPtr.Zero, IntPtr.Zero))
            {
                throw new InvalidOperationException("Failed to signal Karisma STA dispatcher.");
            }

            return cancellationToken.CanBeCanceled
                ? completion.Task.WaitAsync(cancellationToken)
                : completion.Task;
        }

        public void Dispose()
        {
            if (_disposed)
            {
                return;
            }

            _disposed = true;
            PostThreadMessage(_threadId, WmQuit, UIntPtr.Zero, IntPtr.Zero);
            _thread.Join();
            _threadReady.Dispose();
        }

        private void Run()
        {
            PeekMessage(out _, IntPtr.Zero, 0, 0, 0);
            _threadId = GetCurrentThreadId();
            _threadReady.Set();

            while (GetMessage(out var message, IntPtr.Zero, 0, 0) > 0)
            {
                if (message.message == WmDispatch)
                {
                    DrainQueue();
                    continue;
                }

                TranslateMessage(ref message);
                DispatchMessage(ref message);
            }

            DrainQueue();
        }

        private void DrainQueue()
        {
            while (_queue.TryDequeue(out var action))
            {
                action();
            }
        }

        private const uint WmQuit = 0x0012;

        [DllImport("kernel32.dll")]
        private static extern uint GetCurrentThreadId();

        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool PostThreadMessage(uint idThread, uint msg, UIntPtr wParam, IntPtr lParam);

        [DllImport("user32.dll")]
        private static extern int GetMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);

        [DllImport("user32.dll")]
        private static extern bool TranslateMessage([In] ref NativeMessage lpMsg);

        [DllImport("user32.dll")]
        private static extern IntPtr DispatchMessage([In] ref NativeMessage lpMsg);

        [DllImport("user32.dll")]
        private static extern bool PeekMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);

        [StructLayout(LayoutKind.Sequential)]
        private struct NativeMessage
        {
            public IntPtr hwnd;
            public uint message;
            public UIntPtr wParam;
            public IntPtr lParam;
            public uint time;
            public NativePoint pt;
            public uint lPrivate;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct NativePoint
        {
            public int x;
            public int y;
        }
    }
}
