#nullable disable
using EllieBot.Voice;
using System.Reflection;

namespace EllieBot.Modules.Music.Services;

public sealed class AyuVoiceStateService : IEService
{
    // public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy);
    // public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; };

    private readonly ConcurrentDictionary<ulong, IVoiceProxy> _voiceProxies = new();
    private readonly ConcurrentDictionary<ulong, SemaphoreSlim> _voiceGatewayLocks = new();

    private readonly DiscordSocketClient _client;
    private readonly MethodInfo _sendVoiceStateUpdateMethodInfo;
    private readonly object _dnetApiClient;
    private readonly ulong _currentUserId;

    public AyuVoiceStateService(DiscordSocketClient client)
    {
        _client = client;
        _currentUserId = _client.CurrentUser.Id;

        var prop = _client.GetType()
                          .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance)
                          .First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient");
        _dnetApiClient = prop.GetValue(_client, null);
        _sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType()
                                                        .GetMethod("SendVoiceStateUpdateAsync",
                                                        [
                                                            typeof(ulong), typeof(ulong?), typeof(bool),
                                                                typeof(bool), typeof(RequestOptions)
                                                        ]);

        _client.LeftGuild += ClientOnLeftGuild;
    }

    private Task ClientOnLeftGuild(SocketGuild guild)
    {
        if (_voiceProxies.TryRemove(guild.Id, out var proxy))
        {
            proxy.StopGateway();
            proxy.SetGateway(null);
        }

        return Task.CompletedTask;
    }

    private Task InvokeSendVoiceStateUpdateAsync(
            ulong guildId,
            ulong? channelId = null,
            bool isDeafened = false,
            bool isMuted = false)
        // return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted);
        => (Task)_sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient,
            [guildId, channelId, isMuted, isDeafened, null]);

    private Task SendLeaveVoiceChannelInternalAsync(ulong guildId)
        => InvokeSendVoiceStateUpdateAsync(guildId);

    private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId)
        => InvokeSendVoiceStateUpdateAsync(guildId, channelId);

    private SemaphoreSlim GetVoiceGatewayLock(ulong guildId)
        => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1));

    private async Task LeaveVoiceChannelInternalAsync(ulong guildId)
    {
        var complete = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

        Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
        {
            if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId && newState.VoiceChannel?.Id is null)
                complete.TrySetResult(true);

            return Task.CompletedTask;
        }

        try
        {
            _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;

            if (_voiceProxies.TryGetValue(guildId, out var proxy))
            {
                _ = proxy.StopGateway();
                proxy.SetGateway(null);
            }

            await SendLeaveVoiceChannelInternalAsync(guildId);
            await Task.WhenAny(Task.Delay(1500), complete.Task);
        }
        finally
        {
            _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
        }
    }

    public async Task LeaveVoiceChannel(ulong guildId)
    {
        var gwLock = GetVoiceGatewayLock(guildId);
        await gwLock.WaitAsync();
        try
        {
            await LeaveVoiceChannelInternalAsync(guildId);
        }
        finally
        {
            gwLock.Release();
        }
    }

    private async Task<IVoiceProxy> InternalConnectToVcAsync(ulong guildId, ulong channelId)
    {
        var voiceStateUpdatedSource =
            new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
        var voiceServerUpdatedSource =
            new TaskCompletionSource<SocketVoiceServer>(TaskCreationOptions.RunContinuationsAsynchronously);

        Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState)
        {
            if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId)
            {
                if (newState.VoiceChannel?.Id == channelId)
                    voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId);

                voiceStateUpdatedSource.TrySetResult(null);
            }

            return Task.CompletedTask;
        }

        Task OnVoiceServerUpdated(SocketVoiceServer data)
        {
            if (data.Guild.Id == guildId)
                voiceServerUpdatedSource.TrySetResult(data);

            return Task.CompletedTask;
        }

        try
        {
            _client.VoiceServerUpdated += OnVoiceServerUpdated;
            _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated;

            await SendJoinVoiceChannelInternalAsync(guildId, channelId);

            // create a delay task, how much to wait for gateway response
            using var cts = new CancellationTokenSource();
            var delayTask = Task.Delay(2500, cts.Token);

            // either delay or successful voiceStateUpdate
            var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task);
            // either delay or successful voiceServerUpdate
            var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task);

            // wait for both to end (max 1s) and check if either of them is a delay task
            var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask);
            if (results[0] == delayTask || results[1] == delayTask)
                // if either is delay, return null - connection unsuccessful
                return null;
            else
                cts.Cancel();

            // if both are succesful, that means we can safely get
            // the values from  completion sources

            var session = await voiceStateUpdatedSource.Task;

            // session can be null. Means we disconnected, or connected to the wrong channel (?!)
            if (session is null)
                return null;

            var voiceServerData = await voiceServerUpdatedSource.Task;

            VoiceGateway CreateVoiceGatewayLocal()
            {
                return new(guildId, _currentUserId, session, voiceServerData.Token, voiceServerData.Endpoint);
            }

            var current = _voiceProxies.AddOrUpdate(guildId,
                _ => new VoiceProxy(CreateVoiceGatewayLocal()),
                (gid, currentProxy) =>
                {
                    _ = currentProxy.StopGateway();
                    currentProxy.SetGateway(CreateVoiceGatewayLocal());
                    return currentProxy;
                });

            _ = current.StartGateway(); // don't await, this blocks until gateway is closed
            return current;
        }
        finally
        {
            _client.VoiceServerUpdated -= OnVoiceServerUpdated;
            _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated;
        }
    }

    public async Task<IVoiceProxy> JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true)
    {
        var gwLock = GetVoiceGatewayLock(guildId);
        await gwLock.WaitAsync();
        try
        {
            await LeaveVoiceChannelInternalAsync(guildId);
            return await InternalConnectToVcAsync(guildId, channelId);
        }
        finally
        {
            gwLock.Release();
        }
    }

    public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy)
        => _voiceProxies.TryGetValue(guildId, out proxy);
}