using System.Runtime.ExceptionServices;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Hosting;

namespace HellionChat;

// Orchestrates Host.StartAsync / StopAsync and the framework-thread dispose.
// Plugin.ctor builds the host and assigns it via the Host property, so
// PluginLifecycle never constructs the host itself.
internal sealed class PluginLifecycle : IAsyncDisposable
{
    private readonly IFramework _framework;
    private readonly Plugin _plugin;

    private int _disposeStarted;
    private bool _hostStartRequested;

    public PluginLifecycle(IFramework framework, Plugin plugin)
    {
        _framework = framework;
        _plugin = plugin;
    }

    // Plugin.ctor fills this immediately after PluginHostFactory.Build and
    // before invoking LoadAsync; LoadAsync may NRE-suppress on Host! safely.
    public IHost? Host { get; set; }

    public async Task LoadAsync(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        try
        {
            _hostStartRequested = true;
            await Host!.StartAsync(cancellationToken).ConfigureAwait(false);

            // WindowSystem.AddWindow mutates an internal List<>; v1.4.9 Stage-2
            // verified the list is non-thread-safe, so we marshal the entire
            // registration block to the framework thread.
            await _framework
                .RunOnFrameworkThread(() => RegisterWindows(_plugin))
                .ConfigureAwait(false);
        }
        catch
        {
            try
            {
                await DisposeAsync().ConfigureAwait(false);
            }
            catch
            {
                // Swallow secondary dispose failure so the original load throw wins.
            }

            throw;
        }
    }

    private static void RegisterWindows(Plugin plugin)
    {
        plugin.WindowSystem.AddWindow(plugin.ChatLogWindow);
        plugin.WindowSystem.AddWindow(plugin.SettingsWindow);
        plugin.WindowSystem.AddWindow(plugin.DbViewer);
        plugin.WindowSystem.AddWindow(plugin.InputPreview);
        plugin.WindowSystem.AddWindow(plugin.CommandHelpWindow);
        plugin.WindowSystem.AddWindow(plugin.SeStringDebugger);
        plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
        plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
    }

    public async ValueTask DisposeAsync()
    {
        // Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
        if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
            return;

        Exception? failure = null;

        if (_hostStartRequested && Host is not null)
            failure = await CaptureFailureAsync(failure, () => Host.StopAsync())
                .ConfigureAwait(false);

        failure = await DisposeHostOnFrameworkThreadAsync(failure).ConfigureAwait(false);

        ThrowIfFailed(failure);
    }

    private async Task<Exception?> DisposeHostOnFrameworkThreadAsync(Exception? failure)
    {
        try
        {
            await _framework
                .RunOnFrameworkThread(() =>
                {
                    failure = CaptureFailure(failure, () => Host?.Dispose());
                })
                .ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            failure ??= ex;
        }

        return failure;
    }

    private static Exception? CaptureFailure(Exception? failure, Action action)
    {
        try
        {
            action();
        }
        catch (Exception ex)
        {
            failure ??= ex;
        }

        return failure;
    }

    private static async ValueTask<Exception?> CaptureFailureAsync(
        Exception? failure,
        Func<Task> action
    )
    {
        try
        {
            await action().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            failure ??= ex;
        }

        return failure;
    }

    private static void ThrowIfFailed(Exception? failure)
    {
        if (failure is not null)
            ExceptionDispatchInfo.Capture(failure).Throw();
    }
}
